Webhooks
Webhooks are how Dominder tells your system that something changed. Every transition between "healthy" and "failing" for a domain fires a POST to your configured webhook URL, signed with your API key.
Setting your webhook URL
From youraccount page, setWebhook URL to a publicly reachable HTTPS endpoint on your service. A domain with no webhook URL configured simply won't fire any events.
Events
| Event | Fired when |
|---|---|
domain.failing | A check transitions a domain'slastStatus tofailing from any other state (ok,warning, orunknown). |
domain.recovered | A check takes the domain out offailing back took orwarning. |
domain.failing event at the start and onedomain.recovered event when it clears. This is deliberately quiet - add your own
"still failing" logic on top if you need it.Request headers
POST https://your-app.example.com/dominder-webhook
Content-Type: application/json
User-Agent: Dominder-Webhook/1.0
X-Dominder-Event: domain.failing
X-Dominder-Signature: sha256=ab12cd34...
X-Dominder-Event- the event name, same aseventin the body.X-Dominder-Signature- HMAC-SHA256 of the raw request body, keyed with your API key, hex-encoded, prefixed withsha256=. SeeVerifying the signature.
Payload shape
{
"event": "domain.failing",
"domain": {
"id": "66a1c0ffeec0ffee12345678",
"hostname": "example.com"
},
"status": "failing",
"previousStatus": "ok",
"consecutiveFailures": 1,
"checkedAt": "2026-04-22T12:39:00.000Z",
"results": [
{ "kind": "status", "ok": true, "message": "HTTPS 200" },
{ "kind": "ssl", "ok": false, "message": "Certificate expired on 2026-04-21T23:59:59.000Z" },
{ "kind": "record", "ok": true, "message": "A example.com -> 93.184.216.34 matches (93.184.216.34)" }
]
}event- one ofdomain.failing,domain.recovered.domain.hostnameanddomain.ididentify which domain changed.status- the new aggregate status.previousStatus- what it was before this check.consecutiveFailures- number of failing runs in a row (always0in adomain.recoveredpayload).results- one entry per enabled check. SeeMonitoring checks for the possiblekindvalues and the shape of each message.
Verifying the signature
We sign the raw request body with an HMAC-SHA256 keyed by your API key. You shouldalways verify the signature before acting on a webhook - anyone can discover your webhook URL.
Node.js (Express)
const express = require('express');
const crypto = require('crypto');
const app = express();
app.post(
'/dominder-webhook',
express.raw({ type: 'application/json' }), // raw Buffer, not parsed
(req, res) => {
const header = req.get('x-dominder-signature') || '';
const signature = header.replace(/^sha256=/, '');
const expected = crypto
.createHmac('sha256', process.env.DOMINDER_API_KEY)
.update(req.body)
.digest('hex');
const ok =
signature.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(signature, 'hex'), Buffer.from(expected, 'hex'));
if (!ok) return res.status(401).send('bad signature');
const payload = JSON.parse(req.body.toString('utf8'));
// ... handle payload ...
res.status(200).send('ok');
}
);
Python (Flask)
import hmac, hashlib, os
from flask import Flask, request, abort
app = Flask(__name__)
@app.post("/dominder-webhook")
def hook():
header = request.headers.get("X-Dominder-Signature", "")
signature = header.removeprefix("sha256=")
expected = hmac.new(
os.environ["DOMINDER_API_KEY"].encode("utf-8"),
request.get_data(),
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(signature, expected):
abort(401)
payload = request.get_json()
# ... handle payload ...
return "ok"
PHP (plain)
$raw = file_get_contents('php://input');
$header = $_SERVER['HTTP_X_DOMINDER_SIGNATURE'] ?? '';
$signature = preg_replace('/^sha256=/', '', $header);
$expected = hash_hmac('sha256', $raw, getenv('DOMINDER_API_KEY'));
if (!hash_equals($expected, $signature)) {
http_response_code(401);
exit('bad signature');
}
$payload = json_decode($raw, true);
// ... handle payload ...
Best practices
- Return 2xx quickly. Do your real work off the request thread (queue, background job). We treat anything outside 200-299 as a delivery failure.
- Be idempotent. Use
domain.id+checkedAtas an idempotency key in case a request is delivered twice. - Validate the signature before parsing. Don't trust the body until you've verified
X-Dominder-Signature. - Rotate the API key if a secret leaks. Since it's also the HMAC key, any leaked key lets an attacker forge webhooks to your endpoint.
Retries and delivery guarantees
Dominder attempts webhook delivery once per transition with a 10-second timeout. If delivery fails (non-2xx response, timeout, network error), we log it server-side but do not automatically retry. The next transition will trigger a new webhook.
If retries matter to you, run a cron that pollsGET /api/v1/domainsevery minute and reconciles against your own store - thelastStatus,lastCheckedAt, andlastFailureAtfields give you everything a webhook would.
Testing locally
Usewebhook.site orngrok to get a public URL that forwards to your laptop:
ngrok http 3000
# -> https://abcd-1234.ngrok-free.app
# set your webhook URL on /account to that tunnel + your path
# e.g. https://abcd-1234.ngrok-free.app/dominder-webhookThen force a failure - for example, add a domain with an SSL check
targetingexpired.badssl.com. Within one check cycle you should see the webhook arrive.