# Webhooks

A webhook makes oak.space send an HTTP `POST` to a URL of yours when something happens in a repo — to notify a chat channel, kick off an external build, or update a dashboard.

## Events

| Event | Fires when |
|---|---|
| `branch.created` | A branch is pushed for the first time. |
| `push` | An existing branch gets new commits (including a revert). |
| `merge` | A branch is squash-merged onto `main`. |

These are the same events [CI workflows](/docs/ci-workflows#on) trigger on.

## Add a webhook

Under the repo's **Settings → Webhooks** (you need write access), enter:

- the **URL** to deliver to — it must resolve to a public address;
- a **secret**, to sign deliveries (recommended);
- which **events** to send. Choosing none means all of them.

Or over the API:

```bash
curl -X POST https://oak.space/api/acme/web/webhooks \
  -H "Authorization: Bearer $OAK_API_KEY" -H "Content-Type: application/json" \
  -d '{"url": "https://ci.example.com/oak", "secret": "s3cret", "events": ["push", "branch.created"]}'
```

List them with `GET /api/{owner}/{name}/webhooks` and remove one with `DELETE /api/{owner}/{name}/webhooks/{id}`.

## Payload

Each delivery is a JSON `POST`:

```http
POST /oak HTTP/1.1
Content-Type: application/json
User-Agent: oak-webhooks/1
X-Oak-Event: push
X-Oak-Delivery-Id: 7f3c…
X-Oak-Signature-256: sha256=5d61…

{
  "event": "push",
  "owner": "acme",
  "repo": "web",
  "branch": "fix-login-redirect",
  "commit": "8fcf5aed1b2c…",
  "pusher": "zdgeier",
  "timestamp": "2026-10-06T17:04:11.512Z"
}
```

| Field | Value |
|---|---|
| `event` | `push`, `branch.created`, or `merge`. |
| `owner`, `repo` | The repository. |
| `branch` | The branch the event is about. |
| `commit` | The resulting commit — for `merge`, the new squash commit on `main`. |
| `pusher` | Who pushed or merged. |
| `timestamp` | When the event happened (RFC 3339, UTC). |

Fields that don't apply are left out rather than sent as `null`. `X-Oak-Delivery-Id`, when present, is unique per delivery — use it to ignore duplicates.

## Verify the signature

When a webhook has a secret, `X-Oak-Signature-256` is `sha256=` followed by the hex HMAC-SHA256 of the raw request body, keyed with the secret. Compute it over the bytes exactly as received, before parsing the JSON, and compare in constant time:

```python
import hmac, hashlib

def verify(secret: str, body: bytes, header: str) -> bool:
    expected = "sha256=" + hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, header or "")
```

```js
import { createHmac, timingSafeEqual } from "node:crypto";

function verify(secret, rawBody, header = "") {
  const expected = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
  return expected.length === header.length && timingSafeEqual(Buffer.from(expected), Buffer.from(header));
}
```

## Discord

Paste a Discord channel webhook URL (`https://discord.com/api/webhooks/…`) and Oak recognizes it: instead of the JSON payload, it posts a readable message to the channel. No secret is needed, and no signature is sent.

## Delivery

- Deliveries are best-effort, sent shortly after the event.
- Oak checks that the URL resolves to a public address both when you add it and again before every delivery, and skips it otherwise — so a webhook can't be pointed at internal networks.
- Respond quickly with any `2xx`; do slow work after responding.

## Using webhooks for external CI

A webhook is how you'd drive an external CI system: listen for `push` and `branch.created`, then fetch that commit with an [API key](/docs/api-keys) — `oak clone acme/web --branch <branch>`. External systems can't yet report their status into Oak's [merge gate](/docs/merging#the-ci-merge-gate); for that, use [Oak's native CI](/docs/ci).
