Webhook Events

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.

8
event types in v1
HMAC-SHA256
every payload signed
7 days
retry window
1-click
replay from dashboard

Trusted by teams building the future of SaaS

  • Common Ninja
  • Embeddable
  • Vidocu
  • Brackets Ninja

Why push, not poll

Sub-second freshnessreal-time

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.

HMAC-signed, every payloadSHA-256

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.

Retries that won't quit7-day backoff

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.created

POST /v1/domains lands. You receive the canonical domain object.

domain.verified

DNS resolved correctly + cert provisioned. Your customer's URL is live.

domain.failed

DNS pointing at the wrong host, SSRF blocked, registrar rejected — anything that means the hostname cannot serve.

domain.monitor_updated

Cert renewed, monitor status changed, DNS drift detected. The granular health stream.

domain.expired

Hostname billing cycle ended, cert went past expiry without renewal. Cleanup signal.

domain.deleted

Hostname removed via API. Useful for syncing your tenant table.

domain_purchase.completed

Buy-a-Domain registration succeeded. Domain belongs to your end-user, the customer is the legal registrant.

domain_purchase.failed

Stripe 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.

Reference code

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.

Register

One call. Secret returned once.

curl — register endpoint
$ 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": []
  }
}
Verify

Constant-time HMAC compare

server.ts — verify
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.

What you skip

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.
FAQ

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.