Changelog
Public API changes. Reverse-chronological. Breaking changes flagged.
This page tracks visible API changes — new endpoints, response shape
tweaks, error-code additions, deprecations. Internal refactors and UI
changes don't make it here. The same releases, one entry per date, are
published machine-readable at
https://www.wesign.now/api-changelog.json;
how much notice each kind of change gets is on Versioning.
2026-10-22 — The MCP server
The spec is info.version 2026-10-22, frozen at
/openapi/2026-10-22.json. Against 2026-10-21 only its description
changes: it names the MCP server instead of saying there is none. Requests
and responses under /v1 are unchanged. Nothing is breaking.
Docs
- An MCP server for AI agents.
https://www.wesign.now/api/mcp(Streamable HTTP,Authorization: Bearer <api key>) lets an agent send a PDF or a template for signature, follow every signer, remind, withdraw and fetch the links to the sealed PDF and its audit trail. Its tools run the/v1operations themselves: the same monthly caps, rate limit (one token per tool call;whoami, likeGET /v1/me, takes none), tier gates and error codes, and every tool call is in the API log as/mcp/<tool>. It never signs: every signer signs themselves. See MCP server. (mcp-server)
2026-10-21 — API keys on every plan; the spec names wesign.now only
The spec is info.version 2026-10-21, frozen at
/openapi/2026-10-21.json. Against 2026-10-20 it documents a new 402
on POST /v1/signing-requests and names one server. Nothing is breaking.
Each item ends with its id in
api-changelog.json.
Changed
- API keys on every plan. API keys and webhooks come with every plan,
not only Enterprise: a workspace owner or admin turns the developer
settings on and creates a key
(Minting a key). The documents a
workspace creates through the API count toward a monthly cap per plan
(Free: its overall monthly document cap; Branded: a monthly API cap;
Enterprise: none). Template sends and embedded signing stay Enterprise
(
402 enterprise-required). (keys-on-every-plan)
Added
402 api_monthly_limitonPOST /v1/signing-requests: the plan's monthly cap on documents created through the API is reached. Same shape astier_required(meta: { tier, cap, used }); nothing is created — wait for the next month or upgrade. Seeapi_monthly_limit. (api-monthly-limit)
Docs
- One server, one header family. The spec names
https://api.wesign.now/v1only, declares theX-WeSign-*headers on the webhooks and no longer lists User-Agent values: the User-Agent is not part of the contract — verifyX-WeSign-Signature. Requests, responses and deliveries are unchanged. (spec-wesign-only)
2026-10-20 — Every template in the list; a failed read is 503
The spec is info.version 2026-10-20, frozen at
/openapi/2026-10-20.json. Against 2026-10-19 it documents a 503 on
GET /v1/templates. Nothing is breaking.
Fixed
- Every template in the list.
GET /v1/templateslists every non-archived template of the workspace. Before, the list ended at the 1000th template (most recently updated first) without saying so. When a database read fails on our side it answers503 temporarily_unavailablewithRetry-After: 5, asGET /v1/documents/{id}/signeddoes, instead of200with an emptytemplatesarray — an empty list now always means the workspace has no template. (templates-list-complete)
2026-10-19 — A language per signer, everywhere
The spec is info.version 2026-10-19, frozen at
/openapi/2026-10-19.json. Against 2026-10-17 it adds locale to the
instantiate recipient and to the signer objects of the create responses,
describes every locale (the whole rule is on the Locale schema) and
notes that a signingUrl can come without a language segment.
Nothing is breaking: locale stays optional on the request and on every
signer, and a call without any language still succeeds. Each item ends with
its id in
api-changelog.json. The
whole rule, with an example, is on
Signing requests → Language per signer.
Added
- A language per template recipient.
POST /v1/templates/{id}/instantiatetakes an optionallocaleon each recipient, exactly assigners[].localeonPOST /v1/signing-requests: slot 1 can get its email, reminders, SMS code, signing page and signed copy in English and slot 2 in German. Areview: trueinstance keeps it; confirm takes nolocale. Before, the strict recipient object refusedlocalewith400 unknown_recipient_field. (instantiate-recipient-locale) - Each signer's language in the response. Each signer in the
201ofPOST /v1/signing-requests, and each recipient in the200and the staged201ofinstantiate, carrieslocale: the language requested for them.GET /v1/signing-requests/{id},GET /v1/documents/{id}andsigning_request.sentshow the same value, and it does not change when the signer's browser takes over the signing page. AnIdempotency-Keyreplay of a call made before this release returns its stored body, withoutlocale. (signer-locale-in-response)
Changed
- No English default; your workspace language instead. The request-level
localeno longer defaults toen. A signer without their ownlocalegets the request'slocale, then the language remembered for them in your workspace, then your workspace's language (a new setting in Settings → Signing), then English. The workspace language is unset for every existing workspace, so nothing changes until an owner sets it. To keep a call English whatever an owner sets, send"locale": "en". Oninstantiatethe same order, without the remembered step, names the language ofreview_urland spells a written-out date that names none. (workspace-default-language) - A person's language is remembered in your workspace. When someone
signs or declines, the language of the page they signed on is remembered for
their email address; a per-signer
localeyou give is remembered too, but never replaces a language the person signed in until they sign again. The next invitation to that address without a per-signer or requestlocaleuses it. A request-levellocaleis never remembered, and another workspace never uses yours. Remembered from this release on. (remembered-signer-language) - A signer's own account language wins, privately. A signer whose own
wesign.now account has a language set is sent in it: email, reminders, SMS
code and signing page. Responses, read-backs,
signingUrland webhooks still show the language you requested, so no call reveals whether an address has an account. (account-language-private) - No language set: the signer's browser decides the page. When only
your workspace language or English applied,
signingUrl/signing_urlhas no language segment (https://yourco.wesign.now/sign/<token>) and the signing page opens in the signer's browser language, else English. The email still goes out in your workspace language, else English; once the signer has opened the document, reminders, the SMS code and their signed copy follow the page's language. Links with a language segment, and every link returned before, keep working. Read the language fromlocale, never from the link. (browser-language-when-unset) - Observer emails in the request's language. The observer notice and
"Completed" are in the request's
locale, else your workspace language, else English, instead of the first signer's and the completing signer's. The emails to the document's sender follow the language the sender uses in the app, else the request's. (observer-language)
Fixed
- The SMS step speaks the signer's language. Its messages on the
signing page (a wrong or expired code, too many attempts, a missing or
undeliverable number, the monthly allowance used up) are shown in the
page's language; they were English in every language. API answers and
the
sms_failedreasons are unchanged. (sms-step-messages-localised)
2026-10-17 — Two signers sharing a role are refused; honest field count; taller date box on the appended page
The spec is info.version 2026-10-17, frozen at
/openapi/2026-10-17.json. Against 2026-10-16 it documents two new 400
codes, states that a signer's role and a recipient's slot are unique per
request, and redefines anchors.fields on the 201. Each item ends with its
id in
api-changelog.json.
Breaking
- Each signer needs its own role.
POST /v1/signing-requestsrefuses two or more signers with the samerolewith400 duplicate_role, in every placement mode, before anything is stored or sent. Fields are assigned by role, so signers sharing one each got every field of that role: on anauto_appendpage every signer's block was bound to all of them, and this was accepted without a warning.meta.roleis the shared role,meta.emailsevery signer that carries it. Give each signer its own role (signer1,signer2) and use those roles in your markers and explicit fields; withdraw a document sent with a shared role and send it again. AnIdempotency-Keyreplay of a call created before this release still answers its201. Likeexplicit-field-strict-geometry, this moves a mistake that happened anyway to the moment you can act on it, so it takes effect at once. (duplicate-role-refused) - Each template recipient needs its own slot.
POST /v1/templates/{id}/instantiaterefuses two or more recipients naming the sameslotwith400 duplicate_slot(meta.slot,meta.emails),validate_onlyincluded. (duplicate-slot-refused)
Fixed
anchors.fieldscounts what was stored. The201'sanchors.fieldsis the number of fields stored for the signers. It counted the fields placed on the PDF, which was lower than what was stored when two signers shared a role. The shape is unchanged. (anchors-fields-count)
Changed
- A taller date box on the appended signature page. On the page
auto_appendadds (also whenanchorsfinds no marker), each signer's date box is 26 pt tall instead of 18 pt, its top level with the signature box, so the sign page shows both its label and the date inside it. The date stamped into the sealed PDF is unchanged. (auto-append-date-box)
2026-10-16 — New seal certificate and PAdES baseline B-T from 2026-10-22
The spec is info.version 2026-10-16, frozen at
/openapi/2026-10-16.json. Against 2026-10-15 it adds a SEAL CERTIFICATE
paragraph to info.description and dates the cert_serial description of
document.completed; no schema, path or code changes.
Changed — scheduled for 2026-10-22 (00:00 UTC)
- A new seal certificate, in PAdES baseline B-T. From 2026-10-22,
00:00 UTC, every new seal is made with a new self-signed certificate,
CN=wesign.now Signing Authority, O=wesign.now, OU=E-Signature Service, C=CH(RSA-3072, serial6a5258df714ab427831c680e605d8231, SHA-256 fingerprint8a3d0158b768dd0d4244b4e20c6d1cbd99b1de4e651842a170ed42195bcb5d6d), in the PAdES baseline B-T format of ETSI EN 319 142-1:ETSI.CAdES.detached, the signing certificate bound by ESS signing-certificate-v2, and an RFC 3161 time stamp over the signature value embedded in the signature. When no time-stamp authority answers at that moment, or its token cannot be embedded, the seal is still made, as baseline B without the embedded time stamp (thesignedevent'smeta.seal_timestamp_errorsays why). The time stamp over the sealed file (tsa_provider,tsa_signed_at) is still taken whenever an authority answers; when none answered for that seal's embedded time stamp, it is not asked again and both are null. Seals made before keep the previous seal certificate (RSA-2048,adbe.pkcs7.detached, serial0062c9cfa7868e51c7ed510fece02d96, SHA-256 fingerprint0568a78abeb5b81e28c2ee4184a0d33b7b93f74647967c8e5b28391de626bcdc) and stay valid; /verify recognises both. Nothing changes shape:cert_serialcarries the new serial for seals made from that moment. Each signer'ssignedaudit event names the certificate that sealed that signature, so a document signed across it shows both serials there;cert_serialondocument.completedand the audit trail carry the last seal's. If your code checks our seal, validate the signature and compare its certificate against /seal-certificates.json — the new certificate is listed there now, before it seals anything — rather than pinning one certificate; if you pin by name, serial or fingerprint, add the new one before 2026-10-22 and keep the old one. See Our seal certificates. (seal-certificate-wesign)
2026-10-15 — One certificate per document, issued at completion; caption footprint, field keys and signed uploads named in warnings; isolation and auto_append frame fixes; seal and ingest documentation corrected
The spec is info.version 2026-10-15, frozen at
/openapi/2026-10-15.json. Against 2026-10-14 it adds three ApiWarning
codes, the ExistingSignature schema, Error.warnings,
audit_trail_sha256 on both document.completed bodies,
auditTrailSha256 and ingestNotes on Document, the ETag header and the
500 certificate_mismatch of GET /v1/documents/{id}/audit-trail, the
422 codes pdf_user_password_required, pdf_decrypt_failed and (from
2027-01-31) pdf_already_signed on POST /v1/signing-requests, the 400 unknown_field on create, instantiate, generate and confirm, and rewrites the
descriptions named below. Three changes are breaking, all from
2027-01-31, 00:00 UTC; until then each is a warning on every affected
response. Each item ends with its id in
api-changelog.json.
Breaking — scheduled for 2027-01-31 (00:00 UTC)
- A caption below the page is refused. An explicit signature or initials
box whose caption would cross the bottom edge of the visible page answers
422 field_out_of_bounds,meta.reason: "caption_outside_page", withcaption_depth_ptandspace_below_ptin itsmeta.problemsentry. Nothing is created. Keepy + h ≤ 1 − D / H(below). (explicit-field-caption-400) - Unknown keys inside a field or
role_labelare refused with400 unknown_field:meta.fieldslists every path,meta.acceptedthe accepted keys spelt as paths (fields[].page…fields[].origin,signers[].role_label.en,signers[].role_label.de). Send onlypage,x,y,w,h,kind,roleandoriginin a field. (explicit-field-unknown-keys-400) - An upload that already carries a digital signature is refused with
422 pdf_already_signed;meta.signatureslists each one as{ field_name, page, sub_filter, signed_at, certification }(at most 50). Nothing is created. Send the document before anyone signs it digitally. (signed-upload-refusal)
Changed
- The audit trail is issued once, at completion, and stored.
GET /v1/documents/{id}/audit-trailreturns exactly the same bytes on every request, and the completion emails attach that same file. It prints the SHA-256 of the signed PDF as completed ("Signed PDF SHA-256", thesha256ofdocument.completed), its times in the time zone of the signature captions with the zone named on each time, the sender as "created by", and the events up to the last signature; nothing recorded afterwards and no later change to the workspace alters it. Before, it was rendered on each request — in UTC, with the hash of the file as sent, the workspace's current name and owner, and every event so far — so it could change after completion and differed from the emailed certificate. A document completed before 2026-10-15 gets its certificate at the first request after that date; a copy fetched earlier remains a valid record but is not byte-identical to it. New answers:503 temporarily_unavailablewhen the stored certificate cannot be read at that moment (it is never re-rendered in its place) and500 certificate_mismatch. See Download the audit-trail PDF. (audit-trail-issued-at-completion) - Room for the caption under a signature. With
placement: "explicit", every signature and initials box needs 42 pt under it for its caption, 8.5 pt more for each extra line a statedjob_titleorcompanytakes: keepy + h ≤ 1 − D / H, H the visible page height in points (0.950 on A4 portrait for D = 42). A box whose caption would cross the bottom edge is still created and named inwarnings[](caption_outside_page,field: "fields[i]"). See explicit placement. (explicit-field-caption-footprint) - Unknown keys inside a field are named. A key an explicit field does not
accept (
typeinstead ofkind, a misspeltorgin) used to be dropped without a trace, and the field fell back to its default. It is now named inwarnings[]by its path (fields[0].type), on the 201 and on a422 field_out_of_boundsorpage_geometry_unsupported; so is a key other thanenanddein arole_labelobject (signers[0].role_label.fr). At most 100 per list, then one entry counting the rest. (explicit-field-unknown-keys)
Added
- The certificate's hash.
audit_trail_sha256ondocument.completed(full and minimal bodies;nullwhen the certificate could not be issued at that moment — the first download issues it),auditTrailSha256onGET /v1/documents/{id}, and theETagof the audit-trail download. (audit-trail-sha256) api_created.POST /v1/signing-requestswrites one audit event per signing request,meta: { source: "v1_api", api_key_id, key_name }, as its first event; it shows inauditEventsofGET /v1/signing-requests/{id}(the first 50 events, so a request with 50 or more shows one fewer of its later ones) and prints on the certificate as "Sign request created via API". (api-created-audit-event)- A field over a caption is named in
warnings[](field_over_caption): a field inside the caption area under a signature or initials box, at least across the box's width, prints over the caption. Never refused. (field-over-caption-warning) - Anchored fields get the caption check as notices only, never refused,
with
field= the marker as your PDF spells it ([[ls:signature:tenant]]). Theauto_appendpage is not checked. (anchor-caption-warning) - A signed upload is named. A PDF that already carries a digital
signature cannot keep it: the copy presented for signing breaks it and the
seal removes it, leaving only its visible appearance. This used to happen
without a word; the call now names it in
warnings[](existing_signature_removed,fieldfileorfile_url) and creates the document as before. (signed-upload-warning) ingestNotesonGET /v1/documents/{id}: what we did to the uploaded file —decrypted,repaired,form-baked,signature-removed, and for app uploadsflattened/compressed;[]for a clean upload and for API documents created before 2026-10-15. (document-ingest-notes)- The seal on the
signedevent. Itsmetanamesseal_key_version,seal_profile(adbe.pkcs7.detached|ETSI.CAdES.detached),seal_timestamp(the time-stamp embedded in a PAdES B-T seal) andseal_timestamp_error(why a B-T seal has none). (signed-event-seal-meta)
Fixed
- A caption is no longer cut at the right edge. When its widest line
would run past the right edge of the visible page, 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 is drawn where it was; documents signed
before keep their cut caption. (
caption-stays-on-page) - Isolation keeps an offset page in place. When a page's drawing
instructions are malformed, the seal redraws it inside a clean wrapper so
the signature shows in every viewer. On a page whose MediaBox does not
start at 0/0, that wrapper moved the content by the offset and could cut off
an edge; it now keeps the page's coordinates, boxes and
/Rotateexactly, and is used only after the seal has rendered it and found it unchanged. (isolated-page-offset-mediabox) - The
auto_appendpage is always A4 portrait, unrotated and uncropped, even when the upload sets a/Rotateor/CropBoxonce for all its pages. (auto-append-page-frame) - Protected PDFs get their own codes. An upload that needs its open
password answers
422 pdf_user_password_required, one whose protection could not be removed422 pdf_decrypt_failed— no longer422 placement_failed. Nothing is created, as before. Every422of ingest and placement carries the call'swarnings[]. (ingest-decrypt-codes)
Docs
- The audit trail, corrected. It is not itself a sealed PDF; it prints
masked IP addresses and no user agents (they are recorded, not printed);
the hash it prints is the signed PDF's. (
audit-trail-docs) - Assurance level and API creator on the certificate. Each "Signed by"
row says "Electronic signature + SMS 2FA" or "Electronic signature", like
the caption; a document created over the API names
<workspace> · API key <name>as its creator. (audit-trail-assurance-level) - The 2026-12-31 refusal of unknown top-level keys, announced on
2026-09-24, starts at 00:00 UTC that day on create, instantiate
(
validate_onlyincluded), generate (every mode; the PDF stream answers JSON instead) and confirm, with400 { error, code: "unknown_field", meta: { fields, accepted } }, before anything is read, rendered, created or confirmed. A retry with the sameIdempotency-Keyof a call that succeeded before replays its answer. See Unknown keys. (unknown-field-400-shape) - The seal, corrected. Our seal is not "PAdES-B" and carries no embedded
time stamp today: it is made with our self-signed seal certificate
in the ISO 32000-1
adbe.pkcs7.detachedformat, the PAdES Basic profile of ETSI TS 102 778-2, and the RFC 3161 time stamp (not eIDAS-qualified) is kept with our records. This corrects the 2026-10-03 note (docs-corrections-2026-10-03). On a date we will announce in advance here, new seals move to a new certificate,CN=wesign.now Signing Authority(RSA-3072), in the PAdES baseline B-T format with the time stamp inside the seal; earlier seals stay valid, and /verify recognises both. Our certificates: /seal-certificates.json and Our seal certificates. (seal-format-docs) - Date fields. A
datefield is filled by wesign when that signer's signature is sealed: the signing day in the issuer's time zone, asDD.MM.YYYY(MM/DD/YYYYfor a US workspace), 8–14 pt and shrunk to fit down to 6 pt. The signing page shows the same text, computed when the page is opened. See explicit placement. (explicit-date-field-docs) - What we do to your file. A new section lists how an upload is prepared
— unlocked, form fields baked, re-written by our PDF engine, an existing
signature removed — and says that
sha256and/pdfare the prepared copy, never your upload. See What we do to your file. (ingest-docs)
2026-10-14 — Explicit fields on a page with an unreadable box or rotation: page_geometry_unsupported
The spec is info.version 2026-10-14, frozen at
/openapi/2026-10-14.json. Against 2026-10-13 it adds two meta.reason
values to page_geometry_unsupported. Each item ends with its id in
api-changelog.json.
Fixed
- An unreadable page frame gets the precise answer. A
placement: "explicit"field on a page whose MediaBox is missing, whose MediaBox or CropBox is not an array of four numbers, or whose/Rotateis not a number is refused with422 page_geometry_unsupported(meta.reasonbox_unreadableorrotation_unreadable) instead of the generic422 placement_failed. The request failed before as well, and nothing is created in either case. (explicit-field-unreadable-geometry)
2026-10-13 — Explicit fields: the visible page is the frame, and a field that cannot be placed exactly is refused
The spec is info.version 2026-10-13, frozen at
/openapi/2026-10-13.json. Against 2026-10-12 it documents the frame of
ExplicitField and two new 422 codes on POST /v1/signing-requests. Each
item ends with its id in
api-changelog.json.
Breaking
placement: "explicit"refuses what it used to place somewhere else. With422and nothing created:page_geometry_unsupportedfor a page carrying a field whose CropBox is not wholly inside the MediaBox, whose box is reversed or under 1 pt, or whose/Rotateis not a multiple of 90 — the signing page and the seal read such a page differently;field_out_of_boundsfor apagethe document does not have (the signer never saw the field and the signature was drawn on the last page),x + w > 1ory + h > 1(the field ran off the page),worhof 0 (a 160 × 50 pt box was drawn instead), and anorigin: "center"field reaching past an edge (it was shifted onto the page).meta.problemslists every offending page and field. Like the 2026-09-24sms_allowance_exhaustedrefusal, this moves a misplacement that happened anyway to the moment you can act on it, so it takes effect at once. (explicit-field-strict-geometry)
Docs
- The frame of an explicit field.
x,y,wandhare fractions of the visible page — the CropBox (the MediaBox when there is none) after/Rotate, from its visible top-left corner — on the signing page and in the signed PDF alike. This is how they were always read; it is now the contract. Portrait, landscape,/Rotate90/180/270, a MediaBox not starting at 0/0 and a CropBox inside the MediaBox are supported, and the uploaded PDF is never rotated or cropped. See explicit placement. (explicit-field-frame)
2026-10-12 — An API log in Settings → API; X-Request-Id on every answer
The spec is info.version 2026-10-12, frozen at
/openapi/2026-10-12.json. Against 2026-10-11 it names the X-Request-Id
response header in info.description; no path, parameter, request,
response body, status code or schema changed. Each item ends with its id
in api-changelog.json.
Added
- An API log in Settings → API, and an
X-Request-Idon every answer. Every call a workspace's keys make is recorded after its response — method, endpoint, status, ourcodeanderrormessage, the document or signing request it concerns, the request id, the duration — and shown to the workspace's owners and admins for 30 days, newest first, filterable by key, endpoint and failures only. Request and response bodies are never stored. Calls with a missing, malformed or unknown key are logged too (not shown to any workspace); calls made with one of your own keys — refused (revoked, IP, scope) or sent to an endpoint that does not exist — appear in your log. Every answer under/v1now carries anX-Request-Idresponse header, Vercel's request id or one we mint: quote it to support with the time of the call. See Seeing your calls. (api-request-log)
2026-10-11 — A key with no embed origins accepts any https origin; unknown paths and methods answer JSON; clearer error messages
The spec is info.version 2026-10-11, frozen at
/openapi/2026-10-11.json. Against 2026-10-10 it adds two responses —
components.responses.UnknownEndpoint and MethodNotAllowed — and changes
descriptions: POST /v1/embedded/sign-sessions, its 400 response and the
origin field; the bearer security scheme and the 403 ip_not_allowed
response, which no longer call the IP allowlist the opposite of the
embed-origin list; info.description, the Unauthorized and
InvalidRequest responses, Error.meta, and the 400/409 responses whose
messages and meta changed below. No path, parameter, status code or schema
changed. Each item ends with its id in
api-changelog.json.
Added
- A path no endpoint has answers JSON
404 unknown_endpoint, not an HTML page.meta.did_you_meannames the closest endpoints with their method —GET /v1/signinganswersGET /v1/signing-requests/{id},POST /v1/signing-requests, … —meta.pathechoes what you called andmeta.docslinks the endpoint list. Onapi.wesign.nowa path that left out/v1(/signing-requests, or the bare root) answers the same, naming the/v1endpoint. No key is needed and it does not count against the rate limit. See Unknown endpoints and methods. (unknown-endpoint-json-404) - A method an endpoint does not have answers
405 method_not_allowedwith a JSON body (meta.allowed) and anAllowheader — before, the body was empty.GET /v1/documentsandGET /v1/signing-requestsalso say that no endpoint lists them, and what to call instead. (method-not-allowed-json-405)
Changed
- An empty list of embed origins allows any https origin. An API key
whose list of allowed embed origins is empty (Settings → API, Allowed
embed origins) now mints a session for any well-formed https
origin; before, such a key was refused every origin with400 origin_not_allowed. A key with one or more origins on its list accepts only those, exactly as before, so that code now only comes from a key that has a list. Each session is still pinned to the one origin it is minted for: the frame is served withContent-Security-Policy: frame-ancestors <that origin>and posts its messages to that origin only, never'*'. A malformedorigin(a path, query, credentials, a wildcard orhttp:) is still refused with400 invalid_originon every key. Keys with no registered origins now accept any origin: to keep the old behaviour, register on each key the origins it may name (Embedded signing). (embed-origins-empty-allows-any) - Error messages say what was wrong and what to do. Every
codeand status is unchanged. A schema400 invalid_requestlists every problem with its field (signers[0].email: Invalid email address; …) and carries them all inmeta.problems: [{ path, message }]— onPOST /v1/signing-requests,/generateand/instantiatetheerrorused to be the validator's issue list as a JSON string, elsewhere only the first issue without its field. A body that is not JSON says so (onPOST /v1/hooksandPOST /v1/embedded/sign-sessionsit used to be read as{}).401 invalid_keycarriesmeta.reason(missing,not_an_api_key,not_recognized);404 not_foundnames the id;409 invalid_stateon remind/withdraw carriesmeta.status,409 expiredmeta.expires_at,409 not_completemeta.signed,meta.total,meta.waiting. A5xxthat is our failure says what failed and what to retry, and no longer includes a database, storage or renderer message — nor, on remind's502 email_failed, the mail provider's (a domain that accepts no email is still named). If you parsed theerrorof a400 invalid_requestas JSON, readmeta.problemsinstead. See Errors. (clearer-error-messages) - A request withdrawn through the API is explained to the signer. The
signing link now says the request 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 the
sender. A withdrawal from the dashboard keeps the old wording, and the
sender's document timeline names the API key and how long after sending it
happened. The withdraw response is unchanged. Do not create and withdraw
requests to real addresses as a test: use
send_emails: falseand addresses you control. (withdrawn-by-connected-software)
2026-10-10 — A failed read is 503 on the download endpoints; retention deletes the signed PDF
The spec is info.version 2026-10-10, frozen at
/openapi/2026-10-10.json; it adds a 503 response to /signed and
/audit-trail and corrects their 404, 409 and 410 descriptions. Each
item ends with its id in
api-changelog.json.
Fixed
- A failed database read answers
503, never404.GET /v1/documents/{id}/signedandGET /v1/documents/{id}/audit-trailanswer503 temporarily_unavailablewithRetry-After: 5(and theRateLimit-*headers) when a database read fails on our side, instead of404 not_found,404 no_signersor409 not_stored;/audit-trailno longer renders a certificate without its events when they cannot be read. A404from these endpoints is now final, and409 not_storedonly means that the sealed PDF's record is not there yet. Treat the503as retryable: waitRetry-Afterseconds, then retry with backoff (Status codes and retries). Stop retrying a404from these two endpoints. A failed API-key lookup can still answer401 invalid_keyon any endpoint, so keep retrying a401on a key that worked before a few times. (signed-downloads-db-read-503) - Retention deletes the files. With Delete signed documents after N
days on (Settings → Signing → Auto-delete), retention now deletes the
files from our storage: the sealed PDF in the nightly run from which
/signedanswers410 deleted_by_retention(N days after the last signature, unchanged), and each signer's own copy in the first run N days after that signer's signature. Before, the run marked the PDF deleted and/signedanswered410as documented, but the files stayed in storage. A file that a template, a Present link or another document still uses is kept, for that use. Files already marked deleted are removed by the next nightly runs, as long as the workspace still has signed retention on and the document is due under its current period. A share link to a signed document now answers410 deleted_by_retentiononce retention has marked it deleted (before, it kept serving the file), and a signer's download link no longer serves a deleted PDF when a database read fails. Theblob_auto_deletedaudit event (inauditEventsofGET /v1/signing-requests/{id}) carriesfiles_released: trueandfiles_deleted; a signing request that retention marked deleted before this release gets a secondblob_auto_deletedevent, withbacklog: true, when its files are removed. (signed-retention-deletes-files)
2026-10-09 — Docs revision: retrieving the signed document
A docs-only revision: no endpoint, parameter, status code or schema
changed. The spec is info.version 2026-10-09, frozen at
/openapi/2026-10-09.json; it differs from 2026-10-08 in descriptions
only. Each item ends with its id in
api-changelog.json.
Docs
- Retrieve the signed document. A new section in
Documents, linked from
the webhooks page, both quickstarts, the recipes and the Integrator FAQ:
keep every
document_id; ondocument.completedverify, answer2xx, then fetchsigned_pdf_urlandaudit_trail_urlwith afullkey of the owning workspace; comparesha256with the PDF bytes; which status codes to retry and with what backoff; and keep your own copy. A workspace with Delete signed documents after N days on (Settings → Signing → Auto-delete) has/signedanswer410 deleted_by_retentionfrom the first nightly deletion run (03:20 UTC) after N days since the last signature. No event announces it, and the API exposes neither the setting nor a deletion date;/audit-trailandGET /v1/documents/{id}keep answering. No endpoint lists documents: reconcile from the ids you stored,GET /v1/documents/{id}and the delivery log. Onedocument.completedper document, after its last signer; each document of an envelope completes on its own. Fetch and store both files when the event arrives; do not plan to fetch them later. (retrieve-signed-document-docs) - Corrections.
sha256onGET /v1/documents/{id}, and the SHA-256 on the audit trail, hash the PDF as it was sent for signature, never the signed PDF (the certificate attached to the completion emails prints the signed PDF's hash): in the API, onlydocument.completed(and, in the full body, the last signer'ssigning_request.signed) carries the signed PDF's hash.409 not_storedmeans the signing record could not be read, a passing failure on our side — not "still being written". A database failure can also answer404on both download endpoints, so retry an id that a verifieddocument.completednamed before treating a404as final, and401on any endpoint, so retry a401on a key that worked before a few times before treating the key as wrong./audit-trailnever answers410and stays available after retention. The307of/signedapplies only to PDFs stored before April 2026. The download file name is sanitised (every character other thanA–Z,a–z,0–9,.,_and-becomes_), withCache-Control: private, no-store. The template quickstart said the signed PDF embeds the RFC 3161 time stamp; it does not. The Integrator FAQ saiddocument.completedis delivered exactly once; delivery is at least once. (signed-pdf-docs-corrections)
2026-10-08 — Timestamps from DigiCert, verified before they are kept
Changed
- Sealed PDFs are timestamped by DigiCert (RFC 3161, trusted by Adobe
Acrobat out of the box), with freetsa.org as the fallback. Every
timestamp reply is verified before it is kept: it must cover the sealed
PDF's SHA-256 and our request nonce, and be signed by the authority's
timestamping certificate under a pinned root. On
document.completedand the audit certificate,tsa_providerreads"DigiCert"(before:"freetsa.org") andtsa_signed_atis the time inside the token in whole seconds (before: our server's clock at sealing, with milliseconds). Both stay informational; no request changes. (tsa-digicert-verified)
2026-10-07 — Invitations to a domain that accepts no email are not sent
Changed
- An invitation to an address whose domain accepts no email is not
sent: the domain publishes a null MX (RFC 7505), does not exist, or has
neither an MX nor an address record, so the email could only bounce, days
later. The signing request is created and stays
pendingas before, its signing URL works, andemailedisfalse, as for any failed send; a sequential release emitssigning_request.sentwith"emailed": false, and a create call's firstsigning_request.sentgoes out as before. The document's sender is emailed at once that the address cannot receive email. The check is a DNS lookup made as the invitation goes out: when it fails or takes longer than 1.5 s, the invitation is sent as before. Such a signer can only be reached on another address: withdraw the request and create it again with the corrected address, or deliver the signing URL yourself. (no-mail-domain-not-emailed) - Breaking:
POST /v1/signing-requests/{id}/remindto such an address answers502 email_failedand sends nothing;errornames the domain. Before, it answered 200 and the reminder bounced later. Like the 2026-09-24sms_allowance_exhaustedrefusal, this moves a failure that happened anyway to the moment you can act on it, so it takes effect at once. Do not retry such a 502 blindly. (remind-no-mail-domain)
2026-10-06 — A Send Later emits signing_request.sent when it goes out
Fixed
- A Send Later emits
signing_request.sentwhen it goes out at the scheduled time, not when it is scheduled, in the body and number of the same send without Send Later: an envelope from the editor once per document, for its first signer, also when several signers of a parallel send go out at the same moment; a template quick-send once per recipient it invites, withtemplate_idandtemplate_version. There is nosource. Later sequential signers still get theirs on release ("source": "sequential_release"), and a Send Later cancelled before its time emits nothing. Before, a Send Later emitted nosigning_request.sentfor a document's first signers, and the webhooks guide said so. If a workspace-wide hook should act only on documents you created over the API, ignore adocument_idyou do not know; acallback_urlhook is unaffected. (send-later-sent-event)
2026-10-05 — Documents sent from the app emit signing_request.sent
Fixed
- The editor's Send emits
signing_request.sent, as the spec lists a dashboard send among its sources. This covers one file or an envelope of several, a document made from a template included. One event per document, for its first signer, after the invitations went out; not again for the other signers of a parallel send. The body is the dashboard-send shape:signercarriesemail,nameandslot(the signing order in a sequential send, elsenull), next tolocale; there is nosource, and the signer block has noreference,companyorjob_title. A minimal hook gets the identifiers, withroleandreferencenull. Later sequential signers still get theirs on release ("source": "sequential_release"). A send that fails or is only partly sent emits none, and so does a Send Later, as before. Before, the editor's Send emitted nosigning_request.sentfor a document's first signers, and the webhooks guide said so; the guide now matches the spec. If a workspace-wide hook should act only on documents you created over the API, ignore adocument_idyou do not know; acallback_urlhook is unaffected. (app-send-sent-event)
2026-10-04 — Completion emails reply to your workspace
Changed
- Completion emails reply to your workspace's reply-to address, like
invitations and reminders: the "Completed" emails to signers, to the
sender and to observers. A workspace without one keeps
support@wesign.now. Before, completion emails always replied tosupport@wesign.now(seereply-to-docsunder 2026-10-03). No API request or response changes. (completion-reply-to-workspace)
2026-10-03 — Docs revision: a start-here path, signing links on wesign.now, webhook order
A docs-only revision: no endpoint, parameter or schema changed. The spec is
info.version 2026-10-03, frozen at /openapi/2026-10-03.json; it differs
from 2026-10-02 in descriptions, summaries and examples only. Each item ends with its
id in api-changelog.json.
Docs
- Start here, in the spec and in /llms.txt. The spec's
info.descriptionnow opens with what the API does and the path through it:GET /v1/me; send a PDF, or fill a template (GET /v1/templates/{id}, avalidate_onlydry run, then the same body) with anIdempotency-Key; register one hook ("payload": "minimal"for identifiers only), verify, deduplicate, do not rely on order, answer within 10 seconds; fetch the signed PDF and the audit trail. It states that there is no sandbox, how to test without one, and that there is no SDK and no MCP server.externalDocsnow points to https://www.wesign.now/docs. (spec-start-here) - Signing links are on wesign.now. The
signingUrl/signing_urlthe API returns is the link the invitation email carries:https://<slug>.wesign.now/<locale>/sign/<token>on a branded workspace, elsehttps://www.wesign.now/<locale>/sign/<token>. This has been live since a deploy after 2026-09-26 that no note listed, and the 2026-09-24 notes (email-links-wesign) said these links stay on their old host. Links returned earlier keep working. If you allowlist or rebuild the host, accept wesign.now too; better, store each link as returned. (signing-link-host-docs) - Completion emails reply to support@wesign.now. The 2026-09-24 notes
(
email-links-wesign) and the API overview said they use the workspace's reply-to address; they do not. The one exception is an envelope sent from the web app whose other documents are released after a signer declines one. Invitations, reminders and the observer notice use the workspace's reply-to address when it has one, as documented. (reply-to-docs) - New certificates say wesign.now. A document first sealed since a
deploy after 2026-09-26 names wesign.now in the caption under each
signature, the seal's Reason and Location and the audit certificate; a
document sealed earlier keeps the name it was sealed under. The seal
certificate is unchanged.
(
certificate-brand-docs) callback_urlhooks get the full body. The API cannot make them minimal; for identifiers only, register a workspace hook with"payload": "minimal"and do not passcallback_url. (callback-url-full-body-docs)- Webhook order and reconciliation. Events can arrive in any order, and
rarely an event that could not be recorded for delivery is never sent.
Reconcile with
GET /v1/documents/{id}. See Order and reconciliation. (webhook-order-docs) - Pages brought in line with the spec: template slots publish
roleand pin;send_emails: falseholds for a later sequential signer; the seal is PAdES-B and the time stamp is kept with our records;/signedalso answers410 deleted_by_retentionand500 storage_failed. (docs-corrections-2026-10-03)
2026-10-02 — Webhook management for integrators: replace on re-subscribe, minimal payloads, delivery log
An integrator asked for a precise webhook contract: managing hooks over the
API, a body without personal data, and a delivery log they can read
themselves. This release delivers it. The body of existing hooks changes
only by additive fields (a top-level role and reference, the full
signer block on signing_request.expired); their delivery timing changes
as listed below. The spec is info.version 2026-10-02, frozen at
/openapi/2026-10-02.json. Each item ends with its id in
api-changelog.json.
Breaking
- Re-subscribing a URL replaces the hook.
POST /v1/hookswith atarget_urlthat an active workspace-wide hook already targets switches that hook off and returns a new one (newid, newsecret), naming the old one inreplaced_hook_ids; its deliveries still being retried move to the new hook. Before, a second POST created a second active hook and both received every event. To keep two subscriptions to one endpoint, give them different URLs. See REST hooks. (hooks-same-url-replaces) - https and a public address at registration. A non-https
target_urlanswers400 https_required; one that is or resolves to a private, loopback, link-local or reserved address answers400 url_not_public. Before, such hooks were accepted and every delivery failed. Hooks registered over http earlier keep delivering. (hooks-https-and-address-check) - Unknown event names have their own code.
400 unknown_event(before:invalid_request) withmeta.unknownandmeta.valid_events. (hooks-unknown-event) DELETE /v1/hooks/{id}answers204with no body (before:200{ "ok": true }). (hooks-delete-204)- 10-second timeout per delivery attempt (before: 15). Answer
2xxfirst and process afterwards. (webhook-timeout-10s)
Added
- Minimal payloads.
"payload": "minimal"onPOST /v1/hooks: every delivery carries onlyevent,event_id,created_at,document_id, plussigning_request_id,role,referenceon signer events andsigned_pdf_url,sha256,audit_trail_urlondocument.completed, withUser-Agent: wesign-webhooks/1. The default stays"full": the documented body, unchanged apart from this release's additive fields. Left out on a re-POST that replaces a minimal hook,payloadstays"minimal".GET /v1/hooksreportspayload. See Payload modes. (hooks-payload-minimal) - Top-level
roleandreferenceon everysigning_request.*event, for every hook; for a template recipientroleis the slot's role label. (webhook-top-level-role-reference) - Delivery log.
GET /v1/hooks/{id}/deliveries: the last 100 deliveries with event,event_id,document_id, attempts, your endpoint's last status and the first 500 characters of its answer, latency and state. The dashboard shows the same log. (hooks-delivery-log) - Redeliver.
POST /v1/hooks/{id}/deliveries/{delivery_id}/redeliversends a delivery again with the sameevent_id, signed afresh, and returns the outcome; the dashboard has a Redeliver button. A replaced hook cannot redeliver, so redeliver given-up deliveries before you re-POST. Test pings and redeliveries go to https hooks only (409 https_required), at most 10 a minute per hook (429 rate_limited). (hooks-redeliver) - Test ping.
POST /v1/hooks/{id}/testsends that hook a signed{"event":"ping",…}and returns the outcome. The dashboard's Send test sends the same ping to that one hook (before: a syntheticsigning_request.sentwith"test": trueto every subscribed hook of the workspace). (hooks-test-ping)
Changed — additive
signing_request.expiredcarries the fullsignerblock, like the other signer events (before:{ email, name }). (webhook-expired-signer-block)- A 12-hour step joins the retry schedule: eight attempts over about 45 hours
(before: seven over about 33). A redirect is still a failure; the delivery
log now records its Location. (
webhook-retry-12h)
Fixed
GET /v1/hooksanswers500 db_failedwhen the database read fails; before, it answered an empty list. (hooks-list-read-failure)- The retry scheduler no longer picks up a delivery whose first attempt is
still in flight, which could rarely send it twice. (
webhook-first-attempt-once)
2026-10-01 — Signer company and function, one seal at a time, all-or-nothing creates
Integrators asked how observers, AES signatures and signer identity work in
detail. Answering them turned up defects our own review then confirmed; this
release fixes them and adds what was missing. The spec is info.version
2026-10-01, frozen at /openapi/2026-10-01.json. Each item ends with its
id in api-changelog.json.
Breaking
- Anchor field keys are used. The third part of a
textornamemarker ([[ls:text:tenant:person.full_name]]) is stored as the box's key; before, it was accepted and ignored. A box keyed with one of the signer's own details (person.full_name,contact.email,company.legal_name, …) opens with that detail, and the signer can change it. Anamemarker without a third part is keyedperson.full_name, so it opens with the signer's name; before, it was an empty box. See Anchor placeholders. (anchor-text-field-key) - Two write failures that were ignored now fail the create. A failed
callback_urlwebhook onPOST /v1/signing-requestsused to be skipped: the 201 handed out the secret of a hook that was never stored, so no callback ever came. A failed field write onPOST /v1/signing-requestsorinstantiateused to leave a signer with a link and nothing to sign. Both now answer500 db_failed, and the call is rolled back (see below). (create-checked-writes)
Changed — additive, but check your client
- Creates are all or nothing.
POST /v1/signing-requestsandinstantiatewrite the document, its callback webhook, every signing request, its fields and its audit rows before anything is sent; then the invitations go out, then the observer notice (POST /v1/signing-requestsonly), then the onesigning_request.sent. A failed write deletes everything the call created, the PDF it uploaded or rendered included, contacts nobody and answers500 db_failedwithmeta.rolled_back: trueand anerrorthat ends "Nothing was created or sent; retry the request."; theIdempotency-Keyis released. In the rare case that the clean-up fails as well,meta.rolled_backisfalseandmeta.document_idnames the leftover document. Before, a failure onsigners[1]answered500aftersigners[0]had been invited and the observers notified, and the leftover one-signer document could complete on that one signature. See If the call fails. (create-all-or-nothing) - "Completed" only when every signer signed. When a co-signer declined,
was withdrawn or expired, the last remaining signature no longer sends a
"Completed" email or a certificate: observers and earlier signers receive
nothing, and the signer and the sender each get a progress note without
attachments. Before, observers and earlier signers could be emailed
"Completed" with a PDF that lacked the missing signature.
document.completedandGET /v1/documents/{id}already followed this rule. (observer-completed-every-signer) - The emailed certificate is always English. The audit certificate
attached to completion emails used to be in the language of the signer
whose signature completed the document. It is now English for everyone,
like
GET /v1/documents/{id}/audit-trail, with its times in the time zone of the caption under each signature. (certificate-always-english)
Added
- Company and function per signer. Optional
company(at most 200 characters) andjob_title(at most 120) onsigners[], instantiaterecipients[]and confirmrecipients[]. They are your statement; we never verify them. They print under each signature —Anna Muster · Geschäftsführerin · Muster AG · anna@muster-ag.ch, shrunk to fit the box down to 6 pt, else with function and company on a line of their own under the name, never cut and never off the page — and on the audit certificate, the one attached to the completion emails and the one fromGET /v1/documents/{id}/audit-trailalike, labelled "stated by the sender". They are read back ascompanyandjob_titleon every signer object and in the signer block of the webhooks. A line break or other control character, or a longer value, is a400 invalid_request. See Company and function. (signer-company-job-title) - One signer block on every signer webhook.
signing_request.signedanddocument.completedcarry{ email, name, role, slot, reference, company, job_title }, like the signer events; they carried{ email, name }.signing_request.sentfrom the API or a sequential release and the six signer events addcompanyandjob_title.signing_request.expiredis unchanged. (webhook-signer-block) - A higher SMS allowance on request. Email support@wesign.now with your
workspace ID and the codes you expect per month; we raise the workspace's
monthly allowance, included in your subscription. There is no price per
SMS.
GET /v1/me→sms_allowance.monthlythen shows the plan's codes plus the raise, and every402 sms_allowance_exhaustedcheck uses that number. See the monthly allowance. (sms-allowance-raise) - Docs for AI assistants.
/llms.txtlists every docs page with a one-line summary;/llms-full.txtis every page as plain Markdown. New page: the Integrator FAQ. (ai-assistant-docs)
Fixed
- Parallel signers. The signatures of one document are sealed one at a
time. Before, the completed PDF could lack an earlier signature while every
status read
signed, whatever thesigning_mode: when two signatures landed within the same seconds, or, rarely, when the earlier signed file could not be read at the next signature (the seal then started again from the original file); anddocument.completedcould fire twice. Now the completed PDF carries every signature, a signer who presses Sign while another signature is being sealed waits a few seconds (the signing page and an embedded frame retry by themselves), and only the signature that completed the document sendsdocument.completedand the "Completed" emails. Keep deduplicatingdocument.completedondocument_id. If a PDF completed before this release lacks a signature, email support@wesign.now with itsdocument_id. See Parallel signers. (parallel-signing-chain) - Chinese, Japanese and Korean text in a sealed PDF or a certificate renders
in every PDF viewer. It used to show as empty boxes in Poppler-based
viewers and as wrong characters in macOS Preview. This has been live since
the 2026-09-25 deploy; the 2026-09-25 notes did not list it. PDFs sealed
before that deploy keep the old font. (
cjk-text-all-viewers)
Docs corrected
- No standalone phone verification. Verifying a number by SMS without a
signing request, listed on the SMS page as planned for 2026-11-15, is not
offered and not planned. Check numbers in your own interface; after the
create,
signing_request.sms_failednames an unusable number andPATCH /v1/signing-requests/{id}corrects it. (no-standalone-phone-verification) - The stored signer name is "first last" as soon as either
first_nameorlast_nameis non-blank, andnameis then ignored; the docs said "when both are given". (signer-name-rule-docs) observer_emailsaddresses are checked before they would be trimmed: a leading or trailing space is a400 invalid_request. The docs said they were trimmed. (observer-address-docs)- The RFC 3161 time stamp is kept with our records and reported as
tsa_providerandtsa_signed_at; it is not embedded in the PDF, as the Documents page said. (tsa-token-docs)
Migration notes
- On
500 db_failedfromPOST /v1/signing-requestsorinstantiate, retry the same body (the sameIdempotency-Keyis fine). Withmeta.rolled_back: false, first withdraw any signing request ofmeta.document_idthatGET /v1/documents/{id}still shows aspending. - Read webhook signer blocks by key:
signing_request.signedanddocument.completednow carryrole,slot,reference,companyandjob_titletoo. - Optional: send
companyandjob_titlefor signers who sign on behalf of a company, and stop printing them yourself next to the box. Leave about 40 pt below each signature box (about 50 pt when function and company need a line of their own), and some room to its right, for the caption. - If your PDFs carry
namemarkers, ortextmarkers with a third part, check what those boxes now open with. - Keep deduplicating
document.completedondocument_idas well asevent_id.
2026-09-25 — Monthly cap on instantiate, quiet sequential sends, who signs each slot, file names in every script
The spec is info.version 2026-09-25, frozen at /openapi/2026-09-25.json.
Breaking
send_emails: falseholds for the later signers of a sequential send onPOST /v1/signing-requests,instantiateandconfirm. The choice is stored on every signer, so when a signer's turn comes they are not emailed either. Before, the next signer was emailed on release whatever the call said. If you relied on that email, sendsend_emails: true, or deliver the signer's link when the release'ssigning_request.sentarrives (see Added — signer events). Requests created before this release are still emailed on release.402 tier_requiredoninstantiate: an instance counts against the workspace's monthly document cap exactly likePOST /v1/signing-requests. At the cap nothing is created and the body is{ error, code, meta: { tier, cap, used } }. This applies toreview: truetoo — a staged instance counts from the moment it exists, and its confirm is not counted again. Avalidate_onlydry run reports it inproblemswithok: false. Enterprise, the only tier with template channels, has no cap, so no workspace meets this today.
Added
signing_request.sentalso fires when a sequential document releases its next signer — once per released signer (once per document of an envelope), whatever sent the document — with"source": "sequential_release",occurred_atandemailed(falsewithsend_emails: false: deliver the link yourself). It may arrive before the previous signer'ssigning_request.signed. Before, no event marked the release. A receiver that treatssentas "the document went out" should skipsource: "sequential_release". See Following a sequential send.- Who signs each slot:
recipients[]onGET /v1/templates/{id}carries the author'sroleand pin when set.pinned: "email"comes withpinned_email, the one addressinstantiateandconfirmaccept for that slot — send it instead of meeting400 pinned-slot-mismatch.pinned: "sender"is whoever sends in the app; an API key does not have to match it. An unnamed, unpinned slot is still{ "slot": n }. See Several signers. - The reminder cooldown's
429 rate_limitedonPOST /v1/signing-requests/{id}/remindcarriesRetry-After: 60, like every other 429. It still has noRateLimit-*headers: it is not the per-key window.
Fixed
POST /v1/signing-requests/{id}/withdrawanswers500 db_failedwhen the status update fails; it answered400, which read as a fault in the request. Nothing changed — the request is still in flight and no webhook fired — so retry.- File names keep the letters of every script. A template called
Vollmacht MüllermakesVollmacht_Müller.pdf, notVollmacht_M_ller.pdf— and that is the name the invitation email shows. The same goes for afilenamesent toPOST /v1/signing-requestsorgenerate. Digits,_,.and-are kept as before; spaces and any other character still become_. generate's PDF answer names the file in RFC 5987 form (filename*=UTF-8''…) with an ASCIIfilenamefallback, so the download keeps its real name.
Docs corrected
- An archived template answers
409 template_unlockedoninstantiateandgenerate, with or without a version pin. The dashboard's archive dialog said such calls would return410; it now says409as well. - The reminder cooldown was documented without
Retry-After; it has one now (above).
Migration notes
- Handle
402 tier_requiredoninstantiateas you do onPOST /v1/signing-requests. - Retry a
500 db_failedfromwithdraw; it is not a request error. - Sequential sends with
send_emails: false: store every signer's link from the create answer and deliver the next one onsigning_request.sentwith"source": "sequential_release"— we no longer email it. A receiver that countssentas once per document should skip that source.
2026-09-24 — SMS you can rely on: before-view gate, read-back, allowance up front, signer events
An integrator that sends every contract with an SMS second factor reviewed
the API and found the factor could go missing without a trace. This release
closes every such gap and documents the rest. The spec is
info.version 2026-09-24, frozen at /openapi/2026-09-24.json.
Breaking
- Signer objects are strict on
POST /v1/signing-requests: an unknown key —phoneinstead ofphone_e164, a typo — is a400 unknown_signer_fieldnaming every offender (meta.signers,meta.accepted). Before, it was dropped silently. - Template recipients are strict on
POST /v1/templates/{id}/instantiate, and they now takephone_e164,require_sms_verification,sms_gateandreference. Before, the SMS fields were dropped silently: a document sent directly from a template went out without the SMS gate. Any other recipient key is a400 unknown_recipient_field. - Confirm recipients are strict on
POST /v1/documents/{id}/confirm(400 unknown_recipient_field), a body that is not JSON is a400 invalid_requestinstead of "confirm unchanged", and a refused confirm now writes nothing (before, the recipient edits were saved even when the confirm was refused). One exception: a discard or review expiry landing while the confirm renders answers409after the edits were saved, on an instance nobody can confirm any more. 402 sms_allowance_exhaustedonPOST /v1/signing-requests, a directinstantiateandconfirm: when the signers who must verify by SMS outnumber the codes left in the workspace's monthly allowance, nothing is created. Before, the call succeeded and the signer could not sign later. Body:{ error, code, sms_allowance, required }.- Dates must be real calendar days:
2026-02-30in adatefield is422 invalid_date(it passed the shape check before). - Anchor markers with a dotted or long field key are placed on
POST /v1/signing-requests. A text marker whose FIELD_KEY contains a dot or has 42–64 characters ([[ls:text:tenant:person.date_of_birth]]) used to be skipped silently; it is now a text field. So its ROLE must be one of the signers' roles (400 unknown_role), and a PDF whose only markers were such markers no longer falls through toauto_append— every signer then needs a marker (400 signer_has_no_anchor). metadatais validated onPOST /v1/signing-requests,instantiateandgenerate. Before this release the key was dropped silently, so any value passed; a value that is not a flat object within the limits is now a400 invalid_metadata.send_emailsmust be a boolean onPOST /v1/signing-requests(JSONtrue/false, multipart"true"/"false"). Before, the key was ignored; a string such as"false"or"no"is now a400 invalid_request.- The multipart
filenameis read onPOST /v1/signing-requests(it was ignored), so a value over 200 characters is now a400. - A body that is not JSON is a
400 invalid_requestoninstantiateandgenerate, as onconfirm. Before, it was read as{}:instantiaterefused it with a message aboutrecipients, andgeneraterendered the template without your values — streaming it even when the lost body asked forreview: true. An empty body is still{}. signingUrlisnullwhile staged onGET /v1/signing-requests/{id}(and thePATCHanswer), next to a newstaged: true, asGET /v1/documents/{id}already reported. Before, a staged recipient read a link that opened only a waiting notice and could not be signed.502 render_failedoninstantiate: when the filled PDF of a rich-text template cannot be rendered or stored, nothing is created — no document, no signing request, no email — and theIdempotency-Keyis released. Before, the call succeeded with the template's own PDF: the signers received the contract with its{{placeholders}}unfilled. Body:{ error, code }, as on confirm. A PDF template is never rendered.
Changed — additive, but check your client
- Dates print in the template's format. In a rendered or signed
document, a date placeholder prints as its template picked; one placed
before formats existed prints
15.03.1990— an API-filled date used to print1990-03-15.{{date_today}}stays written out, now in the placeholder's language, else the document's, else the call'slocale(before: English, except on areview: trueinstance, which used the call'slocale). A stagedgeneratekeeps nolocale, so there it ends in English. The stamped signing date (adatefield or marker) prints in the field's format, else the workspace's house format —15.03.1990, or03/15/1990for a workspace in the United States — instead of24 Sept 2026, and the signing page shows it exactly as it will be stamped. Nothing you send and nothing echoed back changed. validate_onlyoninstantiatealways carriesproblemsand answersok: false(still HTTP 200) when the real call would be refused for the SMS allowance. Readok, not only the status.- Six new webhook events reach every hook whose
eventslist is empty — everycallback_urlhook included — without opting in. Answer2xxto event types you do not handle. viewedis now a real status. Nothing ever set it before; a request now movespending→viewedthe first time the signer's browser loads the document. Treat it likepending.- Idempotency: the hash of
POST /v1/signing-requestsnow includesmetadata,send_emailsand the multipartfilenamewhen you send them — a retry across this deploy with the same key and such a body answers422 idempotency_key_reuse(only inside the key's 24 hours). - SMS refusals are on the audit trail. A code request refused because
the allowance is used up or no usable number is stored now writes
sms_verify_failed(before: nothing), visible inauditEvents. - Dotted field keys:
person.date_of_birth,company.legal_name,company.uidare valid keys — lowercase[a-z][a-z0-9_]*parts joined by single dots, up to 64 characters (before: no dots, 40 characters) — in the field registry (GET /v1/fields), templates (GET /v1/templates/{id}),field_values, formulas and anchor markers. A dot is part of the name, never nesting:field_valuesstays a flat map. Every existing key is still valid; if your client checks keys against the old^[a-z][a-z0-9_]{0,39}$, widen it. A formula reads a dotted table ({{= SUM(order.items.total) }}), and a{{person.date_of_birth}}in a template body is filled — it used to print as literal text. signer_requiredisfalseon every positioned text field ofGET /v1/templates/{id}. It used to echo the field's stored flag, but an empty text field no longer blocks signing (see Fixed).- Email links point at wesign.now. Invitation, reminder and completion
emails carry the wesign.now design and link to
https://www.wesign.now/…, orhttps://<slug>.wesign.now/…for a branded workspace. Replies go to the workspace's own reply-to address when it has one, else to support@wesign.now. The sending address did not change, and neither did any URL the API returns: the signing links on create, instantiate, confirm and the GETs keep their old host, as does the link in the invitation SMS of confirmchannelsms/both. Both hosts open the same signing page for the same token; links already sent keep working. If you filter or allowlist our emails by the domain of their links, add wesign.now.
Scheduled
- 2026-12-31 — an unknown top-level key on
POST /v1/signing-requests,instantiate,generateandconfirmis refused with400 unknown_field. Until then it is ignored and named inwarnings[]— on thegeneratePDF stream, which has no JSON body, in theX-Unknown-Fieldsresponse header. See Unknown keys. - 2026-12-31 —
recipients[].phoneon confirm stops being accepted; usephone_e164. Until then each use adds adeprecated_fieldwarning, and both with different values are a400 conflicting_fields.
Added — SMS
sms_gateper signer and recipient:"before_sign"(default, unchanged) or"before_view"— nothing of the document (title, file name, pages, prefilled values) is shown until the code is verified, and every invitation email leaves it unnamed. The code is texted when the signer taps for it on that page, never on page load. Not yet in embedded signing. See SMS verification.- A per-signer read-back on every signer of the create, instantiate,
confirm and
GET /v1/signing-requests/{id}responses:require_sms_verification,phone_masked,sms_verified_at,sms_gate,signature_level(AES|SES),reference. PATCH /v1/signing-requests/{id}{ "phone_e164": … }corrects a signer's phone while the request is open and unverified (409 not_editableotherwise). Sends nothing; audit eventphone_changed, both numbers masked.GET /v1/mereturnssms_allowance: { monthly, used, remaining, resets_at }next tocapabilities.- The SMS code is sent in the signing request's
locale(en, de, fr, it, es, nl) instead of the language Twilio derives from the number's country (every Swiss number used to get German, Dutch and Liechtenstein numbers English).
Added — signer events
signing_request.viewed,.declined(withreason),.withdrawn(withwithdrawn_by),.sms_sent,.sms_failed(with areasoncode) and.sms_verified, on one shared payload{ signing_request_id, document_id, signer { email, name, role, slot, reference }, occurred_at }. See Signer events.signing_request.sentcarriessigner.reference, and a directinstantiate(withoutreview) now emits it. Itssigner.slotis the stored value like on every other event:nullon a parallelPOST /v1/signing-requestseven when asigning_orderwas passed (that ignored value was echoed before).
Added — references and options
reference(≤ 200 characters) per signer and recipient on create, instantiate and confirm.metadataonPOST /v1/signing-requests,instantiateandgenerate: a flat object stored on the document and echoed on the create response,GET /v1/documents/{id}and every webhook naming the document.send_emailsonPOST /v1/signing-requests(defaulttrue), withemailedper signer. It never stops the SMS code, which the signer requests on the page.warnings[]on the JSON success bodies of create, instantiate, generate and confirm; the directinstantiate200now also carries thefield_valueswarnings it used to drop.capabilitiesonGET /v1/me;enabledon every hook,GET /v1/hooks?include_disabled=true, andPOST /v1/hooks/{id}/rotate(new secret once, the old one valid for 24 hours, twov1=entries in the signature header meanwhile).
Added — day-first dates
- A
datevalue infield_valuesmay be written day first —DD.MM.YYYY,DD/MM/YYYY,DD-MM-YYYYorDD MM YYYY, day and month one or two digits (15.03.1990,5.3.1990,15 03 1990) — as well asYYYY-MM-DD, oninstantiateandgenerate. It is stored and echoed asYYYY-MM-DD. Month-first is never tried:03/04/1990is 3 April, and03/15/1990is422 invalid_date. Theinvalid_datemessage now names the accepted formats.
Added — date and time formats, and a time type
- The template author picks how each date or time placeholder prints:
15.03.1990,15/03/1990,03/15/1990or written out (24. September 2026, in a chosen language);14:30or2:30 PM.GET /v1/templates/{id}shows the pick read-only asdate_format,time_formatandlangonfield_values[]entries, anddate_format(+lang) ondateentries ofsigning_fields[]. Absent means the default. It never changes what you send, what is stored or what is echoed (see How dates and times print). - A
timefield type:HH:MMorHH.MMon a 24-hour clock, orh:MM AM/PM(14:30,9:05,2:30 PM,2:30pm), stored and echoed as 24-hourHH:MM; anything else is422 invalid_time.GET /v1/fieldscan listtimedefinitions, and the spec now enumerates everyFieldDefinition.type(a new type is an additive change: treat one you do not know liketext).
Added — who fills a field, standard names, what is still missing
- Filled by on each text box of a PDF template. Sender / API: the
input is
owner: "sender"andrequired: trueunless the author made it optional or gave it a default; the value you send prints as fixed text the signer cannot change. It is document content: every signer sees it from their first view, whichever slot its box sits on and in any signing order, and it is sealed once.sloton such an input only says which signer's field set stores it. Signer:owner: "signer", never required; you may pre-fill it and the signer may change it. Every existing field stays Signer, so no contract changes until a template's author chooses and locks a new version. See Which fields you must supply. - Correct values before sending. A PDF-template instance staged with
review: truecan have its Sender / API values corrected onreview_url(Edit values, for a signed-in member of your workspace; a Signer box or a platform value cannot be edited there). Confirm checks the values again against the template version the instance was staged from and copies each one onto the signing fields its seal prints, so the signers sign what the reviewer approved. Confirm itself takes nofield_values: such a key is ignored and named inwarnings[]asunknown_field. - Standard field names: 19 English dotted keys anchored to international
vocabularies (
person.full_name,person.date_of_birth,company.legal_name,company.uid,address.postal_code,contact.email,signing.place, …). An input using one carriesstandard: true,labels(its name in en, de, fr, it, es, nl), anexample, a hintpattern(never enforced) and an Englishdescription; every other input carriesstandard: false.GET /v1/fieldsmarks registry definitions the same way. Custom keys stay first-class. The list is in Fields & placeholders. missing_optionalon avalidate_onlyanswer ofinstantiateandgenerate, on the200and on the dry-run422: the inputs you may still supply and left empty, with what you need to ask your user for them. It never makesokfalse and is absent from a real call's422.generatenever requires or lists a positioned field.- A new guide, Prepare a fill in your app: read the contract, ask your user, dry-run, send.
Fixed
- The multipart
filenamefield onPOST /v1/signing-requestsis honoured (it was ignored); over 200 characters is a400(see Breaking). - An empty text field no longer blocks the signer.
- An anchor marker whose field key contains a dot or is longer than 41 characters is placed; it was skipped silently (see Breaking).
- A text box of an API-instantiated document now carries its key and label
onto the signing page, as a quick-send from the app always did. A Signer box
keyed with an auto-fill name (
full_name,email, …, or the standardperson.full_name,contact.email, …) now opens with the signer's own details when you sent no value and the author set no default. - Confirming a staged instance checks its positioned text fields against the
template version it was staged from, not the current one, so re-locking
the template meanwhile can no longer block the confirm. A generated file
(
generatewithreview: true) never requires a positioned field at confirm. - A rich-text template whose filled PDF could not be rendered is no longer
sent unfilled from
instantiate(see Breaking). - One valid seal on a multi-signer PDF. The completed file
(
GET /v1/documents/{id}/signed,document.completed→signed_pdf_url) carries one PAdES seal, valid over the whole document. Each later seal used to be added on top of the earlier one, which Acrobat and pdfsig then reported as invalid ("Digest Mismatch") next to the valid last seal. If a multi-signer PDF completed before this release shows an invalid earlier signature, ask support to re-seal it. - Earlier signers get the completed PDF. When the last signer signs, everyone who signed earlier receives the sealed PDF by email, in their own language. Before, that email went out only when the workspace sent signers the Certificate of Completion (by default only the initiator gets it), so whoever signed first never received the final document. The certificate still follows that setting.
- Formulas compare dates as days. A comparison (
<,>,<=,>=,=,!=) whose two sides are dates compares the days, whatever accepted spelling each side uses, as conditional sections do:{{= IF(person.date_of_birth < "2008-09-24", "adult", "minor") }}now gives the same answer for1990-03-25and25.03.1990(a day-first value was compared as text).15 03 1990now reads as a date, not the number 15031990. Numbers and plain text compare as before.
Docs corrected
- Confirm's phone field: the docs said the signing-request spelling
phone_e164works on confirm; confirm took onlyphoneand silently ignoredphone_e164. Both work now,phone_e164is canonical. - SMS availability: every plan, within a monthly allowance (Free 25, Pro 50, Branded 200, Teams/Enterprise 100 per paid seat) — the pricing matrix showed none on Free.
- The embedded page said a decline reason reaches you by webhook; no event
carried it until
signing_request.declined. GET /v1/meis documented with its full body;GET /v1/signing-requests/{id}returns the first 50 audit events (oldest first), not the last.- The code is asked at the Sign press by default, not "before the document
is shown" — that is now
sms_gate: "before_view". - The OpenAPI spec carries worked examples for
GET /v1/templates,GET /v1/templates/{id},instantiate(request, direct200, staged201, dry runs,422), confirm and discard. Template.statusis an enum —draft,locked,archived; the list never shows an archived template,GET /v1/templates/{id}still answers for one.sourceon a template input is legacy: readownerandrequired.review_urland the422docslink are pages in the web app (not on the API host) that only a signed-in member of your workspace can open.- The direct
instantiate200carries notemplate_id; the staged201and the dry run do. - The dry-run note implied that
instantiatechecks a monthly document cap. It applies none, dry run or not. - The
400/402/422bodies that are either a specific error or the genericErrorare declaredanyOf, notoneOf: every specific body also matchedError, so a strictoneOfrejected them all. signing_request.sent→signer.slotis the recipient slot on every template send, in either signing mode; the spec saidnullin parallel mode.
Migration notes
- Send only documented keys in
signers[],recipients[]and confirmrecipients[]; watchwarnings[]and clear every top-levelunknown_fieldbefore 2026-12-31. - On confirm, rename
phonetophone_e164before 2026-12-31. - If you instantiate templates with SMS signers, move them onto
recipients[].phone_e164+require_sms_verificationand assertsignature_level: "AES"on the response. - Handle
402 sms_allowance_exhausted; readGET /v1/me→sms_allowanceto warn before sending. - Make your webhook receiver answer
2xxto unknown events, and add the six signer events you want. - Treat
viewedlikepending. - On
instantiatevalidate_only, checkok. - Send
datevalues day first or asYYYY-MM-DD, never month first — also when a template prints them03/15/1990. Sendtimevalues asHH:MMorh:MM AM/PM. - If a PDF carries text anchor markers with a dotted or 42–64-character field key, check that their role is one of your signers' roles.
- Decide what to send from
requiredonGET /v1/templates/{id}, never fromowner: a template whose author sets a text field to Sender / API requires it from its next locked version on. Avalidate_onlydry run names anything you missed. - Optional: map your data to the standard field names once, and offer your
user what
missing_optionallists. - Read
signingUrlonGET /v1/signing-requests/{id}as nullable; a staged recipient's link comes from the confirm. - Retry a
502 render_failedfrominstantiate: nothing was created, so send the same body again (with the sameIdempotency-Key, if you use one). - If you filter or allowlist our emails by the domain of their links, add wesign.now. The signing links the API returns keep their old host.
2026-09-10 — Pin an API key to your own IP addresses
Added — a per-key IP allowlist
- Every API key now carries a list of IPv4/IPv6 addresses and CIDR blocks
(
203.0.113.7,203.0.113.0/24,2001:db8::1,2001:db8::/32), managed on the key's card in Settings → API, up to 50 entries. Entries are canonicalised on save, so what the card shows is what we match. - An empty list means unrestricted. Nothing changes for any existing key, and nothing changes for you until you deliberately add an entry. This is the opposite of the embed-origin list, where empty means "cannot embed" — worth reading twice, it is the half people get wrong.
- The address we compare is the one our platform observed the call arriving
from.
X-Forwarded-Foris not used for the decision: a caller can write that header itself, so trusting it would be decoration. For the same reason we do not and will not authorise REST calls byOrigin— see Security.
Added — 403 ip_not_allowed
- A key that is valid but called from an address its allowlist does not cover
answers
403 { error, code: "ip_not_allowed", ip }instead of the generic401 invalid_key— a different problem deserves a different code.ipis the address we saw (nullwhen it could not be determined, in which case a restricted key fails closed). Documented on every key-authenticated operation in the OpenAPI spec. - The control gates API keys only: never the workspace UI, never the signing pages. A mistyped address locks out an integration, never a person — sign in from anywhere and empty the list.
Full contract: Restricting a key to your own IP addresses.
2026-09-10 — Embedded signing: sign inside your own page
Your signer can now sign in an <iframe> or a mobile webview on your
domain, with your branding around it. Everything here is additive; no
existing endpoint changed shape.
Added — POST /v1/embedded/sign-sessions
- Exchanges an existing signing request for a short-lived, single-signer
session:
{ signing_request_id, origin, ttl_seconds?, locale?, theme? }→201 { id, embed_url, expires_at, signing_request_id }.ttl_secondsis 60…900 (default 300) and out-of-range is refused, not clamped.embed_urlis on the workspace's own signing host, so the signer never sees a domain their firm did not put in front of them. Enterprise only (402 enterprise-required); needs a key with theembeddedscope (afullkey satisfies it); on the standard 60/minute per-key bucket. GET /v1/embedded/sign-sessions/{id}→{ id, status, expires_at, first_seen_at, last_seen_at, signing_request_id }withstatusone ofactive/expired/revoked.DELETErevokes, idempotently, and answers{ "ok": true }. Neither is Enterprise-gated: revocation is a safety valve and never depends on billing.- New error codes:
invalid_origin(400),origin_not_allowed(400),request_not_signable(409),embedding_unavailable(503, while a deployment has not applied the migration).
Added — ancestor origins are registered per API key
- Embedding is a permission, not a default. The origins allowed to frame a
session are listed on the key in Settings → API (up to 20, exact
https://host[:port]match, no wildcards). A key with an empty list cannot mint a session.
Added — postMessage schema v1 and a documented CSP
- The frame posts
{ v: 1, type, session_id, signing_request_id, at, detail? }towindow.parentwithtargetOriginset to the registered origin, never'*'. Types:wesign.ready/viewed/signed/declined/expired/error. They are a UX signal only — webhooks remain the source of truth. - Hosts allow exactly one directive:
frame-src, naming the workspace's signing host on both roots (Content-Security-Policy). We answer embed paths withContent-Security-Policy: frame-ancestors <the session's one origin>and omit the app's globalX-Frame-Optionsthere, defaulting toframe-ancestors 'none'whenever the session cannot be read.
Full contract, a host-side listener and a copy-pasteable example page: Embedded signing.
Unchanged — the retired endpoint
POST /v1/embedded/sessions— no hyphen, the 2026 guest-placement flow — still answers410 embedded_sessions_retired. The two share nothing but a word.
2026-09-10 — One-time hook secret, honest errors, expiry events
Changed — document.completed
audit_trail_urlis nowhttps://api.wesign.now/v1/documents/{document_id}/audit-trail— key-authenticated, same host, same Bearer key and same status rules assigned_pdf_url. Previously a token link on the web origin whose route no longer existed (it answered 404).- The event fires only when every signer row on the document is
signed— the same ruleGET /v1/documents/{id}(status: signed),/signedand/audit-trailalready applied. A document with a declined, withdrawn or expired signer no longer receives it; before, it fired and both URLs answered409 not_complete. See what "completed" means.
Added — POST /v1/hooks returns the secret once
- The create response now carries
secret(whsec_…), shown once.GET /v1/hooksnever returns it; lose it and you delete + resubscribe. Additive — the response keeps every existing field.
Added — signing_request.expired is emitted
- An hourly cron flips a
pending/viewedrequest toexpiredonce itsexpires_athas passed and emits the event exactly once per request:signing_request_id,document_id,signer { email, name },expired_at. Queued sequential followers behind an expired signer do not emit.GET /v1/signing-requests/{id}and thesigners[]onGET /v1/documents/{id}now reportstatus: "expired"for such rows (previously the row stayedpendingand expiry was only visible viaexpiresAt).remindon one answers409 expired,withdraw409 invalid_state— both as documented.
Changed — POST /v1/templates/{id}/instantiate error bodies
- Every error now carries a stable
code. New:invalid_key(401),invalid_request(400 body validation),not_found(404),version_not_found(404,meta.version+meta.current_version),missing_recipients(400,meta.slots),db_failed(500). Messages are unchanged; only thecodefield was added.
2026-09-09 — api.wesign.now is the canonical host
The product now also runs as wesign.now, and the public API takes the new name first. Everything here is additive: every old spelling keeps working, nothing is removed.
Base URL
- Canonical:
https://api.wesign.now/v1. Every example on this site now uses it. - The previous API host is a permanent alias — identical routing and responses. Do not migrate a working client on our account.
- Path-style URLs on the web hosts (
https://<root>/api/v1/…) were never the contract; the apex only redirects. See Base URL.
API keys
- New keys are minted as
wsk_live_<32 hex>; the dashboard shows the first 12 characters (wsk_live_a1b…). - Existing
lsk_live_keys are accepted forever. No rotation needed.
Webhook headers
- Every delivery now carries
X-WeSign-Signature,X-WeSign-Event-IdandX-WeSign-Event, with the same values as the headers it carried before. The scheme is unchanged:t=<unix seconds>,v1=<hex HMAC-SHA256(secret, "<t>.<rawBody>")>. - The docs now spell out the exact message format, a 300 s replay tolerance, event-id idempotency, and give Node and PHP verifiers — Verifying a signature.
Changed — document.completed.signed_pdf_url
- Now
https://api.wesign.now/v1/documents/{document_id}/signed: a key-authenticated API URL on the canonical host (Bearer key of the owning workspace;409 not_completeuntil every signer signed;404 not_foundfrom another workspace). - Previously it was an unauthenticated token link on the web origin. A
receiver that fetched it without a key must now send the bearer; one
that pins the origin should pin
api.wesign.now. No integrator was live on the old shape.audit_trail_urlmoved the same way one day later — see the entry above.
Docs
- Second factor by SMS
(
phone_e164+require_sms_verification) is documented. - Embedded signing states what exists today and what the iframe phase will be.
- The events table lists only events that are actually emitted.
2026-09-06 — Review before sending
"review": trueonPOST /v1/templates/{id}/instantiateand/generatestages the instance instead of sending it; newPOST /v1/documents/{id}/confirmand/discard, newGET /v1/documents/{id}/pdffor a finalized file-only instance,"validate_only": truedry runs, and thetemplate_instance.staged/.confirmed/.discardedwebhook events — see Templates.POST /v1/templates/{id}/instantiatetakes anIdempotency-Key(same semantics asPOST /v1/signing-requests) and sits on the per-key rate limit; its409carriescode: "template_unlocked".- Breaking:
POST /v1/templates/{id}/generatesits behind the same Enterprise template entitlement asinstantiate(402 enterprise-required). - Fixed: documents created over the API are attributed to the key's
creator. Before,
POST /v1/signing-requestsandinstantiatefailed with500on the document insert.
2026-07-31 — Retirements (breaking, zero production usage)
placement="manual"onPOST /v1/signing-requests→400 placement_retired.POST /v1/documents(ingest-by-URL for manual placement) →410 placement_retired.file_urlonPOST /v1/signing-requestscovers ingest-by-URL.POST /v1/embedded/sessions→410 embedded_sessions_retired.GET/DELETE /v1/embedded/sessions/{id}still answer for existing rows.
All three answer a stable, descriptive 410 rather than a 404, so an
old integration fails loud.
2026-04-30 — v1 launch
The v1 public API launches with the full reference live.
New endpoints
| Method | Path | Purpose |
|---|---|---|
| POST | /v1/signing-requests | Create — multipart upload, four placement modes |
| GET | /v1/signing-requests/{id} | Read one signer + its first 50 audit events |
| POST | /v1/signing-requests/{id}/remind | Re-send invite |
| POST | /v1/signing-requests/{id}/withdraw | Cancel in-flight |
| GET | /v1/documents/{id} | Holistic doc + signers + rollup status |
| GET | /v1/documents/{id}/signed | Final PAdES-sealed PDF |
| GET | /v1/documents/{id}/audit-trail | Generated audit-trail PDF |
Concepts
Idempotency-Keyheader with a Stripe-style pending lock for concurrent retries — see Idempotency.- 60 req/min/key rate limiter with
Retry-After— see Rate limits. - Workspace-scoped Bearer keys; rotation via mint-then-revoke.
- Multipart placement modes: anchors / auto_append / explicit / manual — see Placement modes. Manual mode collapses the previous "magic-link" mode into a single URL that works with or without a workspace login.
- Doc-scoped callback webhooks via
callback_urlon POST. HMAC-SHA256 signed events, 7-attempt exponential-backoff retries.
Subdomain branding
Branded + Teams tier workspaces get their own subdomain
(<slug>.<root>/sign/...) for signer-facing pages. Wildcard cert
provisions automatically; no DNS work per-customer.
Migration from /docs/api
If you bookmarked the old monolithic
/docs/api page: it now permanently redirects
to /docs. The content has been split into the reference pages
listed in the sidebar.
If you used a deep section anchor like /docs/api#errors, it
redirects to /docs (apex). Direct links to specific topics are now
e.g. /docs/errors, /docs/webhooks.
Versioning policy
Additive changes ship to /v1 with a changelog entry. Breaking changes are
announced here at least 90 days ahead, with the one exception of refusing
input that never had an effect; a change that cannot be announced that way
would ship as /v2, with /v1 kept for at least 12 months after /v2 GA.
The full policy, deprecation signals and the dated spec snapshots:
Versioning.
