---
title: Webhooks
description: 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](/self-hosting/integrations#encryption-key)) and are sent by [worker-jobs](/self-hosting/services/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:

```json
{
  "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.*`: `group` with `id`, `type`, `message`, `culprit`, `level`, `status` and `firstSeenAt`, and the `environments` it happened in.
- `release.created`: `release` with `id`, `version`, `projectVersion`, `ref`, `url` and `releasedAt`.
- `release.deployed`: the same `release` and a `deploy` with `id`, `environment`, `name`, `url`, `startedAt` and `finishedAt`.
- `alert.*`: as above. `group` is set for issue rules: new, regressed and spike.
- `webhook.test`: `webhookId`. `project` is `null`.

## Signature

Requests follow [Standard Webhooks](https://www.standardwebhooks.com), 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>`:

```ts
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 `2xx` counts 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.
