Check a status webhook
A status webhook is a POST request with a JSON body, sent when an incident starts, changes or ends. Every request is signed with the secret shown when you added the webhook. Check the signature before you trust the body.
What a request carries
When you add a webhook, one test request goes out first. It is the only body with no incident in it.
{ "v": 1, "event": "ping" }After that, each incident event sends one body. The event is incident.started, incident.updated or incident.resolved.
{
"v": 1,
"event": "incident.updated",
"incident": {
"id": "5b0c1f0e-3f6e-4f1e-9d2b-0a6c5d1e2f34",
"title": "Runs are slow",
"components": ["runs"],
"impact": "degraded",
"state": "identified",
"started_at": "2026-10-01T11:40:00.000Z",
"resolved_at": null,
"url": "https://example.com/status"
},
"update": {
"state": "identified",
"body": "A worker pool is full. We are adding capacity.",
"created_at": "2026-10-01T11:55:00.000Z"
},
"unsubscribe_url": "https://example.com/status/unsubscribe?token=..."
}url and unsubscribe_url are addresses on this site, shown here with a placeholder host. The webhook gets the events for the parts of Forge you chose, and either only the start and end of each incident or every update.
The signature
Each request has a header named X-Forge-Status-Signature.
X-Forge-Status-Signature: t=1700000000,v1=3b1f...t is the time the request was signed, in seconds since 1970. v1 is the HMAC-SHA256 of the text t, a dot, then the body exactly as received, under your signing secret, written in lowercase hex.
Check it
This computes the value to compare with v1.
BODY='{"v":1,"event":"ping"}'
T=1700000000
SECRET='your signing secret'
printf '%s.%s' "$T" "$BODY" | openssl dgst -sha256 -hmac "$SECRET"Compare the two values with a constant-time comparison. Reject a request whose timestamp is more than five minutes old, so a captured request cannot be sent again later.
Answering
Answer with any 2xx status within five seconds. A redirect counts as a failure and is not followed. After ten failures in a row the webhook stops, and your list on the status page shows it as stopped. Remove it there, or with the removal link shown when you added it.