Skip to content

Webhooks

Bona Fide sends an HTTPS POST to your endpoint for every envelope event you subscribe to. Deliveries follow the Standard Webhooks specification, so any of its libraries can verify them.

In Settings › Webhooks, or with an API key that has the org:manage scope:

Terminal window
curl https://bonafide-api.republic.global/v1/webhook-endpoints \
-H "Authorization: Bearer $BONA_FIDE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://app.example.com/hooks/bona-fide",
"description": "Production",
"events": ["envelope.completed", "envelope.sealed", "envelope.declined"]
}'

The response includes secret (whsec_…), only this once. An empty events list subscribes to every event. The URL must be public https://. An endpoint belongs to one mode: endpoints created with a test key receive only test-mode envelopes’ events, live ones only live events.

PATCH /v1/webhook-endpoints/{id} changes the URL, events or description and pauses or resumes it (enabled). POST /v1/webhook-endpoints/{id}/test sends a webhook.test ping.

Type When
envelope.created The envelope was created
envelope.ready The document is rendered and the envelope is ready for its recipients
recipient.sent It became a recipient’s turn (and, with email delivery, their link was sent)
recipient.viewed A recipient opened the signing page for the first time
recipient.signed A recipient submitted (or was countersigned automatically)
recipient.declined A recipient declined
envelope.completed Every recipient finished. data.values has the variables and every typed value
envelope.sealed The sealed PDF is ready to download
envelope.declined The envelope ended because a recipient declined
envelope.voided You voided the envelope
envelope.expired expiresAt passed before completion
signing_link.used A visitor started an envelope from an always-on signing link

acceptance.created is reserved in the schema and not emitted yet.

{
"id": "evt_9sKq2Lm4Tz",
"type": "envelope.completed",
"createdAt": "2026-10-13T14:09:52.004Z",
"testMode": false,
"data": {
"envelope": {
"id": "env_8Gm2kQ4rT1",
"status": "completed",
"seal": "pending",
"recipients": [{ "id": "rcp_5Jw0pLs9Xe", "role": "client", "status": "completed", "…": "…" }],
"externalId": "user_8841",
"metadata": {},
"…": "…"
},
"values": {
"variables": { "client": { "name": "Acme Robotics, Inc." }, "effective_date": "2026-10-13" },
"recipients": {
"client": { "client_title": "CEO", "start_date": "2026-11-01" }
}
}
}
}

data.envelope is the full envelope at the time of the event. data.values is only on envelope.completed: the variables you sent, and each role’s field values by field id.

Each delivery carries:

Header Value
webhook-id The event id (evt_…); the same on every retry. Use it to de-duplicate
webhook-timestamp Unix seconds when this attempt was signed
webhook-signature v1,<base64 HMAC-SHA256>, space-separated when more than one secret signs
user-agent BonaFide-Webhooks/1

The signature is HMAC-SHA256 over {webhook-id}.{webhook-timestamp}.{raw body}, keyed with the base64-decoded part of the secret after whsec_. Verify the raw body (before JSON parsing), compare in constant time, and reject timestamps more than five minutes away from your clock.

import express from 'express';
import { Webhook } from 'standardwebhooks'; // npm i standardwebhooks
const wh = new Webhook(process.env.BONA_FIDE_WEBHOOK_SECRET!); // whsec_…
const app = express();
app.post('/hooks/bona-fide', express.raw({ type: 'application/json' }), (req, res) => {
let event;
try {
event = wh.verify(req.body, req.headers as Record<string, string>); // throws if forged or too old
} catch {
return res.sendStatus(400);
}
queue.add(event); // do the work asynchronously and answer quickly
res.sendStatus(204);
});

A delivery succeeds on any 2xx within 10 seconds. Redirects are not followed. Anything else is retried with backoff, after 5 seconds, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours, 14 hours, 20 hours and 24 hours, for at most three days after the event. Then the delivery is marked failed.

  • Answer quickly and process asynchronously; de-duplicate on webhook-id.
  • Deliveries can arrive out of order: check data.envelope.status rather than assuming a sequence.
  • GET /v1/webhook-endpoints/{id}/deliveries lists recent attempts with status codes, errors and timings, and each endpoint shows its 24-hour success rate (health).
  • POST /v1/events/{id}/redeliver sends an event to an endpoint again, for example after an outage.

POST /v1/webhook-endpoints/{id}/secret/rotate returns a new secret immediately. For the next 24 hours every delivery is signed with both the new and the old secret (two entries in webhook-signature), so you can deploy the new secret without dropping events.