Skip to main content

Webhooks

CaptainDNS webhooks push your profile's events to the HTTP URL you configure. No more polling: every alert and every policy change reaches your endpoint within seconds, with a signed payload you can verify on the receiving side.

Overview

CaptainDNS sends a JSON POST to the configured URL whenever an event happens on your profile (monitor down, MTA-STS deployment failure, and so on). If you set a secret when creating the channel, the request is signed with HMAC-SHA256 and timestamped so you can reject replays. Each channel subscribes to one or more categories (monitoring, deployment, dns): only events belonging to the categories you checked are pushed to your endpoint.

On failure, the delivery is retried automatically up to 6 times with a backoff of 10s, 1min, 10min, 1h, 6h, 24h. The history of each delivery is available in the dashboard (Deliveries tab) and permanently failed deliveries can be replayed manually.

Setup

Webhook channels are created and managed from the CaptainDNS dashboard, under Notifications. Three fields are required:

  • URL: publicly reachable HTTPS endpoint. HTTP URLs are rejected.
  • Secret (optional but recommended): shared string between CaptainDNS and your receiver. Used to sign every request. Without a secret, no signature header is sent.
  • Categories: at least one of monitoring, deployment, dns. Filtering is enforced server-side at dispatch time.

The number of active channels per profile (webhooks and Slack integrations combined) depends on your plan:

PlanActive channels
Free1
Starter3
Pro10
Business25
Enterprise100

Above the quota, creation returns 402 webhook_quota_exceeded. To raise this limit, upgrade your plan or contact support.

