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 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:
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:
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:
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 "")
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 โ oak clone acme/web --branch <branch>. External systems can't yet report their status into Oak's merge gate; for that, use Oak's native CI.