---
title: "Outbound webhooks"
description: "Push incident events to your own systems with signed webhook deliveries."
canonical: https://firefight.app/docs/api/webhooks/
---

# Outbound webhooks

Webhooks push events out of Firefight as they happen, so your systems react in real time instead of polling the [REST API](https://firefight.app/docs/api/overview.md). Wire them into status pages, ticketing systems, data warehouses, or anything else that should know when an incident moves.

## Creating a subscription

Go to **Settings → Webhooks** and add a webhook with a name, a destination URL, and the events it should receive. Managing webhooks requires workspace admin access. The URL must use `http` or `https`, and it must be publicly reachable. Deliveries to addresses that resolve to private networks are refused.

Each webhook gets its own signing secret, generated when you create it. Open the webhook's detail panel to reveal the secret and to browse its recent deliveries. You can also send a test delivery from settings, which replays a recent matching event from your workspace against your endpoint.

## Events

You choose the events per webhook. A webhook only receives what it subscribes to.

### Incident events

| Event | Fires when |
|---|---|
| `incident.created` | An incident is declared |
| `incident.updated` | An incident's core fields change |
| `incident.resolved` | An incident is resolved |
| `incident.reopened` | A closed incident is reopened |
| `incident.escalated` | An incident is escalated |
| `incident.marked_duplicate` | An incident is marked as a duplicate |
| `incident.merged_into` | An incident is merged into another |
| `lead.assigned` | An incident lead is assigned |

### Action events

| Event | Fires when |
|---|---|
| `action.created` | An action item is created |
| `action.picked_up` | Someone picks up an action item |
| `action.completed` | An action item is completed |

### Runbook events

| Event | Fires when |
|---|---|
| `runbook.attached` | A runbook attaches to an incident |
| `runbook.applied` | A runbook's steps are turned into action items |

### Postmortem events

| Event | Fires when |
|---|---|
| `postmortem.generated` | A postmortem is generated for an incident |

### Relationship events

| Event | Fires when |
|---|---|
| `relationship.created` | An incident is linked to a related incident |

## Delivery

Firefight sends each event as an HTTP `POST` with a JSON body and the User-Agent `Firefight-Webhooks/1.0`. Your endpoint has 7 seconds to respond. Any `2xx` response counts as success, and anything else, including timeouts and connection errors, counts as failure.

Every request carries these headers.

| Header | Contents |
|---|---|
| `X-Webhook-Version` | Payload schema version, currently `1` |
| `X-Webhook-Event` | The event type, for example `incident.resolved` |
| `X-Webhook-Delivery` | Unique ID of this delivery |
| `X-Webhook-Attempt` | Attempt counter for this delivery |
| `X-Webhook-Timestamp` | ISO 8601 time the request was signed |
| `X-Webhook-Signature` | Signature in the form `v1=<hex digest>` |

Failed deliveries are not retried automatically. Each webhook keeps a delivery history in **Settings → Webhooks** showing the state, response code, and payload of every attempt from the last 7 days, and you can replay any delivery from there. A replayed delivery reuses the exact original payload bytes, so its signature still verifies.

If a webhook fails 10 times in a row over the span of an hour or more, Firefight deactivates it so a dead endpoint does not accumulate failures forever. Fix the endpoint, then re-enable the webhook in **Settings → Webhooks**. Any successful delivery resets the failure counter.

## Payload

Every payload shares the same envelope, with event-specific detail under `data`. This is an `incident.created` delivery.

```json
{
  "id": "3c8e5f2a-9b1d-4e7c-a6f0-8d2b4c6e9a13",
  "version": "1",
  "event_type": "incident.created",
  "workspace_id": "7a1c3e5b-2d4f-4a68-9c0e-1b3d5f7a9c2e",
  "occurred_at": "2026-07-18T09:12:44Z",
  "data": {
    "incident": {
      "id": "0d9b2c1e-7f3a-4b8e-9c5d-2a6e8f1b4d70",
      "identifier": "INC-42",
      "name": "Checkout latency spike in eu-west",
      "summary": "p99 latency on the checkout service exceeded 4s.",
      "status": { "id": "b1f6a2d4-8c3e-4f5a-9b7d-1e2c3a4b5d6f", "name": "Investigating", "lifecycle_stage": "active" },
      "severity": { "id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "name": "SEV2", "rank": 2 },
      "type": null,
      "lead": null,
      "source": "datadog",
      "declared_by": { "id": "e4a8b2c6-1d3f-4e5a-b7c9-0f1a2b3c4d5e", "type": "user", "name": "Maja Kovac", "email": "maja@example.com" },
      "declared_at": "2026-07-18T09:12:44Z",
      "detected_at": null,
      "resolved_at": null,
      "created_at": "2026-07-18T09:12:44Z",
      "updated_at": "2026-07-18T09:12:44Z",
      "custom_fields": {},
      "channel_id": "C0912345678",
      "channel_name": "inc-42-checkout-latency-spike"
    },
    "actor": { "id": "e4a8b2c6-1d3f-4e5a-b7c9-0f1a2b3c4d5e", "type": "user", "name": "Maja Kovac", "email": "maja@example.com" }
  }
}
```

The `actor` is whoever caused the event. Its `type` is `user` for a person and `api_key` for an integration, in which case `email` is `null`. Action, postmortem, and relationship events carry their own detail under `data` alongside the incident.

## Verifying signatures

Every delivery is signed with the webhook's secret using HMAC-SHA256. The signed input is the scheme, the timestamp, and the raw body joined with colons.

```
v1:<X-Webhook-Timestamp>:<raw request body>
```

To verify, compute the HMAC-SHA256 hex digest of that string with your signing secret and compare it against the digest in `X-Webhook-Signature` using a constant-time comparison. Reject the delivery if it does not match.

```js
const crypto = require("crypto")

function verify(secret, timestamp, rawBody, signatureHeader) {
  const expected = "v1=" + crypto
    .createHmac("sha256", secret)
    .update(`v1:${timestamp}:${rawBody}`)
    .digest("hex")
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signatureHeader))
}
```

:::note
Check that `X-Webhook-Timestamp` is recent, for example within 5 minutes, before comparing signatures. That blocks replay of captured requests. Legitimate replays from the delivery history are re-signed with a fresh timestamp.
:::

:::tip
Use an `https` URL and treat the signing secret like a password. Each webhook has its own secret, so a leak only affects that one subscription. Delete and recreate the webhook to rotate it.
:::
