Webhooks
With webhooks, bb-sign sends a signed JSON payload to your URL each time an envelope changes status, and your system reacts immediately without polling.
The signature scheme is defined normatively in the webhook signature specification; this page is the integration guide.
Register a webhook
Section titled “Register a webhook”In Settings, tab Webhooks, select New webhook.
- Endpoint URL: “Must be HTTPS and reachable from the internet. We refuse private and internal addresses.” The check runs again on every delivery.
- Events to receive: select the ones you need from the six available:
| Event | When |
|---|---|
envelope.sent |
An envelope was sent to its signers |
envelope.completed |
Every signer has signed |
envelope.cancelled |
An envelope was cancelled |
envelope.expired |
An envelope reached its expiry without being completed |
signer.signed |
One signer signed; others may be pending |
signer.viewed |
A signer opened the document |
The signing secret is shown once, when the webhook is created. Store it in your secret store. An organization can have up to 20 webhooks.
Delivery contents
Section titled “Delivery contents”{ "id": "3f8c...", "type": "envelope.completed", "apiVersion": "2026-08-09", "createdAt": "2026-08-09T14:22:31Z", "data": { "envelope": { "id": "...", "title": "Q3 Agreement", "status": "COMPLETED", "createdAt": "...", "expiresAt": null }, "signer": { "id": "...", "email": "...", "name": "...", "signOrder": 1, "status": "SIGNED", "signedAt": "..." } }}data.signer is included only in signer.* events. Full schemas are in the
API reference as WebhookEvent, so a generated client receives them typed.
Every delivery carries three headers:
| Header | Use |
|---|---|
X-BBSign-Signature |
The signature you verify. t=<unix>,v1=<hex> |
X-BBSign-Event-Id |
Your idempotency key, stable across retries |
X-BBSign-Event-Type |
The event name, so you can route before parsing the body |
Get the signed document
Section titled “Get the signed document”When you receive envelope.completed, call GET /api/v1/envelopes/{id}/documents to get the
file. The response includes signedUrl once the envelope is COMPLETED, valid for
3,600 s. The payload deliberately carries no download
links: a presigned link in a payload would remain usable after delivery and could end up stored in
logs. The walkthrough shows the whole sequence.
Verify the signature over the raw body
Section titled “Verify the signature over the raw body”The signature covers the exact bytes received. Most web frameworks parse the JSON before your handler sees it, and re-serializing that object produces different bytes (key order and whitespace change), so verification fails even though the object is correct. Capture the raw body before any parsing.
import { verifyWebhookSignature } from '@binarybridges/bb-sign-sdk/webhooks'
app.use(express.json({ verify: (req, _res, buf) => { req.rawBody = buf } }))
app.post('/webhooks/bb-sign', (req, res) => { if (!verifyWebhookSignature(req.get('X-BBSign-Signature'), req.rawBody, SECRET)) { return res.sendStatus(401) } res.sendStatus(200) void handle(JSON.parse(req.rawBody.toString('utf8')))})In Next.js, use await request.text() instead of .json(). In Fastify, register a
content-type parser with parseAs: 'buffer'. The JavaScript SDK
page includes both examples in full.
@PostMapping("/webhooks/bb-sign")public ResponseEntity<Void> receive( @RequestHeader("X-BBSign-Signature") String signature, @RequestBody byte[] rawBody) { // byte[], NOT a DTO
if (!Webhooks.verify(signature, rawBody, System.getenv("BBSIGN_WEBHOOK_SECRET"))) { return ResponseEntity.status(401).build(); } WebhookEvent event = new ObjectMapper().readValue(rawBody, WebhookEvent.class); // ... handle asynchronously return ResponseEntity.ok().build();}The Java SDK page explains why the parameter is byte[].
Implement your own verifier
Section titled “Implement your own verifier”The specification is normative and short. Three requirements deserve particular attention:
- Compare in constant time.
==on the digest reveals, byte by byte, how much of a forged signature is correct, which is enough to construct a valid one. - Check the timestamp. Without a freshness check, a captured delivery could be replayed indefinitely.
- Accept any matching
v1. During a secret rotation the header carries two values; requiring the first to match defeats the rotation window.
The test vectors contain 16 cases, including the ones that must fail. The server and both SDKs are tested against that same file; test your verifier against it as well.
Delivery and retries
Section titled “Delivery and retries”Respond 2xx immediately and process afterwards. Acknowledge receipt as soon as the signature is valid and process the event asynchronously.
Retries. A failed delivery is retried on a backoff schedule spanning roughly
15 hours, with delays of
1m, 5m, 30m, 2h, 12h. Any 2xx counts as success; any
other response counts as a failure.
Automatic disabling. After 3 consecutive deliveries that exhaust their retries, the subscription is disabled and the organization’s admins are emailed. The panel shows the status Disabled automatically, with the note: “We disabled this endpoint after its retries failed repeatedly. Fix the endpoint, then enable it again.” bb-sign also probes the endpoint every 6 h, and a successful response re-enables it.
Events while disabled. They are recorded as Not sent and are not queued for later, so your system does not receive a flood of stale events when it comes back. The history shows exactly which events were not delivered.
At-least-once delivery. The same event can arrive twice, for example after a network blip or a
restart. X-BBSign-Event-Id is identical in both cases; use it to deduplicate.
Stable body across retries. The body is rendered once and resent byte for byte, so an
envelope.sent retried hours later describes the moment of sending. Only the signature is
recomputed, because it includes the delivery time.
Review the delivery history
Section titled “Review the delivery history”In Settings, tab Webhooks, History shows every event, every attempt, the response code and the error. Entries are kept for 90 days. It is the first place to diagnose a delivery:
| Status | Meaning | Action |
|---|---|---|
| Delivered | The event arrived and your endpoint returned 2xx | None |
| Rejected / Unreachable | Your endpoint returned an error or did not respond; the attempt list shows which | Check your logs for that attempt; the next retry is already scheduled |
| Gave up | Retries were exhausted | Fix the endpoint and fetch the envelope’s status with the API; this event is not resent |
| Not sent | The endpoint was disabled at the time | Enable the endpoint and reconcile the period with GET /api/v1/envelopes |
| Could not be built | An error on bb-sign’s side | Contact support |
Rotate the secret
Section titled “Rotate the secret”In Settings, tab Webhooks, select Rotate secret. “Both the old and the new secret will
be accepted for a grace period, so your receiver keeps working until you redeploy it with the new
one.” The grace period is 24 h. Deliveries in
that window carry two v1 values; a correct verifier accepts either, as both SDKs do.
Test the endpoint
Section titled “Test the endpoint”Send test delivers a synthetic event to your endpoint and shows the response status code. It uses the same signing process as real deliveries, so it validates your verifier end to end.