Overview
Register HTTPS endpoints to receive signed events as connections progress.
Register HTTPS endpoints to receive signed events as connections progress and as monitored domains drift.
Register an endpoint
curl -X POST http://localhost:8080/v1/webhooks \
-H "Authorization: Bearer <WIDGET_JWT>" \
-H "Content-Type: application/json" \
-d '{"url":"https://api.acme.example/webhooks/cd","events":["connection.live","domain.record_missing"]}'
# → { "id": "whk_…", "url": "...", "events": [...], "secret": "whsec_…", "created_at": "..." }The secret (whsec_…) is returned once — store it to verify signatures. An
empty events array subscribes to all events; "*" also matches all.
Manage endpoints: GET /v1/webhooks lists them (secrets omitted; tenant-wide
with an sk_ key, app-scoped with a widget JWT) and
DELETE /v1/webhooks/{id} removes one.
Event catalog
| Event | Fires when |
|---|---|
connection.created | A connection is created. |
connection.applied | Records were written through a rail (data.via = automatic for OAuth, async for one-click setup, unset for API-token apply). |
connection.live | Every desired record resolved; the connection is live. |
connection.failed | A propagating connection's records never appeared within 24h. |
connection.records.updated | A managed connection was re-applied (POST /v1/connections/{id}:reapply); its desired records were re-written and the connection re-verified. |
connection.reapply.failed | A managed re-apply push failed; if the stored grant was revoked, the connection is flagged for re-consent. |
connection.disconnected | A connection was disconnected (DELETE /v1/connections/{id}); for a managed connection the template was reverted through the grant first. |
connection.records_outdated | A fleet-wide reconciliation pass found a non-managed connection whose applied records were computed against a superseded edge target; the envelope carries a connect_link to re-run setup (see Managed connections). |
domain.record_missing | Monitor saw a baseline record disappear or get repointed (drift). |
domain.record_restored | A previously missing baseline record is observed again. |
domain.purchased | A domain purchase completed — the domain is registered (and only then is the card captured). |
purchase.error | A purchase failed; the card authorization was released. |
purchase.confirmation.expired | A pending purchase's checkout session timed out unpaid — the buyer never confirmed. Release the cart. |
secure_status | A certificate's provisioning status changed (secure_status on the envelope). |
power_status | A reverse-proxy host's provisioning status changed (power_status on the envelope). |
That table is the complete vocabulary: it is exactly the set the platform can
emit, and the events enum in the OpenAPI spec matches it.
An unknown event string is still accepted at registration, but it can never be
delivered — subscribe only to the events above.
Status: the drift events (
domain.record_missing/domain.record_restored) are produced by the hourly Monitor sweep, which delivers them only whereMONITOR_ALERTS_ENABLED=1. With the flag off (the hosted default) the sweep still runs in shadow mode but no drift webhooks are sent.domain.purchased(andpurchase.error) require the Sell purchase path, which is live on the hosted service (gated bySELL_PURCHASE_ENABLED).
Payload envelope
Deliveries are a flat JSON object:
{
"id": "evt_…",
"type": "connection.live",
"domain": "app.customer.com",
"subdomain": "app",
"provider": "cloudflare",
"setup_type": "automatic",
"propagation_status": "success",
"data": {
"records_propagated": [ { "type": "CNAME", "host": "app.customer.com", "value": "edge.customdomain.ai" } ],
"records_non_propagated": []
},
"created_at": "2026-07-07T12:00:00Z"
}propagation_status is pending, success, or timeout. On propagation
events, data carries the per-record propagated / non-propagated arrays. Fields
that don't apply to an event are omitted.
Two more conditional fields ride connection events:
updated_objects— the records a write actually created or changed. Present onconnection.appliedandconnection.records.updatedonly; the events that just observe a connection never claim to have written anything.redirection_status—pending,successorfailedfor the apex↔www redirect, and present only on connections created withwww_redirect: true.
Delivery & retries
- A delivery succeeds on any
2xx. Each attempt is persisted. - Failures retry with exponential backoff (30s, 1m, 2m, doubling, capped at 6h
between attempts) for up to 12 attempts total, spanning up to about a day,
before the delivery is dead-lettered (given up on). A non-transient
4xx(other than429) is not retried at all — it's a permanent misconfiguration a retry can't fix. - After a
429, or three consecutive failed attempts, deliveries to that endpoint URL are suppressed for about 5 minutes (a cooldown) so a struggling receiver isn't hammered while it recovers. - Inspect attempts:
GET /v1/webhook-deliveries(attempts,status_code,delivered,error; filter with?app_id=and?type=).
Endpoints should be idempotent — the same event can arrive more than once on
retry. Deduplicate on the envelope id, or on domain + type.
Next: verify signatures.