Webhooks
Push, don't poll. Provenance delivers signed, retried webhook events to your endpoints when things happen — a claim escalates, a screening completes, an agent approval resolves, a token is revoked. It's the same contract Stripe trained everyone on, down to the signature scheme.
Events
| Event | Product | Fired when |
|---|---|---|
| attestation.created | both | a successful POST /v1/claims/attest |
| trace.job_completed | Trace | an attribution trace job finishes (data: job_id, trace_id, manifest_hash, backend) |
| trace.canary_tripped | Trace | a canary trip stopped an agent: a verify denied canary_tripped, or a kill switch block on a traced inference or /v1/trace/check (data: trace_id, canary_hash, canary_readings, tripped_families, mode, source, agent_id, agent_session_id, lock_id, jti, matched_rule_id) |
| trace.diff_completed | Trace | a Neuron-CI diff completes (data: diff_id, base_model, target_model, pass, families_drifted, report_hash) |
| trace.disclosure_requested | Trace | a verified person asks to see masked neurons on one of your trace shares (data: request_id, status, scope_kind, trace_id, model_version_id) |
| trace.disclosure_granted | Trace | a disclosure request is granted, by you or by auto mode (data: request_id, scope_kind, trace_id, model_version_id, expires_at, decided_by) |
| claim.provenance_updated | Attest | a claim's provenance/risk result changes |
| claim.escalated | Attest | risk status crosses to failed |
| case.status_changed | Attest | an adjuster case transitions state |
| screening.completed | Attest | a synthetic-media screening returns a real score |
| approval.requested / approval.resolved | Authorize | a step-up approval is created / decided |
| intent.revoked | Authorize | a token/session/device revocation is created |
| ping | — | the Send test button |
Subscribe an endpoint to specific types, or ["*"] for all. You only receive events for products your account is entitled to.
The envelope
Every delivery is a POST with this JSON body:
{ "event_id": "01J…", "type": "approval.resolved", "created_at": "2026-07-05T…Z",
"environment": "sandbox", "account_id": "…", "data": { … } }
event_id is stable across retries — use it to dedupe (delivery is at-least-once, not exactly-once).
Verifying the signature
Each delivery carries Provenance-Signature: t=<unix>,v1=<hex hmac> — HMAC-SHA256 over `${t}.${rawBody}`, keyed by your endpoint's signing secret (shown once at creation). Verify over the raw body bytes, before JSON-parsing:
import { verifyWebhookSignature } from "@provenance/sdk";
// In your handler, with the RAW request body string:
const result = await verifyWebhookSignature(rawBody, req.headers["provenance-signature"], endpointSecret);
if (!result.valid) return res.status(400).end(); // reject: result.reason tells you why
const event = JSON.parse(rawBody); // now safe to trust
Re-serializing the body changes whitespace and breaks the signature — always verify the exact bytes you received.
Delivery, retries & disabling
- At-least-once. A non-2xx (or timeout — 10s) schedules a retry with exponential backoff: 1m, 5m, 30m, 2h, 8h (5 attempts) before a delivery is
exhausted. - Auto-disable. An endpoint that accumulates enough exhausted deliveries is disabled (you'll see
disabled_reason); re-enable it from the dashboard orPUT …/{id}once your listener is healthy. - Delivery log.
GET /v1/webhook-endpoints/:id/deliveries(dashboard Webhooks → Deliveries) shows each attempt's status, code, and error.
Managing endpoints
# Create (secret is in the response — store it now).
curl -s -X POST $API/v1/webhook-endpoints -H "Authorization: Bearer $ACCT" \
-H "Content-Type: application/json" \
-d '{ "url": "https://your-app.example/webhooks", "event_types": ["*"] }'
curl -s -X POST $API/v1/webhook-endpoints/$ID/test -H "Authorization: Bearer $ACCT" # signed ping
SDK: prov.webhooks.create(...), .list(), .update(id, …), .rotateSecret(id), .test(id), .deliveries(id). The dashboard's Webhooks panel does all of it, including the delivery log.
Security notes
- The signing secret is stored, because we need it to sign every delivery. Rotate it any time; the old secret stops verifying immediately.
- In a live posture, endpoint URLs must be https and are SSRF-guarded against private/loopback addresses. Sandbox allows
localhostso you can test against a local listener.