
Every domain event,
pushed to your endpoint.
HMAC-signed, idempotency-keyed, retried with exponential backoff for seven days. Eight event types cover the full lifecycle of a customer hostname — from create through verify, renew, fail, and delete.
Free up to 50 custom domains. Webhooks included on every tier.
Trusted by teams building the future of SaaS
Why push, not poll
A polling loop catches a domain.verified state change after up to a minute. A webhook hits your endpoint within seconds. Multiply across thousands of customer hostnames and the difference is structural.
Every request carries an X-Domainee-Signature header. Compute HMAC-SHA256 over the raw body with the per-endpoint secret, compare with timingSafeEqual. Spoofing is impossible without the secret.
Exponential backoff over a week: 30s, 1m, 5m, 30m, 2h, 6h, daily. Then the event lands in the dead-letter queue for one-click replay. Nothing is lost during your endpoint's downtime.
Why we don't bill per webhook: webhooks are infrastructure, not a metered feature. They're the only sane way to keep your DB in sync with our edge state. You shouldn't pay extra to know your own customers' status.
The full event stream
Eight event types, each emitted exactly when the underlying state changes. Subscribe to everything or filter on the types you care about.
domain.createdPOST /v1/domains lands. You receive the canonical domain object.
domain.verifiedDNS resolved correctly + cert provisioned. Your customer's URL is live.
domain.failedDNS pointing at the wrong host, SSRF blocked, registrar rejected — anything that means the hostname cannot serve.
domain.monitor_updatedCert renewed, monitor status changed, DNS drift detected. The granular health stream.
domain.expiredHostname billing cycle ended, cert went past expiry without renewal. Cleanup signal.
domain.deletedHostname removed via API. Useful for syncing your tenant table.
domain_purchase.completedBuy-a-Domain registration succeeded. Domain belongs to your end-user, the customer is the legal registrant.
domain_purchase.failedStripe charge or registrar registration failed; we auto-refunded.
Three steps to push-based domain state
Once it's wired, you never touch our API for state again. Webhooks are the canonical source of truth in your DB.
01
Register the endpoint
POST /v1/webhook-endpoints with a URL. The response includes the signing secret — shown ONCE, store it immediately.
02
Verify HMAC
Compute HMAC-SHA256 of the raw body with the secret, compare against the X-Domainee-Signature header using timingSafeEqual.
03
Sync your DB
Idempotency-key on event.id, then update your tenant table. Return 200. We never call your endpoint again for that event.
Register an endpoint, verify a payload.
The whole integration is two requests on your side: one to register, one to handle. The signing secret is returned at creation time and never again.
One call. Secret returned once.
$ curl https://api.domainee.dev/v1/webhook-endpoints \
-H "Authorization: Bearer sk_live_…" \
-d '{
"url": "https://acme.com/webhooks/domainee",
"events": []
}'
{
"endpoint": {
"id": "whe_PtR3…",
"url": "https://acme.com/webhooks/domainee",
"secret": "whsec_aB12cDxyz…",
"events": []
}
}Constant-time HMAC compare
import crypto from "node:crypto";
app.post("/webhooks/domainee",
express.raw({ type: "application/json" }),
(req, res) => {
const sig = req.header("x-domainee-signature");
const expected = "sha256=" +
crypto
.createHmac("sha256", process.env.WHSEC!)
.update(req.body)
.digest("hex");
if (
!sig ||
!crypto.timingSafeEqual(
Buffer.from(sig),
Buffer.from(expected),
)
) {
return res.status(401).end();
}
const event = JSON.parse(req.body.toString());
// …handle event by type. Idempotency on event.id.
res.status(200).end();
},
);Raw bytes matter. If you parse JSON before verifying, the signature won't match (the bytes re-serialize). Use express.raw or your framework's equivalent on this single route.
The webhook plumbing you don't maintain
- A polling loop hitting GET /v1/domains every minute for thousands of tenants.
- A retry queue with exponential backoff and dead-letter handling for your end of the call.
- A signing-secret rotation flow for when an old endpoint key needs to age out.
- A replay UI for the times your endpoint was down for a deploy window.
- An audit trail of who created which webhook endpoint and when (we ship it).
- A monitor that pages you when delivery success rate dips — we send the alert.
Frequently asked
- How do you sign webhooks?
- HMAC-SHA256 over the raw request body, using the signing secret returned at endpoint-creation time. The hex digest ships in the X-Domainee-Signature header (prefixed with sha256=). Verify with timingSafeEqual.
- How do retries work?
- On any non-2xx response (or connection failure), we retry with exponential backoff: 30s, 1m, 5m, 30m, 2h, 6h, then daily for up to 7 days. After that the event is dead-lettered to your dashboard for one-click replay.
- How do I make my handler idempotent?
- Every event has a unique id (evt_…). Track which IDs you've processed in Redis or a DB table; on receipt, check the ID first and return 200 immediately if you've seen it. Retries are common enough that any handler that mutates state needs this.
- Can I filter events per endpoint?
- Yes. POST /v1/webhook-endpoints with an events array containing the types you care about. Empty array = subscribe to everything. You can have multiple endpoints subscribed to different slices of the event stream.
- What if my endpoint is down?
- We hold the event for up to 7 days of retries. After your endpoint recovers, replay from the dashboard or via the API. Nothing is lost during downtime as long as your endpoint comes back inside the retry window.
- Is there a sandbox/test environment for webhooks?
- Use any localhost tunnel (ngrok, Cloudflare Tunnel, Tailscale Funnel) as the endpoint URL. We don't validate the URL on creation; you can iterate from your dev laptop without deploying anything.
- Do you sign the timestamp to prevent replay attacks?
- Yes. The signature header includes a t= timestamp. Verify it's within 5 minutes of now() before trusting the body. The reference implementation in the docs does this for you.
- How does this compare to polling?
- Webhooks: ~3-5 reqs per customer per month, push-driven, sub-second freshness. Polling at 1-minute granularity: 60 reqs/customer/hour = 1.4M reqs/customer/month, with up-to-60s freshness lag. Webhooks win on every dimension once you're past ~50 customers.
Still have questions? Ask our team →
Stop polling. Start receiving.
Mint an API key, register your first endpoint, drop the verify-and-handle snippet into your backend.


