Errors

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.

Every error says what was wrong and what to do next: which field, which id, which state. Log it whole. It never carries our own internals — when a 5xx is our failure (db_failed, storage_failed, render_failed, …) the message says so and what to retry, and the technical detail stays in our logs. Every answer under /v1 is JSON, including a path or a method that does not exist — see Unknown endpoints and methods.

{
  "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:

CodeTop-level fields
ip_not_allowedip — the observed caller address (produced by the authentication layer, before any route-level meta exists).
sms_allowance_exhaustedsms_allowance: { monthly, used, remaining, resets_at }, required.
invalid_metadataproblems: [{ path, message }].
invalid_tagsproblems: [{ path, message }]; on a count refusal also meta: { template_tags, max } (instantiate, generate) or { current, max } (PATCH /v1/documents/{id}).
template_input_invalidOn instantiate and generate: problems[], warnings[], template_id, template_name, template_version, docs, and missing_optional[] on a validate_only call. On confirm: problems[] only.
invalid_recipientsproblems: [{ signing_request_id, field, message }].
pinned-slot-mismatchslot.
field_out_of_bounds, page_geometry_unsupported, placement_failed, pdf_user_password_required, pdf_decrypt_failed, pdf_already_signed (POST /v1/signing-requests); unknown_fieldwarnings[] — the call's notices that were not the reason for the refusal, when there are any (on unknown_field: keys inside fields that are still only named, until 2027-01-31).

The meta of the common codes:

Codemeta
invalid_request (schema)problems: [{ path, message }] — every problem, path written as you would (signers[0].email). The first five are also spelled out in error.
invalid_keyreason: missing (no Authorization header), not_an_api_key (a header, but not Bearer wsk_live_… / lsk_live_…) or not_recognized (unknown, revoked or lacking the scope — never which).
invalid_state (remind, withdraw)status — the request's state now (signed, declined, withdrawn, expired, queued).
expired (remind)status, expires_at.
not_completesigned, total, waiting ({ status: count } for the signers not done).
unknown_endpointmethod, path, did_you_mean, docs.
method_not_allowedmethod, path, endpoint, allowed, docs.

A success body can carry warnings: [{ field, code, message }] — see Warnings below.

Common codes

Authentication / authorization

StatusCodeWhen
401invalid_keyBearer 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. meta.reason says which part to fix: missing, not_an_api_key or not_recognized (the last never says which of unknown, revoked or out of scope). A database failure on our side can answer it too, so retry a 401 on a key that worked before a few times before treating the key as wrong.
402enterprise-requiredThe 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.
402tier_requiredPOST /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. Enterprise (and legacy Teams) workspaces have no cap by default. No other endpoint answers this code.
402api_monthly_limitPOST /v1/signing-requests: the plan's monthly cap on documents created through the API is reached (meta.tier, meta.cap, meta.used) — see below. Nothing was created. Enterprise has no API cap.
402sms_allowance_exhaustedPOST /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.
403sending_pausedPOST /v1/signing-requests, POST /v1/templates/{id}/instantiate (and the MCP tools that call them): sending from the workspace is paused while a report about its invitations is reviewed. Nothing was created or sent; reminders are not mailed either. Contact support@wesign.now.
403ip_not_allowedThe 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.
404not_foundResource doesn't exist, OR belongs to a different workspace than the key (never told apart). The message names the id and the usual mix-up: a signingRequestId (from signers[] of a create response) and a documentId are different ids — /v1/signing-requests/{id} takes the first, /v1/documents/{id} the second. An id may be sent with or without its dashes; a shortened one (its first 8 or 16 hex digits) is not an id, and the message says a full id is needed.
404workspace_not_foundPOST /v1/signing-requests: the key's workspace could not be read. Retry; contact support if it persists.

api_monthly_limit

402 on POST /v1/signing-requests once the documents your workspace created through the API this month reach your plan's cap:

  • Free: 10 documents a month, the plan's own cap
  • Branded: 100 documents a month through the API
  • Enterprise: no cap

The body has the shape of tier_required: { "error": "…", "code": "api_monthly_limit", "meta": { "tier": "branded", "cap": …, "used": … } }. Nothing was created. Wait for the next month, or upgrade the plan (Pricing).

Request shape

StatusCodeWhen
400invalid_requestBody validation failed — every problem with its field in error (The request body is invalid — signers[0].email: Invalid email address; send_emails: expected boolean, received string.) and all of them in meta.problems: [{ path, message }] — 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.
400unknown_signer_fieldA signer object on POST /v1/signing-requests carried a key it does not accept (meta.signers: [{ index, fields }], meta.accepted).
400unknown_recipient_fieldThe same for a recipient on instantiate or confirm (meta.recipients, meta.accepted).
400unknown_fieldPATCH /v1/signing-requests/{id} got a key besides phone_e164, or PATCH /v1/documents/{id} one besides tags, add_tags and remove_tags (meta.fields, meta.accepted). From 2026-12-31 (00:00 UTC) also any unknown top-level key of the create, instantiate, generate and confirm calls, and from 2027-01-31 a key inside a fields[] entry or a role_label object of create (meta.fields = the paths). A refusal can carry warnings[] for notices not refused yet — see Unknown keys.
400conflicting_fieldsConfirm: phone_e164 and its deprecated alias phone disagree. instantiate: a recipient's slot, role and role_label name different slots. Both with meta.recipients: [{ index, fields }].
400invalid_requestGET /v1/documents, GET /v1/signing-requests: a query parameter the list does not take, a bad value (a tag included), or a cursor it did not issue (meta.problems: [{ path, message }], meta.accepted). PATCH /v1/documents/{id}: the body names none of tags, add_tags, remove_tags, or both forms.
400invalid_phonePATCH /v1/signing-requests/{id}: phone_e164 is not E.164.
400invalid_metadatametadata is not a flat object within the limits; top-level problems[].
400invalid_tagsA tag name is empty, longer than 40 characters or holds a control character, a document would carry more than 10 tags, or a name is in both add_tags and remove_tags; top-level problems[]. See Tags.
400invalid_idempotency_keyIdempotency-Key header value not 1–255 ASCII-printable chars.
413file_too_largeThe PDF — uploaded or fetched from file_url — exceeds 25 MB.
415unsupported_media_typeBody is not multipart/form-data or JSON, the multipart body could not be parsed, or the uploaded file is not application/pdf.

Fetching file_url

POST /v1/signing-requests with a file_url fetches the PDF before anything else happens. Nothing is created when the fetch fails.

StatusCodeWhen
400blocked_urlThe host is localhost or ends in .local / .internal, cannot be resolved, or resolves to a private or reserved address.
400redirect_not_allowedThe URL answered with a redirect. Redirects are never followed; pass the final URL.
502fetch_failedThe fetch failed: a network error, a non-2xx answer (the error names the status) or an empty body.
504fetch_timeoutThe URL did not answer within 20 seconds.

Placement

StatusCodeWhen
400duplicate_roleTwo or more signers share a role (any placement mode). Fields are assigned by role, so each signer needs its own (meta.role, meta.emails).
400unknown_roleA marker in the PDF, or an explicit field, names a role no signer has (meta.role; meta.page for a marker).
400signer_has_no_anchorThe PDF has markers, but none for this signer's role (meta.role).
400duplicate_anchorThe 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).
400placement_retiredplacement="manual" on POST /v1/signing-requests — retired 2026-07-31, refused before any document is minted. Use anchors, explicit coordinates or auto-append.
410placement_retiredAny call to POST /v1/documents — retired 2026-07-31 with the manual flow; file_url on POST /v1/signing-requests covers ingest-by-URL.
422signer_without_signatureplacement="explicit": a signer's role has no signature or initial field, so the signer would have nothing to sign (meta.roles).
422field_out_of_boundsplacement="explicit": a field would not lie wholly on its page — meta.reason is page_out_of_range, outside_page (x + w > 1, y + h > 1, or a centred field past an edge) or empty_box (w or h is 0); from 2027-01-31 also caption_outside_page (a signature or initials box whose caption would cross the page's bottom edge; caption_depth_pt, space_below_pt in its meta.problems entry — until then a caption_outside_page warning). meta.page, and meta.problems = every problem. Nothing is created.
422page_geometry_unsupportedplacement="explicit": a page carrying a field has no visible frame we can place on exactly — meta.reason is crop_box_outside_media_box, box_not_normalized, box_degenerate, box_unreadable (a MediaBox missing, or a box that is not four numbers), rotation_not_quarter_turn or rotation_unreadable (/Rotate not a number). meta.page, and meta.problems = every problem. Nothing is created.
422placement_failedpdf-lib threw while masking anchors / appending the signature page.
422pdf_user_password_requiredThe PDF needs its open password to be read. Send a copy without the password. Nothing is created.
422pdf_decrypt_failedThe PDF is encrypted and its protection could not be removed. Send an unprotected copy. Nothing is created.
422pdf_already_signedFrom 2027-01-31: the PDF already carries a digital signature, which preparing it would remove (meta.signatures: [{ field_name, page, sub_filter, signed_at, certification }], at most 50). Until then the call succeeds and names it in warnings[] (existing_signature_removed). Nothing is created. See What we do to your file.

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.

Idempotency

StatusCodeWhen
409idempotency_in_progressA previous call with the same Idempotency-Key is still processing. Retry after the Retry-After seconds.
422idempotency_key_reuseSame Idempotency-Key was used with a different request body. Use a fresh key.

See Idempotency for the full retry-safe pattern.

Signing requests: remind, withdraw, correct the phone

StatusCodeWhen
409invalid_stateremind/withdraw on a request that is signed, declined, withdrawn — or still queued (a sequential follower, or a recipient of a staged template instance). meta.status is its state. withdraw also answers it when the request changed state during the call.
409expiredremind on a request whose TTL has elapsed (meta.expires_at). A reminder cannot reopen it: send a new request.
409not_editablePATCH /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).
429rate_limitedremind: 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.
500db_failedwithdraw: the status update failed. Nothing changed — the request is still in flight and no webhook fired. Retry.
502email_failedOur email provider refused or failed the reminder (error says the failure is ours; the provider's own message stays in our logs), or the signer's address is on a domain that accepts no email (error names the domain; retrying does not help — correct the address). Nothing was recorded on the audit trail; otherwise retry later.
503email_not_configuredThe deployment has no email provider configured (development only).

Documents: tags

StatusCodeWhen
409tags_conflictPATCH /v1/documents/{id} with add_tags / remove_tags: the document's tags changed between our read and our write 4 times in a row, so nothing of the call was written (meta.attempts). Retry.

A failed save is a 500 db_failed (nothing changed, retry) and a failed read a 503 temporarily_unavailable (retry after Retry-After), as below.

Documents: downloads

StatusCodeWhen
404no_signers/signed or /audit-trail on a document with no signers (a finalized file-only instance) — use /pdf. Final.
409not_complete/signed or /audit-trail requested before every signer signed (meta.signed, meta.total, meta.waiting). Wait for document.completed.
409not_storedEvery signer signed but the sealed PDF's record is not there yet. It is stored before the last signer's status turns signed, so this is a passing state on our side, also right after document.completed. Retry with backoff.
409not_rendered/pdf: the document has no stored file.
410deleted_by_retention/signed: the workspace's retention period for signed PDFs has passed, so the sealed PDF was deleted. /pdf (since revision 2026-10-28): the workspace's retention for expired and declined documents deleted the document's file. Final. /audit-trail never answers it and keeps serving. See Keep your own copy.
500render_failed/audit-trail: a certificate due at this request (a document completed before 2026-10-15, or one whose certificate could not be issued at completion) could not be issued. Retry.
500certificate_mismatch/audit-trail: the stored certificate's bytes do not match their record — our failure, reported to us. Retry later; if it persists, email support@wesign.now with the document_id.
503temporarily_unavailable/signed, /audit-trail, /pdf, /events or the lists (GET /v1/documents, GET /v1/signing-requests) — /pdf, /events and the lists since revision 2026-10-28, PATCH /v1/documents/{id} and GET /v1/tags since 2026-10-30: a database read failed on our side, so nothing can be said about the document yet — or, on /audit-trail, the stored certificate could not be read at that moment (it is never re-rendered in its place). Never final: wait Retry-After seconds (5), then retry with backoff. Before the 2026-10-10 release /signed and /audit-trail answered the same failure with 404 not_found, 404 no_signers or 409 not_stored, and /audit-trail could render a certificate without its events.

Templates

StatusCodeWhen
400missing_recipientsinstantiate: a slot that has fields was given no recipient (meta.slots).
400duplicate_slotinstantiate: two or more recipients name the same slot (meta.slot, meta.emails).
400unknown_roleinstantiate: a recipient's role or role_label names no slot of the template, or more than one (meta.recipients: [{ index, field, value }], meta.roles: [{ slot, role }]).
400pinned-slot-mismatchinstantiate or confirm: a slot the template pins to an address got another one (top-level slot).
404version_not_foundinstantiate, generate or GET /v1/templates/{id}?version=N named a version that has no snapshot (meta.version, meta.current_version).
409template_unlockedinstantiate or generate: the template is not locked — a draft, or archived (with or without a version pin).
409template_api_disabledinstantiate or generate: the template's API switch is off. Its author turns it on in the dashboard: Templates → the template → Where it's used.
409interactive-table-requires-sequentialinstantiate or confirm: an unresolved interactive table with several parallel recipients.
409not_renderedgenerate: the template has nothing to render.
422signer_without_signatureinstantiate: a recipient's slot has no signature or initial field.
422template_input_invalidinstantiate, generate or confirm: one or more field_values failed validation. problems[] lists every one with its own code — see below.
500insert_failedgenerate with review: true: the staged document could not be stored. Nothing was created; retry.
502render_failedinstantiate: 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.
503review_unavailablegenerate with review: true: staging a generated file is not available on this deployment.
503temporarily_unavailableGET /v1/templates: a database read failed on our side, so the list cannot be given yet. Never final: wait Retry-After seconds (5), then retry with backoff. Before the 2026-10-20 release the same failure answered 200 with an empty templates array.

Confirm and discard

StatusCodeWhen
409not_stagedconfirm or discard: the document is not a staged instance (or no longer one).
409discardedconfirm on an instance that was discarded.
409already_confirmeddiscard on an instance that was already confirmed. (A repeated confirm is not an error: it answers 200 with already_confirmed: true and sends nothing.)
410review_expiredconfirm after the 14-day review window closed.
422invalid_recipientsconfirm: bad email, non-E.164 phone, SMS without a number, before_view without SMS, duplicate signing_order.
502update_failedconfirm: writing a recipient edit or the document's values failed. Nothing was sent; retry the confirm.
502confirm_failedconfirm: releasing the recipients (or finalizing a file-only instance) failed. Retry — the confirm resumes where it stopped.
502discard_faileddiscard: withdrawing the staged rows (or deleting a file-only instance) failed. Retry.

Problem codes of template_input_invalid

Each entry of problems[] is { field, label, code, message, allowed? }:

codeMeaning
field_requiredA required input was missing or blank. Only asked when the field is visible: an input inside a conditional section that is hidden is never required.
invalid_shapeA table (collection) got a single value, or a single value got rows.
invalid_booleanA boolean field got something other than true/false, yes/no or a UI-locale label.
invalid_agreeAn agree field got something other than a yes (allowed: ["yes"]).
invalid_enumNot one of the field's options (allowed lists them).
invalid_emailNot an email address.
invalid_numberNot a number.
invalid_dateNot a real calendar day in an accepted day-first or ISO spelling.
invalid_timeNot a 24-hour or AM/PM time.
invalid_ibanNot a valid IBAN.
invalid_vatNot a valid VAT ID.

The same body's warnings[] can carry unknown_field and unknown_column (below); they never cause the 422 on their own.

Embedded signing

StatusCodeWhen
400invalid_originorigin 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:.
400origin_not_allowedThe origin is well-formed, but this API key has a list of allowed embed origins and the origin is not on it. Add it under Settings → API → Embedded, "Allowed embed origins", or empty the list to allow any https origin. The message lists what is on it. A key with an empty list never answers this.
409request_not_signableThe 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.
503embedding_unavailableThis 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.

Webhook subscriptions

StatusCodeWhen
400https_requiredPOST /v1/hooks: target_url is not an https URL.
400url_not_publicPOST /v1/hooks: target_url is, or resolves to, a private, loopback, link-local or reserved address (or localhost, *.local, *.internal, or a name that does not resolve).
400unknown_eventPOST /v1/hooks: events names an event we do not know. meta.unknown lists those names, meta.valid_events every valid one.
404not_foundPOST /v1/hooks/{id}/rotate, GET /v1/hooks/{id}/deliveries, POST /v1/hooks/{id}/test, …/redeliver: unknown id, another workspace's hook, or a document-scoped callback_url hook — or, on redeliver, no such delivery on that hook. (DELETE /v1/hooks/{id} answers 204 for these.)
409rotation_conflictTwo rotations of the same hook raced. Retry; the other call's secret is the live one.
409hook_disabledPOST /v1/hooks/{id}/test or …/redeliver on a switched-off (or replaced) hook. Re-enable it in the dashboard first.
503payload_mode_unavailablePOST /v1/hooks with payload: "minimal" on a deployment without support for minimal payloads. Production supports them, so you should not see it there. Nothing was created; retry later or omit payload.

Retired endpoints

StatusCodeWhen
410embedded_sessions_retiredPOST /v1/embedded/sessions. See Embedded signing.
409already_consumedDELETE /v1/embedded/sessions/{id} on a session of that era that was already used.
410placement_retiredPOST /v1/documents.
400placement_retiredplacement="manual" on POST /v1/signing-requests.

Rate limiting + storage

StatusCodeWhen
429rate_limited60 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.)
429new_account_daily_limitPOST /v1/signing-requests or instantiate with emails on: in its first 7 days a Free workspace sends at most 20 invitation emails a day (UTC; one per signer, however many documents). Nothing was created. Retry-After counts to 00:00 UTC; meta.used, meta.cap. Paid plans have no such limit.
429new_account_domain_limitThe same calls: in its first 7 days a Free workspace sends to at most 10 different recipient email domains a day (UTC). Nothing was created. Retry-After counts to 00:00 UTC; meta.used, meta.cap.
500storage_failedBlob upload or download threw — most often a transient platform error, retry.
500db_failedA 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. GET /v1/documents/{id}/signed and /audit-trail answer a failed read with 503 temporarily_unavailable instead (see Documents: downloads).

