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 /v1 operations themselves: the same monthly caps, rate limit (one token per tool call; whoami, like GET /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_limit on POST /v1/signing-requests: the plan's monthly cap on documents created through the API is reached. Same shape as tier_required (meta: { tier, cap, used }); nothing is created — wait for the next month or upgrade. See api_monthly_limit. (api-monthly-limit)

Docs

  • One server, one header family. The spec names https://api.wesign.now/v1 only, declares the X-WeSign-* headers on the webhooks and no longer lists User-Agent values: the User-Agent is not part of the contract — verify X-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/templates lists 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 answers 503 temporarily_unavailable with Retry-After: 5, as GET /v1/documents/{id}/signed does, instead of 200 with an empty templates array — 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}/instantiate takes an optional locale on each recipient, exactly as signers[].locale on POST /v1/signing-requests: slot 1 can get its email, reminders, SMS code, signing page and signed copy in English and slot 2 in German. A review: true instance keeps it; confirm takes no locale. Before, the strict recipient object refused locale with 400 unknown_recipient_field. (instantiate-recipient-locale)
  • Each signer's language in the response. Each signer in the 201 of POST /v1/signing-requests, and each recipient in the 200 and the staged 201 of instantiate, carries locale: the language requested for them. GET /v1/signing-requests/{id}, GET /v1/documents/{id} and signing_request.sent show the same value, and it does not change when the signer's browser takes over the signing page. An Idempotency-Key replay of a call made before this release returns its stored body, without locale. (signer-locale-in-response)

Changed

  • No English default; your workspace language instead. The request-level locale no longer defaults to en. A signer without their own locale gets the request's locale, 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". On instantiate the same order, without the remembered step, names the language of review_url and 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 locale you 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 request locale uses it. A request-level locale is 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, signingUrl and 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_url has 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 from locale, 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_failed reasons 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-requests refuses two or more signers with the same role with 400 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 an auto_append page every signer's block was bound to all of them, and this was accepted without a warning. meta.role is the shared role, meta.emails every 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. An Idempotency-Key replay of a call created before this release still answers its 201. Like explicit-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}/instantiate refuses two or more recipients naming the same slot with 400 duplicate_slot (meta.slot, meta.emails), validate_only included. (duplicate-slot-refused)

