Outbound Webhooks
When DDMARC raises an alert, it can POST that alert to a URL you own. This page describes exactly what arrives, and is equally explicit about the delivery guarantees that do not exist.
Not available yet — signature verification, retries and event subscriptions
Outbound webhook delivery works today, but it is deliberately minimal. There is no request signing, no retry policy, and no per-event subscription model. If you write a receiver that verifies an HMAC signature before processing, that check will never pass, because no signature header is ever sent — your handler would silently drop every alert while appearing correct in code review. Authenticate deliveries another way: keep the URL secret, put a random token in the path, or terminate it behind mutual TLS or an allowlist.
How Delivery Works
There is no webhook resource to create. Each user has a single webhook URL on their notification preferences, and DDMARC fans an alert out to every enabled URL in the organization.
Save a URL
In the dashboard, go to Notifications → Settings → Webhook, or send PATCH /api/v1/alerts/preferences with webhook_url and webhook_enabled: true. The URL must be HTTPS.
Choose which alerts fire
The per-alert-type toggles on the same preferences object decide what raises an alert at all. For anything conditional, create an alert rule and list webhook in its notification_channels.
Receive the POST
DDMARC sends one JSON POST per alert, per enabled URL, with a 10 second timeout. Any 2xx (200, 201, 202 or 204) counts as delivered. Reply fast and do your work asynchronously.
Payload
A generic endpoint receives four top-level fields. Everything about the alert itself is nested under alert.
{
"event_type": "alert.dns_record_changed",
"timestamp": "2026-07-19T04:31:07.918204+00:00Z",
"organization_public_id": "org_7f2c91ab",
"alert": {
"alert_id": 88213,
"alert_type": "dns_record_changed",
"title": "DMARC record changed on example.com",
"message": "The published policy moved from p=quarantine to p=none.",
"severity": "warning",
"domain": "example.com",
"domain_id": 42,
"organization_id": 17
}
}| Field | Type | Description |
|---|---|---|
| event_type | string | Always alert.<alert_type>, for example alert.high_failure_rate |
| timestamp | string | When the delivery was built, ISO 8601 |
| organization_public_id | string | Your organization's opaque public id |
| alert | object | The alert body: alert_id, alert_type, title, message, severity, domain, domain_id, organization_id |
severity is one of info, warning or critical. Alerts raised directly from report ingestion may carry a few extra keys inside alert, so treat it as an open object and read only the fields you need.
Slack and Discord Get a Different Body
The payload above is what a generic endpoint receives. DDMARC inspects the URL you saved and, if it recognises a chat platform, sends that platform's native format instead of the JSON envelope.
| URL contains | Body sent |
|---|---|
| hooks.slack.com | Slack Block Kit: a header block with a severity emoji, a section block with the message, and a context block showing type and severity. No action buttons. |
| discord.com/api/webhooks | A Discord embed with a severity-coloured stripe and Type and Severity fields. |
| anything else | The generic JSON payload shown above. |
Microsoft Teams, Telegram and Jira are separate channels with their own configuration fields rather than URL sniffing. See the integrations overview.
Alert Types
event_type is always the alert type prefixed with alert. These are the types that exist. Which of them you actually receive depends on your plan, your notification preferences and your alert rules.
Authentication
- dmarc_fail
- spf_fail
- dkim_fail
- high_failure_rate
- forensic_spike
Senders and reports
- new_sender
- new_report_received
DNS
- dns_issue
- dns_record_changed
- dns_regressed
- dns_verified
Policy and rollouts
- policy_published
- rollout_started
- rollout_stage_advanced
- rollout_blocked
- rollout_completed
Managed hosting
- hosted_spf_refresh_failed
- hosted_spf_delegation_lost
- hosted_dkim_cname_lost
- hosted_dkim_key_age
- hosted_mta_sts_delegation_lost
- hosted_mta_sts_serving_broken
- hosted_bimi_delegation_lost
Other
- lookalike_domain
- rule_triggered
Send a Test Delivery
Sends a synthetic alert so you can confirm your receiver works before waiting for a real one. Two variants exist: one for an ad-hoc URL, one for the URL already saved on your preferences.
POST /api/v1/alerts/webhook/testcurl -X POST "https://api.ddmarc.com/api/v1/alerts/webhook/test" \
-H "X-API-Key: $DDMARC_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "url": "https://hooks.example.com/ddmarc" }'{
"success": true,
"status_code": 200,
"error": null
}{
"success": false,
"status_code": null,
"error": "URL resolves to a private address"
}A failed test is still 200 — branch on success. Use POST /api/v1/alerts/webhook/test-saved with no body to test the URL already stored on your preferences; that variant returns 400 if no URL is saved or webhooks are disabled.
Delivery Guarantees
What you can rely on
- HTTPS is required; plain HTTP URLs are rejected when you save them.
- Outbound requests are SSRF-hardened: the hostname is resolved and validated once, private, loopback, link-local and cloud metadata ranges are refused, IPv4-mapped and 6to4 addresses are unwrapped before the check, the connection is pinned to the validated IP with SNI preserved, and redirects are not followed.
- The request times out after 10 seconds.
- Any 2xx status is treated as success; anything else is a failure.
Design around this
- • A missed delivery is gone. Poll
GET /api/v1/alertsif you need a complete record. - • Treat the webhook as a notification channel, not as a source of truth for state.
- • Return 2xx immediately and queue the work; slow handlers will hit the 10 second timeout and be recorded as failed.
- • Because the URL is the only credential, rotate it if it ever leaks — there is nothing else to revoke.
Not Implemented
These are commonly expected of a webhooks product and are absent from DDMARC today. They are listed explicitly so you do not build against them.
Request signing
No HMAC signature, no shared secret, no X-DDMARC-Signature header. The only header sent is Content-Type: application/json. A receiver that rejects unsigned requests will drop every DDMARC delivery.
Retries and backoff
Delivery is a single POST. A timeout, a connection error or a non-2xx response is logged and abandoned; nothing is queued or replayed.
Automatic disabling
A consistently failing endpoint is never disabled, and no failure counter is exposed. Watch your own receiver.
Per-endpoint event subscriptions
There is no way to say "send me only dns_record_changed". Which alerts fire is controlled by your notification preferences and alert rules, and every alert that fires goes to your one webhook URL.
Event ids and deduplication
The payload has no event id. The alert dictionary does carry alert_id, which is stable per alert, but nothing guarantees at-most-once delivery for you to deduplicate against.
A webhooks CRUD resource
There is no /webhooks collection to create, list, update or delete endpoints. Configuration is one URL on your notification preferences.