Webhooks
Outgoing webhooks, their events, signatures and retries.
Organization owners add webhooks under Settings → API. A webhook receives a signed JSON request for its events in every project of the organization.
The endpoint must use HTTPS and resolve only to public addresses. Private, loopback, link-local and other special ranges are refused, even through DNS rebinding. Redirects are not followed.
Self-hosted, webhooks need INTEGRATIONS_ENCRYPTION_KEY (see Integrations) and are sent by worker-jobs.
Events
| Event | Sent when |
|---|---|
error.created |
A new error group appears. |
error.regressed |
A resolved error group happens again. |
release.created |
A project joins a release. |
release.deployed |
A deploy of a release is recorded for a project. |
alert.firing |
Any alert rule fires. |
alert.resolved |
An alert rule recovers. |
A muted rule or one in cooldown sends no alert events. alert.resolved comes only from rules that notify on recovery. Send test sends a webhook.test event.
Payload
Every request is a POST with this JSON envelope:
{
"id": "01JC8Z3V4Q9KX2T5M7N0P1R2S3",
"type": "alert.firing",
"createdAt": "2026-11-06T09:30:00.000Z",
"organization": { "id": "org_123", "slug": "acme" },
"project": { "id": "prj_123", "name": "Storefront", "slug": "storefront" },
"data": {
"event": { "id": "01JC8Z3V4Q9KX2T5M7N0P1R2S4", "type": "firing" },
"rule": {
"id": "01JC8Z...",
"name": "Checkout errors",
"kind": "error_count",
"comparison": "above",
"threshold": 10,
"windowMinutes": 15
},
"environments": ["production"],
"value": 42,
"group": null
}
}
data depends on the event:
error.*:groupwithid,type,message,culprit,level,statusandfirstSeenAt, and theenvironmentsit happened in.release.created:releasewithid,version,projectVersion,ref,urlandreleasedAt.release.deployed: the samereleaseand adeploywithid,environment,name,url,startedAtandfinishedAt.alert.*: as above.groupis set for issue rules: new, regressed and spike.webhook.test:webhookId.projectisnull.
Signature
Requests follow Standard Webhooks, so its libraries verify them.
| Header | Value |
|---|---|
webhook-id |
The event id. Retries of the same event keep it. |
webhook-timestamp |
Unix seconds when the request was sent. |
webhook-signature |
v1, followed by the base64 HMAC-SHA256 of the request. |
The signing secret on the webhook page starts with whsec_. The HMAC key is the base64 after that prefix. The signed content is <webhook-id>.<webhook-timestamp>.<raw body>:
import { createHmac, timingSafeEqual } from "node:crypto";
export const isValidWebhook = (secret: string, headers: Headers, body: string) => {
const signed = `${headers.get("webhook-id")}.${headers.get("webhook-timestamp")}.${body}`;
const expected = createHmac("sha256", Buffer.from(secret.replace(/^whsec_/, ""), "base64"))
.update(signed)
.digest("base64");
const received = headers.get("webhook-signature")?.replace(/^v1,/, "") ?? "";
return (
expected.length === received.length &&
timingSafeEqual(Buffer.from(expected), Buffer.from(received))
);
};
Reject timestamps older than a few minutes to stop replays. Use webhook-id to skip retries you already handled.
Retries and disabling
- Any
2xxcounts as delivered. - Another status, a 10-second timeout or a network error is retried with a growing delay: eight attempts over about an hour.
- A host that resolves to a blocked address fails at once.
- After ten deliveries in a row fail, the webhook turns off and shows the last error. Test deliveries don’t count. Turning it back on resets the count; failed deliveries are not resent.
The webhook page lists deliveries of the last 30 days with status, response code, duration, request body and the first 2 KB of the response. worker-jobs deletes older ones nightly.