Webhooks
HTTPS callbacks for sign actions and document workflows — configuration, correlation, and server-side verification.
Webhooks notify your backend when a signature flow completes. Treat the webhook as the source of completion for sign actions; the browser is only for opening links or showing QR codes.
When required
| Surface | Webhook required |
|---|---|
Signing with action: sign | Yes |
Signing wildcard with action: sign | Yes |
| Document signing / review | Yes |
Signing with action: confirm | No — use flow ping instead |
On v1 signing endpoints, a webhook was required for every action. On v2, only sign requires a webhook; confirm can be tracked with ping alone.
Webhook object
| Field | Required | Notes |
|---|---|---|
host | Yes | HTTPS URL your server accepts POST on |
reference | Yes | Opaque id you generate to correlate with your database |
{
"webhook": {
"host": "https://your-app.com/api/webhooks/pasby",
"reference": "order-789"
}
}Derive host from your public app origin, for example https://your-app.com/api/webhooks/pasby. In local development, pasby cannot reach localhost — use a tunnel (ngrok, Cloudflare Tunnel, etc.) and set that HTTPS origin in your environment.
Signing modes and webhooks
| Mode | Typical UX | NIN at start? | Your server returns |
|---|---|---|---|
| Same-device | User opens a link on this device | Yes | link to open |
| Wildcard | QR from seeds | No | seeds[] |
| Different-device | Push to holder’s devices | Yes | Wait for webhook / ping |
For multi-signer products, bind each signer to exactly one method (same-device, wildcard, or different-device). Wildcard and a strict per-signer NIN constraint are incompatible by design.
Correlating events
When you start a flow:
- Generate a unique
reference(e.g. nanoid) and pass it inwebhook.reference. - Store the flow
identifier(req_…ordata.request.id) returned by pasby. - Index both ids → your business record (envelope id, order id, signer index).
On webhook delivery, look up by reference first, then by flow identifier if needed. After successful processing, delete mapping rows to avoid replay confusion.
Security
To know for sure that a webhook was sent by pasby instead of a malicious actor, verify the request signature before you parse or trust the JSON body.
Each webhook POST includes a header named x-pasby-signature. Compute an HMAC-SHA256 hex digest of the raw request body (the exact bytes pasby sent, before JSON parsing) using your app secret — the same value you pass as x-access-secret on API calls. Compare your digest to the header value.
Frameworks that auto-parse JSON often discard the raw body. Configure your route to read the raw payload first (for example express.raw({ type: "application/json" }) in Express, or request.get_data() in Flask), verify the signature, then parse JSON.
Verifying a request
import crypto from "node:crypto";
const signature = req.headers["x-pasby-signature"];
const hash = crypto
.createHmac("sha256", appSecret)
.update(rawBody)
.digest("hex");
if (hash === signature) {
// Request is verified
} else {
// Request could not be verified — return 401
}If your generated digest matches x-pasby-signature, the request came from pasby. Keep your app secret on the server only — never commit it or ship it in client-side code.
Signature verification proves origin and integrity of the callback payload. Still call flow ping before you record a signature or advance your workflow, so you confirm the flow was not cancelled and the signer matches your business rules.
Webhook handler checklist
- Verify signature — HMAC-SHA256 of the raw body with your app secret; reject mismatches with
401. - Parse JSON and validate against your schema (e.g. Zod).
- Filter events — ignore non-completion events (e.g. only handle
signature.event.completedwhen that matches your integration). - Resolve mapping → business record; return
200for unknown references if retries would otherwise loop (confirm retry policy with pasby support). - Verify server-side — call flow ping for the flow id; if
cancelled, acknowledge and stop. - Trust ping fields where possible:
nin,signature,signedAt. - Enforce business rules — active signer, required form fields, strict NIN match if configured.
- Record idempotently — duplicate webhook retries must not create duplicate signatures.
- Advance workflow — next signer or finalize document.
- Clean up reference / identifier mappings.
- Audit — log event type, reference, and flow id (never secrets).
Example completion payload (signing)
Shape may vary by contract tier; log sandbox payloads and adjust your schema when fields change:
{
"status": "successful",
"reference": "order-789",
"event": "signature.event.completed",
"data": {
"identifier": "req_…",
"signature": "…",
"signedAt": 1716820121,
"createdAt": 1716820052
}
}Always confirm JSON fields and event types against sandbox payloads for your contract tier.
Common failure modes
| Symptom | Likely cause |
|---|---|
| Webhook never fires | Wrong host, firewall, or non-public origin |
| 400 invalid body | Payload shape drift — update validation |
| Unknown reference | Mapping not saved before response returned |
| Signer mismatch | Non-active signer tried to sign |
| NIN failed | Strict NIN constraint vs actual signer from ping |
| Duplicate signatures | Missing idempotency on signerIndex or reference |
Related
- Signing guide
- Document signing
- Integration recipes
- SampleCode — reference webhook and ping handlers