API reference

Sign documents with one API call

Nex-Sign is an embeddable e-signature API. Put placeholder tokens like $signature in your DOCX, POST it with a small JSON payload, and we convert it, email each signer a secure magic link, capture the signature, and return a sealed, tamper-evident PDF — then notify your webhook. Base URL https://nex-sign.com. Full machine-readable reference: openapi.json.

Quickstart

  1. Get a token — create an account and copy your API token (shown once, re-copyable from the dashboard).
  2. Prepare a DOCX — type a token wherever a signature or date goes, e.g. Signed: $signature and Date: $date_signed.
  3. Send it — one authenticated POST with the file + a JSON payload that maps each token to a signer.
  4. We handle the rest — conversion, delivery, signing, sealing, and webhooks.
Start free — 20 documents

Authentication

Every request is authenticated with a Bearer token; the business is inferred from the token. Every response uses a uniform envelope — success, data, error, meta.

Authorization: Bearer <YOUR_TOKEN>

Send a document

POST /v1/envelopes — a multipart request with one or more files and a JSON payload. An envelope can hold several documents and several signers. Returns 202 Accepted; signing runs asynchronously.

curl -X POST https://nex-sign.com/v1/envelopes \
  -H "Authorization: Bearer <YOUR_TOKEN>" \
  -F "files=@agreement.docx" \
  -F 'payload={
    "subject": "Please sign your agreement",
    "externalId": "order-1024",
    "recipients": [
      { "email": "signer@acme.com", "fullName": "Alex Client" }
    ],
    "fields": [
      { "documentName": "agreement.docx",
        "recipientEmail": "signer@acme.com",
        "type": "signature",
        "anchorKeyword": "$signature" },
      { "documentName": "agreement.docx",
        "recipientEmail": "signer@acme.com",
        "type": "date_signed",
        "anchorKeyword": "$date_signed" }
    ]
  }'
  • recipients — the people who sign (email + fullName; optional routingOrder).
  • fields — where each field goes: a type, an anchorKeyword to find in the DOCX, the recipientEmail it belongs to, and which document — by documentIndex (0-based upload position, recommended) or documentName (rejected if two files share a name).
  • externalId (optional) — your own reference, echoed back on the envelope and every webhook.
  • sequential (optional) — see signing order.

Tip — one signature, many spots. If the same anchorKeyword appears several times in a document (e.g. a signature on page 1 and page 8), you declare it once. We place a signing block at every occurrence, and the signer adopts a signature a single time to fill them all.

Field types

Two field types cover the SES tier. Each field maps one anchor to one recipient.

signatureA signature the recipient adopts (typed or drawn). Placed as an image at the anchor.
date_signedThe date the recipient signs — filled automatically as text, no input needed.

Placing fields — text tokens or bookmarks

Choose per field how we locate it in the DOCX:

  • Text token (anchorType: "text", the default) — anchorKeyword is any literal string in the document, e.g. $signature or $date_signed. Just type it where the field should appear. Works in any font, size, weight, or table cell, and at every occurrence. Pick tokens unlikely to appear in real prose.
  • Word bookmark (anchorType: "bookmark") — anchorKeyword is a Word bookmark name. Bookmarks are structural, so they can never collide with visible text. In Word: click where the field goes → Insert → Bookmark → name it (e.g. nxsig1).

Your tokens don’t show in the output. Every declared anchor is replaced (a signature image, or the date text) and hidden in the sealed PDF and signed DOCX — you never see $signature in the result. Undeclared tokens are left untouched, so only place tokens you map to a field.

Signing order

Default is parallel — every recipient is emailed at once. Set "sequential": true for a chain: each signer is emailed only after the previous one signs (order from routingOrder, or recipient position). Any number of signers.

Reminders

Nex-Sign chases unsigned recipients for you, so you don’t have to build a scheduler. There are two paths, and both send the same reminder email — a distinct template, subject Reminder: please sign …, never a second copy of the original invite.

Automatic — one nudge, no code

Every recipient who still hasn’t signed 24 hours after their invite is emailed once, with a fresh magic link. Once per recipient, ever. It is cancelled automatically if they sign, if their invite bounces, if the envelope is completed or voided, or if you remind them yourself first — a human chase replaces the system one. Reminders consume no document credits.

On demand — POST /v1/envelopes/:id/remind

One authenticated call, no body. It emails every recipient on the envelope who hasn’t signed — you don’t name them, and anyone who already signed, declined or bounced is skipped. On a sequential envelope only the current step is emailed. The response is 202: delivery is queued, so it confirms acceptance, not that mail has left.

curl -X POST \
  https://nex-sign.com/v1/envelopes/9fa2c8e1-2b7d-4f0a-8c11-5e6d3a1b0c42/remind \
  -H "Authorization: Bearer <YOUR_TOKEN>"

HTTP/1.1 202 Accepted
{ "success": true, "data": { "id": "9fa2c8e1-...", "status": "sent", ... },
  "error": null, "meta": null }

