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/casesAuthenticated 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:
{
"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— nosenderIdorsenderEmailwas sent, so the case is owned by whoever created the API key. See Senderdefault-signing-schemas— nosettings.signingSchemaswere sent, so the standard eIDs apply. Theusingarray 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:
| Parameter | Type | Default | Description |
|---|---|---|---|
publicUrls | boolean | false | Return 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
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
title | string | Yes | — | Case title. Max 300 characters |
recipients | array | Conditional | — | Max 100. Required when state is open |
documents | array | Conditional | — | Max 20. Required when state is open |
state | string | No | open | open sends the case, draft saves it for later editing |
referenceId | string | No | — | Your own reference. Max 200 characters |
folder | string | No | company default | Folder name or id. Must exist — see Folders |
settings | object | No | — | How the case behaves — language, expiry, reminders, signing methods. See Settings |
messageTemplateId | string | No | — | Template for the message recipients receive. Must belong to your company |
presetId | string | No | — | Preset id. See Presets |
senderId | string | No | the key's creator | Attribute the case to a colleague, by user id. See Sender |
senderEmail | string | No | the key's creator | Attribute the case to a colleague, by email. See Sender |
tags | array | No | — | Tag 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 }
}
}| Field | Type | Required | Default | Description |
|---|---|---|---|---|
autoSend | boolean | No | true | On an open case, whether to send it now. false creates it ready to send |
language | string | No | company language | Signing page language. See Languages |
template | number | No | company default | Signing page template number, 1–99 |
expiresInDays | number | No | 30 | Days until the case expires, 1–3650 |
message | string | No | template message | Custom message to recipients. Max 5000 characters |
attachSignedFiles | boolean | No | true | Attach signed PDFs to the completion email |
signingSchemas | array | No | standard eIDs | Allowed signing methods. See Signing Schemas |
reminders | object | No | — | Reminder policy. See Reminders |
logo | string | No | — | Signing page logo. See Logo |
integrations | object | No | — | Integration 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
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
name | string | Yes | — | Max 200 characters |
email | string | Conditional | — | Required unless type is sms |
phone | string | Conditional | — | Required when type is sms. Max 40 characters |
type | string | No | email | email, sms, or eboks |
signing | boolean | No | true | Whether this recipient must sign. false makes them a read-only observer |
group | number | No | 0 | Signing order, 0–100. Lower groups are contacted first |
position | string | No | — | Signature title. Max 200 characters |
needsCpr | boolean | No | false | Require CPR validation |
cpr | string | No | — | Required when needsCpr is true. Max 20 characters |
signingSchema | string | No | — | Lock this recipient to one signing method |
redirectUrl | string | No | — | Where to send the recipient after signing |
message | string | No | — | Per-recipient message. Max 2000 characters |
messageTemplateId | string | No | — | Override the case template for this recipient |
eboks | object | No | — | e-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.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
name | string | Yes | — | File name including extension. Max 300 characters |
url | string | One of | — | An http(s) URL. Stored by reference — see File sources |
content | string | One of | — | Base64 file data. Max 20 MB decoded |
documentId | string | One of | — | A document from your NextSign library. See Library documents |
versionId | string | No | current version | Pin a specific library document version |
tags | object | No | — | Values for a .docx library document's fields, keyed by field path. Checked against the document — see Library documents |
signObligated | boolean | No | true | Whether signing this document is required |
documentMustBeRead | boolean | No | false | Require the recipient to open it before signing |
signatories | array | No | all | Indexes 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:
| Field | Looks the user up by |
|---|---|
senderId | Their NextSign user id |
senderEmail | Their 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:
{
"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:
{
"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:
| Field | Type | Range | Default | Description |
|---|---|---|---|---|
send | boolean | — | true | Master switch. false means no reminders at all |
amount | number | 1–20 | 2 | How many reminders each recipient receives |
daysBetween | number | 1–365 | 3 | Days between reminders |
autoSend | boolean | — | true | Whether 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.
Logo
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.
| Value | Method | In the default set |
|---|---|---|
urn:grn:authn:dk:mitid:low | MitID (low) | Yes |
urn:grn:authn:dk:mitid:substantial | MitID (substantial) | Yes |
urn:grn:authn:dk:mitid:business | MitID Erhverv | Yes |
urn:grn:authn:se:bankid | Swedish BankID | Yes |
urn:grn:authn:de:personalausweis | German Personalausweis | Yes |
draw | Drawn signature | No |
draw-cpr | Drawn signature with CPR | No |
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
| Value | Language |
|---|---|
da | Danish |
en | English |
de | German |
se | Swedish |
no | Norwegian |
Omit settings.language and the case follows your company's default.
Full Example
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]
}
]
}'{
"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
| Field | Description |
|---|---|
id | The case id. Store it — it identifies the case in the dashboard and in every webhook |
sender | The resolved owner — id, name and email from their NextSign profile |
activeGroup | The recipient group currently being asked to sign |
recipients[].id | The recipient's id |
recipients[].uid | Short handle for the recipient. This is what documents[].signatories contains |
documents[].id | The document's id, assigned when the case is created |
documents[].signatories | Who signs this document, as recipients[].uid values |
recipients[].status | pending, signed, denied, or not signing |
recipients[].signingUrl | The signing link. Present only on open cases |
documents[].type | The file's MIME type, e.g. application/pdf. null for a document you supplied as a url |
documents[].url | The document's stored address. Protected — add ?publicUrls=true for a downloadable link. See Document URLs |
delivery | Per-channel send result. See Delivery |
notices | Non-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 adraft, orsettings.autoSendwasfalse. The key is still present, so readattemptedrather than testing whetherdeliveryexistswebhook— appears on everyopencase, including one created withautoSend: false. Your integration is told the case exists even when we did not mail anyoneok: false— that channel failed. The case still exists, and the response is still201
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 "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
{
"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.
error | Meaning |
|---|---|
validation-failed | See details |
invalid-folder | No folder with that name or id |
message-template-not-found | messageTemplateId is not found for your company |
preset-not-found | Not found for your company |
sender-user-not-found | senderId or senderEmail is not a member of your company |
invalid-signing-page-logo | settings.logo is not a PNG/JPEG, and not a NextSign URL |
document-not-found | documentId is not in your library, or not visible to this key |
document-version-not-found | versionId does not belong to that document |
document-conversion-failed | The .docx could not be converted — usually a corrupt file |
document-production-failed | A 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.