Rox
Register · Developers
PUBLIC DOCS
v1 · api.roxregister.com
Sign in to Platform
OverviewQuickstartsGuidesSDKsAPI referenceErrorsWebhooksJWS vectors
Guides / Consuming webhooks
Consuming webhooks
Sealing is asynchronous by design: the API answers 202, then the record reaches exactly one terminal outcome, anchored or failed. Webhooks push that outcome to you the moment it happens, which beats polling on every axis: less traffic, no missed transitions, and your pipeline reacts to record.failed instead of discovering it later.
Register an endpoint, verify every delivery
Registration is a signed mutation like sealing; the response contains the endpoint secret (whsec_...) exactly once, so store it before anything else. Every delivery is then signed v1=HMAC-SHA256(secret, ts + "." + body) over the exact raw body bytes. Verify constant-time and enforce the freshness window before trusting a payload; reject anything that fails with a 401.
# Register (a JWS mutation, same RRv1 signing as /v1/seal, path /v1/webhooks):
BODY='{"url":"https://api.ish.example/rox/events","events":["record.anchored","verification.mismatch","gas.low"]}'
TS=$(($(date +%s) * 1000)); NONCE=$(uuidgen | tr '[:upper:]' '[:lower:]')
BODY_HASH=$(printf '%s' "$BODY" | shasum -a 256 | cut -d' ' -f1)
printf 'RRv1\nPOST\n/v1/webhooks\n%s\n%s\n%s' "$BODY_HASH" "$TS" "$NONCE" > /tmp/rr-input
SIG=$(openssl pkeyutl -sign -inkey request-key.pem -rawin -in /tmp/rr-input \
  | basenc --base64url -w0 | tr -d '=')

curl -sS -X POST https://api.roxregister.com/v1/webhooks \
  -H "Authorization: Bearer $RR_API_KEY" -H "Content-Type: application/json" \
  -H "X-RR-Key-Id: ish-prod-2026-01" -H "X-RR-Timestamp: $TS" \
  -H "X-RR-Nonce: $NONCE" -H "X-RR-Signature: $SIG" \
  -d "$BODY"
# → 201 {"id":"wh_…","secret":"whsec_…"}   ← secret shown EXACTLY ONCE

# Each delivery you receive is signed; recompute and compare:
#   X-RR-Webhook-Timestamp: unix SECONDS
#   X-RR-Webhook-Signature: v1=<hex>
printf '%s.%s' "$TS_HEADER" "$RAW_BODY" \
  | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" -hex
# must equal the signature after "v1=", compare constant-time in real code
The SDKs do the verification for you: constructWebhookEvent (Node) and verify_webhook_signature (Python) check the MAC and the timestamp and hand you the parsed envelope.
Process events reliably
  • Acknowledge fast, work later. Return 2xx as soon as the signature checks out and the event is queued; do the real work asynchronously. Slow handlers time out and get retried as failures.
  • Deduplicate by event id. Retries mean at-least-once delivery: the same id can arrive twice. Keep a processed-id set (the 48h window of your queue is plenty) and make handlers idempotent.
  • Do not assume order. Retries and concurrent dispatch can reorder deliveries; key your state transitions on the record status, not on arrival order.
  • Handle both terminal outcomes. Wire record.anchored and record.failed from day one. A record that anchored is provable; a record that failed needs your attention, not silence.
  • Alert on tampering. verification.mismatch means someone verified bytes that do not match a sealed record: treat it as a security signal, not noise.
Delivery contract and failure behavior
Failed deliveries are retried on the schedule 1m → 5m → 30m → 2h → 12h, then marked dead and you are notified. An endpoint that fails 20 consecutive times is auto-disabled; re-enable it from your Platform after fixing the receiver. The full envelope, header set and event catalogue live on the webhook events page; endpoint management calls (POST /v1/webhooks, list, deliveries log, delete) are wrapped by both SDKs.