Datadog
Datadog delivers to BatonDeck through a webhook integration referenced from a monitor.
- Auth:
api_key— Datadog does not sign its webhooks at all, so a shared header is the control. - Endpoint:
POST https://<your-oncall-host>/i/<integrationId> - Header:
x-batondeck-key: <your key> - One monitor notification = one incident ticket.
1. Create the integration
create_integration {
projectId, name: "prod-datadog", source: "datadog", boardId: "B-…",
auth: { mode: "api_key", apiKey: "<a long random value you generate>" },
routing: { mode: "oncall", onCallAgent: "claude-oncall" },
mapping: {
severityMap: { p1: "urgent", p2: "urgent", p3: "high", p4: "normal", p5: "low" }
}
}Set severityMap. $ALERT_PRIORITY renders Datadog's P1–P5, none of which are BatonDeck
severity words, so every ticket lands at priority normal without a map. Set the monitor's priority
too — an unset one renders empty and also falls through to normal.
2. Create the webhook
Integrations → Webhooks → New.
| Field | Value |
|---|---|
| Name | batondeck (this is what you reference from a monitor) |
| URL | https://<your-oncall-host>/i/ing_0123456789abcdef0123456789abcdef |
| Custom Headers | {"x-batondeck-key": "<your key>"} |
| Payload | the template below |
3. Payload template
{
"alert_id": "$ALERT_ID",
"event_type": "$EVENT_TYPE",
"transition": "$ALERT_TRANSITION",
"title": "$EVENT_TITLE",
"body": "$EVENT_MSG",
"severity": "$ALERT_PRIORITY",
"link": "$LINK",
"tags": "$TAGS"
}The one thing to get right: replace the default payload. Datadog pre-fills the Payload box with its own template, and that template has no
alert_id. BatonDeck refuses it with400 unrecognized_payload — missing alert_id, becausealert_idis the correlation key: without it, every notification about one alert would open a new ticket. Paste the template above over the default.
Keep "tags": "$TAGS" quoted. It renders as a comma-joined string, and BatonDeck splits it:
service:web becomes the label service=web, a bare tag becomes a label with an empty value.
Unquoting it produces invalid JSON.
Do not add "date": "$DATE". occurredAt must be an ISO 8601 string; $DATE renders epoch
milliseconds, which falls back to receipt time anyway. Leaving it out gets you the same timestamp
with one less thing to explain.
4. Reference it from a monitor
A webhook does nothing until a monitor calls it. Add @webhook-batondeck to the monitor's
notification message — matching the webhook's name.
5. What lands on the ticket
| Ticket field | Comes from |
|---|---|
| Correlation key | alert_id |
| Firing / resolved | transition — Recovered resolves; Triggered, Re-Triggered, Warn, No Data and anything else fire |
| Severity | severity, defaulting to warning |
| Title | title, else "Datadog alert" |
| Description | body |
| Labels | tags, split on commas, key:value pairs becoming labels |
| Links | link |
Annotation datadog_transition | the lowercased transition |
Annotation datadog_event_type | event_type |
| Occurred at | the time BatonDeck receives the delivery |
Whatever Datadog puts in $ALERT_ID is what correlates. If it varies per alert cycle, a monitor that
recovers and re-triggers opens a second ticket rather than reopening the first — which matches
BatonDeck's own rule that a re-fire after a close always starts a fresh ticket.
When nothing arrives
| Symptom | What it means |
|---|---|
| No rows in the delivery log | The monitor never called the webhook. Check the monitor message contains @webhook-batondeck, then use the webhook's Test button. If that test 400s on alert_id, check whether Datadog substituted the variable before assuming the config is wrong. |
api_key_mismatch | The header name or value differs. api_key mode reads only x-batondeck-key; an Authorization header is consulted only in jwt mode. |
400 unrecognized_payload | The detail names the problem — almost always the default payload still in place. |
404 from the endpoint | Wrong integration id, or the integration is disabled. Both answer identically on purpose. |
Next: On-call ingestion overview · PagerDuty
