Home
On-call

Datadog

Datadog delivers to BatonDeck through a webhook integration referenced from a monitor.

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 P1P5, 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.

FieldValue
Namebatondeck (this is what you reference from a monitor)
URLhttps://<your-oncall-host>/i/ing_0123456789abcdef0123456789abcdef
Custom Headers{"x-batondeck-key": "<your key>"}
Payloadthe 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 with 400 unrecognized_payload — missing alert_id, because alert_id is 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 fieldComes from
Correlation keyalert_id
Firing / resolvedtransitionRecovered resolves; Triggered, Re-Triggered, Warn, No Data and anything else fire
Severityseverity, defaulting to warning
Titletitle, else "Datadog alert"
Descriptionbody
Labelstags, split on commas, key:value pairs becoming labels
Linkslink
Annotation datadog_transitionthe lowercased transition
Annotation datadog_event_typeevent_type
Occurred atthe 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

SymptomWhat it means
No rows in the delivery logThe 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_mismatchThe header name or value differs. api_key mode reads only x-batondeck-key; an Authorization header is consulted only in jwt mode.
400 unrecognized_payloadThe detail names the problem — almost always the default payload still in place.
404 from the endpointWrong integration id, or the integration is disabled. Both answer identically on purpose.

Next: On-call ingestion overview · PagerDuty