Risk event webhooks

Receive versioned archdev.risk_event deliveries, verify the Stripe-style signature, and deduplicate on the webhook id.

Public contract, versioned. archdev.risk_event v1 is defined by services/go/archdev/configs/json_schemas/archdev-risk-webhook-v1.yaml. Breaking changes ship as a new version with a new schema file; v1 payloads keep validating. Signed examples (one per event family) live in services/go/archdev/fixtures/risk-webhook/.

Body

Each delivery is a JSON object with this shape:

Field Meaning
id Message id. Equals the ArchDev-Webhook-Id header; the idempotency key.
type Always archdev.risk_event.
version Always 1 for v1.
created_at RFC 3339 UTC creation time.
org.id Owning organization.
event plan.created, plan.started, plan.updated, task.created, task.started, task.updated, task.closed, pr.created, pr.updated, pr.merged (deprecated alias for pr.closed), or pr.closed. New merges are delivered as pr.closed.
subject Discriminated subject matching the event family: plan carries path; task carries id; pull_request carries number, title, and url.
repository full_name as owner/repo.
risk combined (low, medium, high, critical), uncertainty and consequence (low, medium, high).
summary Message content: human-readable risk summary.
room thread_id, message_id (equals the body id), and url.

Headers

Header Value
ArchDev-Webhook-Id The body id. Deduplicate on it.
ArchDev-Signature t=<unix seconds>,v1=<hex>[,v1=<hex>...], where each v1 entry is hex(hmac_sha256(secret, t + "." + raw_body)) over the exact raw request bytes. Multiple v1 entries are allowed during secret rotation; a signature is valid when any listed entry matches any accepted secret.

Verification

  1. Read the raw request body bytes. Never re-serialize parsed JSON: key order and whitespace change the HMAC.
  2. Split the header on ,. Require an integer t and at least one v1 hex entry.
  3. Reject when |now - t| exceeds 300 seconds (5-minute tolerance in both directions).
  4. Compute the expected HMAC per accepted secret and compare with a constant-time compare (crypto/subtle.ConstantTimeCompare in the Go reference at services/go/archdev/internal/riskwebhook/riskwebhook.go).
  5. Treat redelivery as normal: delivery is at-least-once, up to 3 attempts with backoff within one delivery run, with the same id and same bytes on every attempt. Store handled webhook ids and ignore repeats.

Sender

The platform Script archdev-risk-webhook is the only sender. The Go package at services/go/archdev/internal/riskwebhook/ is the reference signer/verifier and contract test oracle; it never sends deliveries.

Reference

cd services/go/archdev && go test -count=1 ./internal/riskwebhook/