Gareth Wilson Gareth Wilson

Guide to Persona Webhooks: Features and Best Practices

Published


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

FeatureDetails
ConfigurationDashboard > Webhooks (per-event-type subscription)
Signature headerPersona-Signature (t=<ts>,v1=<hex>)
Signature schemeHMAC-SHA256 hex over {t}.{raw body}, keyed with the wbhsec_ secret
RotationTwo space-separated t=/v1= pairs; accept any v1 match
Replay protectionNo documented timestamp tolerance (enforcement is up to you)
PayloadJSON:API; event name at data.attributes.name
Retries5s timeout, then up to 7 more attempts with exponential backoff
Success codes200, 201, 202, 204
RedeliveryEvents retained 30 days; manual redelivery in the dashboard
SDKNo 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>):

EventFires when
inquiry.completedAn inquiry is completed by the individual
inquiry.approvedAn inquiry is approved
inquiry.declinedAn inquiry is declined
inquiry.failedAn inquiry fails
verification.passedA verification passes
verification.failedA 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 t is 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 v1 matches.
  • 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-at rather 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.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.