Fixed

  • anchors.fields counts what was stored. The 201's anchors.fields is 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_append adds (also when anchors finds 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, serial 6a5258df714ab427831c680e605d8231, SHA-256 fingerprint 8a3d0158b768dd0d4244b4e20c6d1cbd99b1de4e651842a170ed42195bcb5d6d), 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 (the signed event's meta.seal_timestamp_error says 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, serial 0062c9cfa7868e51c7ed510fece02d96, SHA-256 fingerprint 0568a78abeb5b81e28c2ee4184a0d33b7b93f74647967c8e5b28391de626bcdc) and stay valid; /verify recognises both. Nothing changes shape: cert_serial carries the new serial for seals made from that moment. Each signer's signed audit event names the certificate that sealed that signature, so a document signed across it shows both serials there; cert_serial on document.completed and 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", with caption_depth_pt and space_below_pt in its meta.problems entry. Nothing is created. Keep y + h ≤ 1 − D / H (below). (explicit-field-caption-400)
  • Unknown keys inside a field or role_label are refused with 400 unknown_field: meta.fields lists every path, meta.accepted the accepted keys spelt as paths (fields[].page … fields[].origin, signers[].role_label.en, signers[].role_label.de). Send only page, x, y, w, h, kind, role and origin in a field. (explicit-field-unknown-keys-400)
  • An upload that already carries a digital signature is refused with 422 pdf_already_signed; meta.signatures lists 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-trail returns 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", the sha256 of document.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_unavailable when the stored certificate cannot be read at that moment (it is never re-rendered in its place) and 500 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 stated job_title or company takes: keep y + 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 in warnings[] (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 (type instead of kind, a misspelt orgin) used to be dropped without a trace, and the field fell back to its default. It is now named in warnings[] by its path (fields[0].type), on the 201 and on a 422 field_out_of_bounds or page_geometry_unsupported; so is a key other than en and de in a role_label object (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_sha256 on document.completed (full and minimal bodies; null when the certificate could not be issued at that moment — the first download issues it), auditTrailSha256 on GET /v1/documents/{id}, and the ETag of the audit-trail download. (audit-trail-sha256)
  • api_created. POST /v1/signing-requests writes one audit event per signing request, meta: { source: "v1_api", api_key_id, key_name }, as its first event; it shows in auditEvents of GET /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]]). The auto_append page 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, field file or file_url) and creates the document as before. (signed-upload-warning)
  • ingestNotes on GET /v1/documents/{id}: what we did to the uploaded file — decrypted, repaired, form-baked, signature-removed, and for app uploads flattened / compressed; [] for a clean upload and for API documents created before 2026-10-15. (document-ingest-notes)
  • The seal on the signed event. Its meta names seal_key_version, seal_profile (adbe.pkcs7.detached | ETSI.CAdES.detached), seal_timestamp (the time-stamp embedded in a PAdES B-T seal) and seal_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 /Rotate exactly, and is used only after the seal has rendered it and found it unchanged. (isolated-page-offset-mediabox)
  • The auto_append page is always A4 portrait, unrotated and uncropped, even when the upload sets a /Rotate or /CropBox once 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 removed 422 pdf_decrypt_failed — no longer 422 placement_failed. Nothing is created, as before. Every 422 of ingest and placement carries the call's warnings[]. (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_only included), generate (every mode; the PDF stream answers JSON instead) and confirm, with 400 { error, code: "unknown_field", meta: { fields, accepted } }, before anything is read, rendered, created or confirmed. A retry with the same Idempotency-Key of 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.detached format, 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 date field is filled by wesign when that signer's signature is sealed: the signing day in the issuer's time zone, as DD.MM.YYYY (MM/DD/YYYY for 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 sha256 and /pdf are 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 /Rotate is not a number is refused with 422 page_geometry_unsupported (meta.reason box_unreadable or rotation_unreadable) instead of the generic 422 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. With 422 and nothing created: page_geometry_unsupported for a page carrying a field whose CropBox is not wholly inside the MediaBox, whose box is reversed or under 1 pt, or whose /Rotate is not a multiple of 90 — the signing page and the seal read such a page differently; field_out_of_bounds for a page the document does not have (the signer never saw the field and the signature was drawn on the last page), x + w > 1 or y + h > 1 (the field ran off the page), w or h of 0 (a 160 × 50 pt box was drawn instead), and an origin: "center" field reaching past an edge (it was shifted onto the page). meta.problems lists every offending page and field. Like the 2026-09-24 sms_allowance_exhausted refusal, 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, w and h are 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, /Rotate 90/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-Id on every answer. Every call a workspace's keys make is recorded after its response — method, endpoint, status, our code and error message, 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 /v1 now carries an X-Request-Id response 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_mean names the closest endpoints with their method — GET /v1/signing answers GET /v1/signing-requests/{id}, POST /v1/signing-requests, … — meta.path echoes what you called and meta.docs links the endpoint list. On api.wesign.now a path that left out /v1 (/signing-requests, or the bare root) answers the same, naming the /v1 endpoint. 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_allowed with a JSON body (meta.allowed) and an Allow header — before, the body was empty. GET /v1/documents and GET /v1/signing-requests also 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 with 400 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 with Content-Security-Policy: frame-ancestors <that origin> and posts its messages to that origin only, never '*'. A malformed origin (a path, query, credentials, a wildcard or http:) is still refused with 400 invalid_origin on 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 code and status is unchanged. A schema 400 invalid_request lists every problem with its field (signers[0].email: Invalid email address; …) and carries them all in meta.problems: [{ path, message }] — on POST /v1/signing-requests, /generate and /instantiate the error used 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 (on POST /v1/hooks and POST /v1/embedded/sign-sessions it used to be read as {}). 401 invalid_key carries meta.reason (missing, not_an_api_key, not_recognized); 404 not_found names the id; 409 invalid_state on remind/withdraw carries meta.status, 409 expired meta.expires_at, 409 not_complete meta.signed, meta.total, meta.waiting. A 5xx that is our failure says what failed and what to retry, and no longer includes a database, storage or renderer message — nor, on remind's 502 email_failed, the mail provider's (a domain that accepts no email is still named). If you parsed the error of a 400 invalid_request as JSON, read meta.problems instead. 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: false and 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, never 404. GET /v1/documents/{id}/signed and GET /v1/documents/{id}/audit-trail answer 503 temporarily_unavailable with Retry-After: 5 (and the RateLimit-* headers) when a database read fails on our side, instead of 404 not_found, 404 no_signers or 409 not_stored; /audit-trail no longer renders a certificate without its events when they cannot be read. A 404 from these endpoints is now final, and 409 not_stored only means that the sealed PDF's record is not there yet. Treat the 503 as retryable: wait Retry-After seconds, then retry with backoff (Status codes and retries). Stop retrying a 404 from these two endpoints. A failed API-key lookup can still answer 401 invalid_key on any endpoint, so keep retrying a 401 on 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 /signed answers 410 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 /signed answered 410 as 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 answers 410 deleted_by_retention once 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. The blob_auto_deleted audit event (in auditEvents of GET /v1/signing-requests/{id}) carries files_released: true and files_deleted; a signing request that retention marked deleted before this release gets a second blob_auto_deleted event, with backlog: 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; on document.completed verify, answer 2xx, then fetch signed_pdf_url and audit_trail_url with a full key of the owning workspace; compare sha256 with 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 /signed answer 410 deleted_by_retention from 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-trail and GET /v1/documents/{id} keep answering. No endpoint lists documents: reconcile from the ids you stored, GET /v1/documents/{id} and the delivery log. One document.completed per 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. sha256 on GET /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, only document.completed (and, in the full body, the last signer's signing_request.signed) carries the signed PDF's hash. 409 not_stored means the signing record could not be read, a passing failure on our side — not "still being written". A database failure can also answer 404 on both download endpoints, so retry an id that a verified document.completed named before treating a 404 as final, and 401 on any endpoint, so retry a 401 on a key that worked before a few times before treating the key as wrong. /audit-trail never answers 410 and stays available after retention. The 307 of /signed applies only to PDFs stored before April 2026. The download file name is sanitised (every character other than A–Z, a–z, 0–9, ., _ and - becomes _), with Cache-Control: private, no-store. The template quickstart said the signed PDF embeds the RFC 3161 time stamp; it does not. The Integrator FAQ said document.completed is 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.completed and the audit certificate, tsa_provider reads "DigiCert" (before: "freetsa.org") and tsa_signed_at is 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 pending as before, its signing URL works, and emailed is false, as for any failed send; a sequential release emits signing_request.sent with "emailed": false, and a create call's first signing_request.sent goes 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}/remind to such an address answers 502 email_failed and sends nothing; error names the domain. Before, it answered 200 and the reminder bounced later. Like the 2026-09-24 sms_allowance_exhausted refusal, 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.sent when 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, with template_id and template_version. There is no source. 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 no signing_request.sent for 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 a document_id you do not know; a callback_url hook 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: signer carries email, name and slot (the signing order in a sequential send, else null), next to locale; there is no source, and the signer block has no reference, company or job_title. A minimal hook gets the identifiers, with role and reference null. 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 no signing_request.sent for 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 a document_id you do not know; a callback_url hook 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 to support@wesign.now (see reply-to-docs under 2026-10-03). No API request or response changes. (completion-reply-to-workspace)

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.description now 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}, a validate_only dry run, then the same body) with an Idempotency-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. externalDocs now points to https://www.wesign.now/docs. (spec-start-here)
  • Signing links are on wesign.now. The signingUrl / signing_url the API returns is the link the invitation email carries: https://<slug>.wesign.now/<locale>/sign/<token> on a branded workspace, else https://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_url hooks get the full body. The API cannot make them minimal; for identifiers only, register a workspace hook with "payload": "minimal" and do not pass callback_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 role and pin; send_emails: false holds for a later sequential signer; the seal is PAdES-B and the time stamp is kept with our records; /signed also answers 410 deleted_by_retention and 500 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/hooks with a target_url that an active workspace-wide hook already targets switches that hook off and returns a new one (new id, new secret), naming the old one in replaced_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_url answers 400 https_required; one that is or resolves to a private, loopback, link-local or reserved address answers 400 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) with meta.unknown and meta.valid_events. (hooks-unknown-event)
  • DELETE /v1/hooks/{id} answers 204 with no body (before: 200 { "ok": true }). (hooks-delete-204)
  • 10-second timeout per delivery attempt (before: 15). Answer 2xx first and process afterwards. (webhook-timeout-10s)