// Rate limited — nothing was sent. Wait for Retry-After, then try again.
HTTP/1.1 429 Too Many Requests
Retry-After: 18000
{
  "success": false,
  "data": null,
  "error": {
    "code": "rate_limited",
    "message": "Everyone on this envelope was reminded in the last 6 hours."
  },
  "meta": null
}

Rate limits

The manual endpoint is rate limited on three levels, so a retry loop can’t bury a signer in email. Hit any of them and nothing is sent — the call returns 429 with error.code: "rate_limited" and a Retry-After header in seconds. Wait it out; don’t poll.

Per signerOne reminder email per recipient per 6 hours. Signers still inside the window are skipped, not rejected — a remind on a 3-signer envelope still reaches the other two.
Per envelopeUp to 3 reminds per document per rolling 24 hours.
Per accountUp to 200 reminder emails per rolling hour across every envelope — the layer that catches a script reminding thousands of documents at once.

The automatic reminder is exempt: it is already capped at one per signer and no caller can drive it.

What you get back

The create call returns the envelope immediately. Poll GET /v1/envelopes/:id or — better — listen for webhooks to know when it’s done.

HTTP/1.1 202 Accepted
{
  "success": true,
  "data": {
    "id": "9fa2c8e1-2b7d-4f0a-8c11-5e6d3a1b0c42",
    "externalId": "order-1024",
    "subject": "Please sign your agreement",
    "status": "sent",
    "sequential": false,
    "sentAt": "2026-07-21T10:09:02Z",
    "completedAt": null,
    "createdAt": "2026-07-21T10:09:01Z"
  },
  "error": null,
  "meta": null
}

Envelope status

sentInvites emailed; awaiting signatures.
in_progressAt least one recipient has signed (multi-signer envelopes).
completedEvery recipient signed — sealed PDF & signed DOCX are ready.
declinedA recipient declined to sign.
voidedYou cancelled the envelope.
expiredThe signing window elapsed before completion.

Downloading documents

Get the docId from GET /v1/envelopes/:id, then GET /v1/envelopes/:id/documents/:docId/download?type=… returns JSON { "url", "expiresInSeconds": 900 } — a pre-signed link valid for 15 minutes (not a redirect). Four artifacts:

sourceThe original DOCX you uploaded.
renderedThe signer-facing preview PDF (markers shown, unsigned).
signed_docxThe signed DOCX — an editable working copy.
signed_pdfThe sealed, tamper-evident PDF with an audit certificate — the legal record.

Error codes

Failures use the same envelope with success: false and a stable error.code:

validation_failedA field in the payload is missing or malformed (422).
conflicte.g. a field references an unknown/ambiguous document (409).
payment_requiredNo document credits remaining (402).
unauthorizedMissing or invalid API token (401).
not_foundThe envelope or document does not exist (404).
rate_limitedToo many requests — retry after a short wait (429).

All endpoints

POST/v1/envelopesCreate & send an envelope. Multipart: files + a JSON payload. Returns 202.
GET/v1/envelopesList envelopes (cursor-paginated: ?limit, ?cursor, ?status).
GET/v1/envelopes/:idEnvelope detail — status, recipients, documents & artifact availability.
GET/v1/envelopes/:id/auditThe envelope audit trail (created, sent, viewed, signed, completed…).
GET/v1/envelopes/:id/documents/:docId/download?type=Short-lived signed download URL. type: source | rendered | signed_docx | signed_pdf.
POST/v1/envelopes/:id/remindEmail a reminder to every recipient who has not signed yet. No body. Rate limited (429).
POST/v1/envelopes/:id/voidVoid the envelope (cancel signing). Body: an optional reason.
POST/v1/envelopes/:id/recipients/:recipientId/signing-linkGenerate a fresh signing link (URL) for a recipient — send it via SMS or any channel.
GET/v1/business/email-templateGet this business signing-invite branding.
PATCH/v1/business/email-templateUpdate the invite branding (intro, button label/colour/align).

Webhooks

Webhooks push signing progress to your app in real time, so you never have to poll. When an envelope moves — sent, viewed, signed, completed — we POST a small JSON event to your endpoint, signed so you can trust it came from us.

1 · Configure your endpoint

In your dashboard, open API access and set the business's Webhook endpoint — the receiving URL and a signing secret you choose. Configure both in the dashboard (not via the API token). The secret never leaves your account and is what we use to sign every delivery, so your receiver can confirm each request is genuinely from us. Use an https:// URL in production.

2 · How a delivery looks

Each event is a single POST with Content-Type: application/json and these headers:

X-NexSign-EventThe event name, e.g. envelope.completed — lets you route without parsing the body.
X-NexSign-SignatureHex HMAC-SHA256 of the raw body, keyed with your webhook secret. Verify before trusting.
Content-TypeAlways application/json; the body is a single JSON event object.

3 · Events

envelope.sentThe envelope was dispatched to signers.
recipient.viewedA signer opened the document for the first time.
recipient.remindedA signer was nudged — automatically (once, 24h after the invite) or by your own remind call. Read automatic to tell them apart.
recipient.signedA recipient signed (fires for each).
envelope.completedEvery recipient has signed; sealed PDF ready.
envelope.voidedThe envelope was canceled — its links no longer sign.
recipient.bouncedAn invite email could not be delivered.

