NextSign API
Events

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.

LabelTrigger ValueDescription
Case createdcase_createdSent when a case is created
Recipient signedrecipient_signedSent when a recipient signs
Recipient approvedrecipient_approvedSent when an approver (recipient with signingSchema: "approve") approves the documents
Case signedcase_signedSent when the case is fully signed
Case deniedcase_deniedSent when a recipient denies the case
Workflow completedwebflow.completedSent when a webflow completes
Case reminder sentcase.reminder.sentSent when a reminder email is sent
Case expiredcase.expiry.expiredSent when a case expires
Case expiry 7 dayscase.expiry.7daysSent when a case is 7 days from expiry
Delivery failedcase.delivery.failedSent 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/nextsign

Localhost 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:

KeyValue
X-Webhook-Secretyour-shared-secret
X-API-Version2026-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 2xx HTTP 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:

Example Case Payload
{
  "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:

Example Flow Payload
{
  "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

Example Delivery Failed Payload (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.

Email

Fires when a case email to a recipient bounces. That is the invitation or a reminder.

Example Delivery Failed Payload (email)
{
  "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"
  }
}
FieldDescription
emailThe 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
bounceTypehard: 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
errorMessageThe receiving server's reason when it gave one, otherwise email-bounced
fallbackChannelOnly 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

FieldTypeDescription
triggerstringMachine-readable trigger value such as case_signed
caseobjectPresent for case-related triggers
flowobjectPresent for flow-related triggers
case._idstringCase identifier, when case is present
flow._idstringFlow 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

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

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 3000

Then 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 OK quickly
  • 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.