Webhooks
Receive real-time HTTP notifications when document events occur in your account.
Webhooks let your application receive real-time POST requests whenever something happens to your documents — created, sent, signed, completed, and more.
Setting Up a Webhook
- Go to Settings > Webhooks
- Click Add Webhook
- Enter your endpoint URL (must be HTTP or HTTPS)
- Select the events you want to subscribe to
- Save — a signing secret (
whsec_...) is generated and shown once. Copy it immediately.
Event Types
| Event | Description |
|---|---|
* | Subscribe to all events |
document.created | A new document was created |
document.updated | A document's title or metadata was updated |
document.deleted | A document was deleted |
document.sent | A document was sent to recipients for signing |
document.viewed | A recipient opened the signing page |
document.signed | A recipient signed or approved the document |
document.completed | All recipients have signed — document is fully complete |
HTTP Request Format
Every webhook delivery is a POST request to your endpoint URL with a JSON body.
Headers
| Header | Description |
|---|---|
Content-Type | application/json |
User-Agent | SignSecure-Webhooks/1.0 |
X-SignSecure-Event | The event type, e.g. document.signed |
X-SignSecure-Delivery | Unique delivery ID, e.g. evt_abc123 |
X-SignSecure-Signature | Signature for verification (see below) |
Envelope
Every webhook payload has the same top-level structure:
{
"id": "evt_abc123",
"event": "document.signed",
"createdAt": "2026-03-11T10:30:00.000Z",
"data": {
// event-specific fields — see below
}
}| Field | Type | Description |
|---|---|---|
id | string | Unique event ID (evt_ prefix) |
event | string | The event type |
createdAt | string | ISO 8601 timestamp |
data | object | Event-specific payload |
Event Payloads
document.created
Fired when a new document is created.
{
"id": "evt_abc123",
"event": "document.created",
"createdAt": "2026-03-11T10:30:00.000Z",
"data": {
"documentId": "doc_xyz789",
"title": "Employment Agreement",
"fileName": "employment-agreement.pdf",
"status": "draft",
"createdAt": "2026-03-11T10:30:00.000Z",
"recipientsCount": 2
}
}document.updated
Fired when a document's title or metadata changes.
{
"id": "evt_abc123",
"event": "document.updated",
"createdAt": "2026-03-11T10:35:00.000Z",
"data": {
"documentId": "doc_xyz789",
"title": "Employment Agreement v2",
"status": "draft",
"updatedAt": "2026-03-11T10:35:00.000Z",
"changes": {
"title": "Employment Agreement v2"
}
}
}document.sent
Fired when a document is sent to recipients for signing.
{
"id": "evt_abc123",
"event": "document.sent",
"createdAt": "2026-03-11T11:00:00.000Z",
"data": {
"documentId": "doc_xyz789",
"title": "Employment Agreement",
"sentAt": "2026-03-11T11:00:00.000Z",
"recipients": [
{
"name": "Jane Doe",
"email": "jane@example.com",
"role": "signer"
},
{
"name": "Bob Smith",
"email": "bob@example.com",
"role": "approver"
}
]
}
}document.viewed
Fired when a recipient opens the document signing page.
{
"id": "evt_abc123",
"event": "document.viewed",
"createdAt": "2026-03-11T11:15:00.000Z",
"data": {
"documentId": "doc_xyz789",
"title": "Employment Agreement",
"status": "pending",
"viewedBy": {
"name": "Jane Doe",
"email": "jane@example.com",
"role": "signer"
},
"viewedAt": "2026-03-11T11:15:00.000Z"
}
}document.signed
Fired when a recipient signs or approves the document.
{
"id": "evt_abc123",
"event": "document.signed",
"createdAt": "2026-03-11T11:20:00.000Z",
"data": {
"documentId": "doc_xyz789",
"title": "Employment Agreement",
"signedBy": {
"name": "Jane Doe",
"email": "jane@example.com",
"signatureMethod": "electronic",
"actionType": "signed"
},
"signedAt": "2026-03-11T11:20:00.000Z",
"remainingRecipients": 1
}
}signatureMethod is one of: electronic, aadhaar_otp, dsc_usb.
actionType is either signed or approved.
document.completed
Fired when all recipients have signed and the document is fully complete.
{
"id": "evt_abc123",
"event": "document.completed",
"createdAt": "2026-03-11T11:30:00.000Z",
"data": {
"documentId": "doc_xyz789",
"title": "Employment Agreement",
"completedAt": "2026-03-11T11:30:00.000Z",
"recipients": [
{
"name": "Jane Doe",
"email": "jane@example.com",
"signedAt": "2026-03-11T11:20:00.000Z"
},
{
"name": "Bob Smith",
"email": "bob@example.com",
"signedAt": "2026-03-11T11:28:00.000Z"
}
]
}
}Verifying Signatures
Every webhook request includes an X-SignSecure-Signature header. Use it to verify the request came from SignSecure and wasn't tampered with.
The header format is:
t=1710150600,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bdt— Unix timestamp (seconds) when the signature was generatedv1— HMAC-SHA256 hex digest
Verification Steps
- Extract
tandv1from the header - Construct the signed payload:
{t}.{raw_json_body} - Compute HMAC-SHA256 of the signed payload using your webhook secret
- Compare the computed signature with
v1
Node.js Example
import crypto from "node:crypto";
function verifyWebhookSignature(rawBody, signatureHeader, secret) {
const parts = Object.fromEntries(
signatureHeader.split(",").map((p) => p.split("="))
);
const timestamp = parts.t;
const signature = parts.v1;
const signedPayload = `${timestamp}.${rawBody}`;
const expected = crypto
.createHmac("sha256", secret)
.update(signedPayload)
.digest("hex");
if (expected !== signature) {
throw new Error("Invalid webhook signature");
}
// Optional: reject if timestamp is too old (e.g. > 5 minutes)
const age = Math.floor(Date.now() / 1000) - parseInt(timestamp, 10);
if (age > 300) {
throw new Error("Webhook timestamp too old");
}
return true;
}Python Example
import hmac
import hashlib
import time
def verify_webhook_signature(raw_body: str, signature_header: str, secret: str):
parts = dict(p.split("=", 1) for p in signature_header.split(","))
timestamp = parts["t"]
signature = parts["v1"]
signed_payload = f"{timestamp}.{raw_body}"
expected = hmac.new(
secret.encode(), signed_payload.encode(), hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected, signature):
raise ValueError("Invalid webhook signature")
# Optional: reject stale timestamps
age = int(time.time()) - int(timestamp)
if age > 300:
raise ValueError("Webhook timestamp too old")
return TrueRetry Behavior
If your endpoint doesn't respond with a 2xx status code (or times out after 30 seconds), SignSecure retries the delivery:
| Attempt | Delay |
|---|---|
| 1st | Immediate |
| 2nd | 1 minute later |
| 3rd | 10 minutes later |
After 3 failed attempts, the delivery is marked as permanently failed. You can manually retry failed deliveries from the webhook detail page in Settings > Webhooks.
Best Practices
- Respond quickly — return a
200status code as fast as possible. Process the event asynchronously if needed. - Verify signatures — always validate the
X-SignSecure-Signatureheader before trusting the payload. - Handle duplicates — use the
idfield to deduplicate events in case of retries. - Use HTTPS — always use an HTTPS endpoint in production.
- Monitor deliveries — check the webhook delivery log in Settings to catch failures early.