Skip to content
OpenPerf
Esc
↑↓navigate↵open⌘Jpreview
On this page

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.*: 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, 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 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.

Was this page helpful?