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:
| Code | Top-level fields |
|---|---|
ip_not_allowed | ip — the observed caller address (produced by the authentication layer, before any route-level meta exists). |
sms_allowance_exhausted | sms_allowance: { monthly, used, remaining, resets_at }, required. |
invalid_metadata | problems: [{ path, message }]. |
invalid_tags | problems: [{ path, message }]; on a count refusal also meta: { template_tags, max } (instantiate, generate) or { current, max } (PATCH /v1/documents/{id}). |
template_input_invalid | 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. |
invalid_recipients | problems: [{ signing_request_id, field, message }]. |
pinned-slot-mismatch | slot. |
field_out_of_bounds, page_geometry_unsupported, placement_failed, pdf_user_password_required, pdf_decrypt_failed, pdf_already_signed (POST /v1/signing-requests); unknown_field | warnings[] — 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:
| Code | meta |
|---|---|
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_key | reason: 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_complete | signed, total, waiting ({ status: count } for the signers not done). |
unknown_endpoint | method, path, did_you_mean, docs. |
method_not_allowed | method, path, endpoint, allowed, docs. |
A success body can carry warnings: [{ field, code, message }] — see
Warnings below.
Common codes
Authentication / authorization
| Status | Code | When |
|---|---|---|
| 401 | invalid_key | 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. 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. |
| 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. Enterprise (and legacy Teams) workspaces have no cap by default. No other endpoint answers this code. |
| 402 | api_monthly_limit | POST /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. |
| 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 | sending_paused | POST /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. |
| 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 (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. |
| 404 | workspace_not_found | POST /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
| Status | Code | When |
|---|---|---|
| 400 | invalid_request | Body 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. |
| 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, 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. |
| 400 | conflicting_fields | Confirm: 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 }]. |
| 400 | invalid_request | GET /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. |
| 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_tags | A 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. |
| 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. |
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.
| Status | Code | When |
|---|---|---|
| 400 | blocked_url | The host is localhost or ends in .local / .internal, cannot be resolved, or resolves to a private or reserved address. |
| 400 | redirect_not_allowed | The URL answered with a redirect. Redirects are never followed; pass the final URL. |
| 502 | fetch_failed | The fetch failed: a network error, a non-2xx answer (the error names the status) or an empty body. |
| 504 | fetch_timeout | The URL did not answer within 20 seconds. |
Placement
| Status | Code | When |
|---|---|---|
| 400 | duplicate_role | Two or more signers share a role (any placement mode). Fields are assigned by role, so each signer needs its own (meta.role, meta.emails). |
| 400 | unknown_role | 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 | field_out_of_bounds | placement="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. |
| 422 | page_geometry_unsupported | placement="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. |
| 422 | placement_failed | pdf-lib threw while masking anchors / appending the signature page. |
| 422 | pdf_user_password_required | The PDF needs its open password to be read. Send a copy without the password. Nothing is created. |
| 422 | pdf_decrypt_failed | The PDF is encrypted and its protection could not be removed. Send an unprotected copy. Nothing is created. |
| 422 | pdf_already_signed | From 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
| Status | Code | When |
|---|---|---|
| 409 | idempotency_in_progress | A previous call with the same Idempotency-Key is still processing. Retry after the Retry-After seconds. |
| 422 | idempotency_key_reuse | Same 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
| Status | Code | When |
|---|---|---|
| 409 | invalid_state | remind/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. |
| 409 | expired | remind on a request whose TTL has elapsed (meta.expires_at). A reminder cannot reopen it: send a new request. |
| 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 (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. |
| 503 | email_not_configured | The deployment has no email provider configured (development only). |
Documents: tags
| Status | Code | When |
|---|---|---|
| 409 | tags_conflict | PATCH /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
| Status | Code | When |
|---|---|---|
| 404 | no_signers | /signed or /audit-trail on a document with no signers (a finalized file-only instance) — use /pdf. Final. |
| 409 | not_complete | /signed or /audit-trail requested before every signer signed (meta.signed, meta.total, meta.waiting). Wait for document.completed. |
| 409 | not_stored | Every 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. |
| 409 | not_rendered | /pdf: the document has no stored file. |
| 410 | deleted_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. |
| 500 | render_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. |
| 500 | certificate_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. |
| 503 | temporarily_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
| Status | Code | When |
|---|---|---|
| 400 | missing_recipients | instantiate: a slot that has fields was given no recipient (meta.slots). |
| 400 | duplicate_slot | instantiate: two or more recipients name the same slot (meta.slot, meta.emails). |
| 400 | unknown_role | instantiate: 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 }]). |
| 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, generate or GET /v1/templates/{id}?version=N named 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 | template_api_disabled | instantiate or generate: the template's API switch is off. Its author turns it on in the dashboard: Templates → the template → Where it's used. |
| 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. |
| 503 | temporarily_unavailable | GET /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
| Status | Code | When |
|---|---|---|
| 409 | not_staged | 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. |
Problem codes of template_input_invalid
Each entry of problems[] is { field, label, code, message, allowed? }:
code | Meaning |
|---|---|
field_required | A 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_shape | A table (collection) got a single value, or a single value got rows. |
invalid_boolean | A boolean field got something other than true/false, yes/no or a UI-locale label. |
invalid_agree | An agree field got something other than a yes (allowed: ["yes"]). |
invalid_enum | Not one of the field's options (allowed lists them). |
invalid_email | Not an email address. |
invalid_number | Not a number. |
invalid_date | Not a real calendar day in an accepted day-first or ISO spelling. |
invalid_time | Not a 24-hour or AM/PM time. |
invalid_iban | Not a valid IBAN. |
invalid_vat | Not 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
| Status | Code | When |
|---|---|---|
| 400 | invalid_origin | 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 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. |
| 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.
Webhook subscriptions
| Status | Code | When |
|---|---|---|
| 400 | https_required | POST /v1/hooks: target_url is not an https URL. |
| 400 | url_not_public | POST /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). |
| 400 | unknown_event | POST /v1/hooks: events names an event we do not know. meta.unknown lists those names, meta.valid_events every valid one. |
| 404 | not_found | POST /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.) |
| 409 | rotation_conflict | Two rotations of the same hook raced. Retry; the other call's secret is the live one. |
| 409 | hook_disabled | POST /v1/hooks/{id}/test or …/redeliver on a switched-off (or replaced) hook. Re-enable it in the dashboard first. |
| 503 | payload_mode_unavailable | POST /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
| Status | Code | When |
|---|---|---|
| 410 | embedded_sessions_retired | POST /v1/embedded/sessions. See Embedded signing. |
| 409 | already_consumed | DELETE /v1/embedded/sessions/{id} on a session of that era that was already used. |
| 410 | placement_retired | POST /v1/documents. |
| 400 | placement_retired | placement="manual" on POST /v1/signing-requests. |
Rate limiting + storage
| Status | Code | When |
|---|---|---|
| 429 | rate_limited | 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.) |
| 429 | new_account_daily_limit | POST /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. |
| 429 | new_account_domain_limit | The 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. |
| 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. 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.
| Status | Code | When |
|---|---|---|
| 404 | unknown_endpoint | No 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. |
| 405 | method_not_allowed | The 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[].codeon avalidate_only200—sms_allowance_exhaustedwhen a dry run ofinstantiatefinds the SMS allowance short (withok: false). A dry run whosefield_valuesfail still answers422 template_input_invalid, like the real call. See SMS verification.reasononsigning_request.sms_failed—invalid_number,landline,blocked,rate_limited,allowance_exhausted,not_configured,provider_error. See Webhooks.detailon the embedded frame'swesign.errormessage —not_found,revoked,key_revoked,unavailable,withdrawn,queued,sms_required(abefore_viewsigner, who cannot verify inside a frame yet),sign_failed,sealing_in_progress,pades_failed,seal_persist_failedorhttp_<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:
| Code | Meaning |
|---|---|
unknown_field | A 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_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). |
caption_outside_page | The 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_caption | A field lies inside the caption area under a signature or initials box and prints over it. Never refused. |
existing_signature_removed | The 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()
}Webhooks
Real-time signer events delivered to your URL with HMAC-SHA256 signatures, full or minimal payloads, exponential-backoff retries and a delivery log with redelivery.
Idempotency
Safely retry network blips without double-sending. Stripe-style Idempotency-Key with a pending lock for concurrent retries.
