Reference
Webhooks & events
Event names, delivery payloads, signing and verification, retries, the deliveries API, and polling or replaying events.
Webhooks send a signed HTTP POST to your URL when something happens in a repository. Every event is first written to a durable, per-repository log, so nothing a webhook or client can miss: deliveries are retried, and clients can replay or poll by sequence number.
Create a webhook#
Web UI: repository → Settings → Webhooks → Add Webhook. Enter the Payload URL (https), a Secret (at least 16 characters, used to sign deliveries), tick the Trigger Events and Active.
API (repository admin, scope repo:admin):
curl -s https://weave.directory/api/v1/repos/$REPO_ID/webhooks \
-H "Authorization: Bearer $WEAVE_PAT" -H "Content-Type: application/json" \
-d '{"name": "ci", "url": "https://ci.example.com/weave", "secret": "a-long-random-secret-value",
"events": ["workstream.weaved", "resolution_point.*", "message.sent"]}'
events defaults to ["workstream.weaved"]. Secrets are encrypted at rest and never returned (secretSet: true). Rotate one with PATCH {"secret": "…"}. See the API reference for every webhook route.
Event names#
| Event | Sent when | data |
|---|---|---|
stitch.created | A stitch was submitted (before the weave check) | workstream, changeset, change_id, base_head, author, actor, agent, intent, why, ops (count) |
workstream.weaved | A stitch weaved (the head moved) | entry (history entry) |
workstream.promoted | A promotion weaved | entry |
workstream.inherited | A child inherited a parent's stitch | entry |
workstream.created | A workstream was created | workstream |
workstream.deleted | A workstream was removed (archived) | workstream, archived: true, actor, force, unpromoted, closed_change_requests, closed_suggestions, released_claims, closed_resolution_points |
workstream.restored | A removed workstream was restored | workstream, archived: false, actor |
workstream.protection_changed | Protection was set or removed | protection, actor |
change_request.opened / change_request.resolved | A change request opened / was resolved | change_request |
suggestion.created | A suggestion was routed to an owner | suggestion |
suggestion.resolved / suggestion.reopened | Accepted, rejected, stale or withdrawn / back to pending | suggestion |
resolution_point.opened | A rejected clash kept both versions | resolution_point |
resolution_point.resolved | A point was resolved (or closed by archiving) | resolution_point |
message.sent | Any message in the repository, direct ones included | message |
tag.created / tag.deleted | A tag was created / deleted | tag, actor (and why on delete) |
attestation.created | An attestation was recorded | attestation, actor |
purge.requested / purge.approved / purge.cancelled | A lawful purge changed state (also sent on expiry) | purge, kind, state, requestedBy, decidedBy, manifest, matches (never the reason or the text) |
repo.deleted | The repository was purged | repo, actor |
Subscription patterns: exact names, * (everything), <family>.* (workstream.*, stitch.*, change_request.*, suggestion.*, resolution_point.*, message.*, tag.*, attestation.*, purge.*, repo.*), promotion.* (= workstream.promoted) and the legacy push (= workstream.weaved). : is read as .. An empty list or active: false receives nothing.
A webhook subscribed to message.sent receives every message of the repository, including direct ones: treat its URL as sensitive.
The delivery#
POST /weave HTTP/1.1
Content-Type: application/json
User-Agent: Weave-Webhooks/1
X-Weave-Event: workstream.weaved
X-Weave-Event-Id: 6f1c2a9e-0b5d-4a51-9a7e-3c1d2b4e5f60
X-Weave-Delivery: 0c9e…
X-Weave-Timestamp: 1793361600
X-Weave-Signature: sha256=5d41402abc4b2a76b9719d911017c592…
{ "seq": 412, "eventId": "6f1c2a9e-0b5d-4a51-9a7e-3c1d2b4e5f60", "event": "workstream.weaved",
"repo": "cm3k…", "createdAt": "2026-10-30T12:00:00Z",
"data": { "entry": { "seq": 37, "workstream": "dev", "changeset": "…", "head_before": "…", "head_after": "…",
"kind": "stitch", "author": "alice", "actor": "alice", "at": "2026-10-30T12:00:00Z" } } }
- The body is identical on every attempt.
seqincreases per repository; deliveries are not ordered, so useseq. X-Weave-Timestampis unix seconds.- Redirects are not followed (a
3xxcounts as a failure). Targets on private, loopback or link-local addresses are refused.
Verify the signature#
X-Weave-Signature is sha256= + hex HMAC-SHA256(secret, "<X-Weave-Timestamp>.<raw body>"). Verify it on the raw bytes, in constant time, and reject stale timestamps:
import { createHmac, timingSafeEqual } from "node:crypto";
// rawBody: the request body exactly as received, before JSON parsing.
export function verifyWeaveWebhook(rawBody: Buffer, headers: Record<string, string>, secret: string): boolean {
const timestamp = headers["x-weave-timestamp"];
const signature = headers["x-weave-signature"] ?? "";
if (!timestamp || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false; // 5-minute window
const expected = "sha256=" + createHmac("sha256", secret).update(`${timestamp}.`).update(rawBody).digest("hex");
const a = Buffer.from(signature), b = Buffer.from(expected);
return a.length === b.length && timingSafeEqual(a, b);
}
Then skip the event if you already processed its X-Weave-Event-Id, and answer any 2xx quickly (do the work afterwards). The request times out after 10 seconds.
Retries#
Any 2xx counts as delivered. Anything else — a status, a timeout, a connection error — is retried with exponential backoff (30 s, 1 min, 2 min, … capped at 1 hour, with jitter) until the delivery is 24 hours old, then it is failed. Deliveries are at least once: a delivery can arrive twice, so always dedupe on X-Weave-Event-Id. A redelivery has a new X-Weave-Delivery and the same event id. Finished deliveries and events are kept 30 days.
Deliveries and redelivery#
Repository admins, on the Weave server:
curl -s "https://api.weave.directory/api/weave/$REPO_ID/webhooks/deliveries?status=failed&limit=50" \
-H "Authorization: Bearer $WEAVE_PAT"
curl -s -X POST https://api.weave.directory/api/weave/$REPO_ID/webhooks/deliveries/$DELIVERY_ID/redeliver \
-H "Authorization: Bearer $WEAVE_PAT"
A delivery is {id, webhookId, url, eventId, event, seq, status: pending|delivered|failed, attempts, nextAttemptAt, lastStatusCode?, lastError?, redeliveryOf?, createdAt, updatedAt, deliveredAt?}. Page with before=<next>. Redeliver answers {delivery}: a new delivery of the same event, due now, with its own 24 hours of retries.
Polling and replay#
Without a public URL, poll the durable event log (read access):
curl -s "https://api.weave.directory/api/weave/$REPO_ID/events?since=412&limit=100" \
-H "Authorization: Bearer $WEAVE_TOKEN"
{ "events": [{ "seq": 413, "eventId": "…", "event": "suggestion.created", "repo": "cm3k…", "createdAt": "…", "data": { … } }],
"next": 413, "more": false, "latest": 413, "truncated": false }
Continue with since=next while more. truncated: true means events after since were already pruned: re-read state. Events with recipients (suggestions, resolution points, messages, notices) are only shown to their recipients; seq gaps are normal.
Over the WebSocket (wss://api.weave.directory/ws?repo=<repoId>), send {"Weave": {"id": 1, "request": {"type": "subscribe", "since": 412}}} after the handshake to replay everything after 412 and then receive live events, with no gap and no duplicate.
Extra events that only reach the WebSocket and polling, never webhooks: dependency.notice.