---
title: "Issuing certificates — Certificates for Laravel | Roundly"
description: "Issue and track a certificate with one call — issueIfMissing(), issue() with a DTO, generate() — and what happens along the way."
url: https://roundly-consulting.com/open-source/docs/certificates-for-laravel/issuing
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Certificates for Laravel](https://roundly-consulting.com/open-source/docs/certificates-for-laravel.md)

# Issuing certificates

Issuance goes through the Certificates facade, backed by CertificatesManager. issueIfMissing() returns the active certificate already covering a domain — its own registry record, else a SAN certificate listing it — or issues a new one:

```php
use RoundlyConsulting\Certificates\Facades\Certificates;

$certificate = Certificates::issueIfMissing('app.example.com');

$certificate->status;            // RoundlyConsulting\Certificates\Enums\CertificateStatus::Issued
                                 // (kubernetes: Requested until certificates:sync records the outcome)
$certificate->expires_at;        // CarbonImmutable|null
$certificate->expiresWithin(14); // bool
$certificate->daysUntilExpiry(); // int|null
```

## Issuing from a DTO

issue() always provisions. It takes an IssueCertificateData — the same input the builder, the certificates:issue command and HasCertificates use:

```php
use RoundlyConsulting\Certificates\DataTransferObjects\IssueCertificateData;
use RoundlyConsulting\Certificates\Facades\Certificates;

// Fully specified issuance, recorded in the registry.
$certificate = Certificates::issue(new IssueCertificateData(
    domain: 'shop.example.com',
    driver: 'acme',
    validForDays: 90,
    meta: ['plan' => 'pro'],
    owner: $tenant,
));

// Shorthands.
Certificates::issue(IssueCertificateData::make('shop.example.com'));
Certificates::issue(IssueCertificateData::makeForDomains(['example.com', 'www.example.com']));
```

## What happens on issue

- Domains are lowercased and de-duplicated — hostnames are case-insensitive — and each is checked with the ValidDomain rule. An invalid one throws InvalidDomainException before anything is written.
- The certificate’s own provisioning lock is taken before the row is touched. If another process holds it, issue() throws ProvisioningInProgressException and the row is left as it was.
- Two domains can fold into one certificate name (a.b.com and a-b.com, or \*.example.com and wildcard.example.com). When the (driver, name) row — pruned or not — already belongs to another domain, issue() throws CertificateException before anything changes: the row, its owner and its stored material stay with the domain that registered first.
- The registry row for (driver, name) is created or updated with status requested, the domain list, meta and owner, and CertificateRequested fires. A row pruned earlier is revived as a fresh registration.
- The provider provisions the certificate — generateMany() for several domains when it supports SANs, generate() otherwise — and its status report is read.
- Reported issued: the row becomes issued with the reported expiry, issuer, serial and fingerprint (validForDays, default 90, fills in only when the driver reports no expiry) and CertificateIssued fires.
- Reported still in progress (cert-manager issues asynchronously): the row stays requested until certificates:sync records the outcome — it then sets issued\_at and fires CertificateIssued, or marks the row failed and fires CertificateFailed.
- Reported failed, expired or revoked — or the provider threw: the row becomes failed with last\_error set, CertificateFailed fires and the exception is rethrown. Either way the certificate’s cached status report is dropped.

## Provisioning without the registry

generate() is the lower-level call. With the registry table present it runs issue() — returning false instead of throwing when the certificate is already being provisioned; without it, it provisions straight through the provider under the same per-certificate lock:

```php
// true once it ran; false while another process is provisioning
// this certificate (its per-certificate lock is held).
Certificates::generate('shop.example.com'); // bool
```

## Reading

```php
Certificates::get();                         // Collection<int, RemoteCertificate> from the active driver
Certificates::exists('app.example.com');     // bool
Certificates::find('app.example.com');       // ?Models\Certificate (registry lookup)
Certificates::find('app.example.com', 'acme'); // ...limited to one driver
Certificates::status('app.example.com');     // ?CertificateStatus (registry enum)
Certificates::statusReport('app.example.com'); // ?CertificateStatusReport (live, cached)
Certificates::expiring(14, 'acme');          // Collection<int, Certificate> expiring within 14 days
Certificates::driver('null');                // resolve a specific provider instance
Certificates::certificateName('app.example.com'); // "generated-tls-app-example-com"
Certificates::certificateName('*.Example.com');   // "generated-tls-wildcard-example-com"
```

The full method list is on The Certificates facade page.

## Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

[More ways to support, including crypto](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/certificates-for-laravel.md)
