Agent skill

BoldSign Webhooks Skill

Receive and verify BoldSign webhooks. Use when setting up BoldSign e-signature webhook handlers, debugging X-BoldSign-Signature verification, answering the Verification event during webhook setup, or handling events like Sent, Viewed, Signed, Completed, Declined, Revoked, and Expired.

Install this skill

npx skills add hookdeck/webhook-skills --skill boldsign-webhooks


When to Use This Skill

  • How do I receive BoldSign webhooks?
  • How do I verify the X-BoldSign-Signature header (t=..., s0=..., s1=...)?
  • How do I respond to the BoldSign Verification event when saving a webhook?
  • How do I handle Signed, Completed, or Declined e-signature events?
  • Why is my BoldSign webhook signature verification failing?

Essential Code (USE THIS)

BoldSign signs every event with X-BoldSign-Signature:

X-BoldSign-Signature: t=1668693823, s0=9b7adf82...ca9
X-BoldSign-Signature: t=1668708521, s0=c10e26bf...54ab, s1=62a1abe1...21f6   (during key rollover)
  • t is the Unix timestamp (seconds) of the event; note the space after each comma.
  • s0 is HMAC-SHA256 (hex) of "<t>.<raw body>" keyed with the webhook signing secret (reveal it on the webhook's overview page).
  • s1 is the same HMAC made with the old key, present only while a rolled-over secret is still valid. Accept a match on either.
  • The event name is in event.eventType in the JSON body (also sent in the X-BoldSign-Event header).
  • Setup sends a Verification POST (X-BoldSign-Event: Verification). Answer it with a 200 within 10 seconds; no event processing is needed.

Verification (Node.js)

const crypto = require('crypto');

// rawBody: Buffer of the exact request bytes (express.raw({ type: '*/*' }), not express.json)
function verifyBoldSign(rawBody, signatureHeader, secret) {
  if (!secret || !signatureHeader) return false;
  let timestamp;
  const signatures = [];
  for (const item of signatureHeader.split(',')) {
    const [key, ...rest] = item.trim().split('=');
    if (key === 't') timestamp = rest.join('=');
    else if (key === 's0' || key === 's1') signatures.push(rest.join('='));
  }
  if (!/^\d+$/.test(timestamp ?? '') || signatures.length === 0) return false;
  // Optional replay window, off by default: retries and replays keep the original timestamp
  const expected = crypto.createHmac('sha256', secret)
    .update(`${timestamp}.`).update(rawBody).digest('hex');
  return signatures.some((sig) => {
    const a = Buffer.from(sig), b = Buffer.from(expected);
    return a.length === b.length && crypto.timingSafeEqual(a, b);
  });
}

Verification (Python)

import hashlib, hmac, re

def verify_boldsign(raw_body: bytes, signature_header, secret: str) -> bool:
    if not secret or not signature_header:
        return False
    timestamp, signatures = None, []
    for item in signature_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(), timestamp.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(s.encode(), expected.encode()) for s in signatures)

BoldSign's docs suggest rejecting events older than 5 minutes. The examples leave that off by default, because Hookdeck retries and replays (and BoldSign's own retries) keep the original t. Set BOLDSIGN_TIMESTAMP_TOLERANCE_SECONDS to a positive number to opt in. Use the event id for idempotency instead.

For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.

Common Event Types

The type is in event.eventType. Subscribe to the ones you need when creating the webhook.

EventDescription
SentA signature request is sent to recipients
ViewedA recipient views the document
SignedA recipient signs the document
CompletedSigning completed by all recipients
DeclinedA recipient declines the signature request
RevokedSender revokes the signature request
ExpiredThe signature request expires without completion

Others include Edited, Reassigned, Reminder, DeliveryFailed, SendFailed, AuthenticationFailed, identity verification and KBA events, sender identity events, and template events. See references/overview.md.

For the full event reference, see BoldSign available events.

Important Headers

HeaderDescription
X-BoldSign-Signaturet=<unix seconds>, s0=<hex>[, s1=<hex>]
X-BoldSign-EventEvent type; Verification during webhook setup

Environment Variables

BOLDSIGN_WEBHOOK_SECRET=your_webhook_signing_secret   # "Reveal" on the webhook overview page
BOLDSIGN_TIMESTAMP_TOLERANCE_SECONDS=0   # optional; a positive value opts in to the replay window

Local Development

# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 boldsign --path /webhooks/boldsign

Reference Materials


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 3, 2026

View on GitHub →