Outbound webhooks
Webhooks push events out of Firefight as they happen, so your systems react in real time instead of polling the REST API. Wire them into status pages, ticketing systems, data warehouses, or anything else that should know when an incident moves.
Creating a subscription
Section titled “Creating a subscription”Go to Developer → 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. Firefight shows the secret once, right after you create the webhook, so copy it into the service that receives the deliveries then. After that, the webhook’s detail panel shows only its last four characters. If the receiver loses the secret, choose Replace secret in the detail panel. Firefight asks first, then shows the new secret once, and the old one stops working at once, so deliveries fail the receiver’s check until it has the new one. Replacing the secret needs admin access. A webhook created over the API or MCP never returns its secret, so choose Replace secret on the dashboard to get one for it. Creating, changing or deleting a webhook over the API or MCP waits for an admin to approve it first.
The detail panel lists the webhook’s recent deliveries. You can also send a test delivery, which replays the newest event from your workspace that the webhook subscribes to, signed exactly like a live delivery. Choose Send Test in the webhook’s detail panel, or call the test_outbound_webhook tool over MCP with the webhook’s id. Either way the delivery appears in the webhook’s deliveries list with its response code and timing. A test is refused while nothing the webhook subscribes to has happened in your workspace yet, so declare an incident first on a brand new workspace.
Events
Section titled “Events”You choose the events per webhook. A webhook only receives what it subscribes to.
Incident events
Section titled “Incident events”| Event | Fires when |
|---|---|
incident.created | An incident is declared |
incident.updated | An incident’s fields change, such as its status, severity, type, lead, name, summary or custom fields |
incident.accepted | Someone accepts a triaged incident |
incident.resolved | An incident is resolved |
incident.reopened | A closed incident is reopened |
incident.canceled | An incident is cancelled |
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 |
role.assigned | Someone is given an incident role other than lead |
role.unassigned | An incident role other than lead is cleared |
Action events
Section titled “Action events”| Event | Fires when |
|---|---|
action.created | An action item is created |
action.picked_up | Someone picks up an action item |
action.reassigned | An action item is handed to someone else |
action.completed | An action item is completed |
Runbook events
Section titled “Runbook events”| Event | Fires when |
|---|---|
runbook.attached | A runbook attaches to an incident |
Timeline note events
Section titled “Timeline note events”| Event | Fires when |
|---|---|
milestone.noted | Firefight adds a note to an incident’s timeline |
The payload carries the note’s kind, its statement, who said it, when, the message it was read from, and the link to that message. See Timeline notes.
Postmortem events
Section titled “Postmortem events”| Event | Fires when |
|---|---|
postmortem.generated | AI writes a postmortem draft for an incident |
postmortem.started | Someone starts a blank postmortem for an incident instead of an AI draft |
postmortem.edited | A postmortem’s content is edited |
Relationship events
Section titled “Relationship events”| Event | Fires when |
|---|---|
relationship.created | An incident is linked to a related incident |
Delivery
Section titled “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. Now and then a delivery goes out a second time with the same X-Webhook-Delivery and the next X-Webhook-Attempt, so use the delivery ID to ignore a repeat. A delivery that still could not be sent shows as failed with interrupted. Each webhook keeps a delivery history in Developer → Webhooks showing the event, state, and response code of its recent deliveries, and deliveries are kept for 7 days. Replay any of them from that panel, which needs admin access.
A replay sends the same body as the first time, not the incident as it is now. The id in the body is the original one, so a receiver that already processed it can ignore the repeat.
If a webhook fails 10 times in a row over the span of an hour or more, Firefight deactivates it and posts a notice to your incidents channel naming the webhook. Fix the endpoint, then re-enable the webhook in Developer → Webhooks. Any successful delivery starts the count again.
Payload
Section titled “Payload”Every payload shares the same envelope, with event-specific detail under data. This is an incident.created delivery.
{ "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", "slug": "investigating", "lifecycle_stage": "active" }, "severity": { "id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "name": "SEV2", "slug": "sev2", "rank": 2 }, "type": null, "lead": null, "source": "datadog", "test": false, "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, agent for an agent, api_key for a service key, and system_agent for Firefight’s own, such as Halon. Only a person has an email, and it is null for the rest. The incident’s test is true for a test incident. Action, postmortem, and relationship events carry their own detail under data alongside the incident.
An incident.updated delivery also names what changed. changed_fields lists the fields, and changes gives each one’s value before and after the update. A status, severity or type is shown the same way as on the incident, and a lead as a person is.
"changed_fields": ["status", "severity"],"changes": { "status": { "before": { "id": "b1f6a2d4-8c3e-4f5a-9b7d-1e2c3a4b5d6f", "name": "Investigating", "slug": "investigating", "lifecycle_stage": "active" }, "after": { "id": "c2a7b3e5-9d4f-4a6b-8c8e-2f3d4b5c6e7a", "name": "Mitigated", "slug": "mitigated", "lifecycle_stage": "active" } }, "severity": { "before": { "id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "name": "SEV2", "slug": "sev2", "rank": 2 }, "after": { "id": "d4e5f6a7-8b9c-4d0e-9f1a-2b3c4d5e6f7a", "name": "SEV1", "slug": "sev1", "rank": 1 } }}Verifying signatures
Section titled “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.
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))}