What webhooks report
AutoCertify queues a signed event when it stores the result of a new certificate request to Cloudflare. Use these notifications to record that issuance was active, pending or failed at that point. Deliveries run asynchronously, so they can arrive after your API request completes.
Provider refreshes and DNS checks do not currently enqueue lifecycle webhooks. Continue calling
POST /v1/certificates/:certificateId/refresh to detect later activation, expiry
changes or errors. A GET of the certificate reads the saved status.
Register an endpoint
Register a publicly reachable HTTPS URL and the event names you want.
Localhost, private-network destinations and non-HTTPS URLs are rejected. Replace
https://hooks.example.com/autocertify below with your real receiver URL.
Registration, updates, deletion and secret rotation require an owner or admin session.
Set AUTOCERTIFY_SESSION_TOKEN from the token field returned by your
AutoCertify sign-in exchange. That field contains the verified Auth0 ID token. An automation
API key cannot manage webhook credentials.
curl --fail-with-body -X POST https://api.autocertify.net/v1/webhooks \
-H "Authorization: Bearer $AUTOCERTIFY_SESSION_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"url": "https://hooks.example.com/autocertify",
"events": ["cert.issued", "cert.issue_pending", "cert.issue_failed"]
}' {
"id": "629ed412-af82-4a2c-b8d8-313998a18c45",
"url": "https://hooks.example.com/autocertify",
"events": ["cert.issued", "cert.issue_pending", "cert.issue_failed"],
"createdAt": "2026-02-04T18:10:00.000Z",
"status": "active",
"secret": "<one-time signing secret; redacted in this example>"
}
The actual response includes a signing secret beginning with whsec_. Save that
complete value in your receiver’s secret manager as AUTOCERTIFY_WEBHOOK_SECRET.
List and update responses omit it; a new secret is returned only at creation or rotation.
Supported events
| Event type | Meaning | Payload status |
|---|---|---|
cert.issued | The newly stored provider result has an active custom hostname and certificate. | active |
cert.issue_pending | The newly stored provider result is still awaiting activation. | pending |
cert.issue_failed | The newly stored provider result reports an error. | error |
An empty events array or an array containing * subscribes to all emitted
events. Registration accepts string event names, but only the events above are emitted by this
integration. A request failure before a provider result is stored may appear only in the API
response and domain error state; do not rely on a webhook for every possible failure.
Payload and identity
Each JSON envelope contains id, type, createdAt and
data. The payload is a flat issuance summary, not a nested domain or certificate
resource. The values below illustrate the schema.
{
"id": "b1f3aec6-132f-4700-bdb5-d8539d1c7b68",
"type": "cert.issued",
"createdAt": "2026-02-04T18:43:09.000Z",
"data": {
"domainId": "8a7260d9-4f3f-4cea-bc6f-60a1b87ddf10",
"apex": "shop.example.com",
"certificateId": "49c1a8d4-146e-48fc-ac52-4952e1c10876",
"status": "active",
"provider": "cloudflare_for_saas"
}
} id identifies a delivery for one registered endpoint and stays the same on its
retries. createdAt is generated for each delivery attempt and can change on a retry.
Deduplicate by id. Different registered endpoints receive different delivery IDs
even when they receive the same issuance result.
Signature verification
X-AutoCertify-Event: cert.issued
X-AutoCertify-Timestamp: 1770230589
X-AutoCertify-Signature: sha256=<64 hexadecimal characters>
The signature is HMAC-SHA256 over the exact timestamp string, a literal dot and the raw request
body bytes. The signing key is the complete returned webhook secret. The signature header uses
sha256=<hex>, and the timestamp is a Unix time in seconds. Use the verified
body’s type to route the event; the separate event header is a convenience.
Validate the header format and timestamp, compare decoded signatures in constant time, and parse JSON only after verification. A five-minute timestamp tolerance limits replay of a captured request; delivery-ID deduplication prevents duplicate processing within that window.
A receiver with durable receipts
This Node.js receiver uses express and pg. Create the inbox table in
your application’s PostgreSQL database as a deployment migration, then configure
DATABASE_URL, AUTOCERTIFY_WEBHOOK_SECRET and optionally
PORT. Expose the route through your HTTPS endpoint.
CREATE TABLE autocertify_webhook_inbox (
delivery_id uuid PRIMARY KEY,
event_type text NOT NULL,
envelope jsonb NOT NULL,
received_at timestamptz NOT NULL DEFAULT now()
); import crypto from "node:crypto";
import express from "express";
import pg from "pg";
const secret = process.env.AUTOCERTIFY_WEBHOOK_SECRET;
const databaseUrl = process.env.DATABASE_URL;
if (!secret || !databaseUrl) {
throw new Error("Set AUTOCERTIFY_WEBHOOK_SECRET and DATABASE_URL.");
}
const port = Number(process.env.PORT || 3000);
if (!Number.isInteger(port) || port < 1 || port > 65535) {
throw new Error("PORT must be an integer from 1 to 65535.");
}
const pool = new pg.Pool({ connectionString: databaseUrl });
pool.on("error", (error) => console.error("Webhook database error:", error.message));
const app = express();
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
function validSignature(rawBody, timestamp, header) {
if (!Buffer.isBuffer(rawBody) || !/^[0-9]{1,16}$/.test(timestamp) ||
!/^sha256=[0-9a-f]{64}$/i.test(header)) return false;
const seconds = Number(timestamp);
if (!Number.isSafeInteger(seconds) || seconds <= 0 ||
Math.abs(Date.now() / 1000 - seconds) > 300) return false;
const supplied = Buffer.from(header.slice(7), "hex");
const expected = crypto.createHmac("sha256", secret)
.update(timestamp).update(".").update(rawBody).digest();
return crypto.timingSafeEqual(supplied, expected);
}
// Mount this route before any JSON parser: the signature covers the raw bytes.
app.post("/autocertify", express.raw({ type: "application/json", limit: "1mb" }),
async (req, res) => {
const timestamp = req.get("X-AutoCertify-Timestamp") || "";
const signature = req.get("X-AutoCertify-Signature") || "";
if (!validSignature(req.body, timestamp, signature)) {
return res.status(400).send("Invalid or expired signature.");
}
let event;
try {
event = JSON.parse(req.body.toString("utf8"));
} catch {
return res.status(400).send("Invalid JSON.");
}
if (!event || typeof event.id !== "string" || !uuidPattern.test(event.id) ||
typeof event.type !== "string" || !event.type ||
typeof event.createdAt !== "string" || !Number.isFinite(Date.parse(event.createdAt)) ||
!event.data || typeof event.data !== "object" || Array.isArray(event.data)) {
return res.status(400).send("Invalid event envelope.");
}
try {
// A unique delivery ID makes retries safe across restarts and instances.
// Acknowledge only after the verified envelope is durably stored.
await pool.query(
"INSERT INTO autocertify_webhook_inbox (delivery_id, event_type, envelope) " +
"VALUES ($1, $2, $3::jsonb) ON CONFLICT (delivery_id) DO NOTHING",
[event.id, event.type, JSON.stringify(event)]
);
return res.status(204).end();
} catch (error) {
console.error("Unable to persist webhook:", error.message);
return res.status(503).send("Receipt storage unavailable; retry later.");
}
}
);
app.listen(port, () => console.log("AutoCertify webhook receiver listening on " + port)); The receiver acknowledges a delivery after storing its verified envelope, including duplicate deliveries already present in the inbox. Integrate processing of these stored receipts into your application’s background jobs. Keep business operations idempotent and track completed processing separately from receipt so a worker restart can safely resume.
Delivery timing and retries
Any 2xx response succeeds. Non-2xx responses, connection failures and
timeouts are retried with exponential backoff and jitter. The default is six total
attempts, including the first, with a 10-second request timeout.
The worker checks for due deliveries every 30 seconds by default; deployments can change the
polling interval and attempt limit.
The first retry is scheduled approximately 30 seconds after a failure, and later delays grow exponentially, capped at 30 minutes before jitter. Worker scheduling and queue volume can add delay. Handle duplicate and delayed delivery without assuming event order, and acknowledge promptly after durable storage.
Update, disable or rotate
Use PATCH /v1/webhooks/:webhookId to change url, events
or status (active or disabled). Disabling an endpoint stops
new enqueueing and processing while it is disabled; previously queued deliveries can resume if
you enable it again. Delete it with DELETE /v1/webhooks/:webhookId when you no
longer need it.
POST /v1/webhooks/:webhookId/rotate-secret immediately replaces the signing secret
and returns its new value. Update your receiver’s configuration promptly. Requests already in
flight may carry the old signature; receivers that retain the old secret briefly can verify
those against the same timestamp tolerance during the transition. Future attempts use the
current secret. See the API reference for all
credential-management requirements.