Guide to Persona Webhooks: Features and Best Practices
Persona webhooks notify your systems about identity verification activity: an inquiry completes, an inquiry is approved, a verification passes. If you're gating access, triggering review workflows, or updating records off Persona's KYC/identity flows, webhooks are how you react without polling.
This guide covers how Persona webhooks work, the events you'll handle, how to verify the Persona-Signature, the secret rotation and retry behavior, and the best practices for production.
What are Persona webhooks?
Persona webhooks are HTTP POSTs sent to a URL you register, each carrying a JSON:API envelope. The event name is at data.attributes.name, and the full affected object is at data.attributes.payload.data (the same schema as Persona's API responses). Each webhook is pinned to a configurable API version, and Persona signs deliveries with a Stripe-style scheme in the Persona-Signature header.
Persona webhook features
| Feature | Details |
|---|---|
| Configuration | Dashboard > Webhooks (per-event-type subscription) |
| Signature header | Persona-Signature (t=<ts>,v1=<hex>) |
| Signature scheme | HMAC-SHA256 hex over {t}.{raw body}, keyed with the wbhsec_ secret |
| Rotation | Two space-separated t=/v1= pairs; accept any v1 match |
| Replay protection | No documented timestamp tolerance (enforcement is up to you) |
| Payload | JSON:API; event name at data.attributes.name |
| Retries | 5s timeout, then up to 7 more attempts with exponential backoff |
| Success codes | 200, 201, 202, 204 |
| Redelivery | Events retained 30 days; manual redelivery in the dashboard |
| SDK | No official server-side SDK (npm persona is the frontend inquiry SDK) |
Common events
Persona events are dotted names on the inquiry and verification resources (report events use a slash form, report/<type>.<action>):
| Event | Fires when |
|---|---|
inquiry.completed | An inquiry is completed by the individual |
inquiry.approved | An inquiry is approved |
inquiry.declined | An inquiry is declined |
inquiry.failed | An inquiry fails |
verification.passed | A verification passes |
verification.failed | A verification fails |
Persona adds event types over time, so handle unknown names defensively. Subscribe per event type, and consult Persona's events reference for the full, current catalog.
Setting up Persona webhooks
Create webhooks in Dashboard > Webhooks, subscribing to the specific event types you need. Each webhook reveals a signing secret with a wbhsec_ prefix and is pinned to an API version. IP allowlisting is available if you want to restrict the source.
Securing Persona webhooks
Each delivery carries a Persona-Signature header in the form t=<unix_ts>,v1=<hex>. To verify, build the signed payload {t}.{raw body}, compute an HMAC-SHA256 with your wbhsec_ secret, hex-encode it, and compare against the v1 value. During secret rotation the header carries two space-separated t=/v1= pairs; accept the delivery if any v1 matches. Use the raw body and a constant-time comparison.
const crypto = require("crypto");
const SECRET = process.env.PERSONA_WEBHOOK_SECRET; // wbhsec_...
function verify(rawBody, signatureHeader) {
// Header may carry multiple space-separated "t=...,v1=..." pairs (rotation)
return (signatureHeader || "").split(" ").some((part) => {
const params = Object.fromEntries(
part.split(",").map((kv) => kv.split("=").map((s) => s.trim()))
);
if (!params.t || !params.v1) return false;
const expected = crypto
.createHmac("sha256", SECRET)
.update(`${params.t}.${rawBody}`)
.digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(params.v1);
return a.length === b.length && crypto.timingSafeEqual(a, b);
});
}
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
if (!verify(req.body.toString("utf8"), req.headers["persona-signature"])) {
return res.sendStatus(401);
}
res.sendStatus(200); // acknowledge fast (200/201/202/204 count as success)
processQueue.add(JSON.parse(req.body)); // process asynchronously
});
The same check in Python:
import hashlib
import hmac
import os
SECRET = os.environ["PERSONA_WEBHOOK_SECRET"].encode() # wbhsec_...
def verify(raw_body: str, signature_header: str) -> bool:
for part in (signature_header or "").split(" "):
params = dict(kv.split("=", 1) for kv in part.split(",") if "=" in kv)
t, v1 = params.get("t"), params.get("v1")
if not t or not v1:
continue
expected = hmac.new(SECRET, f"{t}.{raw_body}".encode(), hashlib.sha256).hexdigest()
if hmac.compare_digest(expected, v1):
return True
return False
Persona webhook limitations and pain points
No documented timestamp tolerance
The Problem: The signature includes a timestamp (t), but Persona doesn't document a tolerance window. If you don't enforce one yourself, a captured, validly signed payload can be replayed indefinitely.
Why It Happens: Persona leaves replay-window enforcement to the consumer.
Workarounds:
- Enforce your own freshness window: reject deliveries whose
tis too far from now. - Make processing idempotent so a replay has no additional effect.
How Hookdeck Can Help: Hookdeck deduplicates deliveries at the edge and can enforce checks consistently, filtering replays before they reach your app. See our guide to webhook idempotency.
Rotation produces two signature pairs
The Problem: During secret rotation the Persona-Signature header carries two space-separated t=/v1= pairs. Code that parses only one rejects valid deliveries signed with the other secret.
Why It Happens: Overlapping signatures let you rotate the wbhsec_ secret without dropping deliveries.
Workarounds:
- Split on spaces and accept the delivery if any pair's
v1matches. - Coordinate rotation so old and new secrets overlap.
How Hookdeck Can Help: Hookdeck handles the rotation case at the edge, so your app never has to parse multiple signature pairs.
Ordering isn't guaranteed, and duplicates happen
The Problem: Events can arrive out of order and more than once. A state machine that assumes inquiry.completed precedes inquiry.approved, or that each event is unique, breaks.
Why It Happens: Parallel, at-least-once delivery doesn't preserve order.
Workarounds:
- Order by
data.attributes.created-atrather than receipt order. - Dedupe on the event ID and make side effects idempotent.
How Hookdeck Can Help: Hookdeck deduplicates and durably queues events, and its replay lets you re-process in a controlled order during recovery. See our guide to webhook ordering.
Growing event catalog and 5-second timeout
The Problem: Persona adds event types over time, and it times out after 5 seconds (then retries up to 7 more times). A handler that assumes a fixed event list, or does slow synchronous work, will break or time out.
Why It Happens: The product evolves, and the delivery timeout is short.
Workarounds:
- Handle unknown event names gracefully.
- Acknowledge fast (any of 200/201/202/204) and process asynchronously.
How Hookdeck Can Help: Hookdeck acknowledges Persona within the timeout and durably queues events, so slow processing never causes a timeout, and it lets you route on event type as the catalog grows.
Best practices
Verify over t.body and accept any pair
Build {t}.{raw body}, HMAC-SHA256 hex with your wbhsec_ secret, and accept the delivery if any v1 pair matches, using constant-time comparison.
Enforce your own replay window
Since Persona documents no tolerance, reject deliveries whose t is implausibly old and dedupe on the event ID.
Acknowledge fast, process asynchronously
Return a success code (200/201/202/204) immediately and defer work to a queue, well inside the 5-second timeout. See why to process webhooks asynchronously.
Don't assume ordering
Order by data.attributes.created-at and design handlers to tolerate out-of-order and repeated events.
Handle new event types
Persona adds events over time; branch defensively and don't fail on an unrecognized data.attributes.name.
Make Persona webhooks production-ready
Hookdeck verifies the Persona-Signature, handles rotation, deduplicates, and durably queues every identity event
Conclusion
Persona webhooks deliver identity and verification events in JSON:API envelopes, signed with a Stripe-style Persona-Signature you verify over {t}.{raw body} with your wbhsec_ secret, accepting multiple pairs during rotation. The things to plan around are what's left to you: no documented timestamp tolerance, unguaranteed ordering, duplicates, a 5-second timeout, and a growing event catalog.
That puts replay enforcement, rotation handling, ordering tolerance, fast acknowledgement, and idempotency on you. Hookdeck verifies the signature, handles rotation, deduplicates, and durably queues every event at the edge, so your systems only ever process verified, unique identity events.
Get started with Hookdeck for free and handle Persona webhooks reliably in minutes.