Versioning
What may change inside /v1 and how you hear about it — notice periods, deprecation signals, dated spec snapshots and the machine-readable changelog.
/v1 is the only API version. This page says what we change inside it
without asking, what we announce first and how far ahead, and where you
read it — by a human on the changelog, by a machine in
api-changelog.json and in the
dated spec snapshots.
What never changes
- The path and the hosts.
https://api.wesign.now/v1is canonical;https://api.letssign.now/v1is a permanent alias with identical routing. - Keys.
wsk_live_…keys, andlsk_live_…keys minted before the rename, are accepted forever. - Webhook signing. The
X-WeSign-*headers and the byte-identicalX-LetsSign-*headers, the schemet=<unix seconds>,v1=<hex>over${t}.${rawBody}with one secret (twov1=entries only during a 24-hour rotation window), and theUser-Agent. - Error codes. A
codestring keeps its meaning. The humanerrormessage may be reworded at any time — never parse it.
Additive changes — no notice
These ship to /v1 with a changelog entry and no notice period:
- new endpoints, and new optional request fields;
- new fields in response bodies and webhook payloads;
- new webhook event types — delivered at once to every hook whose
eventslist is empty,callback_urlhooks included; - new error codes, new values of a response enum (a new
sms_failed.reason, say), new warning codes.
So your client must ignore response fields it does not know, answer 2xx
to webhook events it does not handle, and fall back on the HTTP status for
an error code it does not know.
Breaking changes — announced
A change is breaking when a request that worked can now fail, or a field you read changes meaning or disappears. Such a change is:
- announced in the changelog, flagged
breaking: trueinapi-changelog.json, with the day it takes effect (effective) — at least 90 days after the announcement; - signalled on the way: in the meantime, every response to a request the
change would affect carries a
warnings[]entry naming the field and the date (an answer without a JSON body, such as thegeneratePDF stream, carries a response header instead).
One kind of change does not get the 90 days: refusing input that never
had any effect. An unknown key used to be dropped silently, so the call
ran with a default you never chose. Where that silent drop could weaken a
signature — an unknown key in a signer or recipient object, which may be a
misspelt SMS factor — the refusal took effect at once (2026-09-24). Where it
could not, it gets the normal notice (98 days) and a warning on every
affected response: unknown top-level keys are named in warnings[] from
2026-09-24 — on the generate PDF stream, which has no JSON body, in the
X-Unknown-Fields response header — and refused with 400 unknown_field
from 2026-12-31. The same rule made four more refusals immediate on
2026-09-24, each of a value that used to be ignored: a malformed metadata,
a send_emails that is not a boolean, a multipart filename over 200
characters, and a text anchor marker whose field key has a dot or 42–64
characters (it used to be skipped; it is now placed, so its role is
checked).
The 2026-09-24 release also refuses up front, with
402 sms_allowance_exhausted, a call whose SMS signers the monthly allowance
cannot cover. Such a call used to succeed and leave the signer unable to
sign; the refusal moves an existing failure to the moment you can act on it.
It is flagged breaking all the same.
A fix that makes the API do what the documentation always said (a date field
now rejects 2026-02-30) ships as a fix; when a call that passed can now
fail, the changelog flags it breaking too.
Anything that cannot be done this way — a change of shape we cannot announce
field by field — would ship as /v2, and /v1 would keep working for at
least 12 months after /v2 is generally available.
Deprecated fields
A deprecated spelling keeps working until its sunset date, at least 90 days after the deprecation was announced. Every response to a request that used it carries a warning:
{
"warnings": [
{
"field": "recipients[0].phone",
"code": "deprecated_field",
"message": "\"phone\" is a deprecated alias of \"phone_e164\" and is accepted until 2026-12-31. Send phone_e164."
}
]
}We will also send the standard Deprecation and Sunset HTTP headers on
those responses. They are not sent yet — today the deprecated_field
warning is the signal; the changelog will say when the headers ship.
In effect now:
| What | Where | Instead | Announced | Until |
|---|---|---|---|---|
recipients[].phone | POST /v1/documents/{id}/confirm | phone_e164 | 2026-09-24 | 2026-12-31 — after that phone is refused like any unknown key (400 unknown_recipient_field) |
| Unknown top-level keys, ignored with a warning | create, instantiate, generate, confirm | send only documented keys | 2026-09-24 | 2026-12-31 — from then 400 unknown_field |
The legacy path POST /api/templates/{id}/instantiate | the web hosts (www.letssign.now, www.wesign.now) | POST https://api.wesign.now/v1/templates/{id}/instantiate | 2026-06-21, when the /v1 path shipped | No sunset date yet. It runs the same code as the /v1 route and answers identically, with no deprecated_field warning and no deprecation header. A sunset will be announced at least 90 days ahead, like any breaking change. |
Already retired, each with a stable answer rather than a 404:
POST /v1/documents (410 placement_retired), placement: "manual"
(400 placement_retired) and POST /v1/embedded/sessions
(410 embedded_sessions_retired), all on 2026-07-31.
Dated spec snapshots
https://www.wesign.now/openapi.jsonis always the current release. Itsinfo.versionis that release's date.- Each release's spec is also frozen at
https://www.wesign.now/openapi/<date>.jsonand never edited again —openapi/2026-09-24.jsonis the first. Releases before it have no snapshot. - Diff two snapshots to see exactly what a release changed in the contract; pin a client generator to one to build against a fixed contract.
The machine-readable changelog
https://www.wesign.now/api-changelog.json lists every release, newest
first — the same releases as the changelog page, one
entry per date:
{
"format": 1,
"current_openapi": "/openapi/2026-09-24.json",
"entries": [
{
"date": "2026-09-24",
"title": "…",
"openapi": "/openapi/2026-09-24.json", // null before snapshots existed
"changes": [
{
"id": "confirm-phone-e164", // stable within the file
"type": "deprecated", // added | changed | deprecated | removed | fixed | docs
"breaking": false,
"sunset": "2026-12-31", // deprecations only; scheduled changes carry "effective" instead
"endpoints": ["POST /v1/documents/{id}/confirm"],
"summary": "…",
"action": "Rename phone to phone_e164 before 2026-12-31." // null = nothing to do
}
]
}
]
}effective and sunset appear only on changes that have them. Poll it
daily, or on deploy of your integration, and alert on any change with
breaking: true or an action.
No sandbox yet
Every API key is a live key: every call creates real documents, sends real emails and real SMS codes, and counts against the monthly document cap and the SMS allowance. There are no test keys.
Until the sandbox exists:
- use
validate_only: trueoninstantiateandgenerate— it runs the full check (oninstantiatethe SMS allowance included) and creates nothing; - stage with
review: trueoninstantiateorgenerate, inspect the document, andPOST /v1/documents/{id}/discardit — nothing is sent; - send to addresses and mobile numbers you control;
- use
send_emails: falseto keep invitations out of real inboxes (the SMS code is still sent when someone opens the link and asks for it, and automatic reminders, the hand-over to the next sequential signer and completion emails still go out).
Planned for 2026-11-30: a sandbox whose emails and SMS are simulated and visible through the API instead of delivered.
