> ## Documentation Index
> Fetch the complete documentation index at: https://docs.slipway.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Receive signed, retried JSON events at your own endpoint: payload schema, signature verification, and delivery guarantees.

A **webhook channel** POSTs platform events to any HTTPS endpoint you control. Each channel
picks its own [event types](/reference/webhook-events), deliveries are signed, and failures
retry automatically with exponential backoff for about 33 hours before slipway gives up and
disables the endpoint.

Discord and Slack channels receive platform-native messages instead of this schema, but share
the same event selection and retry pipeline.

## Request

Every delivery is an HTTPS POST with these headers:

| Header | Value |
| - | - |
| `Content-Type` | `application/json` |
| `User-Agent` | `slipway-notifier/1` |
| `X-Slipway-Webhook-Id` | The event's unique id. **Identical across retries and redeliveries**, so use it to deduplicate. |
| `X-Slipway-Timestamp` | Unix epoch seconds at the moment this attempt was sent. |
| `X-Slipway-Webhook-Signature` | `v1=<hex>`: HMAC signature binding the id, timestamp, and body. Present only when the channel has a signing secret. See [Verifying signatures](#verifying-signatures). |
| `X-Slipway-Signature` | `sha256=<hex>`: the legacy body-only signature, kept for receivers written against the original scheme. **Deprecated**; verify `X-Slipway-Webhook-Signature` instead. |

Your endpoint must respond with a **2xx status within 15 seconds**. Anything else, including
a timeout, counts as a failed attempt and is retried.

## Body

```json theme={"system"}
{
  "type": "deployment.healthy",
  "id": "0d9f3c1a-8a4e-4a5b-9f6a-2f9a1c7e5d21",
  "timestamp": "2026-05-19T17:22:03Z",
  "data": {
    "organization": { "slug": "ws-a1b2c3d4e5f6", "name": "Acme Inc" },
    "repository": { "id": "5b1f-…-c2e9", "full_name": "acme/orders-api" },
    "project": { "id": "7d2a-…-b4f2", "name": "orders" },
    "service": "api",
    "deployment": {
      "id": "8e4c-…-a3d1",
      "url": "https://app.slipway.sh/acme/projects/7d2a00000000/services/api/logs?deployment=8e4c00000000",
      "commit_sha": "9a1f6c…",
      "branch": "main",
      "status": "healthy",
      "trigger": "push",
      "service": "api",
      "public_urls": {
        "api": "https://api.acme.dev",
        "web": "https://web.acme.dev"
      }
    }
  }
}
```

| Field | Type | Notes |
| - | - | - |
| `type` | string | The event kind; see the [event reference](/reference/webhook-events). |
| `id` | UUID | The event id. Matches `X-Slipway-Webhook-Id`. One logical event keeps one id across every retry and manual redelivery. |
| `timestamp` | RFC3339 | When the event occurred. |
| `data` | object | Event-specific detail. The blocks below appear when they apply to the event. |
| `data.organization` | object | Always present: `slug` (the workspace's permanent `ws-…` ID) and `name` (the display name — renameable, so don't key on it). |
| `data.repository` | object | Deployment-scoped events: `id` and `full_name`. |
| `data.project` | object | `id` and `name`, plus `slug`/`kind` on project-scoped events. |
| `data.service` | string | The service the event concerns, when it concerns exactly one. |
| `data.deployment` | object | Deployment-scoped events: `id`, `url`, `commit_sha`, `branch`, `status`, `trigger`, plus `service`, `pr_number`, `public_urls`, `expires_at`, and `failure_reason` when set. |
| `data.pr_number` | int | Preview events: the pull request that owns the preview. |
| `data.details` | object | Organization events (members, invites, plan, billing, domains): the event's own fields, e.g. `MemberName` and `Role` on `member.joined`. |

> [!NOTE]
>
> The four original kinds (`deployment.healthy`, `deployment.updated`, `deployment.failed`,
> and `deployment.torn_down`) additionally carry the legacy top-level keys (`event`, `ts`,
> `organization`, `repository`, `deployment`) exactly as the original payload documented them,
> so receivers written before per-event selection keep parsing without changes. Treat those
> keys as deprecated and read `data` instead.

## Verifying signatures

Set a signing secret on the channel and every delivery carries:

```
X-Slipway-Webhook-Signature: v1=<hex_digest>
```

The digest is HMAC-SHA256 over the concatenation of the event id, the timestamp, and the
**raw request body**, joined with `.`:

```
hmac_sha256(secret, "<webhook-id>.<timestamp>.<raw_body>")
```

Binding the id and timestamp into the MAC protects you against replay: reject deliveries
whose timestamp is older than your tolerance (5 minutes is typical) and deduplicate on the
webhook id. Verify in constant time before trusting anything in the payload.

```go Go theme={"system"}
func verify(r *http.Request, body []byte, secret string) bool {
    id := r.Header.Get("X-Slipway-Webhook-Id")
    ts := r.Header.Get("X-Slipway-Timestamp")
    sig := r.Header.Get("X-Slipway-Webhook-Signature")

    // Reject stale timestamps to block replays.
    sec, err := strconv.ParseInt(ts, 10, 64)
    if err != nil || time.Since(time.Unix(sec, 0)) > 5*time.Minute {
        return false
    }

    mac := hmac.New(sha256.New, []byte(secret))
    fmt.Fprintf(mac, "%s.%s.", id, ts)
    mac.Write(body)
    want := "v1=" + hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(sig), []byte(want))
}
```

```javascript Node theme={"system"}
import { createHmac, timingSafeEqual } from "node:crypto";

function verify(req, rawBody, secret) {
  const id = req.headers["x-slipway-webhook-id"];
  const ts = req.headers["x-slipway-timestamp"];
  const sig = req.headers["x-slipway-webhook-signature"];

  // Reject stale timestamps to block replays.
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;

  const digest = createHmac("sha256", secret)
    .update(`${id}.${ts}.`)
    .update(rawBody)
    .digest("hex");
  const want = Buffer.from(`v1=${digest}`);
  const got = Buffer.from(String(sig));
  return want.length === got.length && timingSafeEqual(want, got);
}
```

The legacy `X-Slipway-Signature: sha256=<hex>` header (HMAC-SHA256 over the raw body alone)
is still sent for backwards compatibility, so existing receivers keep working. New receivers
should verify the `v1` scheme.

## Retries

A failed attempt (non-2xx, timeout, or connection error) is retried on this schedule, with a
little jitter:

| Attempt | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
| - | - | - | - | - | - | - | - | - |
| Delay after previous | none | 30s | 5m | 30m | 2h | 6h | 12h | 12h |

The final attempt lands roughly 33 hours after the event. If all 8 attempts fail, the
delivery is marked **exhausted** and the channel is **automatically disabled**: nothing else
is delivered to it until someone re-enables it. When that happens, slipway notifies your
org's admins in the in-app inbox and posts a notice to your remaining channels, with a link
to the channel's settings page.

Because retries and redeliveries reuse the event id, design your receiver to be
**idempotent**: process each `X-Slipway-Webhook-Id` at most once.

## Delivery history and redelivery

Every channel's page in the console (**Settings → Notifications → the channel →
Deliveries**) lists recent deliveries with their status, attempt count, and last HTTP
response code. Click a row to inspect the exact payload and the last error, and use
**Redeliver** to queue a fresh copy of any event. It keeps the original event id, so an
idempotent receiver will recognise the duplicate. History is retained for 30 days.

## Choosing events

Each channel subscribes to any combination of event types; the full catalog with
descriptions is in the [webhook events reference](/reference/webhook-events). By default a
channel receives events from **every project** in the organization; to narrow it, open a
project's **Settings → Notifications** and toggle the channel on there, which scopes it to
just the projects you've toggled. Organization-level events (members, billing, domains)
ignore project scoping.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.