Reference

Webhooks & events

Event names, delivery payloads, signing and verification, retries, the deliveries API, and polling or replaying events.

View as Markdown

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):

bash
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#

EventSent whendata
stitch.createdA stitch was submitted (before the weave check)workstream, changeset, change_id, base_head, author, actor, agent, intent, why, ops (count)
workstream.weavedA stitch weaved (the head moved)entry (history entry)
workstream.promotedA promotion weavedentry
workstream.inheritedA child inherited a parent's stitchentry
workstream.createdA workstream was createdworkstream
workstream.deletedA workstream was removed (archived)workstream, archived: true, actor, force, unpromoted, closed_change_requests, closed_suggestions, released_claims, closed_resolution_points
workstream.restoredA removed workstream was restoredworkstream, archived: false, actor
workstream.protection_changedProtection was set or removedprotection, actor
change_request.opened / change_request.resolvedA change request opened / was resolvedchange_request
suggestion.createdA suggestion was routed to an ownersuggestion
suggestion.resolved / suggestion.reopenedAccepted, rejected, stale or withdrawn / back to pendingsuggestion
resolution_point.openedA rejected clash kept both versionsresolution_point
resolution_point.resolvedA point was resolved (or closed by archiving)resolution_point
message.sentAny message in the repository, direct ones includedmessage
tag.created / tag.deletedA tag was created / deletedtag, actor (and why on delete)
attestation.createdAn attestation was recordedattestation, actor
purge.requested / purge.approved / purge.cancelledA lawful purge changed state (also sent on expiry)purge, kind, state, requestedBy, decidedBy, manifest, matches (never the reason or the text)
repo.deletedThe repository was purgedrepo, 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#

http
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…
json
{ "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. seq increases per repository; deliveries are not ordered, so use seq.
  • X-Weave-Timestamp is unix seconds.
  • Redirects are not followed (a 3xx counts 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:

ts
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:

bash
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):

bash
curl -s "https://api.weave.directory/api/weave/$REPO_ID/events?since=412&limit=100" \
  -H "Authorization: Bearer $WEAVE_TOKEN"
json
{ "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.