NextSign API
Cases

Create a Case

Create and send a signing case with POST /v3/api/cases — full request and response reference.

Creates a case with its recipients and documents, and — unless you ask for a draft — sends it.

POST https://api.nextsign.dk/v3/api/cases

Authenticated with an API key — see Authorization.

Quickstart

The smallest request that creates and sends a case: a title, one recipient, one document.

curl https://api.nextsign.dk/v3/api/cases \
  -H "Authorization: Bearer $NEXTSIGN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Rental agreement",
    "recipients": [{ "name": "Andreas Lauridsen", "email": "al@example.com" }],
    "documents": [{ "name": "contract.pdf", "content": "JVBERi0xLjQK..." }]
  }'
const response = await fetch('https://api.nextsign.dk/v3/api/cases', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.NEXTSIGN_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    title: 'Rental agreement',
    recipients: [{ name: 'Andreas Lauridsen', email: 'al@example.com' }],
    documents: [{ name: 'contract.pdf', content: pdfBuffer.toString('base64') }],
  }),
});

const { case: created } = await response.json();
console.log(created.recipients[0].signingUrl);
import base64, os, requests

pdf = base64.b64encode(open("contract.pdf", "rb").read()).decode()

response = requests.post(
    "https://api.nextsign.dk/v3/api/cases",
    headers={"Authorization": f"Bearer {os.environ['NEXTSIGN_API_KEY']}"},
    json={
        "title": "Rental agreement",
        "recipients": [{"name": "Andreas Lauridsen", "email": "al@example.com"}],
        "documents": [{"name": "contract.pdf", "content": pdf}],
    },
)

created = response.json()["case"]
print(created["recipients"][0]["signingUrl"])

You get back 201 with the whole case, including a signing link per recipient. This is the complete body — nothing is elided:

201 Created
{
  "case": {
    "id": "65ab12cd34ef56ab78cd90b0",
    "referenceId": null,
    "title": "Rental agreement",
    "state": "open",
    "language": "da",
    "createdAt": "2026-07-27T14:48:36.356Z",
    "expiresAt": "2026-08-26T14:48:36.356Z",
    "folder": { "id": "65ab12cd34ef56ab78cd90a2", "name": "Default" },
    "sender": {
      "id": "65ab12cd34ef56ab78cd90ef",
      "name": "Anna Beck",
      "email": "anna@example.com"
    },
    "activeGroup": 0,
    "recipients": [
      {
        "id": "65ab12cd34ef56ab78cd91a1",
        "uid": "4t5ZQbtB",
        "name": "Andreas Lauridsen",
        "email": "al@example.com",
        "phone": "",
        "position": "",
        "group": 0,
        "type": "email",
        "signing": true,
        "status": "pending",
        "needsCpr": false,
        "signingSchema": "",
        "signingUrl": "https://www.nextsign.dk/sign/65ab12cd34ef56ab78cd90b0/2/h6gKDYSiPyYTC5Bkq6urKfKKW"
      }
    ],
    "documents": [
      {
        "id": "65ab12cd34ef56ab78cd92c3",
        "name": "contract.pdf",
        "type": "application/pdf",
        "url": "https://nextsign-de.fsn1.your-objectstorage.com/65ab/contract.pdf",
        "signObligated": true,
        "documentMustBeRead": false,
        "signatories": []
      }
    ],
    "delivery": {
      "attempted": true,
      "email": { "ok": true },
      "eboks": { "ok": true },
      "sms": { "ok": true },
      "webhook": { "ok": true }
    },
    "notices": [
      {
        "code": "sender-defaulted-to-key-creator",
        "message": "Neither senderId nor senderEmail was provided. The case was attributed to the API key's creator (anna@example.com)."
      },
      {
        "code": "default-signing-schemas",
        "message": "No settings.signingSchemas provided — the standard eIDs were used.",
        "using": [
          "urn:grn:authn:dk:mitid:low",
          "urn:grn:authn:dk:mitid:substantial",
          "urn:grn:authn:dk:mitid:business",
          "urn:grn:authn:se:bankid",
          "urn:grn:authn:de:personalausweis"
        ]
      }
    ]
  }
}