Added

  • Minimal payloads. "payload": "minimal" on POST /v1/hooks: every delivery carries only event, event_id, created_at, document_id, plus signing_request_id, role, reference on signer events and signed_pdf_url, sha256, audit_trail_url on document.completed, with User-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, payload stays "minimal". GET /v1/hooks reports payload. See Payload modes. (hooks-payload-minimal)
  • Top-level role and reference on every signing_request.* event, for every hook; for a template recipient role is 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}/redeliver sends a delivery again with the same event_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}/test sends 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 synthetic signing_request.sent with "test": true to every subscribed hook of the workspace). (hooks-test-ping)

Changed — additive

  • signing_request.expired carries the full signer block, 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/hooks answers 500 db_failed when 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 text or name marker ([[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. A name marker without a third part is keyed person.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_url webhook on POST /v1/signing-requests used 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 on POST /v1/signing-requests or instantiate used to leave a signer with a link and nothing to sign. Both now answer 500 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-requests and instantiate write 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-requests only), then the one signing_request.sent. A failed write deletes everything the call created, the PDF it uploaded or rendered included, contacts nobody and answers 500 db_failed with meta.rolled_back: true and an error that ends "Nothing was created or sent; retry the request."; the Idempotency-Key is released. In the rare case that the clean-up fails as well, meta.rolled_back is false and meta.document_id names the leftover document. Before, a failure on signers[1] answered 500 after signers[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.completed and GET /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) and job_title (at most 120) on signers[], instantiate recipients[] and confirm recipients[]. 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 from GET /v1/documents/{id}/audit-trail alike, labelled "stated by the sender". They are read back as company and job_title on every signer object and in the signer block of the webhooks. A line break or other control character, or a longer value, is a 400 invalid_request. See Company and function. (signer-company-job-title)
  • One signer block on every signer webhook. signing_request.signed and document.completed carry { email, name, role, slot, reference, company, job_title }, like the signer events; they carried { email, name }. signing_request.sent from the API or a sequential release and the six signer events add company and job_title. signing_request.expired is 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.monthly then shows the plan's codes plus the raise, and every 402 sms_allowance_exhausted check uses that number. See the monthly allowance. (sms-allowance-raise)
  • Docs for AI assistants. /llms.txt lists every docs page with a one-line summary; /llms-full.txt is 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 the signing_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); and document.completed could 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 sends document.completed and the "Completed" emails. Keep deduplicating document.completed on document_id. If a PDF completed before this release lacks a signature, email support@wesign.now with its document_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_failed names an unusable number and PATCH /v1/signing-requests/{id} corrects it. (no-standalone-phone-verification)
  • The stored signer name is "first last" as soon as either first_name or last_name is non-blank, and name is then ignored; the docs said "when both are given". (signer-name-rule-docs)
  • observer_emails addresses are checked before they would be trimmed: a leading or trailing space is a 400 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_provider and tsa_signed_at; it is not embedded in the PDF, as the Documents page said. (tsa-token-docs)

Migration notes

  1. On 500 db_failed from POST /v1/signing-requests or instantiate, retry the same body (the same Idempotency-Key is fine). With meta.rolled_back: false, first withdraw any signing request of meta.document_id that GET /v1/documents/{id} still shows as pending.
  2. Read webhook signer blocks by key: signing_request.signed and document.completed now carry role, slot, reference, company and job_title too.
  3. Optional: send company and job_title for 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.
  4. If your PDFs carry name markers, or text markers with a third part, check what those boxes now open with.
  5. Keep deduplicating document.completed on document_id as well as event_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: false holds for the later signers of a sequential send on POST /v1/signing-requests, instantiate and confirm. 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, send send_emails: true, or deliver the signer's link when the release's signing_request.sent arrives (see Added — signer events). Requests created before this release are still emailed on release.
  • 402 tier_required on instantiate: an instance counts against the workspace's monthly document cap exactly like POST /v1/signing-requests. At the cap nothing is created and the body is { error, code, meta: { tier, cap, used } }. This applies to review: true too — a staged instance counts from the moment it exists, and its confirm is not counted again. A validate_only dry run reports it in problems with ok: false. Enterprise, the only tier with template channels, has no cap, so no workspace meets this today.

Added

  • signing_request.sent also 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_at and emailed (false with send_emails: false: deliver the link yourself). It may arrive before the previous signer's signing_request.signed. Before, no event marked the release. A receiver that treats sent as "the document went out" should skip source: "sequential_release". See Following a sequential send.
  • Who signs each slot: recipients[] on GET /v1/templates/{id} carries the author's role and pin when set. pinned: "email" comes with pinned_email, the one address instantiate and confirm accept for that slot — send it instead of meeting 400 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_limited on POST /v1/signing-requests/{id}/remind carries Retry-After: 60, like every other 429. It still has no RateLimit-* headers: it is not the per-key window.

Fixed

  • POST /v1/signing-requests/{id}/withdraw answers 500 db_failed when the status update fails; it answered 400, 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üller makes Vollmacht_Müller.pdf, not Vollmacht_M_ller.pdf — and that is the name the invitation email shows. The same goes for a filename sent to POST /v1/signing-requests or generate. 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 ASCII filename fallback, so the download keeps its real name.

Docs corrected

  • An archived template answers 409 template_unlocked on instantiate and generate, with or without a version pin. The dashboard's archive dialog said such calls would return 410; it now says 409 as well.
  • The reminder cooldown was documented without Retry-After; it has one now (above).

Migration notes

  1. Handle 402 tier_required on instantiate as you do on POST /v1/signing-requests.
  2. Retry a 500 db_failed from withdraw; it is not a request error.
  3. Sequential sends with send_emails: false: store every signer's link from the create answer and deliver the next one on signing_request.sent with "source": "sequential_release" — we no longer email it. A receiver that counts sent as 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 — phone instead of phone_e164, a typo — is a 400 unknown_signer_field naming every offender (meta.signers, meta.accepted). Before, it was dropped silently.
  • Template recipients are strict on POST /v1/templates/{id}/instantiate, and they now take phone_e164, require_sms_verification, sms_gate and reference. 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 a 400 unknown_recipient_field.
  • Confirm recipients are strict on POST /v1/documents/{id}/confirm (400 unknown_recipient_field), a body that is not JSON is a 400 invalid_request instead 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 answers 409 after the edits were saved, on an instance nobody can confirm any more.
  • 402 sms_allowance_exhausted on POST /v1/signing-requests, a direct instantiate and confirm: 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-30 in a date field is 422 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 to auto_append — every signer then needs a marker (400 signer_has_no_anchor).
  • metadata is validated on POST /v1/signing-requests, instantiate and generate. 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 a 400 invalid_metadata.
  • send_emails must be a boolean on POST /v1/signing-requests (JSON true/false, multipart "true"/"false"). Before, the key was ignored; a string such as "false" or "no" is now a 400 invalid_request.
  • The multipart filename is read on POST /v1/signing-requests (it was ignored), so a value over 200 characters is now a 400.
  • A body that is not JSON is a 400 invalid_request on instantiate and generate, as on confirm. Before, it was read as {}: instantiate refused it with a message about recipients, and generate rendered the template without your values — streaming it even when the lost body asked for review: true. An empty body is still {}.
  • signingUrl is null while staged on GET /v1/signing-requests/{id} (and the PATCH answer), next to a new staged: true, as GET /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_failed on instantiate: 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 the Idempotency-Key is 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 print 1990-03-15. {{date_today}} stays written out, now in the placeholder's language, else the document's, else the call's locale (before: English, except on a review: true instance, which used the call's locale). A staged generate keeps no locale, so there it ends in English. The stamped signing date (a date field or marker) prints in the field's format, else the workspace's house format — 15.03.1990, or 03/15/1990 for a workspace in the United States — instead of 24 Sept 2026, and the signing page shows it exactly as it will be stamped. Nothing you send and nothing echoed back changed.
  • validate_only on instantiate always carries problems and answers ok: false (still HTTP 200) when the real call would be refused for the SMS allowance. Read ok, not only the status.
  • Six new webhook events reach every hook whose events list is empty — every callback_url hook included — without opting in. Answer 2xx to event types you do not handle.
  • viewed is now a real status. Nothing ever set it before; a request now moves pending → viewed the first time the signer's browser loads the document. Treat it like pending.
  • Idempotency: the hash of POST /v1/signing-requests now includes metadata, send_emails and the multipart filename when you send them — a retry across this deploy with the same key and such a body answers 422 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 in auditEvents.
  • Dotted field keys: person.date_of_birth, company.legal_name, company.uid are 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_values stays 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_required is false on every positioned text field of GET /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/…, or https://<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 confirm channel sms/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, generate and confirm is refused with 400 unknown_field. Until then it is ignored and named in warnings[] — on the generate PDF stream, which has no JSON body, in the X-Unknown-Fields response header. See Unknown keys.
  • 2026-12-31 — recipients[].phone on confirm stops being accepted; use phone_e164. Until then each use adds a deprecated_field warning, and both with different values are a 400 conflicting_fields.

