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.
2026-09-10 — Pin an API key to your own IP addresses
Added — a per-key IP allowlist
- Every API key now carries a list of IPv4/IPv6 addresses and CIDR blocks
(
203.0.113.7,203.0.113.0/24,2001:db8::1,2001:db8::/32), managed on the key's card in Settings → API, up to 50 entries. Entries are canonicalised on save, so what the card shows is what we match. - An empty list means unrestricted. Nothing changes for any existing key, and nothing changes for you until you deliberately add an entry. This is the opposite of the embed-origin list, where empty means "cannot embed" — worth reading twice, it is the half people get wrong.
- The address we compare is the one our platform observed the call arriving
from.
X-Forwarded-Foris not used for the decision: a caller can write that header itself, so trusting it would be decoration. For the same reason we do not and will not authorise REST calls byOrigin— see Security.
Added — 403 ip_not_allowed
- A key that is valid but called from an address its allowlist does not cover
answers
403 { error, code: "ip_not_allowed", ip }instead of the generic401 invalid_key— a different problem deserves a different code.ipis the address we saw (nullwhen it could not be determined, in which case a restricted key fails closed). Documented on every key-authenticated operation in the OpenAPI spec. - The control gates API keys only: never the workspace UI, never the signing pages. A mistyped address locks out an integration, never a person — sign in from anywhere and empty the list.
Full contract: Restricting a key to your own IP addresses.
2026-09-10 — Embedded signing: sign inside your own page
Your signer can now sign in an <iframe> or a mobile webview on your
domain, with your branding around it. Everything here is additive; no
existing endpoint changed shape.
Added — POST /v1/embedded/sign-sessions
- Exchanges an existing signing request for a short-lived, single-signer
session:
{ signing_request_id, origin, ttl_seconds?, locale?, theme? }→201 { id, embed_url, expires_at, signing_request_id }.ttl_secondsis 60…900 (default 300) and out-of-range is refused, not clamped.embed_urlis on the workspace's own signing host, so the signer never sees a domain their firm did not put in front of them. Enterprise only (402 enterprise-required); needs a key with theembeddedscope (afullkey satisfies it); on the standard 60/minute per-key bucket. GET /v1/embedded/sign-sessions/{id}→{ id, status, expires_at, first_seen_at, last_seen_at, signing_request_id }withstatusone ofactive/expired/revoked.DELETErevokes, idempotently, and answers{ "ok": true }. Neither is Enterprise-gated: revocation is a safety valve and never depends on billing.- New error codes:
invalid_origin(400),origin_not_allowed(400),request_not_signable(409),embedding_unavailable(503, while a deployment has not applied the migration).
Added — ancestor origins are registered per API key
- Embedding is a permission, not a default. The origins allowed to frame a
session are listed on the key in Settings → API (up to 20, exact
https://host[:port]match, no wildcards). A key with an empty list cannot mint a session.
Added — postMessage schema v1 and a documented CSP
- The frame posts
{ v: 1, type, session_id, signing_request_id, at, detail? }towindow.parentwithtargetOriginset to the registered origin, never'*'. Types:wesign.ready/viewed/signed/declined/expired/error. They are a UX signal only — webhooks remain the source of truth. - Hosts allow exactly one directive:
frame-src https://<slug>.letssign.now https://<slug>.wesign.now. We answer embed paths withContent-Security-Policy: frame-ancestors <the session's one origin>and omit the app's globalX-Frame-Optionsthere, defaulting toframe-ancestors 'none'whenever the session cannot be read.
Full contract, a host-side listener and a copy-pasteable example page: Embedded signing.
Unchanged — the retired endpoint
POST /v1/embedded/sessions— no hyphen, the 2026 guest-placement flow — still answers410 embedded_sessions_retired. The two share nothing but a word.
2026-09-10 — One-time hook secret, honest errors, expiry events
Changed — document.completed
audit_trail_urlis nowhttps://api.wesign.now/v1/documents/{document_id}/audit-trail— key-authenticated, same host, same Bearer key and same status rules assigned_pdf_url. Previously a token link on the web origin whose route no longer existed (it answered 404).- The event fires only when every signer row on the document is
signed— the same ruleGET /v1/documents/{id}(status: signed),/signedand/audit-trailalready applied. A document with a declined, withdrawn or expired signer no longer receives it; before, it fired and both URLs answered409 not_complete. See what "completed" means.
Added — POST /v1/hooks returns the secret once
- The create response now carries
secret(whsec_…), shown once.GET /v1/hooksnever returns it; lose it and you delete + resubscribe. Additive — the response keeps every existing field.
Added — signing_request.expired is emitted
- An hourly cron flips a
pending/viewedrequest toexpiredonce itsexpires_athas passed and emits the event exactly once per request:signing_request_id,document_id,signer { email, name },expired_at. Queued sequential followers behind an expired signer do not emit.GET /v1/signing-requests/{id}and thesigners[]onGET /v1/documents/{id}now reportstatus: "expired"for such rows (previously the row stayedpendingand expiry was only visible viaexpiresAt).remindon one answers409 expired,withdraw409 invalid_state— both as documented.
Changed — POST /v1/templates/{id}/instantiate error bodies
- Every error now carries a stable
code. New:invalid_key(401),invalid_request(400 body validation),not_found(404),version_not_found(404,meta.version+meta.current_version),missing_recipients(400,meta.slots),db_failed(500). Messages are unchanged; only thecodefield was added.
2026-09-09 — api.wesign.now is the canonical host
letssign.now 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. https://api.letssign.now/v1is a permanent alias — identical routing and responses. Do not migrate a working client on our account.- Path-style URLs on the web hosts (
https://letssign.now/api/v1/…) were never the contract; the apex only redirects. See Base URL.
API keys
- New keys are minted as
wsk_live_<32 hex>; the dashboard shows the first 12 characters (wsk_live_a1b…). - Existing
lsk_live_keys are accepted forever. No rotation needed.
Webhook headers
- Every delivery now carries
X-WeSign-Signature,X-WeSign-Event-IdandX-WeSign-Eventand theX-LetsSign-*trio with byte-identical values. The scheme is unchanged:t=<unix seconds>,v1=<hex HMAC-SHA256(secret, "<t>.<rawBody>")>. TheUser-Agentstaysletssign.now-webhooks/1.0. - The docs now spell out the exact message format, both header families, a 300 s replay tolerance, event-id idempotency, and give Node and PHP verifiers — Verifying a signature.
Changed — document.completed.signed_pdf_url
- Now
https://api.wesign.now/v1/documents/{document_id}/signed: a key-authenticated API URL on the canonical host (Bearer key of the owning workspace;409 not_completeuntil every signer signed;404 not_foundfrom another workspace). - Previously it was an unauthenticated token link on the web origin. A
receiver that fetched it without a key must now send the bearer; one
that pins the origin should pin
api.wesign.now. No integrator was live on the old shape.audit_trail_urlmoved the same way one day later — see the entry above.
Docs
- Second factor by SMS
(
phone_e164+require_sms_verification) is documented. - Embedded signing states what exists today and what the iframe phase will be.
- The events table lists only events that are actually emitted.
2026-09-06 — Review before sending
"review": trueonPOST /v1/templates/{id}/instantiateand/generatestages the instance instead of sending it; newPOST /v1/documents/{id}/confirmand/discard, newGET /v1/documents/{id}/pdffor a finalized file-only instance,"validate_only": truedry runs, and thetemplate_instance.staged/.confirmed/.discardedwebhook events — see Templates.
2026-07-31 — Retirements (breaking, zero production usage)
placement="manual"onPOST /v1/signing-requests→400 placement_retired.POST /v1/documents(ingest-by-URL for manual placement) →410 placement_retired.file_urlonPOST /v1/signing-requestscovers ingest-by-URL.POST /v1/embedded/sessions→410 embedded_sessions_retired.GET/DELETE /v1/embedded/sessions/{id}still answer for existing rows.
All three answer a stable, descriptive 410 rather than a 404, so an
old integration fails loud.
2026-04-30 — v1 launch
The v1 public API launches with the full reference live.
New endpoints
| Method | Path | Purpose |
|---|---|---|
| POST | /v1/signing-requests | Create — multipart upload, four placement modes |
| GET | /v1/signing-requests/{id} | Read one signer + last 50 audit events |
| POST | /v1/signing-requests/{id}/remind | Re-send invite |
| POST | /v1/signing-requests/{id}/withdraw | Cancel in-flight |
| GET | /v1/documents/{id} | Holistic doc + signers + rollup status |
| GET | /v1/documents/{id}/signed | Final PAdES-sealed PDF |
| GET | /v1/documents/{id}/audit-trail | Generated audit-trail PDF |
Concepts
Idempotency-Keyheader with a Stripe-style pending lock for concurrent retries — see Idempotency.- 60 req/min/key rate limiter with
Retry-After— see Rate limits. - Workspace-scoped Bearer keys; rotation via mint-then-revoke.
- Multipart placement modes: anchors / auto_append / explicit / manual — see Placement modes. Manual mode collapses the previous "magic-link" mode into a single URL that works with or without a workspace login.
- Doc-scoped callback webhooks via
callback_urlon POST. HMAC-SHA256 signed events, 7-attempt exponential-backoff retries.
Subdomain branding
Branded + Teams tier workspaces get <slug>.letssign.now/sign/... for
signer-facing pages. Wildcard cert provisions automatically; no DNS
work per-customer.
Migration from /docs/api
If you bookmarked the old monolithic
https://letssign.now/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
Breaking changes ship under a new path version (/v2/*). The
existing /v1/* surface stays operational for at least 12 months
after /v2 GA. Non-breaking additions (new optional fields, new
events, additive enum values) ship to /v1 with a changelog entry.