The two notices are there because the request left something out. Neither is a problem — each one tells you what was chosen on your behalf:

  • sender-defaulted-to-key-creator — no senderId or senderEmail was sent, so the case is owned by whoever created the API key. See Sender
  • default-signing-schemas — no settings.signingSchemas were sent, so the standard eIDs apply. The using array lists exactly which ones

Everything else on this page is optional refinement of that request.

Unknown fields are rejected. If a field is not in the tables below, sending it returns 400 with unknown field. This is deliberate: a field name we quietly ignored would leave you with a case that behaves wrongly and no clue why.

Query Parameters

One parameter, and it shapes the response rather than the case:

ParameterTypeDefaultDescription
publicUrlsbooleanfalseReturn downloadable document links. See Document URLs

It is a query parameter rather than a body field because nothing about it is stored — the same body always creates the same case, whatever you ask the response to look like.

Request Body

FieldTypeRequiredDefaultDescription
titlestringYesCase title. Max 300 characters
recipientsarrayConditionalMax 100. Required when state is open
documentsarrayConditionalMax 20. Required when state is open
statestringNoopenopen sends the case, draft saves it for later editing
referenceIdstringNoYour own reference. Max 200 characters
folderstringNocompany defaultFolder name or id. Must exist — see Folders
settingsobjectNoHow the case behaves — language, expiry, reminders, signing methods. See Settings
messageTemplateIdstringNoTemplate for the message recipients receive. Must belong to your company
presetIdstringNoPreset id. See Presets
senderIdstringNothe key's creatorAttribute the case to a colleague, by user id. See Sender
senderEmailstringNothe key's creatorAttribute the case to a colleague, by email. See Sender
tagsarrayNoTag values merged into .docx documents. Max 500

A draft may legitimately be incomplete — no recipients, no documents — exactly like a case started in the dashboard and finished later. An open case is going out, so both are required.


Settings

Everything that shapes how the case behaves — as opposed to what it contains and who it involves — lives in the settings object:

{
  "settings": {
    "language": "da",
    "expiresInDays": 14,
    "reminders": { "send": true, "amount": 2, "daysBetween": 3 }
  }
}
FieldTypeRequiredDefaultDescription
autoSendbooleanNotrueOn an open case, whether to send it now. false creates it ready to send
languagestringNocompany languageSigning page language. See Languages
templatenumberNocompany defaultSigning page template number, 199
expiresInDaysnumberNo30Days until the case expires, 13650
messagestringNotemplate messageCustom message to recipients. Max 5000 characters
attachSignedFilesbooleanNotrueAttach signed PDFs to the completion email
signingSchemasarrayNostandard eIDsAllowed signing methods. See Signing Schemas
remindersobjectNoReminder policy. See Reminders
logostringNoSigning page logo. See Logo
integrationsobjectNoIntegration hooks. See Integrations

Every field is optional, and so is the object itself — omit settings entirely and the case follows your company's defaults.


Recipients

FieldTypeRequiredDefaultDescription
namestringYesMax 200 characters
emailstringConditionalRequired unless type is sms
phonestringConditionalRequired when type is sms. Max 40 characters
typestringNoemailemail, sms, or eboks
signingbooleanNotrueWhether this recipient must sign. false makes them a read-only observer
groupnumberNo0Signing order, 0100. Lower groups are contacted first
positionstringNoSignature title. Max 200 characters
needsCprbooleanNofalseRequire CPR validation
cprstringNoRequired when needsCpr is true. Max 20 characters
signingSchemastringNoLock this recipient to one signing method
redirectUrlstringNoWhere to send the recipient after signing
messagestringNoPer-recipient message. Max 2000 characters
messageTemplateIdstringNoOverride the case template for this recipient
eboksobjectNoe-Boks delivery payload. See the e-Boks Object

signing defaults to true. Everyone you list is a signer unless you say otherwise, so set "signing": false explicitly on anyone who should only receive a copy.

