Docs menu

Webhooks

.md

Get an HTTP callback on push, branch creation, and merge โ€” signed, or posted to Discord.

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#

EventFires when
branch.createdA branch is pushed for the first time.
pushAn existing branch gets new commits (including a revert).
mergeA 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"
}
FieldValue
eventpush, branch.created, or merge.
owner, repoThe repository.
branchThe branch the event is about.
commitThe resulting commit โ€” for merge, the new squash commit on main.
pusherWho pushed or merged.
timestampWhen 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.

Something here wrong or missing? Run oak feedback -m "โ€ฆ" or email [email protected].