CRUD operations run through dedicated REST routes (/v1/notifications/channels/*) protected by the Auth0 session: they are consumed by the dashboard and are not exposed to the public API key.

Request format

Every event produces a request in the following shape:

POST https://your-endpoint.captaindns.com/ HTTP/1.1
Content-Type: application/json
User-Agent: CaptainDNS-Webhook/2.0 (+https://www.captaindns.com/en/docs/api/webhooks)
X-CaptainDNS-Event-ID: 7a3c9b1e-2f4d-4a6b-8c7d-9e1f2a3b4c5d
X-CaptainDNS-Delivery-ID: b8c1f4e2-5a6b-4c7d-8e9f-0a1b2c3d4e5f
X-CaptainDNS-Attempt: 1/6
X-CaptainDNS-Event-Type: MONITOR_DOWN
X-CaptainDNS-Signature: sha256=9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
X-CaptainDNS-Timestamp: 1776550245

{"schema_version":"2","event_type":"MONITOR_DOWN","category":"monitoring", ...}

Details:

  • Method: POST on the exact configured URL.
  • Content-Type: always application/json.
  • User-Agent: CaptainDNS-Webhook/2.0 (+https://www.captaindns.com/en/docs/api/webhooks). Useful to filter traffic in your logs.
  • X-CaptainDNS-Event-ID: stable identifier of the business event. Preserved across all attempts and manual replays. Recommended key to deduplicate on the receiver side.
  • X-CaptainDNS-Delivery-ID: unique identifier for the attempt. Changes on every retry and every replay.
  • X-CaptainDNS-Attempt: counter in the form n/6 (maximum number of attempts).
  • X-CaptainDNS-Event-Type: copy of the event_type field from the body, handy for routing without parsing the JSON.
  • X-CaptainDNS-Signature: present when a secret is configured. Format sha256=<hex>.
  • X-CaptainDNS-Timestamp: present when a secret is configured. Unix timestamp in seconds, used in the signature computation and to guard against replays.
  • Dispatch timeout: CaptainDNS waits at most 10 seconds for a response from your endpoint. Beyond that, the attempt is considered a failure.
  • Retries: 6 attempts in total (1 initial plus 5 retries), backoff 10s, 1min, 10min, 1h, 6h, 24h on statuses 5xx, 408, 429 and network errors (timeout, DNS, TLS). Other 4xx responses (400, 401, 403, 404, 422, and so on) move immediately to failed_permanent without retry. After 20 consecutive failed_permanent deliveries, the channel is auto-disabled and an email is sent to the owner.

Payload format

The JSON body follows the V2 schema (schema_version: "2"):

{
  "schema_version": "2",
  "event_id": "7a3c9b1e-2f4d-4a6b-8c7d-9e1f2a3b4c5d",
  "delivery_id": "b8c1f4e2-5a6b-4c7d-8e9f-0a1b2c3d4e5f",
  "attempt": 1,
  "event_type": "MONITOR_DOWN",
  "category": "monitoring",
  "timestamp": "2026-04-14T10:30:45Z",
  "subject": "[CaptainDNS] Monitor DOWN",
  "status": "sent",
  "data": {
    "monitor_id": "mon_3f8a1c",
    "target": "captaindns.com",
    "failure_reason": "connection timeout"
  }
}
  • schema_version: payload schema version. Always "2" for current deliveries.
  • event_id: stable UUID across all attempts and replays of the same event. Recommended deduplication key.
  • delivery_id: unique UUID per attempt. Useful to correlate with the Deliveries dashboard.
  • attempt: attempt number (1 to 6).
  • event_type: stable event identifier, in SCREAMING_SNAKE_CASE. See the list below.
  • category: webhook category among monitoring, deployment, dns.
  • timestamp: emission date in RFC 3339 UTC format.
  • subject: human-readable label, reused as the email subject for the equivalent mail channel.
  • status: dispatch state on the CaptainDNS side, typically "sent".
  • data: free-form object whose shape depends on the event_type. May be omitted or empty depending on the event.

Treat the payload as tolerant: new fields may appear without a version bump. Your parser must ignore them silently.

Signature verification

When a secret is configured, CaptainDNS signs the request with HMAC-SHA256 so the receiver can verify the origin and integrity of the payload.

Algorithm:

  1. Read the timestamp from the X-CaptainDNS-Timestamp header and the raw request body (before any JSON parsing).
  2. Concatenate <timestamp>.<raw_body>.
  3. Compute the HMAC-SHA256 with your secret.
  4. Compare the hex result against the X-CaptainDNS-Signature header (after stripping the sha256= prefix), in constant time.

Reject any request whose timestamp drifts more than 5 minutes from the current time, to limit replay attacks.

Node.js

import crypto from "node:crypto";

function verifyWebhook(rawBody, signatureHeader, timestamp, secret) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}.`)
    .update(rawBody)
    .digest("hex");
  const received = signatureHeader.replace(/^sha256=/, "");
  return crypto.timingSafeEqual(
    Buffer.from(expected, "hex"),
    Buffer.from(received, "hex"),
  );
}

Important: rawBody must be the raw buffer as received, before JSON.parse. With Express, use express.raw({ type: "application/json" }) on the webhook route. With Next.js, read the stream via await request.text().

Python

import hmac, hashlib

def verify_webhook(raw_body: bytes, signature_header: str, timestamp: str, secret: str) -> bool:
    mac = hmac.new(secret.encode(), digestmod=hashlib.sha256)
    mac.update(f"{timestamp}.".encode())
    mac.update(raw_body)
    expected = mac.hexdigest()
    received = signature_header.removeprefix("sha256=")
    return hmac.compare_digest(expected, received)

With FastAPI, get the raw body via await request.body(). With Flask, use request.get_data() without accessing request.json first.

Event list

The 22 events currently emitted, grouped by webhook category (monitoring, deployment, dns). Each event belongs to a single category, used for server-side filtering based on the categories checked on the channel.

monitoring category (7)

Uptime, TLS health, broken redirects, cross-cutting alerts.

event_typeDescription
MONITOR_DOWNA monitor flips to DOWN after several consecutive failures.
MONITOR_RECOVERYA monitor returns to UP after a DOWN.
MONITOR_DISABLE_WARNINGA permanently failing monitor is nearing automatic disablement.
MONITOR_AUTO_DISABLEDA monitor was automatically disabled after too many prolonged failures.
TLS_EXPIRY_WARNINGAn observed TLS certificate is nearing its expiration date.
REDIRECT_DOWNA hosted redirect is no longer responding correctly.
GENERIC_ALERTCross-cutting alert that does not fit any dedicated category.

deployment category (13)

Activation, failures, and deactivation of hosted mail policies (MTA-STS, TLS-RPT, DMARC, BIMI), redirects, and domain verification.

event_typeDescription
MTASTS_ACTIVATEDA hosted MTA-STS policy switched to enforce mode.
MTASTS_DEPLOY_FAILUREAn MTA-STS deployment failed (DNS, certificate, or policy).
MTASTS_DEACTIVATEDA hosted MTA-STS policy was deactivated.
TLSRPT_ACTIVATEDA hosted TLS-RPT record became active.
TLSRPT_DEPLOY_FAILUREA TLS-RPT deployment failed.
TLSRPT_HIGH_FAILUREAn aggregate TLS-RPT report signals a high failure rate.
DMARC_ACTIVATEDA hosted DMARC policy moved to quarantine or reject.
DMARC_DEPLOY_FAILUREA DMARC deployment failed.
DMARC_ALIGNMENT_DROPThe DMARC alignment rate drops below a critical threshold.
BIMI_CERT_EXPIRYA hosted VMC or CMC certificate is nearing expiration.
REDIRECT_ACTIVATEDA hosted redirect became active.
REDIRECT_DEACTIVATEDA hosted redirect was deactivated.
DOMAIN_REVERIFY_FAILEDA periodic domain ownership re-verification failed.

dns category (2)

DNS diffs and latency anomalies detected by resolve watches.

event_typeDescription
RESOLVE_WATCH_DIFFA resolve watch detects a change in the DNS response.
RESOLVE_LATENCY_ANOMALYA resolve watch detects a significant latency anomaly.

Delivery dashboard

Every attempt is persisted in the webhook_deliveries table and browsable in the dashboard, under the Deliveries sub-tab of the Notifications section. Available columns: timestamp, channel, event_type, returned HTTP status, attempts counter out of 6, status (pending, retrying, sent, failed_permanent).

Deliveries in failed_permanent state (6 attempts exhausted) can be replayed manually from the table. A replay inserts a new row with the same event_id but a new delivery_id and resets the counter to 0: up to 6 fresh attempts are scheduled.

History is kept for 90 days across all plans, then purged by a daily worker. A future release will align retention with the plan's log retention window (log_retention_days).

A Send test button on each active webhook channel sends a dummy payload with event_type: "TEST" in the monitoring category. Rate-limited to 5 tests per minute per profile.

Error codes

Errors returned by the CRUD endpoints use the following codes:

StatusCodeMeaning
400invalid_categoriesThe submitted category list is empty or contains unsupported values.
402webhook_quota_exceededThe profile has reached its plan limit of active channels.
409label_conflictAnother channel already uses this label on the profile.
422channel_not_webhookThe target channel is not a webhook (cannot send a test).
422channel_disabledThe channel is disabled (cannot send a test).
429test_rate_limitedMore than 5 tests per minute per profile.

Receiver best practices

  • Respond quickly: return a 2xx status in under 5 seconds. If processing is long, acknowledge immediately and hand off to an async queue.
  • Verify the signature before trusting anything: never deserialize the payload before validating X-CaptainDNS-Signature, except to read the timestamp.
  • Tolerate unknown fields: your parser must silently ignore unknown keys, both at the root level and inside data. This ensures forward compatibility.
  • Treat event_type as an open enum: ignore types you don't know how to handle instead of failing.
  • Application-level idempotency: the same notification can arrive more than once (network retry, manual replay from the dashboard). Use event_id as your deduplication key: it stays stable across all attempts and all replays of the same event. delivery_id changes on every attempt.
  • Log event_id and delivery_id: useful to correlate with the Deliveries sub-tab of the dashboard during an incident.

Current limits and roadmap

  • Filtering at the category level, not at event_type: channels subscribe to monitoring, deployment, or dns. If you only want to process a specific subset (for example only MONITOR_DOWN), filter on the receiver side based on the event_type field.
  • Dashboard-only CRUD endpoints: the /v1/notifications/channels/* and /v1/notifications/deliveries/* routes are protected by the Auth0 session and are not accessible with a public API key (cdns_live_* / cdns_test_*). Programmatic channel management via API key is on the roadmap.
  • Fixed 90-day retention: alignment with log_retention_days (plan-based) is planned for a later iteration.
  • HMAC-SHA256 signatures only: Ed25519 support and secret rotation with a grace period are under consideration for a future version.

Updates will be announced in the public API changelog.