Sequential signing. Recipients in group 0 are contacted first; group 1 is contacted only once every signer in group 0 has signed, and so on. Recipients with the same group are contacted together.

If needsCpr is true you must also send cpr, or the signer cannot complete verification. Store the plain-text CPR on your side if you need it later — it is encrypted at rest here and is not returned by the API.


Documents

Each document needs a name and exactly one source: url, content, or documentId. Sending two sources is a 400 — accepting several and picking a winner is how a caller ends up sending a different file than they thought.

FieldTypeRequiredDefaultDescription
namestringYesFile name including extension. Max 300 characters
urlstringOne ofAn http(s) URL. Stored by reference — see File sources
contentstringOne ofBase64 file data. Max 20 MB decoded
documentIdstringOne ofA document from your NextSign library. See Library documents
versionIdstringNocurrent versionPin a specific library document version
tagsobjectNoValues for a .docx library document's fields, keyed by field path. Checked against the document — see Library documents
signObligatedbooleanNotrueWhether signing this document is required
documentMustBeReadbooleanNofalseRequire the recipient to open it before signing
signatoriesarrayNoallIndexes into recipients — who signs this document

signatories are positions in your recipients array, so [0, 2] means the first and third recipient. An index outside the array is a 400, not a silent drop.

File sources

The three sources are not checked the same way, and the difference matters.

content is inspected. It must decode to a PDF or a .docx; the bytes decide, and the file name and any declared type are ignored. Anything else is a 400.

This rejects things a four-byte check waves through. A .docx is a ZIP container, so an XLSX, a PPTX or a plain .zip renamed to .docx all share the same prefix. Storing one of those would break the signing page later, a long way from the cause.

The stored file name follows the content. Upload PDF bytes named contract.docx and the document is stored and displayed as contract.pdf.

url is stored by reference. We do not download it while creating the case, so its contents are not inspected and a URL that 404s is still accepted — you will find out at signing time, not at creation time. Make sure it resolves, and stays resolving, for as long as the case is open.

The one exception: a url whose name ends in .docx is fetched immediately and converted, because the signing page cannot render Word files. Here the name decides, not the bytes — so a URL holding a PDF but named .docx is fetched, fails to convert, and comes back as document-conversion-failed.

documentId comes from your library, and is produced and converted the same way the dashboard does it — see Library documents.

A .docx you supply as content or url is converted to PDF before it is attached. If the case has a top-level tags array, those values are merged into it during that conversion.

Library documents

A documentId attaches a document from Dashboard → Documents. Find the id with List Documents; the document must be one your key can see — the company's non-private documents, the ones the key's user created, and documents shared with you — or it is document-not-found.

A .docx in the library is a template, and documents[].tags fills its fields. The keys are the field paths Retrieve a Document lists, and the values are checked against the document before anything is created: unknown keys, values for automatic (global, hidden or computed) fields, wrong types, values outside the author's bounds, and missing required fields all come back as validation-failed, with paths like documents[0].tags.start_date. A pdf document has no fields, so any tags key for it is rejected. The rules are the same as on Create a Case from a Document, which is the simpler endpoint when the case is built around one template.

name is still required on a library document, but the attached file keeps the library version's own name — a .docx becomes .pdf once rendered. On a draft the document is attached by reference with the values still editable in the dashboard, and rendered when the draft is sent.

Files over 20 MB

Upload the file first with Document Upload, then pass the returned URL as url:

{
  "documents": [
    { "name": "Lejekontrakt.pdf", "url": "https://nextsign-dev.hel1.your-objectstorage.com/.../Lejekontrakt.pdf" }
  ]
}

Sender

The sender owns the case. That decides two real things: which colleagues can see it — through the folder's role list — and who the signing email says it came from.

Name one in either of two ways:

FieldLooks the user up by
senderIdTheir NextSign user id
senderEmailTheir email address, matched without case sensitivity
{ "senderId": "65ab12cd34ef56ab78cd90ef" }

Send both and senderId wins — an id identifies a user exactly, where an address is only a way of looking one up.

