Webhooks
Receive real-time notifications when case and flow events occur in NextSign.
Webhooks
NextSign webhooks let NextSign push case and flow events to your system in near real time. Use them when you want your backend to react to signing activity without polling the API.
Common Use Cases
- Update internal systems when a case is created, signed, or denied
- Start document archival or downstream processing after signing
- Keep CRM or ERP records in sync with NextSign activity
- React to reminder and expiry events in your own workflow engine
- Handle completed webflows without polling for status changes
Where to Configure Webhooks
Manage webhooks from the Webhooks page in the NextSign dashboard.
From there you can create, edit, enable or disable webhooks, configure custom headers, and inspect recent delivery runs.
Creating a Webhook
Name
Use a descriptive internal name so the webhook is easy to identify in the dashboard.
Example: Signed case webhook
Triggers (Required)
Select one or more trigger values. Each selected trigger can independently deliver a webhook request to the same URL.
| Label | Trigger Value | Description |
|---|---|---|
| Case created | case_created | Sent when a case is created |
| Recipient signed | recipient_signed | Sent when a recipient signs |
| Case signed | case_signed | Sent when the case is fully signed |
| Case denied | case_denied | Sent when a recipient denies the case |
| Workflow completed | webflow.completed | Sent when a webflow completes |
| Case reminder sent | case.reminder.sent | Sent when a reminder email is sent |
| Case expired | case.expiry.expired | Sent when a case expires |
| Case expiry 7 days | case.expiry.7days | Sent when a case is 7 days from expiry |
You can attach multiple trigger values to a single webhook configuration.
URL (Required)
This is the public endpoint on your server that will receive webhook deliveries.
https://example.com/webhooks/nextsignLocalhost URLs will not receive webhook traffic unless you expose them with a tunneling service such as ngrok.
Method
NextSign supports POST, GET, PUT, and DELETE delivery methods.
- Default:
POST - Recommended:
POST
If you do not configure a Content-Type header yourself, NextSign sends application/json.
Active
Use the active toggle to enable or pause delivery without deleting the webhook configuration.
- On: events are delivered
- Off: events are ignored for this webhook
Headers (Optional)
Custom headers are sent exactly as configured in the dashboard.
Example:
| Key | Value |
|---|---|
X-Webhook-Secret | your-shared-secret |
X-API-Version | 2026-03 |
Common uses:
- Pass a shared secret your server can validate
- Add routing metadata for your integration
- Version your internal webhook consumer
NextSign does not generate an HMAC signature automatically for webhook requests. If you want to verify origin, configure a shared secret header such as X-Webhook-Secret and compare the exact header value in your handler.
Delivery Behavior
When a selected trigger fires, NextSign sends an HTTP request to the configured URL. The request body contains the machine-readable trigger name plus either a case object or a flow object, depending on what caused the webhook.
Expected Response
Your endpoint should:
- Return a
2xxHTTP status code to acknowledge successful delivery - Respond quickly and offload slow work to background jobs or queues
- Treat retries as possible duplicate deliveries
Retry Logic
If delivery fails or your endpoint returns a non-2xx response, NextSign retries using exponential backoff.
- First retry: about 2 minutes after the failed attempt
- Then approximately: 4, 8, 16, 32 minutes, and so on
- Retries continue until delivery succeeds or the retry limit is reached
- The current retry limit is up to 14 total delivery attempts
Webhook deliveries can be repeated when retries happen. Make your webhook handler idempotent.
For a focused guide, see Webhook Retry Logic.
Payload Structure
Case Event Payload
Case-related triggers send a case object:
{
"trigger": "case_signed",
"case": {
"_id": "66263484e601d81006d1a132",
"title": "Contract Agreement",
"updatedAt": "2024-01-15T14:30:00.000Z",
"recipients": [
{
"name": "Andreas Lauridsen",
"email": "al@nextengine.dk"
}
],
"signedDocuments": [
{
"name": "Contract.pdf",
"file": "https://nextsign.hel1.your-objectstorage.com/..."
}
]
}
}Flow Event Payload
Flow-related triggers send a flow object:
{
"trigger": "webflow.completed",
"flow": {
"_id": "66a3c3bdb42398dff0d10123",
"title": "Lead Intake",
"updatedAt": "2024-01-15T14:30:00.000Z"
}
}Payload Fields
| Field | Type | Description |
|---|---|---|
trigger | string | Machine-readable trigger value such as case_signed |
case | object | Present for case-related triggers |
flow | object | Present for flow-related triggers |
case._id | string | Case identifier, when case is present |
flow._id | string | Flow identifier, when flow is present |
The case or flow object contains the current resource data at delivery time. Exact fields can vary depending on the trigger and the resource state.
Implementing a Webhook Endpoint
Node.js / Express
const express = require('express');
const app = express();
app.use(express.json());
app.post('/webhooks/nextsign', (req, res) => {
if (req.headers['x-webhook-secret'] !== process.env.NEXTSIGN_WEBHOOK_SECRET) {
return res.status(401).send('Unauthorized');
}
const { trigger, case: webhookCase, flow } = req.body;
const entity = webhookCase || flow;
switch (trigger) {
case 'case_signed':
console.log(`Case signed: ${entity?._id}`);
break;
case 'case_denied':
console.log(`Case denied: ${entity?._id}`);
break;
case 'webflow.completed':
console.log(`Flow completed: ${entity?._id}`);
break;
default:
console.log(`Unhandled trigger: ${trigger}`);
}
res.status(200).send('OK');
});
app.listen(3000);Python / Flask
from flask import Flask, request, jsonify
import os
app = Flask(__name__)
@app.route('/webhooks/nextsign', methods=['POST'])
def webhook():
if request.headers.get('X-Webhook-Secret') != os.getenv('NEXTSIGN_WEBHOOK_SECRET'):
return jsonify({'error': 'Unauthorized'}), 401
payload = request.json or {}
trigger = payload.get('trigger')
entity = payload.get('case') or payload.get('flow')
if trigger == 'case_signed':
print(f"Case signed: {entity.get('_id')}")
elif trigger == 'case_denied':
print(f"Case denied: {entity.get('_id')}")
elif trigger == 'webflow.completed':
print(f"Flow completed: {entity.get('_id')}")
else:
print(f"Unhandled trigger: {trigger}")
return jsonify({'status': 'ok'}), 200
if __name__ == '__main__':
app.run(port=3000)Best Practices
Use HTTPS
Serve your webhook endpoint over HTTPS so request data stays encrypted in transit.
Validate a Shared Secret Header
If you configure a header such as X-Webhook-Secret, compare it directly against a secret stored in your environment.
Process Asynchronously
Return a success response quickly, then hand off heavier work such as document downloads, CRM sync, or notifications to background processing.
Implement Idempotency
A practical idempotency key can be built from the trigger plus the resource ID and update timestamp.
function getIdempotencyKey(payload) {
const entity = payload.case || payload.flow;
return [payload.trigger, entity?._id, entity?.updatedAt].filter(Boolean).join(':');
}See Idempotency for request-side guidance as well.
Testing Webhooks
Local Development with ngrok
Expose your local server:
ngrok http 3000Then configure the generated HTTPS URL in the NextSign dashboard.
Manual Test Request
curl -X POST https://your-domain.com/webhooks/nextsign \
-H "Content-Type: application/json" \
-H "X-Webhook-Secret: your-shared-secret" \
-d '{
"trigger": "case_signed",
"case": {
"_id": "test-case-123",
"title": "Test Agreement",
"updatedAt": "2024-01-15T14:30:00.000Z"
}
}'Troubleshooting
No Webhooks Arrive
- Verify the webhook is active in the dashboard
- Confirm the URL is public and reachable
- Confirm the correct trigger values are selected
- Review webhook runs in the dashboard to see failed attempts
401 or 403 Responses
- Verify the shared secret header name matches exactly
- Verify the expected header value matches the configured value
- Check for proxy or load balancer rules stripping custom headers
Duplicate Deliveries
- Expect retries after failures or timeouts
- Use idempotency keys when storing or processing events
Timeouts
- Return
200 OKquickly - Move slow processing to workers or queues
- Avoid downloading large files inline before acknowledging the webhook
Use the webhook runs view in the dashboard to inspect recent deliveries, statuses, and retry behavior.