Unknown endpoints and methods

Every answer under /v1 — on api.wesign.now/v1 and /api/v1 on the app hosts — is JSON, a typo included. Neither needs a valid key, and neither counts against your rate limit.

StatusCodeWhen
404unknown_endpointNo endpoint has this path. meta.did_you_mean names the closest ones with their method — GET /v1/signing answers "did_you_mean": ["GET /v1/signing-requests", "GET /v1/signing-requests/{id}", …] — and is empty when nothing is close; meta.docs links the endpoint list. Some mistakes get the exact fix: /v1 twice (/v1/v1/documents/{id}) names the same path with one /v1, your id included, and a guessed part of a document names the real one — /download, /file or /signed-pdf → /signed (the signed PDF) or /pdf (as sent), /audit → /audit-trail, /history → /events, /signers → GET /v1/documents/{id}. On api.wesign.now a path that left out /v1 (/signing-requests) answers it too, naming the /v1 endpoint you meant.
405method_not_allowedThe path is an endpoint, but not with this method — GET /v1/signing-requests/{id}/withdraw. meta.allowed lists its methods, and so does the Allow header.
{
  "error": "No endpoint GET /v1/signing. Did you mean GET /v1/signing-requests, GET /v1/signing-requests/{id} or POST /v1/signing-requests? Every endpoint is listed at https://www.wesign.now/docs/api-overview.",
  "code": "unknown_endpoint",
  "meta": {
    "method": "GET",
    "path": "/v1/signing",
    "did_you_mean": ["GET /v1/signing-requests", "GET /v1/signing-requests/{id}", "POST /v1/signing-requests"],
    "docs": "https://www.wesign.now/docs/api-overview"
  }
}