Every payload includes event, envelopeId, and your externalId (if you set one) so you can match it to your own records:

// envelope.sent — invites dispatched to all signers
{
  "event": "envelope.sent",
  "envelopeId": "9fa2c8e1-...",
  "externalId": "order-1024",
  "subject": "Please sign your agreement",
  "sentAt": "2026-07-21T10:09:02Z"
}

// recipient.viewed — a signer opened the document (first time)
{
  "event": "recipient.viewed",
  "envelopeId": "9fa2c8e1-...",
  "externalId": "order-1024",
  "recipient": {
    "email": "signer@acme.com",
    "fullName": "Alex Client",
    "routingOrder": 1,
    "viewedAt": "2026-07-21T10:22:40Z"
  }
}

// recipient.reminded — a signer was nudged. Fires for BOTH reminder paths;
// "automatic" tells them apart:
//   true  — our built-in nudge, 24h after the invite, once per signer, and
//           never if they already signed or you reminded them yourself first.
//   false — you called POST /v1/envelopes/:id/remind (or clicked Remind).
// One event per email, so a remind on a 3-signer envelope can fire 3 times.
{
  "event": "recipient.reminded",
  "envelopeId": "9fa2c8e1-...",
  "externalId": "order-1024",
  "recipient": {
    "email": "signer@acme.com",
    "fullName": "Alex Client",
    "routingOrder": 1
  },
  "remindedAt": "2026-07-22T09:15:00Z",
  "automatic": true
}

// recipient.signed — one recipient signed (fires for each)
{
  "event": "recipient.signed",
  "envelopeId": "9fa2c8e1-...",
  "externalId": "order-1024",
  "recipient": {
    "email": "signer@acme.com",
    "fullName": "Alex Client",
    "routingOrder": 1,
    "signedAt": "2026-07-21T10:31:00Z"
  },
  "envelopeStatus": "in_progress"      // or "completed" on the last signer
}

// envelope.completed — every recipient has signed; sealed PDF ready
{
  "event": "envelope.completed",
  "envelopeId": "9fa2c8e1-...",
  "externalId": "order-1024",
  "subject": "Please sign your agreement",
  "completedAt": "2026-07-21T10:31:02Z",
  "documents": [{ "id": "doc_...", "name": "agreement.docx" }]
}

// envelope.voided — you cancelled the envelope; its links no longer sign
{
  "event": "envelope.voided",
  "envelopeId": "9fa2c8e1-...",
  "externalId": "order-1024",
  "reason": "Superseded by a new agreement"
}

// recipient.bounced — an invite email could not be delivered
{
  "event": "recipient.bounced",
  "envelopeId": "9fa2c8e1-...",
  "externalId": "order-1024",
  "recipient": { "email": "signer@acme.com", "reason": "bounce" },
  "occurredAt": "2026-07-21T10:10:15Z"
}

4 · Verify every request

Compute HMAC-SHA256 of the raw request body with your secret and compare it to X-NexSign-Signature (constant-time). If it doesn't match, the request isn't from us — reject it. Verify against the raw bytes, not re-serialized JSON, or the signature won't line up.

// Node.js / Express — a complete webhook receiver.
// The route MUST be public (no login/session/bearer guard) or you will
// reject us with 401/403; we authenticate by SIGNING the payload instead.
import express from 'express';
import crypto from 'node:crypto';

const app = express();
const SECRET = process.env.NEXSIGN_WEBHOOK_SECRET;

// Capture the RAW body — the signature is over the exact bytes we sent,
// not the re-serialized JSON.
app.post('/webhooks/nexsign',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const signature = req.header('X-NexSign-Signature') ?? '';
    const expected = crypto
      .createHmac('sha256', SECRET)
      .update(req.body)                 // req.body is a Buffer (raw)
      .digest('hex');

    const ok =
      signature.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
    if (!ok) return res.status(401).end();   // not from Nex-Sign

    const event = JSON.parse(req.body.toString('utf8'));
    // ACK fast, then process asynchronously (queue it, don't block).
    res.status(200).end();
    handleAsync(event);                       // your logic
  });

5 · Retries & reliability

  • Respond 2xx quickly. Acknowledge first, then do your work asynchronously. Any non-2xx (or a timeout) is treated as a failure.
  • We retry. A failed delivery is retried with backoff — up to 12 attempts over ~24 hours — until it gets a 2xx.
  • Be idempotent. A delivery can arrive more than once; de-duplicate on envelopeId + event (and recipient.email for per-recipient events).
  • Order isn't guaranteed. Under retries, recipient.signed and envelope.completed may arrive out of order — treat each event on its own merits.
  • Fetch, don't expect files. Webhooks notify; download the sealed PDF / signed DOCX via GET …/documents/:docId/download when you receive envelope.completed.

Getting 401s in your logs? That's your server rejecting us, not a delivery failure. The webhook route must be public — no login, session, CSRF, or bearer-token guard in front of it. Authenticate us by verifying the signature (above), and make sure the secret on your side matches the one saved on your Nex-Sign business.

Ready to build?

Create an account, grab a token, and send your first 20 documents free.

Get your API token