Added — SMS

  • sms_gate per 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_editable otherwise). Sends nothing; audit event phone_changed, both numbers masked.
  • GET /v1/me returns sms_allowance: { monthly, used, remaining, resets_at } next to capabilities.
  • 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 (with reason), .withdrawn (with withdrawn_by), .sms_sent, .sms_failed (with a reason code) 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.sent carries signer.reference, and a direct instantiate (without review) now emits it. Its signer.slot is the stored value like on every other event: null on a parallel POST /v1/signing-requests even when a signing_order was passed (that ignored value was echoed before).

Added — references and options

  • reference (≤ 200 characters) per signer and recipient on create, instantiate and confirm.
  • metadata on POST /v1/signing-requests, instantiate and generate: a flat object stored on the document and echoed on the create response, GET /v1/documents/{id} and every webhook naming the document.
  • send_emails on POST /v1/signing-requests (default true), with emailed per 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 direct instantiate 200 now also carries the field_values warnings it used to drop.
  • capabilities on GET /v1/me; enabled on every hook, GET /v1/hooks?include_disabled=true, and POST /v1/hooks/{id}/rotate (new secret once, the old one valid for 24 hours, two v1= entries in the signature header meanwhile).

Added — day-first dates

  • A date value in field_values may be written day first — DD.MM.YYYY, DD/MM/YYYY, DD-MM-YYYY or DD MM YYYY, day and month one or two digits (15.03.1990, 5.3.1990, 15 03 1990) — as well as YYYY-MM-DD, on instantiate and generate. It is stored and echoed as YYYY-MM-DD. Month-first is never tried: 03/04/1990 is 3 April, and 03/15/1990 is 422 invalid_date. The invalid_date message 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/1990 or written out (24. September 2026, in a chosen language); 14:30 or 2:30 PM. GET /v1/templates/{id} shows the pick read-only as date_format, time_format and lang on field_values[] entries, and date_format (+ lang) on date entries of signing_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 time field type: HH:MM or HH.MM on a 24-hour clock, or h:MM AM/PM (14:30, 9:05, 2:30 PM, 2:30pm), stored and echoed as 24-hour HH:MM; anything else is 422 invalid_time. GET /v1/fields can list time definitions, and the spec now enumerates every FieldDefinition.type (a new type is an additive change: treat one you do not know like text).

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" and required: true unless 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. slot on 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: true can have its Sender / API values corrected on review_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 no field_values: such a key is ignored and named in warnings[] as unknown_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 carries standard: true, labels (its name in en, de, fr, it, es, nl), an example, a hint pattern (never enforced) and an English description; every other input carries standard: false. GET /v1/fields marks registry definitions the same way. Custom keys stay first-class. The list is in Fields & placeholders.
  • missing_optional on a validate_only answer of instantiate and generate, on the 200 and on the dry-run 422: the inputs you may still supply and left empty, with what you need to ask your user for them. It never makes ok false and is absent from a real call's 422. generate never 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 filename field on POST /v1/signing-requests is honoured (it was ignored); over 200 characters is a 400 (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 standard person.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 (generate with review: 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 for 1990-03-25 and 25.03.1990 (a day-first value was compared as text). 15 03 1990 now 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_e164 works on confirm; confirm took only phone and silently ignored phone_e164. Both work now, phone_e164 is 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/me is 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, direct 200, staged 201, dry runs, 422), confirm and discard.
  • Template.status is an enum — draft, locked, archived; the list never shows an archived template, GET /v1/templates/{id} still answers for one. source on a template input is legacy: read owner and required.
  • review_url and the 422 docs link are pages in the web app (not on the API host) that only a signed-in member of your workspace can open.
  • The direct instantiate 200 carries no template_id; the staged 201 and the dry run do.
  • The dry-run note implied that instantiate checks a monthly document cap. It applies none, dry run or not.
  • The 400/402/422 bodies that are either a specific error or the generic Error are declared anyOf, not oneOf: every specific body also matched Error, so a strict oneOf rejected them all.
  • signing_request.sent → signer.slot is the recipient slot on every template send, in either signing mode; the spec said null in parallel mode.

