Signing requests
Create, read, correct the phone, remind, and withdraw — the endpoints around the signing_request resource.
A signing request is one signer's row on a document. A document with two signers has two signing requests; with five signers, five. Each carries a status, an expiry, an audit log, and a unique signing URL the signer clicks to sign.
Create
The big one — uploads the PDF, places the fields, emails the invitations
(unless send_emails is false), and returns a per-signer URL list. It is
all or nothing: nobody is emailed until everything is stored, and a call
that fails creates nothing — see If the call fails.
Multipart when the PDF rides as a binary form field; JSON when you
point us at a URL. This endpoint never sends an invitation by SMS: a
signer who must verify by SMS asks for the code on the signing page —
see SMS verification.
Request body
Send the PDF one of two ways:
- multipart/form-data with a
filepart (the JSON fields ride as form fields), or - application/json with a
file_url— we fetch the PDF server-side (SSRF-guarded: https/http only, no private hosts, no redirects, ≤ 25 MB).
Prop
Type
Any other top-level key (or multipart field other than file) is ignored
and named in the response's warnings[]; from 2026-12-31 (00:00 UTC) it
is refused with 400 unknown_field — meta.fields names every unknown key,
meta.accepted what the body accepts, and nothing is created. See
Unknown keys.
Field object (placement="explicit")
Prop
Type
A field object accepts exactly these keys. Any other key — type instead of
kind, a misspelt orgin — is ignored, so the field keeps its default (a
signature box, top-left). Each such key is named
in warnings[] by its path, code: "unknown_field", field: "fields[0].type",
on the 201 and on a 422 field_out_of_bounds or page_geometry_unsupported.
From 2027-01-31 (00:00 UTC) the call is refused with 400 unknown_field:
meta.fields lists every path, meta.accepted the accepted keys spelt as
paths (fields[].kind), and nothing is created. Log warnings[] and remove
any key it names.
// "Point us at a PDF, place a signature by its centre, require an SMS code, send" — one JSON call:
{
"file_url": "https://your-dms.example.com/contracts/42.pdf",
"placement": "explicit",
"fields": [
{ "page": 2, "x": 0.70, "y": 0.65, "w": 0.18, "h": 0.06, "kind": "signature", "role": "client", "origin": "center" }
],
"signers": [
{ "role": "client", "email": "sara@acme.ch", "name": "Sara Buyer", "phone_e164": "+41791112233", "require_sms_verification": true }
]
}Signer object
Prop
Type
The signer object is strict. Any other key — phone instead of
phone_e164, a typo — is refused before anything is created, naming every
offender:
{
"error": "Unknown signer field(s): signers[0].phone. Accepted: email, role, name, first_name, last_name, recipient_color, role_label, locale, signing_order, phone_e164, require_sms_verification, sms_gate, reference, company, job_title.",
"code": "unknown_signer_field",
"meta": {
"signers": [{ "index": 0, "fields": ["phone"] }],
"accepted": ["email", "role", "name", "first_name", "last_name", "recipient_color", "role_label", "locale", "signing_order", "phone_e164", "require_sms_verification", "sms_gate", "reference", "company", "job_title"]
}
}Language per signer
Give locale per signer to choose the language of that signer's email, SMS
code and signing page. It is optional. Without one, we use the request's
locale, then the language this person signed in last time with your
workspace, then your workspace language, then English, and the signing page
follows the signer's browser language. A signer who has their own wesign
account with a language set always gets that language.
One document, three languages:
{
"file_url": "https://your-dms.example.com/contracts/42.pdf",
"placement": "auto_append",
"signers": [
{ "role": "client", "email": "anna@example.com", "name": "Anna Client", "locale": "en" },
{ "role": "witness", "email": "jonas@example.ch", "name": "Jonas Zeuge", "locale": "de" },
{ "role": "partner", "email": "marie@example.fr", "name": "Marie Partner" } // no locale
]
}| Signer | Email, reminders, SMS code | Signing page | locale in the 201 | signingUrl |
|---|---|---|---|---|
| Anna | English | English | en | https://yourco.wesign.now/en/sign/… |
| Jonas | German | German | de | https://yourco.wesign.now/de/sign/… |
| Marie, remembered in French | French | French | fr | https://yourco.wesign.now/fr/sign/… |
| Marie, not remembered | your workspace language, else English | her browser's language, else English | your workspace language, else en | https://yourco.wesign.now/sign/… |
How Marie's language is decided, first match wins:
- her own
locale— none here; - the request's
locale— none here; - the language remembered for
marie@example.frin your workspace: the language of the page she last signed or declined on, else the last per-signerlocaleyour workspace gave for her; - your workspace's language (Settings → Signing — unset for every existing workspace until an owner sets it, so check it before you rely on English);
- English.
Steps 1–3 count as a language you set: the link carries it, and the email, SMS code and page all use it. Steps 4 and 5 do not. The email then goes out in that language, but the link has no language segment, and the signing page opens in the signer's browser language (or the one they last chose on wesign.now in that browser), else English. Once their browser has loaded the document, reminders, the SMS code and their signed copy follow the language the page showed them.
- What
localein the response shows. Each signer of the 201 carrieslocale: the language you requested, after these steps.GETon the signer or the document andsigning_request.sentshow the same value, and it stays the same after the signer's browser takes over. Read the language fromlocale, never from the link. - A signer's own account language decides what they are sent in, even
over a
localeyou gave, but it is never shown: the response, the read-backs,signingUrland the webhooks carry the language you requested, so no call tells you whether an address has an account. The link in that signer's invitation email carries their account language; it opens the same signing request. - What is remembered. A per-signer
localeis remembered for that email address in your workspace, and so is the language of the page a person signs or declines on. Alocaleyou give never replaces a language the person signed in until they sign again. A request-levellocaleis a default for one request and is never remembered. Another workspace never uses yours. - No language at all is fine. A call without any
localeis never refused. Before 2026-10-19 it was always English; to keep that whatever your workspace language becomes, send"locale": "en". - Stays English: the caption under each signature, the audit certificate
and the sealed PDF. Observers get the request's
locale, else your workspace language, else English — see Observers.
Company and function
Two optional signer fields say on whose behalf a person signs:
{
"role": "signatory_1",
"email": "anna.muster@muster-ag.ch",
"first_name": "Anna",
"last_name": "Muster",
"job_title": "Geschäftsführerin", // ≤ 120 characters
"company": "Muster AG" // ≤ 200 characters
}- Your statement, not ours. We store and print what you send and never verify it; the certificate labels both "stated by the sender".
- Normalised. Runs of whitespace collapse to one space and the ends are
trimmed; a value that is blank after that means none. A longer value, or
one with a line break, tab or other control character, is a
400 invalid_requestnamingsigners[i].companyorsigners[i].job_title, and nothing is created. - Read back as
companyandjob_title(nullwhen absent) on every signer of the 201, onGETandPATCH /v1/signing-requests/{id}, onGET /v1/documents/{id}→signers[], and in thesignerblock of every signer webhook exceptsigning_request.expired. They cannot be changed after the create. - The same two fields exist on template recipients and on confirm.
What prints under each signature
Under every signature and initials box, the sealed PDF carries a four-line caption, in English with a 24-hour clock:
Anna Muster · Geschäftsführerin · Muster AG · anna.muster@muster-ag.ch
Signed 24/09/2026, 14:30 CEST
Signed on Muster Treuhand · mustertreuhand.wesign.now
IP 203.0.xxx.xxx · Electronic signature + SMS 2FA- The stored name, then
job_titleandcompanywhen you sent them, then the email. Without a name the line starts with the email; without company and function it is{name} · {email}. With them, the line shrinks to fit the box (down to 6 pt). When it still does not fit, or would run past the page's right edge, the line stays{name} · {email}and function and company print on a line of their own under it (wrapped onto more lines if they need them); lines 2–4 move down by as much. Nothing is cut at the side: when the caption's widest line would run past the page's right edge, the whole caption moves left until that line ends 8 pt before the edge (never closer than 8 pt to the left edge). A caption that fits starts at the box's left edge, as before. - When the signature was sealed, in the time zone of the workspace owner's profile (else their country's, else UTC), not the signer's.
- The workspace's name and signing host on a branded workspace,
otherwise
Signed on wesign.now. A document first sealed before late September 2026 keeps the name it was first sealed under, for all its signatures. - The signer's masked IP address and the method:
Electronic signature, orElectronic signature + SMS 2FAfor an SMS signer.
The caption starts about 12 pt and ends 42 pt below the box, 8.5 pt lower
for each line function and company take of their own. A signature or initials
box needs that much room above the bottom edge of the visible page:
y + h ≤ 1 − 42 / H, H being the visible page height in points (0.950 on A4
portrait). A placed box whose caption would cross that edge is named in
warnings[] (caption_outside_page); from 2027-01-31 an explicit field is
refused with 422 field_out_of_bounds (meta.reason caption_outside_page).
Keep your own text and other fields out of that space, at least across the
box's width — the caption's lines are often wider than the box. A
field placed there prints over the caption and is named in warnings[]
(field_over_caption), never refused. Names,
company and function print as values in the script they are written in;
only the labels are English. The same name, function and company appear on
the audit certificate, both the one attached to the completion emails and the
one from GET /v1/documents/{id}/audit-trail.
Second factor by SMS
Yes — a signer can be required to confirm an SMS code. Two fields on the signer, one rule:
| Field | Value |
|---|---|
phone_e164 | The mobile number, E.164: a leading +, then 8–15 digits, no spaces (^\+[1-9]\d{7,14}$). |
require_sms_verification | true |
sms_gate | Optional: "before_view" to hide the document until the code is verified. |
Validation rule: require_sms_verification: true without a
phone_e164 is rejected before anything is created —
400 invalid_request, and the error text names the rule:
phone_e164 is required when require_sms_verification is true. A
number that isn't E.164 fails the same way (phone must be E.164), and so
does sms_gate: "before_view" without the flag. A phone_e164 on its own,
without the flag, is stored on the request but no code is ever asked for.
What the signer experiences depends on sms_gate:
before_sign(default): they open their signing link, read the document, place their signature — and when they press Sign, we text a one-time code tophone_e164. They enter it, and only then is the signature applied.before_view: the link opens on the code step alone; the signer taps Text me a code (it is never sent on page load, so a link scanner cannot trigger it), and the document — title, pages, prefilled values — appears only after the code is verified. The invitation email does not name the document either.
The check is recorded on the audit trail, and the resulting signature is
classed as an advanced electronic signature (AES) rather than the simple
(SES) email-link level — the same distinction you see in the dashboard, and
the signature_level every response reads back. It is available on every
plan, within the workspace's monthly SMS allowance: a call whose SMS signers
the allowance cannot cover is refused with 402 sms_allowance_exhausted and
nothing is created. A request is never downgraded to SES.
{
"signers": [
{
"role": "tenant",
"email": "tenant@example.com",
"name": "Ann Tenant",
"phone_e164": "+41791112233",
"require_sms_verification": true,
"sms_gate": "before_view"
}
]
}The whole contract — what the email and the page show before the code, the
signer's side (resend, attempts, lifetime, lockout), undeliverable numbers,
the allowance and the SMS events — is on
SMS verification. The same fields exist on
template recipients and on
POST /v1/documents/{id}/confirm,
where the phone field is also phone_e164 (phone is a deprecated alias
until 2026-12-31).
send_emails
send_emails: false creates everything — the document, the signing
requests, the links, the signing_request.sent webhook — but sends no
invitation email and no observer notice. Each signer comes back with
emailed: false; you deliver the signingUrl yourself.
In a sequential send that holds for the later signers too: the choice is
stored on every signer, so when a signer's turn comes they are not emailed.
Their release emits signing_request.sent with
"source": "sequential_release" and "emailed": false — your cue to deliver
the signingUrl this response gave you for them. See
Following a sequential send.
It does not silence everything:
- The SMS code still arrives. It is never part of the invitation: the signer asks for it on the signing page, so a signer who opens your link reaches the SMS step exactly as with an emailed invitation.
- The workspace's automatic reminders (Settings → Signing, by default 7 and 14 days after the request was created) still email signers who have not signed. In a sequential send a later signer is counted from creation too, so the first reminder can follow their turn within a day.
- Completion emails go out as usual.
Response
HTTP/1.1 201 Created
Content-Type: application/json
{
"documentId": "8a1e4f9a-…",
"metadata": { "case_id": "ZT-2026-0142" },
"status": "pending",
"placement": "anchors",
"presentedSha256": "4f9a…d21c",
"anchors": { "found": 4, "fields": 4 },
"signers": [
{
"role": "tenant", "email": "tenant@example.com", "locale": "de", "status": "pending",
"signingUrl": "…", "signingRequestId": "…", "emailed": true,
"require_sms_verification": true,
"phone_masked": "•••• •••• 2233",
"sms_verified_at": null,
"sms_gate": "before_view",
"signature_level": "AES",
"reference": "crm-contact-4711",
"company": null,
"job_title": null
},
{
"role": "landlord", "email": "owner@example.com", "locale": "en", "status": "pending",
"signingUrl": "…", "signingRequestId": "…", "emailed": true,
"require_sms_verification": false,
"phone_masked": null,
"sms_verified_at": null,
"sms_gate": "before_sign",
"signature_level": "SES",
"reference": null,
"company": "Owner Estates AG",
"job_title": "Director"
}
],
"observers": [],
"callback": {
"url": "https://yourapp.com/wesign/callback",
"secret": "whsec_…",
"note": "Store this secret. We do not display it again. …"
},
"warnings": []
}Per signer:
| Field | Meaning |
|---|---|
locale | The language requested for this signer — see Language per signer. Never their own account language. |
status | pending, or queued for a sequential signer whose turn has not come. |
emailed | Whether the invitation email went out in this call. false with send_emails: false, for a queued signer (released when their turn comes — emailed then unless send_emails was false), when the send failed, and when the signer's address is on a domain that accepts no email (a null MX, no such domain, or no mail server: we do not send what can only bounce, and the document's sender is told the address needs correcting) — the link in signingUrl works either way. |
require_sms_verification, phone_masked, sms_verified_at, sms_gate, signature_level, reference | The SMS factor as stored — check it here rather than trusting your own request body. phone_masked shows the last four digits only; the full number is never returned. signature_level is AES with SMS, else SES. See Reading it back. |
company, job_title | What was stored from your company and job_title, or null. See Company and function. |
callback is only present when you passed callback_url. The
secret is the HMAC key for that document's deliveries and is shown
exactly once (an Idempotency-Key replay returns the same body, secret
included). warnings lists the call's notices: every key it ignored — a
top-level key, or by its path a key inside a field object or role_label —
the caption notices of
What prints under each signature, and
existing_signature_removed when the upload already carried a digital
signature, which preparing it removes (see
What we do to your file).
Refusals specific to this endpoint, all before anything is stored or sent:
| Status | Code | When |
|---|---|---|
| 400 | unknown_signer_field | An unknown key in a signer object (see above). |
| 400 | duplicate_role | Two or more signers share a role, in any placement mode. Fields are assigned by role, so each signer needs its own (meta.role, meta.emails). |
| 400 | unknown_field | From 2026-12-31 an unknown top-level key; from 2027-01-31 also an unknown key inside a field object or role_label (meta.fields, meta.accepted; until then a warning). A retry with the same Idempotency-Key of a call created before that day still replays its 201. |
| 400 | invalid_request | Any other body problem, including the SMS and sms_gate rules and a company or job_title that is too long or holds a control character; error names the path. |
| 400 | invalid_metadata | metadata is not a flat object within the limits; problems[] per key. |
| 402 | tier_required | The workspace's monthly document cap is reached (meta.tier, meta.cap, meta.used). |
| 402 | sms_allowance_exhausted | The signers with require_sms_verification: true outnumber the SMS codes the workspace has left this month. Body: { error, code, sms_allowance: { monthly, used, remaining, resets_at }, required }. See the allowance. |
| 422 | pdf_user_password_required | The PDF needs its open password. Send a copy without it. |
| 422 | pdf_decrypt_failed | The PDF is encrypted and its protection could not be removed. Send an unprotected copy. |
| 422 | pdf_already_signed | From 2027-01-31: the PDF already carries a digital signature, which preparing it would remove (meta.signatures). Until then a warning (existing_signature_removed). |
A file_url that cannot be fetched is refused before anything is created
too (blocked_url, redirect_not_allowed, fetch_failed,
fetch_timeout, file_too_large — see
Fetching file_url). The full list,
placement codes included, is on Errors.
If the call fails
The call writes everything first — the document, the callback_url
webhook, every signing request and every field — and sends nothing until all
of it is stored. Only then do the invitations go out, followed by the
observer notice and the one signing_request.sent.
If a write fails, the call undoes itself before anybody hears of it: the document, its signing requests, fields, audit rows and callback webhook are deleted, together with the PDF this call uploaded. It answers:
{
"error": "Signing-request insert failed: canceling statement due to statement timeout. Nothing was created or sent; retry the request.",
"code": "db_failed",
"meta": { "role": "signatory_2", "rolled_back": true }
}meta | Meaning |
|---|---|
rolled_back: true | Nothing exists and nothing was sent. Retry the same body; the Idempotency-Key was released, so the same key works. |
rolled_back: false | Rare: the partly created document could not be deleted. document_id names it; nobody was invited, its open signing requests were withdrawn where possible, and we were alerted. Before you retry, withdraw any request that GET /v1/documents/{id} still shows as pending. |
role | The signer whose signing request or fields could not be written, when that was the failing step. |
A failed callback_url webhook or a failed field write fails the call the
same way; neither is ignored.
signingUrl is the same link the invitation email carries:
https://yourco.wesign.now/<locale>/sign/… on a branded workspace, else
https://www.wesign.now/<locale>/sign/…. When no language was set for the
signer (only your workspace language or English applied), it has no language
segment, https://yourco.wesign.now/sign/…, and the signing page opens in the
signer's browser language. The email's link differs only for a signer whose
own wesign account has a language: it carries that language. Links the API
returned before late September 2026 keep their original host and keep
working.
Manual placement is retired. placement="manual" — POST a file,
get a placementUrl, let a human drop the fields — was retired on
2026-07-31 and now answers 400 placement_retired. Keep the human in
the loop before the send instead: place fields with anchors or
explicit coordinates, or stage a template instance for
review.
Read one signer
Status, signing URL, the signer's SMS factor, reference, company and function, and the first 50 audit events (oldest first) for a single signer. Use this when you want a tight poll loop on one recipient without re-fetching the whole document.
{
"id": "11111111-…",
"documentId": "8a1e4f9a-…",
"signer": {
"email": "tenant@example.com", "name": "Ann Tenant", "role": "tenant", "order": null,
"require_sms_verification": true,
"phone_masked": "•••• •••• 2233",
"sms_verified_at": "2026-04-29T10:31:40Z",
"sms_gate": "before_view",
"signature_level": "AES",
"reference": "crm-contact-4711",
"company": null,
"job_title": null
},
"status": "viewed",
"locale": "en",
"channel": "email",
"expiresAt": "2026-05-13T10:30:00Z",
"createdAt": "2026-04-29T10:30:00Z",
"staged": false,
"signingUrl": "https://yourco.wesign.now/en/sign/abc…",
"auditEvents": [
{ "type": "api_created", "createdAt": "2026-04-29T10:30:00Z", "meta": { "source": "v1_api", "api_key_id": "…", "key_name": "Production" } },
{ "type": "email_sent", "createdAt": "2026-04-29T10:30:01Z", "meta": { "to": "…", "source": "v1_api", "api_key_id": "…" } },
{ "type": "sms_verify_sent", "createdAt": "2026-04-29T10:31:02Z", "meta": { "masked_phone": "•••• •••• 2233", "quota_used": 42, "quota_cap": 300 } },
{ "type": "sms_verify_ok", "createdAt": "2026-04-29T10:31:40Z", "meta": {} },
{ "type": "viewed", "createdAt": "2026-04-29T10:31:43Z", "meta": null }
]
}status is one of:
| Status | Meaning |
|---|---|
queued | A sequential signer whose turn has not come, or a recipient of a staged template instance. |
pending | Invited; the signer's browser has not loaded the document yet. |
viewed | The signer's browser loaded the document at least once (with before_view: after the SMS code). Opening the link alone does not count — a plain fetch of the link does not trigger it; a scanner that renders the page in a full browser can. Not guaranteed before signed. |
signed | Signed. |
declined | The signer declined; the reason is in auditEvents[type=declined].meta.reason and on signing_request.declined. |
withdrawn | Withdrawn by you or a user in the dashboard. |
expired | Passed expiresAt unsigned. |
Treat viewed like pending wherever you branch on "still open".
locale is the language requested for the signer, the same value as on the
201, and signingUrl the same link (see
Language per signer).
A recipient of a staged template instance reads staged: true and
signingUrl: null until the instance is confirmed — the link does not work
before then, so take it from the confirm response.
auditEvents returns the oldest 50 events; a request created by
POST /v1/signing-requests starts with api_created (meta.source
v1_api, meta.api_key_id, meta.key_name), and a signed event names the
seal that sealed it (meta.cert_serial, meta.seal_key_version,
meta.seal_profile, meta.seal_timestamp). A long-lived request's newest
events can fall outside it — use the webhooks for a live
view. For a doc-level view of all signers, see
GET document.
Correct the phone number
The signer says the code went to the wrong number? Correct it on the live request — the link, the status and everything else stay as they are.
PATCH https://api.wesign.now/v1/signing-requests/11111111-…
Authorization: Bearer wsk_live_…
Content-Type: application/json
{ "phone_e164": "+41797654321" }200 answers the read-one shape with the new
phone_masked. Allowed while the request is pending, viewed or queued
and no code has been verified yet:
| Status | Code | When |
|---|---|---|
| 400 | invalid_phone | Not E.164. |
| 400 | unknown_field | Any key besides phone_e164 (meta.fields, meta.accepted). |
| 400 | invalid_request | phone_e164 missing or not a string, or the body is not JSON. |
| 404 | not_found | Unknown id, or another workspace's request. |
| 409 | not_editable | Signed, declined, withdrawn or expired — or the signer already verified a code, which fixes the number as evidence (meta.status, meta.sms_verified_at). Withdraw and create a new request instead. |
Nothing is sent by the PATCH; the signer taps "Send a new code" on the
page, and the code goes to the new number. The audit trail records
phone_changed with both numbers masked. Details:
Changing the phone number.
Remind
Re-send the original invite email. Allowed only while the request is
pending or viewed and not yet expired. Empty request body. The
audit trail records the API key as the actor. Every accepted call
sends another email — this endpoint does not take an
Idempotency-Key, so guard retries on your side.
One signer gets at most one reminder a minute, however it is triggered: a call within 60 seconds of the last reminder to that signer — from your key, a user in the dashboard or the workspace's automatic reminders — is refused and sends nothing. A burst of near-identical mail to one inbox is what spam filters punish, so the cooldown protects your delivery too.
200 OK { "ok": true }
404 Not Found { "error": "No signing request 0b9c… in this API key's workspace. Use a signers[].signingRequestId from the create response — the documentId is a different id.", "code": "not_found" }
409 Conflict { "error": "Cannot remind this signing request: the signer has already signed it. Only a pending or viewed request can be reminded.", "code": "invalid_state", "meta": { "status": "signed" } }
409 Conflict { "error": "This signing request expired on 2026-10-31, so a reminder cannot reopen it. Send a new request instead.", "code": "expired", "meta": { "status": "expired", "expires_at": "2026-10-31T23:59:59Z" } }
429 Too Many Requests Retry-After: 60 { "error": "This signer was reminded moments ago — give it a minute before sending another.", "code": "rate_limited" }
502 Bad Gateway { "error": "The reminder was not sent: example.invalid accepts no email (the domain does not exist). Retrying will not help — the signer's email address needs correcting.", "code": "email_failed" }
502 Bad Gateway { "error": "Sending the reminder email failed on our side. Nothing was sent and nothing was recorded. Retry in a few minutes; …", "code": "email_failed" }
503 Service Unavailable { "error": "Email sending is not set up on this deployment yet. …", "code": "email_not_configured" }| Status | Code | When |
|---|---|---|
| 404 | not_found | Unknown id, or another workspace's request. |
| 409 | invalid_state | Signed, declined, withdrawn — or queued (a sequential follower, or a recipient of a staged template instance). |
| 409 | expired | The request's TTL has elapsed. |
| 429 | rate_limited | The per-signer cooldown above. It carries Retry-After: 60 (no RateLimit-* headers) and it is not the per-key rate limit: wait Retry-After seconds, then call again. |
| 502 | email_failed | Our email provider failed the send (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 — withdraw and create the request again with the corrected address, or deliver the link yourself). Nothing was recorded; otherwise retry later. |
| 503 | email_not_configured | The deployment has no email provider (development only). |
Every call that passes the key check spends one token of the per-key rate
limit, whatever it answers — the cooldown 429 included.
Withdraw
Cancel an in-flight signing request. Allowed while it is pending or
viewed. Status flips to withdrawn — subsequent reminder/withdraw
calls return 409, the signing URL no longer accepts signatures, and
signing_request.withdrawn fires with
withdrawn_by: "api". Empty request body.
A queued sequential follower cannot be withdrawn here
(409 invalid_state); once an earlier signer is withdrawn the chain stops,
so the followers stay queued and are never invited.
200 OK { "ok": true }
404 Not Found { "error": "No signing request 0b9c… in this API key's workspace. …", "code": "not_found" }
409 Conflict { "error": "Cannot withdraw this signing request: the signer has already signed it. Only a pending or viewed request can be withdrawn.", "code": "invalid_state", "meta": { "status": "signed" } }
409 Conflict { "error": "This signing request changed state while it was being withdrawn: it was signed, declined or withdrawn at the same moment. Read it again to see which.", "code": "invalid_state" }
500 Internal Server Error { "error": "Withdrawing the signing request failed on our side. Nothing changed — it is still out for signature. Retry.", "code": "db_failed" }The second 409 means a signature, a decline or another withdraw landed
between our read and our write; read the request again to see which.
What the signer sees. A withdrawn request's link opens a notice instead
of the document. A request withdrawn through the API tells the signer it was
withdrawn automatically by the sender's connected software (never which
key); withdrawn within two minutes of the invitation, it adds that this
usually means a test or a technical problem on the sender's side and asks
the signer to contact you. Your dashboard shows the key's name and how long
after sending it happened. So do not create-then-withdraw to test with a
real address: use send_emails: false and an address you control. A
500 db_failed means the status update itself failed: nothing changed, the
request is still in flight and no webhook fired. Retry the call.
Both remind and withdraw are also available on the in-app UI under Documents → details. The API endpoints share business rules with the UI flow — same status gates, same audit-event shape.
