Webhook payloads

The JSON body of alert webhooks, the event types, and how to verify the signature

This page as Markdown

A webhook channel receives a POST with a JSON body for each incident event. Bodies are versioned: fields may be added within a version, never renamed or removed.

Headers

Header Value
content-type application/json
webhook-id The notification’s ID. The same on every retry, so you can drop duplicates.
webhook-timestamp When this attempt was sent, in seconds since the epoch.
webhook-signature v1,. During a secret rotation, two signatures separated by a space.
apitraffic-trace-id Quote this to support to trace a delivery end to end.

Event types

type Sent when
incident.triggered An incident opens.
incident.escalated It gets more severe, or a later escalation step runs.
incident.acknowledged Someone acknowledges it.
incident.resolved It resolves.
incident.reopened It fires again after resolving.
incident.flapping / incident.stabilized An alert keeps changing state / settles down.
incident.comment_added Someone comments.
incident.reminder A critical incident is still unacknowledged (reminders).
incident.test Send test was clicked.
alert.digest A digest is sent.

seq increases with every event of an incident. If events arrive out of order, keep the one with the highest seq.

Incident event body

json
{
  "type": "incident.triggered",
  "version": 1,
  "id": "ntf_2xY…",
  "seq": 1,
  "occurredAt": "2026-10-20T10:00:00.000Z",
  "traceId": "8f0c…",
  "test": false,
  "account": { "sid": "acc_…" },
  "incident": {
    "sid": "inc_…",
    "number": 142,
    "reference": "INC-142",
    "title": "POST /v1/checkout is failing 18.4% of the time",
    "severity": "critical",
    "status": "open",
    "url": "https://app.apitraffic.io/account/alerts/incidents/inc_…",
    "bucketSid": "bkt_…",
    "environmentSid": "env_…",
    "openedAt": "2026-10-20T10:00:00.000Z",
    "onsetAt": "2026-10-20T09:55:00.000Z",
    "acknowledgedAt": null,
    "acknowledgedBy": null,
    "resolvedAt": null,
    "sources": ["attention"],
    "labels": { "signal_type": "error_hotspot", "endpoint": "POST /v1/checkout" }
  },
  "alerts": [
    {
      "fingerprint": "as_…",
      "source": "attention",
      "severity": "critical",
      "status": "firing",
      "title": "POST /v1/checkout is failing 18.4% of the time",
      "subject": { "type": "endpoint", "key": "POST /v1/checkout", "label": "POST /v1/checkout" },
      "metrics": [{ "name": "server_error_rate", "value": 18.4, "unit": "percent" }],
      "firstFiredAt": "2026-10-20T10:00:00.000Z",
      "resolvedAt": null
    }
  ],
  "alertCount": 1,
  "deploy": { "sid": "evt_…", "name": "v2.14.0", "commitSha": "a1b2c3d", "author": "sam" },
  "comment": null
}
  • deploy is set when the incident started after a deploy marker; otherwise null.
  • comment is the comment text on incident.comment_added; otherwise null.
  • Tag values (such as customer IDs) in labels show as [hidden] unless the channel has Include tag values on.

Digest body

json
{
  "type": "alert.digest",
  "version": 1,
  "id": "ntf_…",
  "occurredAt": "2026-10-20T09:00:00.000Z",
  "traceId": "…",
  "test": false,
  "account": { "sid": "acc_…" },
  "digest": {
    "route": { "sid": "rt_…", "name": "Everything else" },
    "from": "2026-10-20T08:01:00.000Z",
    "to": "2026-10-20T08:52:00.000Z",
    "incidentCount": 2,
    "eventCount": 3,
    "open": 1
  },
  "incidents": [
    {
      "sid": "inc_…", "number": 141, "reference": "INC-141",
      "title": "GET /v1/orders/{id} is 140% slower", "severity": "warning", "status": "open",
      "url": "https://app.apitraffic.io/account/alerts/incidents/inc_…",
      "openedAt": "…", "resolvedAt": null,
      "events": [{ "type": "opened", "verb": "Triggered", "seq": 1, "at": "…" }]
    }
  ],
  "moreIncidents": 0
}

Each incident appears once, worst first, with its status when the digest was sent. At most 50 are listed; moreIncidents counts the rest. Digests never include tag values.

Verifying the signature

Signatures follow Standard Webhooks. The signed content is <webhook-id>.<webhook-timestamp>., signed with HMAC-SHA256. The key is your signing secret with the whsec_ prefix removed, base64-decoded.

Node.js

js
const crypto = require('crypto');

function verify(secret, headers, rawBody) {
  const id = headers['webhook-id'];
  const timestamp = headers['webhook-timestamp'];
  // Reject old deliveries (replays): 5 minutes of tolerance.
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
  const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');
  const expected = crypto.createHmac('sha256', key).update(`${id}.${timestamp}.${rawBody}`).digest('base64');
  return headers['webhook-signature'].split(' ').some((sig) => {
    const [, value] = sig.split(',');
    return value && value.length === expected.length
      && crypto.timingSafeEqual(Buffer.from(value), Buffer.from(expected));
  });
}

Python

python
import base64, hashlib, hmac, time

def verify(secret, headers, raw_body: bytes) -> bool:
    msg_id = headers["webhook-id"]
    timestamp = headers["webhook-timestamp"]
    if abs(time.time() - int(timestamp)) > 300:
        return False
    key = base64.b64decode(secret.removeprefix("whsec_"))
    signed = f"{msg_id}.{timestamp}.".encode() + raw_body
    expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()
    return any(
        hmac.compare_digest(sig.split(",", 1)[1], expected)
        for sig in headers["webhook-signature"].split(" ") if "," in sig
    )

Verify against the raw request body, before parsing it as JSON. The Standard Webhooks libraries for most languages work as well.

Responding

Answer with any 2xx status within 10 seconds. Other answers are retried as described in Retries; a 410 Gone disables the channel.