Webhooks
HMAC-signed webhook deliveries with automatic retries.
Webhooks let you react to domain status changes without polling. We POST a JSON payload to your endpoint when a domain is created, verified, fails, or its monitor state changes.
For the endpoint-management API see Webhook endpoints. For each event's payload shape see Webhook events.
Delivery format
Every webhook delivery includes:
x-domainee-signature: sha256=<hex>
x-domainee-event: domain.verified
x-domainee-delivery-id: <uuid>
content-type: application/json
The body is the JSON envelope:
{
"id": "<event-id>",
"type": "domain.verified",
"createdAt": "2026-05-05T11:39:19.406Z",
"data": { ... }
}
Verifying signatures
The signature is HMAC-SHA256(secret, raw_request_body). You must verify
it before trusting the payload — otherwise anyone can forge events to your
endpoint.
Node.js example
import crypto from "node:crypto";
function verifyDomaineeWebhook(rawBody, headerSig, secret) {
const expected = "sha256=" + crypto
.createHmac("sha256", secret)
.update(rawBody)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(headerSig),
Buffer.from(expected),
);
}
app.post("/webhooks/domainee", express.raw({ type: "application/json" }), (req, res) => {
const sig = req.header("x-domainee-signature");
if (!sig || !verifyDomaineeWebhook(req.body, sig, process.env.DOMAINEE_WEBHOOK_SECRET)) {
return res.status(401).end();
}
const event = JSON.parse(req.body.toString());
res.status(200).end();
});
req.body must be the raw bytes, not a parsed JSON object. Frameworks
that auto-parse JSON will produce different bytes when re-serialized, breaking
verification.
Why a custom header (vs. the more common Stripe-Signature)?
Custom-named so host-routing proxies in front of your webhook receiver (Railway, Render, ALB host-based rules, etc.) don't rewrite or strip it.
Retry behavior
A delivery is considered successful when your endpoint returns a 2xx. Anything else (timeout, 4xx, 5xx) triggers retries on this schedule from the moment of the original event:
| Attempt | Time after event |
|---|---|
| 1 | immediate |
| 2 | +1 minute |
| 3 | +5 minutes |
| 4 | +30 minutes |
| 5 | +2 hours |
| 6 | +12 hours |
After attempt 6, delivery is given up and marked as failed.
Idempotency
The x-domainee-delivery-id header is unique per delivery attempt. The
payload's top-level id field is the event id — the same across retries
of the same event. Dedupe on the event id.
Filtering on your side
The x-domainee-event header tells you the type before you parse the body —
useful for routing to different handlers.
const eventType = req.header("x-domainee-event");
switch (eventType) {
case "domain.verified":
return handleVerified(req.body);
case "domain.monitor_updated":
return handleMonitor(req.body);
default:
return res.status(200).end();
}