Domainee Docs

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:

AttemptTime after event
1immediate
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();
}

On this page