---
title: "Webhooks"
description: "Send release, patch, channel and audit log events to your own HTTPS endpoint as signed OCSF events"
---

An outbound webhook sends events in your organization to an HTTPS endpoint you run, as they happen: a release was created, a patch was promoted or rolled back, a member’s role changed. Use one to post to a chat channel, update a changelog, feed an analytics pipeline, or stream the [audit log](/enterprise/audit-logs/) into your SIEM without polling.

Each delivery is an HTTPS `POST` of one [OCSF 1.9.0 API Activity](https://schema.ocsf.io/1.9.0/classes/api_activity) event, signed with a secret only you and Shorebird hold.

## Plans and roles

Webhooks are available on the Business and Enterprise plans. Which events a webhook can send depends on the plan:

* **Business**: the [release, patch and channel events](#release-patch-and-channel-events).
* **Enterprise**: every event in the organization’s [audit log](/enterprise/audit-logs/#events), except those listed in [Events not sent](#events-not-sent).

An enterprise can also have webhooks of its own, which send the events in the [enterprise audit log](/enterprise/audit-logs/#export-your-enterprise-audit-log): changes to the enterprise, and membership and access changes in its organizations. See [Enterprise webhooks](#enterprise-webhooks).

An organization’s Admins and Owners manage its webhooks. See [Member roles](/account/orgs/#member-roles).

## Add a webhook

1. In the [Shorebird console](https://console.shorebird.dev), open your organization’s **Settings** and select the **Webhooks** tab.
2. Click **Add webhook**.
3. Enter the **Endpoint URL**. It must be an `https://` URL on port 443 with a public hostname: not an IP address, `localhost`, or a `.local` name.
4. Optionally, add a **Description**, such as where the events go.
5. Under **Events**, choose **All available events** or **Selected events**, and pick the events to send. Events Shorebird adds later aren’t included, even with **All available events**. Edit the webhook to subscribe to them.
6. Click **Add webhook**, then copy the **Signing secret**. It’s shown only once. You need it to [verify deliveries](#verify-a-delivery).

An organization can have up to 5 webhooks.

Each webhook in the list shows whether it’s **Enabled**, when Shorebird last tried to deliver to it, the HTTP status your endpoint returned, and how many deliveries in a row have failed.

* **Send test ping** sends a [test event](#test-ping) to the endpoint. It’s available while the webhook is on.
* The switch turns the webhook off and on. While it’s off, events aren’t sent to it, and they aren’t sent later when you turn it back on.

The ⋮ menu holds the rest:

* **Edit webhook** changes the URL, description, or events.
* **Rotate secret** replaces the signing secret. See [Rotate the secret](#rotate-the-secret).
* **Delete webhook** stops deliveries to the endpoint at once.

### Enterprise webhooks

Enterprise Owners and Admins add enterprise webhooks from **Enterprise** in the sidebar: open its **Security** tab and use the **Outbound webhooks** card, the same way as an organization’s. An enterprise can also have up to 5 webhooks.

An enterprise webhook sends the events in the enterprise’s own audit log: changes to the enterprise, such as its domains, roles and policies; membership and access changes in every organization in the enterprise, such as members joining, role changes, and service account keys; apps being created, renamed, moved or deleted; and deployment freeze windows and overrides. It doesn’t send releases, patches or channels. For those, add a webhook to the organization.

## Events

Each delivery is one event, in the same format as the [audit log](/enterprise/audit-logs/#event-format). `api.operation` names the event, and `metadata.uid` is its unique ID.

### Release, patch and channel events

These are the events a webhook can send on the Business plan. On Enterprise they’re among the rest of the audit log’s events, along with others such as `patch.rollout`, a patch’s rollout percentage being raised.

* `release.create`: a release was created.
* `release.update`: a release was updated, such as its status or notes.
* `release.delete`: a release was deleted.
* `release_platform.production_set`: a release’s platform was marked as in production or not, or set back to automatic.
* `patch.create`: a patch was created.
* `patch.notes_update`: a patch’s notes were changed.
* `patch.promote`: a patch was promoted to the stable channel.
* `patch.channel_update`: a patch was moved to a channel other than stable.
* `patch.rollback`: a patch was rolled back.
* `patch.rollforward`: a rolled-back patch was rolled forward.
* `channel.create`: a channel was created.
* `channel.delete`: a channel was deleted.

### Events not sent

Some changes happen on a schedule rather than when someone acts. They’re recorded in the audit log, but aren’t sent to webhooks:

* A patch approved to ship at a later time: its `patch.promote`, `patch.channel_update` or `patch.rollforward` when that time comes, or its `patch_approval.cancel` if it can no longer ship then.
* `patch.rollout_update`, a rollout percentage raised by its schedule.

### Test ping

**Send test ping** sends one event with `api.operation` `webhook.test` and `activity_id` 99, signed like any other delivery. Use it to check that your endpoint is reachable and verifies signatures.

```
{
  "class_uid": 6003,
  "category_uid": 6,
  "activity_id": 99,
  "type_uid": 600399,
  "severity_id": 1,
  "status_id": 1,
  "time": 1792180800000,
  "metadata": {
    "version": "1.9.0",
    "uid": "3f2b6c1e-8d4a-4f6b-9a2e-7c5d1b0e4a93",
    "tenant_uid": "1234",
    "product": { "name": "Shorebird", "vendor_name": "Shorebird" }
  },
  "api": { "operation": "webhook.test" },
  "actor": {
    "user": {
      "name": "Shorebird",
      "type_id": 3,
      "type": "System",
      "org": { "uid": "1234" }
    }
  },
  "src_endpoint": { "name": "unknown" },
  "unmapped": {}
}
```

A ping to an enterprise webhook has no `tenant_uid` or `org`, and has the enterprise’s ID in `unmapped.enterprise_id`.

## Receive a delivery

Each delivery is a `POST` with a JSON body and these headers, following the [Standard Webhooks](https://www.standardwebhooks.com/) specification:

* `webhook-id`: the delivery’s ID. It’s the same on every retry of the same event, so use it to drop duplicates. For an audit log event it’s `msg_audit_` followed by the event’s `metadata.uid`, and for a test ping `msg_ping_` followed by its `metadata.uid`.
* `webhook-timestamp`: when this attempt was sent, in seconds since the Unix epoch.
* `webhook-signature`: `v1,` followed by the base64 signature.
* `User-Agent`: `Shorebird-Webhook/1.0`.

Respond with any `2xx` status within 5 seconds. Shorebird doesn’t follow redirects, so a `3xx` response counts as a failure. Do slow work after you respond.

An event can arrive more than once, and events can arrive out of order. Order them by `time`.

### Verify a delivery

Check the signature on every delivery before you trust it. The signature is an HMAC-SHA256 of `{webhook-id}.{webhook-timestamp}.{body}`, keyed with the signing secret’s bytes: the part after `whsec_`, base64-decoded. Sign the body exactly as it arrived, before parsing it as JSON.

Any [Standard Webhooks library](https://github.com/standard-webhooks/standard-webhooks) does this for you. Without one, in Node.js:

```
import crypto from 'node:crypto';

// headers: the request's headers, with lowercase names.
// body: the raw request body, as a string.
function isFromShorebird(secret, headers, body) {
  const id = headers['webhook-id'];
  const timestamp = headers['webhook-timestamp'];
  const signatures = headers['webhook-signature'];
  if (!id || !timestamp || !signatures) return false;

  // Refuse old or future deliveries, so a captured request can't be replayed.
  const age = Math.abs(Date.now() / 1000 - Number(timestamp));
  if (!(age <= 5 * 60)) return false;

  const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');
  const expected = crypto
    .createHmac('sha256', key)
    .update(`${id}.${timestamp}.${body}`)
    .digest();

  return signatures.split(' ').some((signature) => {
    const [version, value] = signature.split(',');
    if (version !== 'v1' || !value) return false;
    const actual = Buffer.from(value, 'base64');
    return (
      actual.length === expected.length &&
      crypto.timingSafeEqual(actual, expected)
    );
  });
}
```

Respond `401` to a delivery that fails the check. Shorebird doesn’t retry a `401`, so that event isn’t delivered again, and it counts toward [turning the webhook off](#failed-deliveries).

### Rotate the secret

**Rotate secret** creates a new signing secret and shows it once. Deliveries are signed with the new secret from then on, and the old one stops working at once. You only see the new secret after rotating, so deliveries sent before your endpoint has it fail the check. To keep those events, have your endpoint respond `503` to a delivery it can’t verify while you switch: Shorebird retries it for a few minutes, and each retry is signed with the new secret. Once the endpoint has the new secret, go back to responding `401`.

## Failed deliveries

A delivery fails when your endpoint doesn’t respond with a `2xx` status within 5 seconds, or can’t be reached.

Shorebird retries a delivery that timed out, couldn’t connect, or got a `408`, `429` or `5xx` response, waiting longer between attempts, starting at 10 seconds, for up to 6 attempts over a few minutes. Any other response, such as a `400` or a `3xx`, isn’t retried, and neither is a hostname that resolves to a private or reserved IP address. A test ping is sent once and isn’t retried.

The webhook’s list entry shows the last HTTP status or error, and how many deliveries in a row have failed. A delivery that is retried counts once, when its last attempt fails. Test pings count too: a failed one adds to the count.

After 25 failed deliveries in a row, Shorebird turns the webhook off, and the console shows **Webhook automatically disabled**. Events aren’t sent while it’s off. Fix the endpoint, then click **Re-enable**, which starts the count again from zero. While a webhook is on, a successful delivery, including a test ping, also resets the count.

## If your plan changes

Webhooks stay listed if your organization moves to a plan without them, and you can still turn them off or delete them. Shorebird stops sending events to them.

A webhook keeps the events it was saved with. After an upgrade from Business to Enterprise, edit a webhook to add the audit log events it should send.

A Business organization’s webhooks send only [release, patch and channel events](#release-patch-and-channel-events). If one of its webhooks also subscribes to other audit log events, such as after a move from Enterprise, the console shows **Some events aren’t on your plan** and Shorebird stops sending to it. Edit it to remove those events.

## Audit log events

On Enterprise, the [audit log](/enterprise/audit-logs/#webhooks) records each change to a webhook: `webhook_endpoint.create`, `webhook_endpoint.update` (including turning it off or on), `webhook_endpoint.delete` and `webhook_endpoint.secret_rotate`. Shorebird turning a webhook off after failed deliveries isn’t recorded.
