v1.0.0
CoyoteCert v1.0.0
The ACME v2 client PHP deserved. It ships. It doesn't fall off cliffs.
CoyoteCert 1.0.0 is a complete rewrite: a fully-featured ACME v2 client for PHP 8.3+ covering first-time issuance, ARI-guided renewal, and revocation. Fluent builder API, a CLI, six DNS providers, three storage backends, 94%+ test coverage across PHP 8.3, 8.4, and 8.5.
What's in the box
Every major CA, wired up and ready
Built-in providers for Let's Encrypt, ZeroSSL, Google Trust Services, SSL.com, and Buypass (production and staging). ZeroSSL auto-provisions EAB credentials from your API key so you never have to copy-paste a token. Google Trust Services and SSL.com accept pre-provisioned EAB directly. CustomProvider handles anything RFC 8555-compliant (internal PKI, private CAs, whatever you're running).
No default CA. Every call is explicit. Trade-offs around rate limits, data residency, certificate lifetime, and trust coverage belong to the caller.
A CLI that ships with the package
composer global require blendbyte/coyotecert and you get coyote issue and coyote status. Issue a wildcard cert in one command. Inspect expiry with another. Drop it in a cron job and forget it. Same providers, same key types, same storage layout as the library, no separate toolchain.
http-01, dns-01, and tls-alpn-01
All three standard ACME challenge types. Six built-in dns-01 handlers cover Cloudflare, Hetzner DNS, DigitalOcean, ClouDNS, AWS Route53 (SigV4 signing built in, no AWS SDK needed), and shell/exec for anything else. tls-alpn-01 ships as an abstract base: extend it, implement deploy() and cleanup(), and call generateAcmeCertificate() for the RFC 8737-encoded cert and key.
ARI: renewal on the CA's schedule, not yours
RFC 9773 (ACME Renewal Information) is supported out of the box. needsRenewal() and issueOrRenew() check the CA's renewalInfo endpoint automatically. If the CA says renew now, you renew now. If the ARI request fails, it falls back to the $daysBeforeExpiry threshold silently. No configuration, no opt-in.
ACME profiles
Let's Encrypt's shortlived profile gives you 6-day certificates with no OCSP or CRL overhead. Call ->profile('shortlived') unconditionally. CoyoteCert passes it through when the provider supports it and ignores it silently when it doesn't.
Pre-flight checks that actually catch problems early
CAA records are verified before the first CA call. If a record blocks your chosen CA, you get a CaaException immediately, before burning a rate-limit slot. http-01 and dns-01 tokens are verified locally before the CA comes to check. Unlike a certain cartoon coyote, we look before we order from the ACME Corporation.
Typed exceptions with the information you actually need
RateLimitException carries the CA's Retry-After seconds directly. AuthException means bad credentials, not a transient 5xx. AcmeException::getSubproblems() tells you exactly which domain in a multi-domain order failed and why.
Storage that fits your stack
Filesystem with file locking. PDO for MySQL, PostgreSQL, and SQLite. In-memory for tests. All three share the same StorageInterface; switching backends never touches issuance code. Account keys are namespaced per CA so multiple providers never collide. PEM files are written alongside JSON automatically so you can point nginx straight at them.
RFC 8738 IP address certificates
Pass an IPv4 or IPv6 address to ->identifiers() and CoyoteCert handles the rest: type: ip on the ACME order, IP: SAN entries in the CSR. Nothing extra required. Mixed hostname + IP certificates work the same way.
ECDSA-first, RSA when you need it
EC P-256 is the default for both the certificate key and the ACME account key. EC P-384, RSA-2048, and RSA-4096 are all available. Fast TLS handshakes by default, maximum compatibility on demand.
PSR-18 HTTP client integration
The built-in curl client needs zero extra dependencies. Swap it for Guzzle, Symfony HttpClient, or any PSR-18 client with one builder call. Request and stream factories are auto-detected when the client implements them.
Modern PHP, no magic
Strict types throughout. Backed enums for key types, revocation reasons, and challenge types. Readonly constructor promotion. No magic methods, no global state, no hidden opinions about your framework.
94%+ test coverage, Pebble-verified
Unit tests with mocked responses plus a live Pebble integration suite across PHP 8.3, 8.4, and 8.5. The integration suite runs the full order lifecycle against a real ACME server on every CI run.
First-party Laravel integration
blendbyte/coyotecert-laravel adds a service provider, Artisan commands (cert:issue, cert:renew, cert:status, cert:revoke), HTTP-01 challenge served through the cache store (no web server changes, works behind load balancers), Laravel Events, queue job support for dns-01, and a daily scheduled renewal task.
Quick start
composer require blendbyte/coyotecertuse CoyoteCert\CoyoteCert;
use CoyoteCert\Challenge\Dns\CloudflareDns01Handler;
use CoyoteCert\Provider\LetsEncrypt;
use CoyoteCert\Storage\FilesystemStorage;
$cert = CoyoteCert::with(new LetsEncrypt())
->storage(new FilesystemStorage('/var/certs'))
->identifiers(['example.com', '*.example.com'])
->email('admin@example.com')
->challenge(new CloudflareDns01Handler(apiToken: 'your-token'))
->issueOrRenew();Account created, order placed, challenge deployed, certificate issued, files written to disk. Run it again tomorrow and it does nothing unless renewal is due.
Full documentation: README
Full Changelog: v0.1.4...v1.0.0