Stable error codes mapped to HTTP statuses. Pin your error handling to the code, not the human-readable message.
Errors are JSON-bodied with a stable code string and a human-readable
error message. The error is for humans (logs, dashboards, oncall
chat) and may improve over time. The code is for code — pin your
catch logic to that.
{ "error": "Signer with role \"tenant\" has no [[ls:...:tenant]] anchor in the PDF", "code": "signer_has_no_anchor", "meta": { "role": "tenant" }}
Match code exactly, as a string. Almost every code is snake_case.
Three are kebab-case — enterprise-required, pinned-slot-mismatch and
interactive-table-requires-sequential — and will stay that way: renaming
a code would break every client that already matches it. Don't normalise
case or separators before you compare.
meta carries structured context when relevant (which role failed,
which signer carried an unknown key, etc). A few codes carry their context
at the top level instead of inside meta:
Code
Top-level fields
ip_not_allowed
ip — the observed caller address (produced by the authentication layer, before any route-level meta exists).
On instantiate and generate: problems[], warnings[], template_id, template_name, template_version, docs, and missing_optional[] on a validate_only call. On confirm: problems[] only.
Bearer token missing, malformed (neither wsk_live_ nor lsk_live_), unknown, or revoked — or a key with the embedded scope calling anything outside /v1/embedded/*, GET /v1/me included. See Key scopes.
402
enterprise-required
The endpoint needs Enterprise: POST /v1/templates/{id}/instantiate, POST /v1/templates/{id}/generate, POST /v1/documents/{id}/confirm and POST /v1/embedded/sign-sessions. Reading templates and fields, discard, and reading or revoking a sign session are never gated. Sales-led — the message names the feature.
402
tier_required
POST /v1/signing-requests and POST /v1/templates/{id}/instantiate (a staged review: true instance included): the workspace's plan has a monthly document cap and it is reached (meta.tier, meta.cap, meta.used). Nothing was created. Teams and Enterprise have no cap by default. No other endpoint answers this code.
402
sms_allowance_exhausted
POST /v1/signing-requests, a direct instantiate, or confirm: the signers who must verify by SMS outnumber the SMS codes the workspace has left this month. Nothing was created or sent. See SMS verification.
403
ip_not_allowed
The key is valid, but its IP allowlist is non-empty and the caller's address is not on it — or could not be determined. The body carries the address we saw as ip (null when unknown). An empty allowlist means unrestricted; see Restricting a key to your own IP addresses.
404
not_found
Resource doesn't exist, OR belongs to a different workspace than the key.
404
workspace_not_found
POST /v1/signing-requests: the key's workspace could not be read. Retry; contact support if it persists.
Body validation failed (zod-style message in error) — including phone_e164 is required when require_sms_verification is true, sms_gate "before_view" requires require_sms_verification: true, a signer's company or job_title that is too long or holds a line break or other control character, and a file_url that is not an http(s) URL — or the body is not valid JSON.
400
unknown_signer_field
A signer object on POST /v1/signing-requests carried a key it does not accept (meta.signers: [{ index, fields }], meta.accepted).
400
unknown_recipient_field
The same for a recipient on instantiate or confirm (meta.recipients, meta.accepted).
400
unknown_field
PATCH /v1/signing-requests/{id} got a key besides phone_e164 (meta.fields, meta.accepted). From 2026-12-31 also any unknown top-level key of the create, instantiate, generate and confirm calls — see Unknown keys.
400
conflicting_fields
Confirm: phone_e164 and its deprecated alias phone disagree (meta.recipients: [{ index, fields }]).
400
invalid_phone
PATCH /v1/signing-requests/{id}: phone_e164 is not E.164.
400
invalid_metadata
metadata is not a flat object within the limits; top-level problems[].
400
invalid_idempotency_key
Idempotency-Key header value not 1–255 ASCII-printable chars.
413
file_too_large
The PDF — uploaded or fetched from file_url — exceeds 25 MB.
415
unsupported_media_type
Body is not multipart/form-data or JSON, the multipart body could not be parsed, or the uploaded file is not application/pdf.
A marker in the PDF, or an explicit field, names a role no signer has (meta.role; meta.page for a marker).
400
signer_has_no_anchor
The PDF has markers, but none for this signer's role (meta.role).
400
duplicate_anchor
The same kind and role twice on one page — signature, initial, date and name may appear once per page per role; text may repeat (meta.role, meta.kind, meta.page).
400
placement_retired
placement="manual" on POST /v1/signing-requests — retired 2026-07-31, refused before any document is minted. Use anchors, explicit coordinates or auto-append.
410
placement_retired
Any call to POST /v1/documents — retired 2026-07-31 with the manual flow; file_url on POST /v1/signing-requests covers ingest-by-URL.
422
signer_without_signature
placement="explicit": a signer's role has no signature or initial field, so the signer would have nothing to sign (meta.roles).
422
placement_failed
pdf-lib threw while masking anchors / appending the signature page.
A PDF with no markers is never an error, whether you passed
placement="anchors" or left the default: the call falls through to an
appended signature page and the response says "placement": "auto_append"
with anchors.found: 0. Check those two fields if a missing marker must
stop your workflow — see Anchor placeholders.
remind/withdraw on a request that is signed, declined, withdrawn — or still queued (a sequential follower, or a recipient of a staged template instance). withdraw also answers it when the request changed state during the call.
409
expired
remind on a request whose TTL has elapsed.
409
not_editable
PATCH /v1/signing-requests/{id}: the request is no longer pending/viewed/queued, or the signer already verified a code (meta.status, meta.sms_verified_at).
429
rate_limited
remind: this signer was already reminded in the last 60 seconds — by your key, a user in the dashboard or the automatic reminders. Carries Retry-After: 60 (no RateLimit-* headers): wait that long before the next attempt. Not the per-key rate limit, which it does not touch.
500
db_failed
withdraw: the status update failed. Nothing changed — the request is still in flight and no webhook fired. Retry.
502
email_failed
Our email provider refused or failed the reminder. Nothing was recorded on the audit trail; retry later.
503
email_not_configured
The deployment has no email provider configured (development only).
instantiate: a slot that has fields was given no recipient (meta.slots).
400
pinned-slot-mismatch
instantiate or confirm: a slot the template pins to an address got another one (top-level slot).
404
version_not_found
instantiate pinned a version that has no snapshot (meta.version, meta.current_version).
409
template_unlocked
instantiate or generate: the template is not locked — a draft, or archived (with or without a version pin).
409
interactive-table-requires-sequential
instantiate or confirm: an unresolved interactive table with several parallel recipients.
409
not_rendered
generate: the template has nothing to render.
422
signer_without_signature
instantiate: a recipient's slot has no signature or initial field.
422
template_input_invalid
instantiate, generate or confirm: one or more field_values failed validation. problems[] lists every one with its own code — see below.
500
insert_failed
generate with review: true: the staged document could not be stored. Nothing was created; retry.
502
render_failed
instantiate: the filled PDF of a rich-text template could not be rendered or stored; nothing was created or sent and the Idempotency-Key is released. generate: the PDF could not be rendered. confirm: the staged document could not be re-rendered; nothing was sent. Retry.
503
review_unavailable
generate with review: true: staging a generated file is not available on this deployment.
confirm or discard: the document is not a staged instance (or no longer one).
409
discarded
confirm on an instance that was discarded.
409
already_confirmed
discard on an instance that was already confirmed. (A repeated confirm is not an error: it answers 200 with already_confirmed: true and sends nothing.)
410
review_expired
confirm after the 14-day review window closed.
422
invalid_recipients
confirm: bad email, non-E.164 phone, SMS without a number, before_view without SMS, duplicate signing_order.
502
update_failed
confirm: writing a recipient edit or the document's values failed. Nothing was sent; retry the confirm.
502
confirm_failed
confirm: releasing the recipients (or finalizing a file-only instance) failed. Retry — the confirm resumes where it stopped.
502
discard_failed
discard: withdrawing the staged rows (or deleting a file-only instance) failed. Retry.
origin on POST /v1/embedded/sign-sessions is not a plain https origin — it carried a path, query, fragment, credentials or a wildcard, or used http:.
400
origin_not_allowed
The origin is well-formed but is not registered on this API key. Add it under Settings → API, "Allowed embed origins". The message lists what is registered.
409
request_not_signable
The signing request is not pending/viewed (a queued sequential follower or staged recipient included), or is past its own expiry, so there is nothing to frame.
503
embedding_unavailable
This deployment has not applied migration 0155 yet. Transient by definition — retry, or contact support.
ttl_seconds outside 60–900 and an unknown locale are refused as
400 invalid_request rather than silently clamped or defaulted. See
Embedded signing.
60 requests per minute per API key exceeded. Retry after the Retry-After header. (remind also answers 429 rate_limited for its per-signer cooldown, with Retry-After: 60 — see above.)
500
storage_failed
Blob upload or download threw — most often a transient platform error, retry.
500
db_failed
A database read or write failed. Retry; if it persists, contact support with the time and endpoint. On POST /v1/signing-requests and instantiate nothing was sent and meta.rolled_back says whether everything the call created was removed (true) or the leftover document is named in meta.document_id (false) — see If the call fails.
Some codes reach you on a 200 or outside the HTTP response altogether:
problems[].code on a validate_only200 — sms_allowance_exhausted
when a dry run of instantiate finds the SMS allowance short (with
ok: false). A dry run whose field_values fail still answers
422 template_input_invalid, like the real call. See
SMS verification.
reason on signing_request.sms_failed — invalid_number,
landline, blocked, rate_limited, allowance_exhausted,
not_configured, provider_error. See
Webhooks.
detail on the embedded frame's wesign.error message — not_found,
revoked, key_revoked, unavailable, withdrawn, queued,
sms_required (a before_view signer, who cannot verify inside a frame
yet), sign_failed, sealing_in_progress, pades_failed,
seal_persist_failed or http_<status>. See
Embedded signing.
A 2xx body — and the 422 template_input_invalid — may carry
warnings: [{ field, code, message }]. A warning never fails the call, but
each one names something that did not happen the way you probably meant:
Code
Meaning
unknown_field
A top-level key the endpoint does not know was ignored (refused from 2026-12-31), or a field_values key the template does not know was not rendered. The message tells the two apart ("… of this endpoint …" / "… of this template …").
deprecated_field
A spelling that still works until its sunset date — today recipients[i].phone on confirm (use phone_e164; accepted until 2026-12-31).
unknown_column
A column of a collection row the table does not have (field is table_key.column).
Log them, and alert on them in your integration tests.