The user must be a member of your company. Anyone else — including a valid user id from another company — is rejected:

400 Bad Request
{
  "error": "sender-user-not-found",
  "details": [{ "field": "senderId", "message": "not a member of this company" }]
}

The display name and email come from that user's NextSign profile, not from your payload. You cannot set them per request, so a case can never claim to have been sent by an address your company does not own.

If you send neither

The case is attributed to whoever created the API key, and the response tells you so:

{
  "code": "sender-defaulted-to-key-creator",
  "message": "Neither senderId nor senderEmail was provided. The case was attributed to the API key's creator (anna@example.com)."
}

That is a sensible default for a key that belongs to one person. If a single key serves a whole team, name the sender on every request — otherwise every case is owned by, and appears to come from, the colleague who happened to create the key.


Folders

folder accepts a folder name or id. It must match a folder that exists:

400 Bad Request
{
  "error": "invalid-folder",
  "details": [{ "field": "folder", "message": "no folder named or with id 'Contracts'" }]
}

A misspelled folder name is rejected rather than quietly filed under the default one. The folder controls which roles can see the case, so falling back silently would put it in front of a different set of colleagues than you intended.

The case's role visibility always comes from the folder.


Presets

presetId applies your company's preset — folder, message template, language, signing schemas, reminders, expiry and file attachment policy.

A preset fills in whatever you did not send. If the preset has locked a field, it overrides the value you sent, because presets are how a company enforces signing policy.

settings.reminders.autoSend is never locked by a preset — a preset governs whether reminders happen, not which system dispatches them.


Reminders

settings.reminders is the reminder policy for the case:

FieldTypeRangeDefaultDescription
sendbooleantrueMaster switch. false means no reminders at all
amountnumber1202How many reminders each recipient receives
daysBetweennumber13653Days between reminders
autoSendbooleantrueWhether we send them

Fields are merged individually, so {"reminders": {"daysBetween": 10}} keeps the default send and amount.

autoSend: false is the seam for sending reminders yourself. The reminder run still happens on schedule and still fires the case.reminder and case.reminder.sent webhooks — it just does not send the email or SMS. Use send: false if you want no reminder activity at all.


Puts your own logo on the signing page, for this case only.

{ "settings": { "logo": "iVBORw0KGgoAAAANSUhEUgAA..." } }

settings.logo accepts either base64 PNG or JPEG data, or a logo URL previously returned by NextSign. Base64 data is validated by content and stored for you; the case then carries the hosted URL. Maximum 8 MB of base64, which is roughly a 6 MB image.

Arbitrary third-party URLs are rejected — a signing page must not depend on a resource we do not control, which can change or disappear long after the case is signed.

If the upload itself fails, the case is still created and you get a signing-page-logo-not-stored notice rather than an error. A logo is decoration; losing it is not worth failing a signing case over.


Integrations

{ "settings": { "integrations": { "microsoft": { "returnPath": "/sites/MySite/Shared Documents" } } } }

returnPath is where signed documents are written back in SharePoint, when your company has an active SharePoint integration. It must be a site-relative path beginning with /. If there is no active integration the field is ignored and you get a notice rather than an error.


Signing Schemas

Case-level settings.signingSchemas lists the methods recipients may choose from. Omit it and the standard eIDs are used.

ValueMethodIn the default set
urn:grn:authn:dk:mitid:lowMitID (low)Yes
urn:grn:authn:dk:mitid:substantialMitID (substantial)Yes
urn:grn:authn:dk:mitid:businessMitID ErhvervYes
urn:grn:authn:se:bankidSwedish BankIDYes
urn:grn:authn:de:personalausweisGerman PersonalausweisYes
drawDrawn signatureNo
draw-cprDrawn signature with CPRNo

A recipient's own signingSchema locks that recipient to one method. If needsCpr is also set, CPR verification still runs through MitID substantial first.

Languages

ValueLanguage
daDanish
enEnglish
deGerman
seSwedish
noNorwegian

Omit settings.language and the case follows your company's default.


Full Example