Migration notes

  1. Send only documented keys in signers[], recipients[] and confirm recipients[]; watch warnings[] and clear every top-level unknown_field before 2026-12-31.
  2. On confirm, rename phone to phone_e164 before 2026-12-31.
  3. If you instantiate templates with SMS signers, move them onto recipients[].phone_e164 + require_sms_verification and assert signature_level: "AES" on the response.
  4. Handle 402 sms_allowance_exhausted; read GET /v1/me → sms_allowance to warn before sending.
  5. Make your webhook receiver answer 2xx to unknown events, and add the six signer events you want.
  6. Treat viewed like pending.
  7. On instantiate validate_only, check ok.
  8. Send date values day first or as YYYY-MM-DD, never month first — also when a template prints them 03/15/1990. Send time values as HH:MM or h:MM AM/PM.
  9. 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.
  10. Decide what to send from required on GET /v1/templates/{id}, never from owner: a template whose author sets a text field to Sender / API requires it from its next locked version on. A validate_only dry run names anything you missed.
  11. Optional: map your data to the standard field names once, and offer your user what missing_optional lists.
  12. Read signingUrl on GET /v1/signing-requests/{id} as nullable; a staged recipient's link comes from the confirm.
  13. Retry a 502 render_failed from instantiate: nothing was created, so send the same body again (with the same Idempotency-Key, if you use one).
  14. 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-For is 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 by Origin — 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 generic 401 invalid_key — a different problem deserves a different code. ip is the address we saw (null when 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_seconds is 60…900 (default 300) and out-of-range is refused, not clamped. embed_url is 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 the embedded scope (a full key 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 } with status one of active / expired / revoked. DELETE revokes, 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? } to window.parent with targetOrigin set 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 with Content-Security-Policy: frame-ancestors <the session's one origin> and omit the app's global X-Frame-Options there, defaulting to frame-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 answers 410 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_url is now https://api.wesign.now/v1/documents/{document_id}/audit-trail — key-authenticated, same host, same Bearer key and same status rules as signed_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 rule GET /v1/documents/{id} (status: signed), /signed and /audit-trail already applied. A document with a declined, withdrawn or expired signer no longer receives it; before, it fired and both URLs answered 409 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/hooks never 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 / viewed request to expired once its expires_at has 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 the signers[] on GET /v1/documents/{id} now report status: "expired" for such rows (previously the row stayed pending and expiry was only visible via expiresAt). remind on one answers 409 expired, withdraw 409 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 the code field 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-Id and X-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_complete until every signer signed; 404 not_found from 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_url moved 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": true on POST /v1/templates/{id}/instantiate and /generate stages the instance instead of sending it; new POST /v1/documents/{id}/confirm and /discard, new GET /v1/documents/{id}/pdf for a finalized file-only instance, "validate_only": true dry runs, and the template_instance.staged / .confirmed / .discarded webhook events — see Templates.
  • POST /v1/templates/{id}/instantiate takes an Idempotency-Key (same semantics as POST /v1/signing-requests) and sits on the per-key rate limit; its 409 carries code: "template_unlocked".
  • Breaking: POST /v1/templates/{id}/generate sits behind the same Enterprise template entitlement as instantiate (402 enterprise-required).
  • Fixed: documents created over the API are attributed to the key's creator. Before, POST /v1/signing-requests and instantiate failed with 500 on the document insert.