Before the 2026-10-11 release a path that did not exist answered an HTML page, and a wrong method an empty 405.

Codes outside an HTTP error

Some codes reach you on a 200 or outside the HTTP response altogether:

  • problems[].code on a validate_only 200 — 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.

Warnings

A 2xx body — and the 422 template_input_invalid, the 422s of ingest and placement on POST /v1/signing-requests and a 400 unknown_field — 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:

CodeMeaning
unknown_fieldA key the endpoint does not know was ignored — a top-level key (refused from 2026-12-31) or, by its path, a key inside a field object or role_label (fields[0].type; refused from 2027-01-31) — or a field_values key the template does not know was not rendered. The message tells them apart.
deprecated_fieldA spelling that still works until its sunset date — today recipients[i].phone on confirm (use phone_e164; accepted until 2026-12-31).
unknown_columnA column of a collection row the table does not have (field is table_key.column).
caption_outside_pageThe caption under a signature or initials box would cross the page's bottom edge (field fields[i], or an anchored field's marker). Explicit fields: refused with 422 field_out_of_bounds from 2027-01-31; anchors: never.
field_over_captionA field lies inside the caption area under a signature or initials box and prints over it. Never refused.
existing_signature_removedThe uploaded PDF already carried a digital signature; preparing it removes the signature and keeps only its appearance (field file / file_url). Refused with 422 pdf_already_signed from 2027-01-31.

Log them, and alert on them in your integration tests.

Detecting an error in code

async function send(opts) {
  const res = await fetch('https://api.wesign.now/v1/signing-requests', {
    method: 'POST',
    headers: { Authorization: `Bearer ${process.env.WESIGN_API_KEY}` },
    body: opts.formData,
  })
  if (!res.ok) {
    const body = await res.json()
    if (body.code === 'idempotency_in_progress') {
      const retryAfter = Number(res.headers.get('retry-after')) || 5
      await sleep(retryAfter * 1000)
      return send(opts) // retry the SAME idempotency key
    }
    if (body.code === 'rate_limited') {
      const retryAfter = Number(res.headers.get('retry-after')) || 60
      await sleep(retryAfter * 1000)
      return send(opts)
    }
    if (body.code === 'tier_required') {
      // The plan's monthly document cap is reached.
      throw new UpgradeRequiredError(body.meta?.tier, body.meta?.cap, body.meta?.used)
    }
    if (body.code === 'sms_allowance_exhausted') {
      // Nothing was created. Top-level, not under meta.
      throw new SmsAllowanceError(body.sms_allowance.remaining, body.required, body.sms_allowance.resets_at)
    }
    throw new ApiError(body.code, body.error, res.status)
  }
  return res.json()
}