Webhooks
The interesting part is what happens when delivery fails
Signing, retries, ordering and idempotency are the whole contract. An integration that handles the happy path is not finished; it has not started.
- Delivery
- At least once
- Signed
- Over the raw body
- Retries
- Exponential backoff
Definition
How do passport webhooks work?
CirculeID posts a signed callback to your endpoint when a passport, event or credential changes. Delivery is at-least-once with exponential backoff, so handlers must verify the signature, treat repeated delivery identifiers as no-ops, and read current state back through the API before acting.
Treat a payload as a notification, not as the record. That single habit removes the entire class of bug where a delayed retry overwrites newer data with older data.
Events
What you can subscribe to
| Event | Fires when | Typical handler |
|---|---|---|
| passport.created | A passport is issued against an identifier | Print or encode the data carrier |
| passport.updated | The record changes materially | Re-read state; refresh a cached storefront view |
| event.recorded | An EPCIS event is appended to an object | Advance an internal workflow |
| credential.issued | A supplier signs a claim against your product | Clear the compliance gap for that field |
| credential.revoked | An issuer withdraws a claim | Re-open the gap; review anything that relied on it |
| passport.gap_detected | A delegated act change leaves a field unmet | Raise it to the compliance owner |
Verification
Verify before you parse
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verify(rawBody, header, secret) {
const [ts, signature] = parseHeader(header);
// Reject replays before doing any work.
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const expected = createHmac('sha256', secret)
.update(`${ts}.${rawBody}`) // raw body, exactly as received
.digest();
// Constant-time: a fast reject leaks the signature one byte at a time.
return timingSafeEqual(Buffer.from(signature, 'hex'), expected);
}Contract
What the platform guarantees
Signed payloads
HMAC over the raw body with a per-endpoint secret, plus a timestamp.
At-least-once delivery
The honest guarantee for a retrying system. Handlers must be idempotent.
Exponential backoff
Retried over an extended window, so a short outage costs you nothing.
Version on every payload
Ignore a delivery describing a version you have already passed.
Visible failure log
Failed deliveries are listed and replayable rather than silently dropped.
Scoped subscriptions
Subscribe per event type, so an endpoint only receives what it acts on.
Answers
Frequently asked questions
How do I know a webhook really came from CirculeID?
Every delivery carries a signature over the raw request body and a timestamp. Verify the signature against your endpoint’s secret before parsing, and reject deliveries whose timestamp is outside your tolerance window. An endpoint that trusts an unverified payload is an endpoint anyone can post to.
Are deliveries ordered?
Per object, best effort — but do not depend on it. Retries and network conditions mean an older event can arrive after a newer one. Every payload carries the object version it reflects, so a handler should ignore a delivery describing a version it has already passed rather than assume arrival order.
What happens if my endpoint is down?
Delivery is retried with exponential backoff over an extended window, and the failure log is visible in your account rather than silent. After the window expires the delivery is marked failed; the underlying change is still queryable through the API, so a webhook outage causes a delay rather than data loss.
Can the same event be delivered twice?
Yes, and you should assume it will be. At-least-once delivery is the honest guarantee for any retrying system. Each delivery carries a stable identifier, so the correct handler records that identifier and treats a repeat as a no-op — which also makes replaying a failed window safe.
Should webhook payloads be trusted as the full record?
Treat the payload as a notification rather than as the source of truth. It tells you something changed and gives you enough to decide whether you care; if you act on it, read the current state back through the API. That way a stale or reordered delivery cannot write old data into your system.
Next step
Point one at a test endpoint
Subscribe in sandbox, break your endpoint on purpose, and watch the retry and replay behaviour before you rely on it.