2026-07-31 — Retirements (breaking, zero production usage)

  • placement="manual" on POST /v1/signing-requests → 400 placement_retired.
  • POST /v1/documents (ingest-by-URL for manual placement) → 410 placement_retired. file_url on POST /v1/signing-requests covers 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

MethodPathPurpose
POST/v1/signing-requestsCreate — multipart upload, four placement modes
GET/v1/signing-requests/{id}Read one signer + its first 50 audit events
POST/v1/signing-requests/{id}/remindRe-send invite
POST/v1/signing-requests/{id}/withdrawCancel in-flight
GET/v1/documents/{id}Holistic doc + signers + rollup status
GET/v1/documents/{id}/signedFinal PAdES-sealed PDF
GET/v1/documents/{id}/audit-trailGenerated audit-trail PDF

Concepts

  • Idempotency-Key header 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_url on 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.

On this page

2026-10-22 — The MCP server2026-10-21 — API keys on every plan; the spec names wesign.now only2026-10-20 — Every template in the list; a failed read is 5032026-10-19 — A language per signer, everywhere2026-10-17 — Two signers sharing a role are refused; honest field count; taller date box on the appended page2026-10-16 — New seal certificate and PAdES baseline B-T from 2026-10-222026-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 corrected2026-10-14 — Explicit fields on a page with an unreadable box or rotation: page_geometry_unsupported2026-10-13 — Explicit fields: the visible page is the frame, and a field that cannot be placed exactly is refused2026-10-12 — An API log in Settings → API; X-Request-Id on every answer2026-10-11 — A key with no embed origins accepts any https origin; unknown paths and methods answer JSON; clearer error messages2026-10-10 — A failed read is 503 on the download endpoints; retention deletes the signed PDF2026-10-09 — Docs revision: retrieving the signed document2026-10-08 — Timestamps from DigiCert, verified before they are kept2026-10-07 — Invitations to a domain that accepts no email are not sent2026-10-06 — A Send Later emits signing_request.sent when it goes out2026-10-05 — Documents sent from the app emit signing_request.sent2026-10-04 — Completion emails reply to your workspace2026-10-03 — Docs revision: a start-here path, signing links on wesign.now, webhook order2026-10-02 — Webhook management for integrators: replace on re-subscribe, minimal payloads, delivery log2026-10-01 — Signer company and function, one seal at a time, all-or-nothing creates2026-09-25 — Monthly cap on instantiate, quiet sequential sends, who signs each slot, file names in every script2026-09-24 — SMS you can rely on: before-view gate, read-back, allowance up front, signer events2026-09-10 — Pin an API key to your own IP addresses2026-09-10 — Embedded signing: sign inside your own page2026-09-10 — One-time hook secret, honest errors, expiry events2026-09-09 — api.wesign.now is the canonical host2026-09-06 — Review before sending2026-07-31 — Retirements (breaking, zero production usage)2026-04-30 — v1 launchMigration from /docs/apiVersioning policy