cURL
curl --location 'https://api.nextsign.dk/v3/api/cases' \
  -H "Authorization: Bearer ${NEXTSIGN_API_KEY}" \
  -H 'Content-Type: application/json' \
  --data-raw '{
    "title": "Lejeaftale",
    "referenceId": "Jrn. 2342-23",
    "folder": "Default",
    "senderEmail": "anna@example.com",
    "settings": {
      "language": "da",
      "expiresInDays": 14,
      "message": "Kære {recipient_name}, som aftalt er hermed dokumenter til underskrift",
      "reminders": { "send": true, "amount": 2, "daysBetween": 3 },
      "signingSchemas": [
        "urn:grn:authn:dk:mitid:substantial",
        "urn:grn:authn:dk:mitid:business"
      ]
    },
    "recipients": [
      {
        "name": "Andreas Lauridsen",
        "email": "al@example.com",
        "group": 0,
        "position": "Director",
        "redirectUrl": "https://example.com/complete"
      },
      {
        "name": "Mia Sørensen",
        "email": "mia@example.com",
        "group": 1
      }
    ],
    "documents": [
      {
        "name": "Lejekontrakt.pdf",
        "content": "JVBERi0xLjQKJeLjz9M...",
        "signObligated": true,
        "signatories": [0, 1]
      }
    ]
  }'
201 Created
{
  "case": {
    "id": "65ab12cd34ef56ab78cd90b0",
    "referenceId": "Jrn. 2342-23",
    "title": "Lejeaftale",
    "state": "open",
    "language": "da",
    "createdAt": "2026-07-27T14:48:36.356Z",
    "expiresAt": "2026-08-10T14:48:36.356Z",
    "folder": { "id": "65ab12cd34ef56ab78cd90a2", "name": "Default" },
    "sender": {
      "id": "65ab12cd34ef56ab78cd90ef",
      "name": "Anna Beck",
      "email": "anna@example.com"
    },
    "activeGroup": 0,
    "recipients": [
      {
        "id": "65ab12cd34ef56ab78cd91a1",
        "uid": "4t5ZQbtB",
        "name": "Andreas Lauridsen",
        "email": "al@example.com",
        "phone": "",
        "position": "Director",
        "group": 0,
        "type": "email",
        "signing": true,
        "status": "pending",
        "needsCpr": false,
        "signingSchema": "",
        "signingUrl": "https://www.nextsign.dk/sign/65ab12cd34ef56ab78cd90b0/2/h6gKDYSiPyYTC5Bkq6urKfKKW"
      },
      {
        "id": "65ab12cd34ef56ab78cd91a2",
        "uid": "9kPmR2xW",
        "name": "Mia Sørensen",
        "email": "mia@example.com",
        "phone": "",
        "position": "",
        "group": 1,
        "type": "email",
        "signing": true,
        "status": "pending",
        "needsCpr": false,
        "signingSchema": "",
        "signingUrl": "https://www.nextsign.dk/sign/65ab12cd34ef56ab78cd90b0/2/Rr8tVqLmXnPzB4dYw2sKfHjNe"
      }
    ],
    "documents": [
      {
        "id": "65ab12cd34ef56ab78cd92c3",
        "name": "Lejekontrakt.pdf",
        "type": "application/pdf",
        "url": "https://nextsign-de.fsn1.your-objectstorage.com/65ab/Lejekontrakt.pdf",
        "signObligated": true,
        "documentMustBeRead": false,
        "signatories": ["4t5ZQbtB", "9kPmR2xW"]
      }
    ],
    "delivery": {
      "attempted": true,
      "email": { "ok": true },
      "eboks": { "ok": true },
      "sms": { "ok": true },
      "webhook": { "ok": true }
    },
    "notices": []
  }
}

The Location response header carries the case path.

Response fields

