# 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.clientId` identifies 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:

```json
{
  "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.

1. For an app-level webhook, create the OAuth app first under **API > OAuth Apps**.
2. Go to **API > Webhooks** and click **Add Webhook**.
3. Choose **Account Level** or **App Level** (and the OAuth app), enter a name and your HTTPS URL, and select the events you want.
4. Optionally add custom headers, and tick **Listen to Team user's events** if you need team activity.
5. Click **Verify**. BoldSign sends a `Verification` event to the URL.
6. Click **Save** once verification succeeds.
7. 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:

```json
{
  "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.

```javascript
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:

```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](/event-gateway) verifies the `X-BoldSign-Signature` header against the timestamp and raw body, answers BoldSign with an immediate `200 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 `200` for `X-BoldSign-Event: Verification` before any signature check.
* Start your server, and any tunnel, before you click **Verify**.
* Do no work for the `Verification` event 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 `s0` or `s1`, 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 `s1` is 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 `200` straight after verification and process asynchronously.
* Answer `200` to 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](/webhooks/guides/implement-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](/webhooks/guides/why-implement-asynchronous-processing-webhooks).

### 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](https://hookdeck.com) 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](https://dashboard.hookdeck.com/signup) for free and handle BoldSign webhooks reliably in minutes.