NextSign API
Platform

Errors

The v3 error envelope, the full list of error slugs, and what is safe to retry.

v3 uses HTTP status codes correctly and returns one error shape everywhere.

The envelope

Every error carries a stable machine-readable error slug:

{ "error": "invalid-folder" }

Anything caused by the request body also carries details, with one entry per problem:

400 Bad Request
{
  "error": "validation-failed",
  "details": [
    { "field": "title", "message": "is required" },
    { "field": "recipients[1].phone", "message": "is required" },
    { "field": "settings.reminders.amount", "message": "must be at least 1" }
  ]
}

field is a path into your payload, including array indexes, so you can point straight at the offending value.

Branch on error, not on message. Slugs are stable; the human-readable text may be reworded.

Validation returns everything at once

Structural problems — wrong types, bad formats, unknown fields, out-of-range values — are collected and returned together, so you fix them in one round-trip instead of one per deploy.

Three checks read the structure and so run once it is sound: whether an open case actually has recipients and documents, whether a document's content decodes to a PDF or .docx, and whether a library document's tags match the fields that document defines. If the body also has a structural problem you will see these on the next request, after the structural fixes — never a case created with one still outstanding.

Tag values are checked field by field, so the field path reaches into them — documents[0].tags.address.city, or tags.terms[1].qty on the document-first endpoint:

{ "field": "documents[0].tags.start_date", "message": "must be a valid date" }

unknown field appears for any key not in the documented schema — including a misspelled one:

{ "field": "recipeints", "message": "unknown field" }

Query parameters are held to the same standard. On the endpoints that take them, field is the parameter name rather than a body path, and an unrecognised parameter is rejected exactly as an unknown body field is — a filter that was quietly ignored is the one mistake you could not spot in the response.

{ "field": "createdAfter", "message": "must be an ISO 8601 date, e.g. 2026-01-31 or 2026-01-31T09:00:00Z" }

Status codes

StatusMeaningSafe to retry?
200Success. Nothing was changed — this was a readYes, always
201Created. The case existsNo — retrying creates a second case
400Your request was rejected. Nothing was createdYes, after fixing it
401Authentication failedOnly after fixing the key
403Authenticated, but not allowedNo
404No such case or document, or not yours to readNo — it will not appear later
500Something failed on our sideYes, with backoff

The one guarantee worth designing around

A 4xx from v3 always means nothing was created.

The case is written to the database only after authentication, validation, folder and template resolution, and document upload and conversion have all succeeded. Everything that can reject your request happens before that point.

Once the case exists, v3 will not turn around and report an error for it. Sending is isolated per channel and reported in delivery:

201 Created
{
  "case": {
    "id": "65ab12cd34ef56ab78cd90b0",
    "delivery": {
      "attempted": true,
      "email": { "ok": false, "error": "ECONNREFUSED" },
      "eboks": { "ok": true },
      "sms": { "ok": true },
      "webhook": { "ok": true }
    }
  }
}

This is the one guarantee to build your retry logic on. There is no window in which a case exists and the response says it failed, so you never have to ask "did that actually go through?" before retrying — the status code answers it.

Handle a failed channel as an operational event — alert, resend from the dashboard — not as a failed creation.

Authentication errors

StatuserrorMeaning
401missing-tokenNo Authorization header
401invalid-tokenThe key does not exist
401key-revokedThe key was revoked
401key-expiredThe key passed its expiry date
403company-disabledThe company account is disabled
403key-has-no-userThe key has nobody to attribute cases to. Keys made in the dashboard always do — tell us if you see this

See Authorization.

Request errors

errorStatusMeaning
validation-failed400One or more fields are invalid. See details
case-not-found404No case with that id in your company
document-not-found404On the /v3/api/documents endpoints: no such document in your library, or not visible to this key. See What you see
invalid-folder400No folder with that name or id
message-template-not-found400messageTemplateId is not in your company
preset-not-found400presetId is not in your company
sender-user-not-found400senderId or senderEmail is not a member of your company
invalid-signing-page-logo400settings.logo is not a PNG/JPEG, and not a NextSign URL
document-not-found400On POST /v3/api/cases: a documentId is not in your library, or not visible to this key. details names the document
document-version-not-found400versionId does not belong to that document
document-conversion-failed400The .docx could not be converted
document-production-failed400A library document could not be produced
unsupported-file-type400A safety net behind the content check. You should never see it — tell us if you do

Document errors name the offending item:

{
  "error": "document-conversion-failed",
  "details": [
    { "field": "documents[1]", "message": "the file could not be converted — is it a valid Word document?" }
  ]
}

A file can be a well-formed .docx container and still be unopenable — a ZIP missing its relationship parts is a valid archive and a broken document. That is what this error means.

Notices are not errors

notices appears on successful responses and records what was defaulted, ignored or degraded:

codeMeaning
sender-defaulted-to-key-creatorNo senderId or senderEmail sent; the case was attributed to whoever created the API key
default-signing-schemasNo settings.signingSchemas sent; the standard eIDs were used
no-sharepoint-integrationsettings.integrations.microsoft.returnPath was ignored
contact-not-savedA recipient could not be added to your contact list
signing-page-logo-not-storedThe logo could not be stored; the case was created without it
signing-urls-unavailableThe case exists — it was created, or you are reading it back — but its signing links could not be built

Log them. Do not treat them as failures.

A notice always has a code and a message, and may carry extra fields describing what was used. default-signing-schemas reports the set it fell back to:

{
  "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"
  ]
}

Suggested client handling

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(payload),
});

const body = await response.json();

if (response.status === 201) {
  // The case exists. Never retry from here.
  for (const [channel, result] of Object.entries(body.case.delivery)) {
    if (result?.ok === false) alertOps(channel, result.error, body.case.id);
  }
  return body.case;
}

if (response.status === 400) {
  // Nothing was created. Fix and resend.
  throw new BadPayload(body.error, body.details);
}

if (response.status >= 500) {
  return retryWithBackoff();   // safe: no case was created
}

throw new AuthError(body.error);
response = requests.post(
    "https://api.nextsign.dk/v3/api/cases",
    headers={"Authorization": f"Bearer {os.environ['NEXTSIGN_API_KEY']}"},
    json=payload,
)
body = response.json()

if response.status_code == 201:
    # The case exists. Never retry from here.
    for channel, result in body["case"]["delivery"].items():
        if isinstance(result, dict) and result.get("ok") is False:
            alert_ops(channel, result.get("error"), body["case"]["id"])
    return body["case"]

if response.status_code == 400:
    # Nothing was created. Fix and resend.
    raise BadPayload(body["error"], body.get("details"))

if response.status_code >= 500:
    return retry_with_backoff()   # safe: no case was created

raise AuthError(body["error"])

There is no idempotency key yet, so a retried 201 creates a second case. Retry only on network failures and 5xx.