FieldDescription
idThe case id. Store it — it identifies the case in the dashboard and in every webhook
senderThe resolved owner — id, name and email from their NextSign profile
activeGroupThe recipient group currently being asked to sign
recipients[].idThe recipient's id
recipients[].uidShort handle for the recipient. This is what documents[].signatories contains
documents[].idThe document's id, assigned when the case is created
documents[].signatoriesWho signs this document, as recipients[].uid values
recipients[].statuspending, signed, denied, or not signing
recipients[].signingUrlThe signing link. Present only on open cases
documents[].typeThe file's MIME type, e.g. application/pdf. null for a document you supplied as a url
documents[].urlThe document's stored address. Protected — add ?publicUrls=true for a downloadable link. See Document URLs
deliveryPer-channel send result. See Delivery
noticesNon-fatal remarks. Never an error

signingToken is deliberately not returned. It is the credential that authenticates a signing session, and signingUrl already contains everything you need. Building signing URLs by hand breaks when the signing domain, language prefix or template number changes — all three are resolved server-side per case.

Delivery

delivery reports each channel independently:

{ "attempted": true, "email": { "ok": false, "error": "ECONNREFUSED" }, "eboks": { "ok": true }, "sms": { "ok": true }, "webhook": { "ok": true } }
  • attempted: false — nothing was sent. Either the case is a draft, or settings.autoSend was false. The key is still present, so read attempted rather than testing whether delivery exists
  • webhook — appears on every open case, including one created with autoSend: false. Your integration is told the case exists even when we did not mail anyone
  • ok: false — that channel failed. The case still exists, and the response is still 201

A failed channel is never reported as a failed request. Returning an error for a case that exists — and may already have been emailed — is what makes an integrator retry into duplicates.

Notices

{ "code": "sender-defaulted-to-key-creator", "message": "Neither senderId nor senderEmail was provided. …" }

Notices tell you something was defaulted, ignored or degraded. They never mean the request failed. Log them; do not branch on them. The full list is in Errors.

Document URLs

documents[].url is the document's stored address. It identifies the file and is stable, but it is protected — fetching it without credentials returns 403.

Add ?publicUrls=true for downloadable links, valid for one hour:

cURL
curl "https://api.nextsign.dk/v3/api/cases?publicUrls=true" \
  -H "Authorization: Bearer $NEXTSIGN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "title": "Rental agreement", … }'
{ "url": "https://nextsign-de.fsn1.your-objectstorage.com/65ab/contract.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Expires=3600&X-Amz-Signature=fa2ca5b25…" }

They are opt-in because they expire: a link minted at creation is dead an hour later, so storing one alongside the case leaves you with something that no longer works. The case id does not expire — ask for a fresh link with Retrieve a Case when you actually need to download.

Documents you supplied as a url are returned exactly as you gave them, with or without this parameter. We store those by reference and never re-host them, which is also why their type is null — we never fetched the bytes to inspect.

A .docx you upload is converted to PDF on the way in, so its name comes back as .pdf and its type as application/pdf. Both describe the file at url, not what you sent.


Errors

400 Bad Request
{
  "error": "validation-failed",
  "details": [
    { "field": "title", "message": "is required" },
    { "field": "recipients[0].email", "message": "must be a valid email" },
    { "field": "documents[0]", "message": "requires one of: url, content, documentId" },
    { "field": "nmae", "message": "unknown field" }
  ]
}

Structural problems in the body come back at once, so you fix them in one round-trip rather than one per deploy. A document's content is checked for being a real PDF or .docx once the structure is valid — see Validation returns everything at once.

errorMeaning
validation-failedSee details
invalid-folderNo folder with that name or id
message-template-not-foundmessageTemplateId is not found for your company
preset-not-foundNot found for your company
sender-user-not-foundsenderId or senderEmail is not a member of your company
invalid-signing-page-logosettings.logo is not a PNG/JPEG, and not a NextSign URL
document-not-founddocumentId is not in your library, or not visible to this key
document-version-not-foundversionId does not belong to that document
document-conversion-failedThe .docx could not be converted — usually a corrupt file
document-production-failedA library document could not be produced

Every 4xx from this endpoint means nothing was created. The case is persisted only after all of these have passed, so a 4xx is always safe to fix and resend. Full detail in Errors.