pasbydocs
Guides

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

SurfaceWebhook required
Signing with action: signYes
Signing wildcard with action: signYes
Document signing / reviewYes
Signing with action: confirmNo — 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

FieldRequiredNotes
hostYesHTTPS URL your server accepts POST on
referenceYesOpaque 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

ModeTypical UXNIN at start?Your server returns
Same-deviceUser opens a link on this deviceYeslink to open
WildcardQR from seedsNoseeds[]
Different-devicePush to holder’s devicesYesWait 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:

  1. Generate a unique reference (e.g. nanoid) and pass it in webhook.reference.
  2. Store the flow identifier (req_… or data.request.id) returned by pasby.
  3. 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

  1. Verify signature — HMAC-SHA256 of the raw body with your app secret; reject mismatches with 401.
  2. Parse JSON and validate against your schema (e.g. Zod).
  3. Filter events — ignore non-completion events (e.g. only handle signature.event.completed when that matches your integration).
  4. Resolve mapping → business record; return 200 for unknown references if retries would otherwise loop (confirm retry policy with pasby support).
  5. Verify server-side — call flow ping for the flow id; if cancelled, acknowledge and stop.
  6. Trust ping fields where possible: nin, signature, signedAt.
  7. Enforce business rules — active signer, required form fields, strict NIN match if configured.
  8. Record idempotently — duplicate webhook retries must not create duplicate signatures.
  9. Advance workflow — next signer or finalize document.
  10. Clean up reference / identifier mappings.
  11. 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

SymptomLikely cause
Webhook never firesWrong host, firewall, or non-public origin
400 invalid bodyPayload shape drift — update validation
Unknown referenceMapping not saved before response returned
Signer mismatchNon-active signer tried to sign
NIN failedStrict NIN constraint vs actual signer from ping
Duplicate signaturesMissing idempotency on signerIndex or reference

On this page