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/v1 is canonical; https://api.letssign.now/v1 is a permanent alias with identical routing.
  • Keys. wsk_live_… keys, and lsk_live_… keys minted before the rename, are accepted forever.
  • Webhook signing. The X-WeSign-* headers and the byte-identical X-LetsSign-* headers, the scheme t=<unix seconds>,v1=<hex> over ${t}.${rawBody} with one secret (two v1= entries only during a 24-hour rotation window), and the User-Agent.
  • Error codes. A code string keeps its meaning. The human error message 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 events list is empty, callback_url hooks 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:

  1. announced in the changelog, flagged breaking: true in api-changelog.json, with the day it takes effect (effective) — at least 90 days after the announcement;
  2. 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 the generate PDF 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:

WhatWhereInsteadAnnouncedUntil
recipients[].phonePOST /v1/documents/{id}/confirmphone_e1642026-09-242026-12-31 — after that phone is refused like any unknown key (400 unknown_recipient_field)
Unknown top-level keys, ignored with a warningcreate, instantiate, generate, confirmsend only documented keys2026-09-242026-12-31 — from then 400 unknown_field
The legacy path POST /api/templates/{id}/instantiatethe web hosts (www.letssign.now, www.wesign.now)POST https://api.wesign.now/v1/templates/{id}/instantiate2026-06-21, when the /v1 path shippedNo 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.json is always the current release. Its info.version is that release's date.
  • Each release's spec is also frozen at https://www.wesign.now/openapi/<date>.json and never edited again — openapi/2026-09-24.json is 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: true on instantiate and generate — it runs the full check (on instantiate the SMS allowance included) and creates nothing;
  • stage with review: true on instantiate or generate, inspect the document, and POST /v1/documents/{id}/discard it — nothing is sent;
  • send to addresses and mobile numbers you control;
  • use send_emails: false to 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.