Guide to BoldSign Webhooks: Features and Best Practices
BoldSign webhooks tell your application what happens to the documents you send for signature: a request goes out, a signer opens it, signs, declines or lets it expire, and the last signature completes it. If you use BoldSign, the e-signature product from Syncfusion, webhooks are how those changes reach your own systems without polling the documents API.
This guide covers how BoldSign webhooks are added and verified, how the X-BoldSign-Signature header is built, the events you can subscribe to and the shape of their payloads, and the practices that keep a BoldSign receiver reliable in production.
What are BoldSign webhooks?
A BoldSign webhook is a named HTTPS URL with a list of events. When one of those events happens, BoldSign sends a JSON POST to the URL. There are two kinds:
- Account-level webhooks fire for documents sent from the BoldSign web app or the API.
- App-level webhooks are tied to one OAuth app and fire for documents sent through it. The payload's
event.clientIdidentifies the app.
Each webhook has its own signing secret and its own event list. An admin can also tick Listen to Team user's events to receive activity from the rest of the team or organisation.
Every event is signed. The X-BoldSign-Signature header carries a Unix timestamp and an HMAC-SHA256 of that timestamp and the raw body, so a receiver can check both where the request came from and when the event was generated.
BoldSign webhook features
| Feature | Details |
|---|---|
| Configuration | BoldSign web app, API > Webhooks > Add Webhook |
| Webhook types | Account-level, or app-level (one OAuth app) |
| URL check | A Verification event POST when you click Verify. Answer 200 within 10 seconds |
| Signature header | X-BoldSign-Signature: t=<unix seconds>, s0=<hex>[, s1=<hex>] |
| Scheme | HMAC-SHA256, lowercase hex |
| Signed content | <t>.<raw body> |
| Secret | One per webhook, revealed on the webhook overview page |
| Secret rotation | Optional overlap: s1 carries a signature from the old secret until it expires |
| Event type | event.eventType in the body, also sent in X-BoldSign-Event |
| Environments | event.environment is Test or Live |
| Response window | 10 seconds, 200 OK |
| Retries | 1 minute, 15 minutes, 45 minutes, 1 hour, 8 hours, 24 hours |
| Auto-disable | After the sixth retry, BoldSign monitors the webhook for 7 days and then disables it if failures persist |
| Source IPs | US 35.243.182.124, Europe 34.141.138.53, Canada 34.130.12.142, Australia 34.40.190.114 |
BoldSign's docs say a webhook that is marked for disabling recovers on its own if the endpoint starts answering again, and you can re-enable one by hand. You can also resend individual events from Webhook History.
Common events
BoldSign lists 33 events in four groups. You choose which ones each webhook receives.
| Group | Events | Fires when |
|---|---|---|
| Document progress | Sent, Viewed, Signed, Completed, SignerSaved, Reminder | A request is sent, a recipient views or signs it, all recipients finish, a signer saves progress without completing, or the sender sends a reminder |
| Document outcomes | Declined, Revoked, Expired, Reassigned | A recipient declines, the sender revokes, the request expires, or a signer passes it to someone else |
| Document changes and failures | Edited, EditFailed, DraftCreated, DeliveryFailed, SendFailed | A sent document is edited, an edit fails, a draft is created, email delivery fails, or the request fails to send |
| Authentication | AuthenticationFailed, IdentityVerificationInitiated, IdentityVerificationSucceeded, IdentityVerificationFailed, KBAInitiated, KBASucceeded, KBAFailed | Signer authentication fails, or identity verification or knowledge-based authentication starts, succeeds or fails |
| Sender identity | SenderIdentityCreated, SenderIdentityUpdated, SenderIdentityDeleted, SenderIdentityRevoked, SenderIdentityVerified, SenderIdentityDenied | A sender identity is created, changed, deleted, revoked, or its invitation is accepted or rejected |
| Templates | TemplateCreated, TemplateEdited, TemplateCreateFailed, TemplateDraftCreated, TemplateSendFailed | A template is created, edited or drafted, or creating or sending from one fails |
The setup Verification event is not in the list. BoldSign sends it only when you verify a webhook URL.
Every payload has an event object with metadata. Document and template events add a context object, and data holds the resource, with data.object naming its kind. Here is BoldSign's SignerSaved sample, trimmed:
{
"event": {
"id": "216e2497-857e-4849-96be-ce3b97f5e074",
"created": 1772023077,
"eventType": "SignerSaved",
"clientId": null,
"environment": "Live"
},
"context": {
"eventType": "SignerSaved",
"actor": { "userType": "Signer", "id": "13a3934e-0af7-46c5-9524-c883ccba96d0" },
"previousState": {}
},
"data": {
"object": "document",
"documentId": "91468ed5-9b0a-4da8-96d3-4e981f35b6a3",
"messageTitle": "API Test Document",
"status": "InProgress",
"senderDetail": { "name": "Sender", "emailAddress": "sender@email.com" },
"signerDetails": [
{ "signerName": "Signer", "signerEmail": "signer@email.com", "status": "NotCompleted", "isViewed": true }
],
"createdDate": 1772023043,
"expiryDate": 1772051843
}
}
event.id is unique per event and is the key to deduplicate on. event.created and the dates inside data are Unix seconds. clientId is null for account-level webhooks. Sender identity and template events carry a different object in data, so check data.object before reading document fields.
Setting up BoldSign webhooks
Have your endpoint running before you start: BoldSign will not save the webhook until the URL verifies.
- For an app-level webhook, create the OAuth app first under API > OAuth Apps.
- Go to API > Webhooks and click Add Webhook.
- Choose Account Level or App Level (and the OAuth app), enter a name and your HTTPS URL, and select the events you want.
- Optionally add custom headers, and tick Listen to Team user's events if you need team activity.
- Click Verify. BoldSign sends a
Verificationevent to the URL. - Click Save once verification succeeds.
- Open the webhook's overview page, click Reveal and store the signing secret server-side, for example as
BOLDSIGN_WEBHOOK_SECRET.
The verification request looks like this:
{
"event": {
"id": "7c872a5b-xxxx-xxxx-xxxx-92530a83a8b9",
"created": 1617180024,
"eventType": "Verification",
"environment": "Test"
}
}
It arrives with an X-BoldSign-Event: Verification header, and your endpoint must answer 200 OK within 10 seconds. BoldSign does not say whether this request is signed. Its own sample receivers return 200 for it before they check the signature, and so should yours, because you do not have the secret yet at this point.
Sandbox and production are separate. Each webhook has its own secret, so a secret from a test webhook will not verify live events.
Securing BoldSign webhooks
Split X-BoldSign-Signature on commas and trim each part, because BoldSign puts a space after every comma. Take t and every s0 or s1 value, compute an HMAC-SHA256 of <t>.<raw body> with your signing secret, encode it as lowercase hex, and accept the request if it matches either signature in constant time. Accepting s1 keeps events verifying while you roll the secret.
const crypto = require('crypto');
const express = require('express');
function parseBoldSignHeader(header) {
let timestamp;
const signatures = [];
for (const item of header.split(',')) {
const [key, ...rest] = item.trim().split('=');
const value = rest.join('=');
if (key === 't') timestamp = value;
else if (key === 's0' || key === 's1') signatures.push(value);
}
return { timestamp, signatures };
}
function verifyBoldSignSignature(rawBody, header, secret) {
if (!header || !secret) return false;
const { timestamp, signatures } = parseBoldSignHeader(header);
if (!/^\d+$/.test(timestamp ?? '') || signatures.length === 0) return false;
const expected = Buffer.from(
crypto.createHmac('sha256', secret).update(`${timestamp}.`).update(rawBody).digest('hex'),
'utf8'
);
return signatures.some((sig) => {
const received = Buffer.from(sig.toLowerCase(), 'utf8');
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
});
}
const app = express();
app.post('/webhooks/boldsign', express.raw({ type: '*/*' }), (req, res) => {
// The setup request: acknowledge it before any signature check.
if (req.get('x-boldsign-event') === 'Verification') return res.sendStatus(200);
const secret = process.env.BOLDSIGN_WEBHOOK_SECRET;
if (!secret) return res.sendStatus(500); // fail closed
if (!verifyBoldSignSignature(req.body, req.get('x-boldsign-signature'), secret)) {
return res.sendStatus(401);
}
// Optional: compare t with the current time. Retries keep the original t,
// so a short window rejects legitimate late deliveries.
const payload = JSON.parse(req.body.toString('utf8'));
// Route on payload.event.eventType, dedupe on payload.event.id, process async.
res.sendStatus(200);
});
The same check in Python:
import hashlib
import hmac
import re
def verify_boldsign_signature(raw_body: bytes, header, secret: str) -> bool:
if not header or not secret:
return False
timestamp, signatures = None, []
for item in header.split(","):
key, _, value = item.strip().partition("=")
if key == "t":
timestamp = value
elif key in ("s0", "s1"):
signatures.append(value)
if not timestamp or not re.fullmatch(r"[0-9]+", timestamp) or not signatures:
return False
expected = hmac.new(
secret.encode("utf-8"), timestamp.encode() + b"." + raw_body, hashlib.sha256
).hexdigest()
return any(
hmac.compare_digest(sig.lower().encode("utf-8"), expected.encode("utf-8"))
for sig in signatures
)
# In FastAPI: raw_body = await request.body()
# verify_boldsign_signature(raw_body, request.headers.get("x-boldsign-signature"),
# os.environ["BOLDSIGN_WEBHOOK_SECRET"])
BoldSign's samples also reject events whose t is more than 5 minutes old, and the docs invite you to adjust that. A timestamp check is reasonable on your own receiver, but BoldSign's retries, resends from Webhook History and any replay from a queue all carry the original t, so a tight window turns a short outage into lost events. If you add one, make it generous, and rely on event.id for duplicate protection.
Make BoldSign webhooks production-ready. Hookdeck Event Gateway verifies the
X-BoldSign-Signatureheader against the timestamp and raw body, answers BoldSign with an immediate200 OK, and durably queues every document, authentication and template event before your service sees it.
BoldSign webhook limitations and pain points
Verification blocks the save
The Problem: BoldSign will not save the webhook, and nothing has been delivered yet that you could inspect.
Why It Happens: The Verify step needs a 200 within 10 seconds. A handler that runs signature verification first, before you have even revealed the secret, rejects the Verification event. So does a server that is not running yet, or one that takes too long to respond.
Workarounds:
- Return
200forX-BoldSign-Event: Verificationbefore any signature check. - Start your server, and any tunnel, before you click Verify.
- Do no work for the
Verificationevent beyond acknowledging it.
How Hookdeck Can Help: Hookdeck records each request a source receives with its headers, so when a Verify attempt fails you can see what BoldSign sent and what came back. BoldSign does not document whether the verification request is signed, so check how it appears on your source the first time you add a webhook.
Rolling the secret breaks receivers that read only s0
The Problem: After you roll the signing secret, events start failing verification on services that have not been updated yet.
Why It Happens: When you roll the secret and keep the old one valid for a while, s0 is signed with the new secret and s1 with the old one. A receiver that only reads s0 and still holds the old secret fails every event until it is redeployed.
Workarounds:
- Accept a match on either
s0ors1, as in the code above. - Keep the old secret valid long enough to deploy the new one everywhere.
- Parse the header by key, not by position, so a missing
s1is not an error.
How Hookdeck Can Help: Hookdeck verifies X-BoldSign-Signature against the secret stored on the source, so updating one value updates verification for every service behind it.
The header format catches out simple parsers
The Problem: Verification fails even with the right secret and the raw body.
Why It Happens: BoldSign writes t=1668693823, s0=9b7a..., with a space after the comma. A parser that splits on , without trimming reads the key as s0, finds no signature and fails. Splitting each part on every = is a second trap if a value ever contains one.
Workarounds:
- Trim each comma-separated part before reading the key.
- Split on the first
=only. - Remember the signed string starts with the timestamp:
<t>.<raw body>, not the body alone.
How Hookdeck Can Help: Hookdeck's BoldSign source type parses the header and checks the signature for you, so this parsing lives in one place instead of in every receiver.
Retries end in a disabled webhook
The Problem: After a long outage, BoldSign stops sending events until someone re-enables the webhook.
Why It Happens: BoldSign treats anything other than a 200 within 10 seconds as a failure, retries six times over roughly 34 hours, and then watches the webhook for 7 days before disabling it if failures continue. BoldSign sends email notifications during this process, but a missed email still means missed signatures.
Workarounds:
- Acknowledge with
200straight after verification and process asynchronously. - Answer
200to event types you do not handle, so new events never count as failures. - Watch the disablement emails, and resend missed events from Webhook History after an outage.
How Hookdeck Can Help: Hookdeck acknowledges BoldSign immediately and stores every event, then retries delivery to your service on a schedule you control and lets you replay anything that failed, so your downtime does not count against BoldSign's limits.
Best practices
Verify against the raw body
The signature covers the exact bytes BoldSign sent, prefixed with the timestamp. In Express, use express.raw() on the webhook route. In Next.js, read await request.text(). In FastAPI, use await request.body(). Parsing and re-serialising the JSON changes the bytes and the signature will not match.
Deduplicate on event.id
Retries and manual resends deliver the same event again with the same event.id. Store the ids you have processed and skip repeats. See our guide to webhook idempotency.
Route on event.eventType and check data.object
event.eventType names the event. data.object tells you whether data is a document, a sender identity or a template, so read it before reaching for documentId or signerDetails.
Separate test and live traffic
event.environment is Test for sandbox activity and Live for production. If one receiver handles both, branch on it so test signatures never trigger real fulfilment.
Fetch the signed document after Completed
The Completed payload tells you signing is finished. Download the final PDF and audit trail from the BoldSign API rather than relying on fields in the webhook body.
Acknowledge fast and process asynchronously
BoldSign allows 10 seconds. Verify, persist the event, return 200, and do the work afterwards. See why to process webhooks asynchronously.
Allowlist the source IPs if you need to
BoldSign publishes one sending address per region. An IP allowlist is a useful extra layer, but the signature remains the check that proves the event is genuine and unaltered.
Conclusion
BoldSign signs every event with X-BoldSign-Signature, a hex HMAC-SHA256 of the timestamp and raw body under the webhook's secret, and adds an s1 signature from the old secret while a rolled secret is still valid. Acknowledge the Verification event before checking signatures, verify against the raw bytes, accept either signature, deduplicate on event.id, and respond within 10 seconds so the retry schedule never leads to a disabled webhook.
Hookdeck Event Gateway sits in front of your receiver, checks every BoldSign signature, and holds each event until your service has processed it, so an outage on your side means a delay rather than lost signature events.
Get started with Hookdeck for free and handle BoldSign webhooks reliably in minutes.