Webhooks
Webhooks let you receive HTTP POST requests to your server when events happen in VisiSign — such as a document being signed or completed. Use them to trigger downstream automations, send notifications, or sync data with other systems.
Creating a webhook endpoint
Section titled “Creating a webhook endpoint”- Go to Settings > Webhooks in VisiSign.
- Enter the HTTPS URL that will receive events.
- Select which events to subscribe to (or leave all selected).
- Click Add Endpoint. The signing secret is shown once — copy it immediately.
Supported events
Section titled “Supported events”| Event | Fires when |
|---|---|
signature_request.sent | A signature request is sent to signers |
signature_request.viewed | A signer opens the signing link |
signature_request.signed | A signer completes their signature |
signature_request.declined | A signer declines to sign |
signature_request.completed | All signers have signed |
signature_request.expired | The request reaches its expiration date |
signature_request.cancelled | The request is cancelled by the sender |
Subscribe to all events by leaving the events array empty, or select specific ones.
Payload format
Section titled “Payload format”Every webhook delivery is an HTTP POST with a JSON body:
{ "event": { "type": "signature_request.signed", "timestamp": "2026-03-04T12:00:00Z" }, "signature_request": { "id": "sr_123", "title": "Service Agreement", "status": "sent", "form_data": {}, "signers": [ { "id": "sig_456", "name": "Jane Smith", "email": "jane@example.com", "status": "signed", "signed_at": "2026-03-04T12:00:00Z" } ] }}Form data extraction
Section titled “Form data extraction”When fields have a mapping_key set (either via the API or the template builder), VisiSign extracts the values signers entered and includes them in the form_data object. This is useful for structured forms like W-9s, onboarding documents, or any form where you need the field values programmatically.
For example, a completed W-9 webhook might include:
{ "signature_request": { "status": "completed", "form_data": { "taxpayer_name": "Jane Smith", "tin": "123-45-6789", "address": "123 Main St, Springfield, IL 62704", "tax_classification": "Individual" }, "files_url": "/v1/signature_requests/sr_123/files" }}form_data is populated when the signature_request.completed event fires. For other events (sent, viewed, signed), it will be an empty object {}.
Verifying signatures
Section titled “Verifying signatures”Each webhook endpoint has a signing secret (prefixed whsec_). Use it to verify that deliveries are genuinely from VisiSign by checking the X-VisiSign-Signature header.
Header format
Section titled “Header format”The header carries a Stripe-style comma-separated list of key/value pairs:
X-VisiSign-Signature: t=1734567890,v1=2c1e85f4...0dtis the Unix timestamp (seconds) at which the delivery was signed.v1is the lowercase hex HMAC-SHA256 of the string<t>.<raw_body>using your signing secret as the key.
To verify, recompute HMAC_SHA256(secret, "<t>.<raw_body>") and constant-time compare against v1. Reject deliveries where t is older than ~5 minutes to defend against replay attacks.
Python
Section titled “Python”import hmac, hashlib, time
def verify_signature(raw_body: bytes, header: str, secret: str, max_age: int = 300) -> bool: parts = dict(kv.split("=", 1) for kv in header.split(",")) t, received = parts.get("t"), parts.get("v1") if not t or not received: return False if abs(time.time() - int(t)) > max_age: return False expected = hmac.new( secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256, ).hexdigest() return hmac.compare_digest(expected, received)parts = header.split(",").map { |kv| kv.split("=", 2) }.to_ht, received = parts["t"], parts["v1"]return false unless t && receivedreturn false if (Time.now.to_i - t.to_i).abs > 300
expected = OpenSSL::HMAC.hexdigest("SHA256", secret, "#{t}.#{raw_body}")Rack::Utils.secure_compare(expected, received)Node.js
Section titled “Node.js”import { createHmac, timingSafeEqual } from "crypto";
function verifySignature(rawBody, header, secret, maxAgeSeconds = 300) { const parts = Object.fromEntries( header.split(",").map((kv) => { const i = kv.indexOf("="); return [kv.slice(0, i).trim(), kv.slice(i + 1).trim()]; }), ); const t = parts.t, received = parts.v1; if (!t || !received) return false; if (Math.abs(Date.now() / 1000 - Number(t)) > maxAgeSeconds) return false;
const expected = createHmac("sha256", secret) .update(`${t}.${rawBody}`) .digest("hex"); const a = Buffer.from(expected); const b = Buffer.from(received); return a.length === b.length && timingSafeEqual(a, b);}Testing
Section titled “Testing”Send a test event from the VisiSign dashboard by clicking Test next to any webhook endpoint. This sends a delivery with a test payload so you can verify your endpoint is receiving and processing events correctly.
Failure handling
Section titled “Failure handling”Endpoints that fail to return a 2xx response accumulate consecutive failures. After 10 consecutive failures, the endpoint is automatically disabled. Fix your endpoint and re-create it in Settings.
Request headers
Section titled “Request headers”Each delivery includes these headers:
| Header | Description |
|---|---|
Content-Type | application/json |
User-Agent | VisiSign-Webhooks/1.0 |
X-VisiSign-Event | The event type (e.g. signature_request.signed) |
X-VisiSign-Signature | Stripe-style t=<unix_ts>,v1=<hex> HMAC-SHA256 over <t>.<raw_body> (see Verifying signatures) |