Provenance

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

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