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 |
| Recipient approved | recipient_approved | Sent when an approver (recipient with signingSchema: "approve") approves the documents |
| 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 |
| Delivery failed | case.delivery.failed | Sent when a delivery to a recipient fails: an e-Boks delivery that is rejected, for example because the CPR is not registered for Digital Post, or a case email (invitation or reminder) that bounces. Carries an event object, see Delivery Failed Payload |
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"
}
}Delivery Failed Payload
case.delivery.failed fires once per failed delivery to a recipient, next to the usual case (or flow) object. The event object says who it was, through which channel, and why. channel is eboks or email.
e-Boks
{
"trigger": "case.delivery.failed",
"case": { "_id": "66263484e601d81006d1a132", "title": "Contract Agreement" },
"event": {
"channel": "eboks",
"recipientId": "66263484e601d81006d1a133",
"recipientName": "Andreas Lauridsen",
"errorMessage": "invalid-address"
}
}errorMessage is either e-Boks' own rejection text (for example when the recipient is not registered for Digital Post) or one of these codes: invalid-cpr, invalid-recipient, invalid-address, invalid-integration (e-Boks is not set up for the company) and invalid-template.
A delivery that fell back to a physical letter was delivered, so it does not fire this trigger.
Fires when a case email to a recipient bounces. That is the invitation or a reminder.
{
"trigger": "case.delivery.failed",
"case": { "_id": "66263484e601d81006d1a132", "title": "Contract Agreement" },
"event": {
"channel": "email",
"recipientId": "66263484e601d81006d1a133",
"recipientName": "Andreas Lauridsen",
"email": "andreas@exampel.dk",
"bounceType": "hard",
"errorMessage": "Host or domain name not found"
}
}| Field | Description |
|---|---|
email | The address that bounced. Compare it with the recipient's current address: if the address was corrected in the meantime, this bounce is about the old one |
bounceType | hard: the address does not exist, or the receiving server refused it permanently. soft: a temporary problem, such as a full mailbox, that did not clear while our email provider kept retrying. Either way, that email will not arrive |
errorMessage | The receiving server's reason when it gave one, otherwise email-bounced |
fallbackChannel | Only present when the recipient was part of a mass sending with a fallback order, and the case was resent to them through this channel (sms or eboks). A failure on that channel is reported on its own |
The same bounce also shows on the case itself, as recipients[].emailStatus in Retrieve a Case.
There is no webhook for an email being opened. Opens are recorded in emailStatus, but they are too unreliable to act on, see the note there.
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.