API automation
Lifecycle webhooks
Receive signed, ordered incident lifecycle events at your own HTTPS endpoint, and build a receiver that stays correct through retries, duplicates, and skips.
WarnFire can send each incident lifecycle transition to HTTPS endpoints you configure. Every message is signed, every message for one incident is numbered without gaps, and WarnFire delivers them to one endpoint strictly in order: the next message for an incident is not sent until you have accepted the one before it.
This page is the receiver contract. The event shapes are also in the API reference .
What is sent
| Event type | Sent when |
|---|---|
incident.triggered | A new incident opens. |
incident.acknowledged | A responder acknowledges it. |
incident.severity_changed | The source re-triggers the open incident at another severity. |
incident.resolved | The source resolves it, or an operator force-resolves it (details.forced). |
incident.assistance_responded | A responder accepts, delays, or declines an assistance request. |
incident.escalation_ended_unacknowledged | Escalation ended without an acknowledgement. details carries the recorded paging evidence and paging_outcome: confirmed_accepted (at least one provider accepted a page for handoff — not proof that anyone received it), unconfirmed (a provider never reported back), or not_paged (no reachable destination). |
webhook.sequence_skipped | A workspace administrator skipped a delivery WarnFire never saw you accept. Its fields sit directly under data, not under details. See Dead deliveries, replay, and skip
. |
Each endpoint subscribes to the event types it wants. An event type an endpoint is not subscribed to is not captured for it and does not take a sequence number on that endpoint.
Bodies never contain a person’s name, a phone number, an email address, a device, or the alert’s own content. They carry identifiers and state, so a receiver looks up anything else it needs through an authenticated API call.
Message format
Requests are POST with Content-Type: application/json and the Standard
Webhooks
headers:
| Header | Meaning |
|---|---|
webhook-id | The message id. It is the same on every retry and replay of the same message and is never reused for different bytes. |
webhook-timestamp | Unix seconds when this attempt was sent. |
webhook-signature | One or more v1,<base64> entries separated by spaces. |
The signature is HMAC-SHA256 with your endpoint’s secret over the string
webhook-id, a period, webhook-timestamp, a period, and the raw request
body. The secret is whsec_ followed by base64; decode the base64 part to
get the key. Accept the message when any entry verifies against any secret
you hold, and reject a timestamp more than five minutes from your clock.
Compute the HMAC over the bytes you received, before parsing.
The body is frozen when the event is captured. Retries and replays send the
same bytes, and a later transition is never reflected in an earlier message:
an incident.acknowledged body does not know the incident was later resolved.
{
"type": "incident.acknowledged",
"timestamp": "2026-09-28T17:04:11.482910Z",
"data": {
"event_id": "evt_918273",
"message_id": "msg_5f0c…",
"sequence": 2,
"incident_sequence": 3,
"first_for_endpoint": false,
"incident": {
"id": "3d8f…",
"status": "acknowledged",
"severity": "critical",
"current_severity": "warning",
"service": { "id": "9b21…", "name": "Checkout" },
"created_at": "2026-09-28T17:01:40.117222Z",
"acked_at": "2026-09-28T17:04:11.482910Z"
},
"details": {}
}
}
sequencenumbers this endpoint’s messages for this incident: 1, 2, 3, … with no gaps. It is the field a receiver orders on.incident_sequencenumbers every captured transition of the incident across all endpoints. Two endpoints with different subscriptions see the sameincident_sequencevalues but differentsequencevalues.first_for_endpointis true on the first message this endpoint receives for the incident, which may not beincident.triggeredif the endpoint was added or subscribed later.event_ididentifies the transition itself and is the same across endpoints;message_idequals thewebhook-idheader.incident.severityis the severity the incident was routed on and does not change.incident.current_severityfollowsincident.severity_changed.detailsdepends on the type:{"from","to"}for a severity change,{"forced"}for a resolution,{"assistance_request_id","outcome"}for an assistance response, and the paging evidence counts for an escalation that ended unacknowledged.
Payloads for each subscription
Every checked incident event uses the complete envelope above. The type,
data.incident.status, and data.details values vary as follows. The examples
below are the complete data.details objects; all other fields, including
event_id, message_id, both sequence numbers, and the incident snapshot, are
present on every event. An endpoint first subscribed after an incident opened
may receive any of these as its first event, with sequence: 1 and
first_for_endpoint: true.
type | data.incident.status | data.details |
|---|---|---|
incident.triggered | triggered | {} |
incident.acknowledged | acknowledged | {} |
incident.severity_changed | Current incident status, often triggered | {"from":"warning","to":"critical"} |
incident.resolved | resolved | {"forced":false} for a source resolution; {"forced":true} for an operator force-resolution. |
incident.assistance_responded | Current incident status | {"assistance_request_id":"<request UUID>","outcome":"accepted"}; outcome is accepted, delayed, or declined. |
incident.escalation_ended_unacknowledged | Current incident status | The paging evidence object below. |
incident.severity_changed changes data.incident.current_severity to the
to value, while data.incident.severity retains the original routing
severity. A null from or to value is possible if a timeline transition
lacks that field; receivers should treat the fields as nullable strings.
An unacknowledged escalation carries these exact keys and types:
{
"terminal_status": "known_failed",
"stop_reason": "ladder_exhausted",
"accepted_destinations": 0,
"unknown_destinations": 0,
"known_failed_destinations": 1,
"no_destination_count": 0,
"skipped_destinations": 0,
"paging_outcome": "not_paged"
}
The counts are recorded attempt evidence, not a delivery or human-read
guarantee. terminal_status and stop_reason are recorded reason strings;
terminal_status may be null and stop_reason may be empty if the originating
timeline entry did not supply them. paging_outcome is
confirmed_accepted when at least one destination was accepted by a provider,
unconfirmed when none was accepted but at least one has an unknown provider
result, and not_paged otherwise.
Receiver contract
Follow these rules and your receiver stays correct through every retry, duplicate, and skip WarnFire can produce.
- Verify before anything else. Reject a bad signature or a stale timestamp with a non-2xx status.
- Answer 2xx only once the event is durably recorded. Receipt is not acceptance. Write to your database or queue first, then respond. Anything other than a 2xx — including a timeout or a dropped connection — is treated as not accepted, and WarnFire sends the message again.
- Be idempotent by
webhook-id. If you crash after recording an event but before responding, WarnFire never saw your 2xx and sends the same message again. Recognise the id, do nothing, and answer 2xx. - Acknowledge anything that does not advance the sequence. Keep the
highest
sequenceyou have applied per endpoint and incident —sequenceis numbered per endpoint, so a receiver that serves more than one endpoint must not share one counter between them. A message at or below that mark is old news: answer 2xx and take no action. Never reject a duplicate. - Treat a skip tombstone as a slot you may already have. A
webhook.sequence_skippedmessage reuses the skipped message’ssequencewith its ownwebhook-id. The original may well have reached you: a skip happens when WarnFire never saw your 2xx, which includes a response that was lost after you recorded the event. So the original and its tombstone can both legitimately receive 2xx from you. If you had applied that sequence, the tombstone is a no-op; if not, record that the slot was skipped. Either way answer 2xx, because the next sequence is held until you do. - Respond within ten seconds. WarnFire’s connection timeout is ten seconds. Do slow work after you have recorded the event, not before you respond.
These rules give you at-least-once, in-order delivery with a stable id per
message. Exactly one downstream action per transition is then your side of
the bargain: record the webhook-id and the action in the same transaction,
or make the action itself idempotent, so a crash between the two cannot
repeat it. The rules alone do not guarantee it.
Ordering and delivery
- Delivery is serialized per endpoint and incident. Message n+1 is sent only after you accept message n. Messages for different incidents, and for different endpoints, are independent and may arrive in any interleaving.
- A failed attempt is retried with backoff for about 24 hours from the first attempt, starting at 30 seconds between attempts and growing to at most four hours.
- A message that is still failing after 24 hours is dead. It stays where it is, and everything after it for that incident on that endpoint waits.
- If every attempt to an endpoint has failed for three days, WarnFire disables the endpoint and emails the workspace owners. Events that occur while an endpoint is disabled are not captured for it and are not sent later.
Dead deliveries, replay, and skip
A dead delivery blocks its incident’s stream until a workspace owner or administrator acts on it in the console, under Configuration → Webhooks.
- Replay sends the same message — same
webhook-id, same bytes — with a fresh 24-hour retry window. - Skip gives up on the message and sends a
webhook.sequence_skippedtombstone in its slot instead. The tombstone has its ownwebhook-id. Itsdataholdssequence,message_id,incident_id,reason(skipped_by_tenant), and the skipped message’sskipped_event_id,skipped_message_id, andskipped_event_type; there is noincidentsnapshot and nodetails. The next message is held until you accept the tombstone, so the stream never has a gap: every sequence number is accepted at least once, as the event, its tombstone, or — when a response was lost — both.
Re-enabling a disabled endpoint does not release its dead deliveries; replay or skip them as well.
Dead deliveries also appear in Delivery Activity , alongside failed pages, so a team watching that view sees them without opening the Webhooks page.
Endpoints and secrets
Endpoints are managed in the console under Configuration → Webhooks by workspace owners and administrators (see roles and tenant isolation ). A workspace can have up to ten endpoints.
- The URL must be
https://and resolve to a public address. Private, loopback, link-local, and cloud-metadata addresses are refused, both when the endpoint is saved and again at connection time, so a DNS answer that changes later is refused too. Redirects are not followed. - The signing secret is shown once, when the endpoint is created or its secret is rotated. Store it in your secret manager; WarnFire cannot show it again.
- Rotation installs a new secret and keeps the previous one signing for 24 hours. During that overlap each message carries two signatures, so a receiver holding either secret verifies. Deploy the new secret to your receiver within the day.
- Send test delivers a signed test message outside the incident stream, with the endpoint’s credential if it has one, so you can check verification before an incident happens. It works on a disabled endpoint too, so you can confirm a fixed receiver before you re-enable it. A disabled endpoint still receives no incident events until you enable it. Every test is kept in the endpoint’s delivery history for 30 days as a Test row, with its HTTP status and error. Filter by tests only or failed tests. A test is never retried, replayed or skipped, and never holds back incident events.
Receiver authentication
If your receiver’s front door needs a credential before it looks at a body,
give the endpoint one. It is sent as an Authorization header on every
delivery, retry, replay, and test message, in addition to the signature —
never instead of it. Verify the signature regardless.
| Mode | You enter | WarnFire sends |
|---|---|---|
| None | Nothing | No Authorization header |
| Bearer token | A token | Authorization: Bearer <token> |
| Basic auth | A username and password | Authorization: Basic <base64(username:password)> |
- A token is 1–4096 characters of
A–Z a–z 0–9 - . _ ~ + /with optional trailing=; a username is up to 256 printable characters without:; a password is up to 4096 printable characters. - The token or password is encrypted at rest with the same key as the signing secret and is never shown again — not in the console, the API, the audit log, or delivery history. Reads report only the mode and, for Basic, the username.
- To change it, enter a new token or password; leaving the field empty keeps the saved one. Switching modes requires the full new credential. Switching to None removes the saved credential.
- Deliveries use the credential the endpoint has now, so fixing a wrong token repairs retries already waiting, without a replay.
- Redirects are not followed, so a credential is only ever sent to the URL you configured.
If a secret is compromised
Two emergency controls are available on the endpoint:
- Revoke previous secret ends a rotation overlap immediately, for a leaked old secret.
- Revoke secret replaces the current secret at once, with no overlap, for a leaked current secret; a previous secret still in overlap is revoked as well. The new secret is shown once.
In both cases, remove the revoked secret from your receiver as well. WarnFire no longer signing with a key does not stop an attacker who holds it from forging messages that your receiver would still verify. Revocation is complete only when neither side trusts the key.
History and visibility
The Webhooks page keeps 30 days of delivery history per endpoint, with each attempt’s status, response code, and error, and shows an endpoint’s failure streak, dead deliveries, and disablement reason. Endpoint creation, changes, rotation, revocation, disablement, replay, and skip are recorded in the workspace audit log.