Templates
List and read templates, instantiate a locked one with field_values, dry-run, review before sending, generate without a signature.
Templates are reusable contracts you author once and send many times. Instead of uploading a PDF + positions per request, you instantiate a locked template and supply the merge values.
List templates
GET https://api.wesign.now/v1/templates
Authorization: Bearer wsk_live_…{
"templates": [
{
"id": "…", "name": "Power of attorney (company)", "description": null,
"status": "locked", "version": 3, "pages": 1, "content_kind": "pdf",
"instantiable": true, "updated_at": "2026-09-24T…"
},
{
"id": "…", "name": "Mandate letter", "description": "Client mandate, one signer.",
"status": "locked", "version": 2, "pages": 2, "content_kind": "richtext",
"instantiable": true, "updated_at": "2026-09-20T…"
}
]
}The list holds every template of the workspace except archived ones, most
recently updated first. Only status: "locked" templates
(instantiable: true) can be instantiated — locking freezes the field_key
set so your integration stays stable.
Get a template
curl -H "Authorization: Bearer $WSK_KEY" \
https://api.wesign.now/v1/templates/$TEMPLATE_IDOne template plus its input schema: the recipient slots, every value you
can send (field_values) and the boxes the signers sign or date
(signing_fields). It is the same contract instantiate and generate
enforce and the editor shows on a locked template. Reading it needs only an
API key, not the Enterprise template entitlement.
An unknown id, or a template of another workspace, is 404 not_found.
The "Power of attorney (company)" template, version 3: two representatives of one company sign in order. The company's name and tax ID are Sender / API boxes on slot 1; each representative's place of signing is a Signer box on their own slot.
{
"template": {
"id": "…", "name": "Power of attorney (company)", "description": null,
"status": "locked", "version": 3, "pages": 1, "content_kind": "pdf",
"instantiable": true, "updated_at": "2026-09-24T…"
},
"recipients": [{ "slot": 1 }, { "slot": 2 }],
"field_values": [
{
"key": "company.legal_name", "kind": "scalar", "owner": "sender", "required": true,
"signer_required": false, "has_default": false, "slot": 1,
"label": "Company name", "type": "text", "source": "lead", "example": "Muster AG",
"standard": true,
"labels": { "en": "Company name", "de": "Firma", "fr": "Raison sociale",
"it": "Ragione sociale", "es": "Razón social", "nl": "Bedrijfsnaam" },
"description": "The company's registered legal name, including its legal form (Muster AG)."
},
{
"key": "company.tax_id", "kind": "scalar", "owner": "sender", "required": true,
"signer_required": false, "has_default": false, "slot": 1,
"label": "Company tax ID", "type": "text", "source": "lead", "example": "DE123456789-00001",
"standard": true,
"labels": { "en": "Company tax ID", "de": "Steuer-ID des Unternehmens (z. B. W-IdNr.)",
"fr": "Numéro fiscal (entreprise)", "it": "Numero fiscale (azienda)",
"es": "Número fiscal (empresa)", "nl": "Fiscaal nummer (bedrijf)" },
"description": "The company's tax number, e.g. the German W-IdNr DE123456789-00001. The format depends on the country."
},
{
"key": "signing.place", "kind": "scalar", "owner": "signer", "required": false,
"signer_required": false, "has_default": false, "slot": 1,
"label": "Place of signing", "type": "text", "source": "lead", "example": "Zürich",
"standard": true,
"labels": { "en": "Place of signing", "de": "Ort der Unterzeichnung", "fr": "Lieu de signature",
"it": "Luogo della firma", "es": "Lugar de la firma", "nl": "Plaats van ondertekening" },
"description": "Place where this signer signs, usually a city. For signer 2: signing.place_s2."
},
{
"key": "signing.place_s2", "kind": "scalar", "owner": "signer", "required": false,
"signer_required": false, "has_default": false, "slot": 2,
"label": "Place of signing", "type": "text", "source": "lead", "example": "Zürich",
"standard": true,
"labels": { … }, // the same six labels as signing.place
"description": "Place where this signer signs, usually a city. For signer 2: signing.place_s2."
}
],
"signing_fields": [
{ "kind": "signature", "slot": 1, "required": true },
{ "kind": "date", "slot": 1, "required": true },
{ "kind": "signature", "slot": 2, "required": true },
{ "kind": "date", "slot": 2, "required": true }
]
}The schema describes the template's current version. A call that pins an
older version is checked against that version's positioned boxes, so dry-run
it with the same version.
template properties
| Property | Meaning |
|---|---|
id, name, description | description is null when the author wrote none. |
status | draft, locked or archived. |
version | The current version. instantiate can pin an older locked one with version. |
pages | Page count of the template's PDF. |
content_kind | richtext (written in the template editor, with {{placeholders}}) or pdf (an uploaded PDF with positioned boxes). |
instantiable | true exactly when status is locked. |
updated_at | Last change. |
Recipient slots
One { "slot": n } per signer slot that has at least one box, in ascending
order. That is all it says: no role, no pin, no address — see
Several signers.
field_values[] properties
One entry per value you can send, in this order: the body's placeholders, then its tables, then the positioned text boxes. A key appears once.
| Property | On | Meaning |
|---|---|---|
key | every entry | The key to use in field_values. |
kind | every entry | scalar (send a string) or collection (send an array of row objects). |
owner | every entry | Who supplies the value: api, sender or signer. See Who fills a value. |
required | every entry | true: the call fails without it (422, problem code field_required). This flag is the whole contract — see Which fields you must supply. |
auto_filled | platform tokens | true on date_today, sender_*, recipient_*, first_name and last_name: the platform fills them, so they are never required. |
signer_required | signer-owned entries, positioned boxes | Whether the signer must answer. Information only, never a requirement on you. Always false on a positioned text box. |
has_default | positioned boxes | The author set a default value. Omit the key and the default prints (Sender / API) or pre-fills the box (Signer). |
slot | positioned boxes | The slot whose field set the box belongs to. Placeholders and tables have none. |
label | scalars | Display name: your field registry's label, else the author's, else the standard name's, else the key made readable. |
type | scalars | text, multiline, email, phone, number, currency, date, time, enum, boolean, agree, country… Treat a type you do not know as text. |
options, option_keys | enum entries | The allowed values as display labels and as keys. Either spelling is accepted. |
max_length | where your registry sets one | A longer value is shortened with …, not refused. |
example | where known | Your registry's example value, else the standard name's. |
standard | every entry | true when the key, or its per-signer base (signing.place_s2 → signing.place), is a standard name. false never means refused. |
labels | standard names; custom keys where the template records its language | The name in en, de, fr, it, es, nl. Never a machine translation. |
pattern | six standard names | A regular expression for the usual shape. A hint: the API never enforces it. |
description | standard names | One English sentence saying what the value is. |
date_format, time_format, lang | date and time entries the author formatted | How the value prints, never how you send it — see How dates and times print. |
mode | collections | api: you send the rows, so it is required. authored: the author's rows print when you send none. interactive: as authored, and the signer picks the rows while signing unless you send them. |
columns | collections | [{ "key", "label" }]: the cell keys of each row object. |
source | every entry | Legacy. The old lead / API split, kept for existing readers. It does not say who fills the value — read owner and required. See source. |
signing_fields[] properties
The boxes signers sign, initial or date: kind (signature, initial or
date), slot and required. A date box is stamped with the day that
signer signs and may carry a date_format (and lang) — see
How dates and times print. Positioned text boxes
are listed in field_values, not here.
Get a template
GET https://api.wesign.now/v1/templates/{id}
Authorization: Bearer wsk_live_…{
"template": { "id": "…", "name": "Mandate", "status": "locked", "version": 2, "instantiable": true, … },
"recipients": [
{ "slot": 1, "role": "Company", "pinned": "email", "pinned_email": "signatory@muster.ch" },
{ "slot": 2, "role": "Client" }
],
"field_values": [ … ],
"signing_fields": [
{ "kind": "signature", "slot": 1, "required": true },
{ "kind": "signature", "slot": 2, "required": true }
]
}The template's whole contract in one answer: the recipient slots to fill
(recipients, see Several signers), the values to send
(field_values, see Which fields you must supply)
and the boxes each signer signs or dates (signing_fields). It answers for a
template in any status; only instantiable: true can be instantiated. Not
Enterprise-gated. What each field_values property tells your app is in
Prepare a fill in your app.
Instantiate
POST https://api.wesign.now/v1/templates/{id}/instantiate
Authorization: Bearer wsk_live_…
Content-Type: application/json{
"recipients": [
{ "slot": 1, "email": "buyer@acme.ch", "name": "Sara Buyer" }
],
"field_values": {
"client_name": "Acme AG",
"plan_tier": "Gold", // enum — validated
"line_items": [ // collection — array of rows
{ "description": "Design sprint", "qty": "1", "unit_price": "900.00", "amount": "900.00" }
]
},
"locale": "de",
"signing_mode": "sequential"
}field_values keys are the template's {{placeholders}} and positioned
field_keys — the same namespace. See
Fields & placeholders for who
fills which value, and for collections. Discover the exact keys, types, enum
options and limits with GET /v1/templates/{id} (this
template's contract) or GET /v1/fields (the
workspace registry).
Body
| Field | Type | Description |
|---|---|---|
recipients | Recipient[], 1–20 | Required. One per signer slot — see Recipients. |
field_values | object | The values to fill — see The field contract. |
metadata | object | Your own reference for the document, echoed on every read and webhook — see metadata. |
locale | "en" | "de" | "fr" | "it" | "nl" | "es" | Default en. The language of every recipient's email, signing page and SMS code, and of the review_url and docs links. |
expires_in_days | int 1..180 | Default 14. |
signing_mode | "parallel" | "sequential" | Default parallel. Sequential signs in slot order. |
version | int ≥ 1 | Pin a locked template version; default the current one. |
send_emails | boolean | Default true. false: no invitation email — not at the send, not when a sequential signer's turn comes; distribute the returned signing_urls yourself when signing_request.sent announces each one. Same limits as on POST /v1/signing-requests: the SMS code, automatic reminders and completion emails still go out. |
review | boolean | Stage for a human instead of sending — see Review before sending. |
validate_only | boolean | Dry run — see Dry run. |
Any other top-level key is ignored and named in warnings[]; from
2026-12-31 it is a 400 unknown_field — see
Unknown keys.
Recipients
| Field | Type | Description |
|---|---|---|
slot | int 1..20 | Required. The template's signer slot, from 1 — see Several signers. Every slot that has fields needs a recipient (400 missing_recipients, meta.slots). |
email | string | Required. Where the invitation goes. A slot the template pins to an address must use it — pinned_email on GET /v1/templates/{id} (400 pinned-slot-mismatch). |
name | string | Display name; fills {{recipient_name}} tokens. |
phone_e164 | string | E.164 mobile number. Required with require_sms_verification: true. |
require_sms_verification | boolean | SMS code before signing (AES). Same rule as on signing requests. |
sms_gate | "before_sign" | "before_view" | Default before_sign. before_view hides the document until the code — see SMS verification. |
reference | string ≤ 200 | Your own id for this signer, returned on every read and as signer.reference on signing_request.sent, signing_request.signed, document.completed and the signer events. |
company | string ≤ 200 | The company this signer signs for, as you state it. Printed under the signature and on the audit certificate; the same rules as on a signer. |
job_title | string ≤ 120 | The signer's function as it should print. Same rules as company. |
The recipient object is strict: any other key is a
400 unknown_recipient_field with meta.recipients: [{ index, fields }]
and meta.accepted, never dropped. A direct send whose SMS recipients the
workspace's monthly SMS allowance cannot cover answers
402 sms_allowance_exhausted before anything is created; a review: true
instance is checked when it is confirmed.
Each instance is one document against the workspace's monthly document
cap, exactly as on POST /v1/signing-requests: at the cap the call answers
402 tier_required with meta: { tier, cap, used } before anything is
created. Unlike the SMS allowance this applies to review: true too — a
staged instance counts from the moment it exists, and its confirm is not
counted again. Enterprise, the tier with template channels, has no cap today.
A rich-text template is rendered with your values before anything is
created. If that render fails the call answers 502 render_failed —
{ error, code }, shaped like a failed confirm — and nothing exists
afterwards: no document, no signing request, no email, no webhook. The
Idempotency-Key is released, so retry the same body. The call never falls
back to sending the template unfilled. A PDF template is not rendered and
never answers this.
The call is all or nothing. The document, every signing request, its fields
and its audit row are written before any invitation or webhook goes out. If
a write fails, everything this call created is deleted again (the rendered
PDF of a rich-text template too; the template's own PDF never), nobody is
contacted, and the call answers 500 db_failed with meta.rolled_back —
the same answer as
POST /v1/signing-requests. The
Idempotency-Key is released, so retry the same body.
// Every signer verifies by SMS before they can see the filled contract
{
"recipients": [
{
"slot": 1, "email": "anna@example.ch", "name": "Anna Muster",
"phone_e164": "+41791112233", "require_sms_verification": true,
"sms_gate": "before_view", "reference": "person-8812",
"job_title": "Geschäftsführerin", "company": "Muster AG"
}
],
"field_values": {
"person.full_name": "Anna Muster",
"person.date_of_birth": "07.03.1984", // a `date` field: day first or ISO
"company.uid": "CHE-123.456.789"
},
"metadata": { "case_id": "ZT-2026-0142" },
"locale": "de"
}Several signers
Every positioned field of a template belongs to a signer slot, and
recipients[] of GET /v1/templates/{id} lists the slots
that have fields. Send one recipient per listed slot: a missing one is
400 missing_recipients, and a slot without a signature or initial box
cannot take one (422 signer_without_signature). With
"signing_mode": "sequential" they sign in slot order; slot 2 is invited once
slot 1 has signed.
The author can name each slot and pin it to one identity:
| On the slot | Meaning | What you send |
|---|---|---|
role | The author's name for the slot ("Landlord", "Client"). The invitation and the signing page show it. | Nothing; it is informational. |
pinned: "email" and pinned_email | The slot is fixed to one address, for example the company's authorised signatory. | Exactly pinned_email as that recipient's email. Any other address is 400 pinned-slot-mismatch (with slot), on instantiate and on confirm. |
pinned: "sender" | The slot is whoever sends. In the app that is the signed-in sender (quick-send, the reviewer who confirms) or the creator of a form link. | Any address: an API key has no signed-in sender, so instantiate and an API confirm do not enforce this pin. |
A slot with neither is just { "slot": n }. Roles and pins are the
template's current ones, also when you pin a version, because they are
enforced as they are now. pinned_email is served only to the template's own
workspace, the same callers the pinned-slot-mismatch message already
names it to.
// GET /v1/templates/{id} → recipients
[
{ "slot": 1, "role": "Company", "pinned": "email", "pinned_email": "signatory@muster.ch" },
{ "slot": 2, "role": "Client" }
]
// POST /v1/templates/{id}/instantiate
{
"recipients": [
{ "slot": 1, "email": "signatory@muster.ch", "name": "Hans Muster" },
{ "slot": 2, "email": "anna@example.ch", "name": "Anna Keller" }
],
"signing_mode": "sequential"
}Response
A direct send answers 200:
{
"document_id": "…",
"metadata": { "case_id": "ZT-2026-0142" },
"template_version": 3,
"signing_mode": "parallel",
"recipients": [
{
"slot": 1, "email": "anna@example.ch",
"signing_request_id": "…", "signing_url": "https://yourco.letssign.now/de/sign/…",
"emailed": true,
"require_sms_verification": true, "phone_masked": "•••• •••• 2233",
"sms_verified_at": null, "sms_gate": "before_view",
"signature_level": "AES", "reference": "person-8812",
"company": "Muster AG", "job_title": "Geschäftsführerin"
}
],
"field_values_echoed": { "person.full_name": "Anna Muster", "person.date_of_birth": "1984-03-07", "company.uid": "CHE-123.456.789" },
"warnings": []
}warnings names every ignored top-level key and every field_values
key the template does not know — check it on every call; a misspelt key is a
warning, not an error. emailed is false for a sequential recipient whose
turn has not come (released later — emailed then unless send_emails is
false), with send_emails: false, and when the send failed. The read-back
fields are the SMS factor as stored,
your reference, and the company and job_title as stored (null when
none).
The three answers of instantiate, side by side:
| Call | Status | Body |
|---|---|---|
| Direct send | 200 | document_id, metadata, template_version, signing_mode, recipients[] (each with signing_url and emailed), field_values_echoed, warnings. |
review: true | 201 | The same, plus template_id, status: "staged", send_emails, expires_in_days, review_url and review_expires_at — and no signing_url or emailed on the recipients. See Review before sending. |
validate_only: true | 200 | ok, template_id, template_version, warnings, problems, missing_optional. Nothing is created. See Dry run. |
The field contract
| Rule | Detail |
|---|---|
| Keys | ^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*$, at most 64 characters — lowercase letters, digits and _, with single dots between parts: client_name, person.date_of_birth, company.uid. Every part starts with a letter. A dot is part of the name, never nesting: field_values stays flat ({"person.date_of_birth": "…"}, not {"person": {…}}), and person.date_of_birth and person_date_of_birth are two different keys. Read the exact keys from GET /v1/templates/{id}. |
| Values are strings | Every scalar is a JSON string; a collection (line-item table) is an array of row objects whose cells are strings. A JSON true, false or number is a 400 invalid_request. |
| Booleans | Case-insensitive, trimmed: yes, true, ja, sí, si, oui, sì / no, false, nein, non, nee. true/yes render as Yes, false/no as No; a localized spelling renders as sent. Anything else ("1", "X", "on") is 422 with problem code invalid_boolean. A consent (agree) field accepts the yes spellings only. |
| Dates | 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, 15 03 1990) — or YYYY-MM-DD. Always a real calendar day with a four-digit year. Stored and echoed as YYYY-MM-DD, whatever spelling you sent (15.03.1990 → 1990-03-15), and printed in the format the template picked for that placeholder (see How dates and times print). Never read month first — even when the template prints 03/15/1990: 03/04/1990 is 3 April, and 03/15/1990 is 422 invalid_date, as are 2026-02-30, a two-digit year (15.03.90) and a month name (15 March 1990). |
| Times | HH:MM or HH.MM on a 24-hour clock (14:30, 9:05, 14.30), or h:MM AM/PM (2:30 PM, 2:30pm, 2.30 PM). Stored and echoed as 24-hour HH:MM (2:30 PM → 14:30) and printed as the template picked. 24:00, 13:00 PM, seconds (14:30:00), 14h30 and 2 PM are 422 invalid_time. |
| Unknown keys | A key the template does not know is not an error: it earns an unknown_field warning and is not rendered. If the template needs the correctly spelled key, that key's field_required problem is what fails the call. |
| Missing required | One 422 template_input_invalid listing every missing key in problems[].field (code field_required). |
| Empty string | "" is a value, not an omission. On a required input it is field_required. On a positioned text box with a default it replaces the default: a Sender / API box prints blank, and a Signer box opens without the default. Omit the key to get the default. |
A caller that keeps its facts under dotted names uses them as they are:
name the template's fields the same way (define person.full_name,
person.date_of_birth, company.legal_name, company.uid in
Settings → API) and send your keys unchanged. Treat every
unknown_field warning as a spelling mismatch between your keys and the
template's.
The platform suggests standard names for the data contracts ask for most —
person.first_name, company.legal_name, company.uid, address.street,
contact.email, signing.place and more (the full list is in
Fields & placeholders). A
template that uses them publishes standard: true on those inputs, so your app
maps its data once and fills every such template. Custom keys stay just as
valid: standard: false never means refused. How an app uses all of this to
ask its user for the right data is in
Prepare a fill in your app.
Validation
Input is checked in a single pass, so a bad payload comes back with every problem at once — you never fix one field, retry, and discover the next.
HTTP/1.1 422 Unprocessable Entity
{
"error": "3 problems with this request.",
"code": "template_input_invalid",
"template_id": "…",
"template_name": "Mietvertrag",
"template_version": 3,
"problems": [
{
"field": "client_name",
"label": "Client name",
"code": "field_required",
"message": "\"Client name\" is required by this template but was not supplied."
},
{
"field": "start_date",
"label": "Start date",
"code": "invalid_date",
"message": "\"Start date\" must be a real date: DD.MM.YYYY, DD/MM/YYYY, DD-MM-YYYY or DD MM YYYY (day first), or YYYY-MM-DD."
},
{
"field": "plan_tier",
"label": "Plan",
"code": "invalid_enum",
"message": "Invalid value \"Platinum\" for \"Plan\". Allowed: Gold, Silver.",
"allowed": ["Gold", "Silver"]
}
],
"warnings": [
{
"field": "notes",
"code": "unknown_field",
"message": "\"notes\" is not an input of this template and was ignored."
}
],
"docs": "https://www.letssign.now/en/settings/api"
}problems block the call. warnings never do — an extra key is usually a
formula-only identifier, a column that has since been renamed, or a typo, and
the value is not rendered. problems carry the field's human label, so you
can show the message to whoever supplied the data. The same warnings array
also names ignored top-level keys of the request body (message
"… is not a parameter of this endpoint …"). docs links to the
Settings → API page of the app, in the call's locale (/de/settings/api
for "locale": "de"); it is a page for a signed-in member of the workspace,
not an API URL.
code | Cause |
|---|---|
field_required | A required input was missing, empty, or an empty collection. |
invalid_shape | A scalar got rows, or a collection got a single value. |
invalid_boolean | Not a yes/no answer. |
invalid_agree | A consent checkbox was not confirmed. |
invalid_enum | Not one of the options — the allowed list comes back in allowed. |
invalid_email | Not a valid email address. |
invalid_number | Not a number. |
invalid_date | Not a real calendar day in one of the accepted spellings — day first (DD.MM.YYYY, DD/MM/YYYY, DD-MM-YYYY, DD MM YYYY) or YYYY-MM-DD. A month-first 03/15/1990 lands here too. |
invalid_time | Not a real time in one of the accepted spellings — HH:MM / HH.MM (24-hour) or h:MM AM/PM. The message names them: "Start time" must be a real time: HH:MM or HH.MM (24-hour), or h:MM AM/PM (12-hour). |
invalid_iban | Not a valid IBAN. |
invalid_vat | Not a valid VAT ID. |
Which fields you must supply
Supply every entry GET /v1/templates/{id} marks required: true. That is
an entry whose owner is sender or api, which is not a platform token
(date_today, sender_*, recipient_*, first_name, last_name — the
platform fills those, and they come back with auto_filled: true), and which
the author did not make optional. A positioned text box with a default value
is not required either. Collections are required when their mode is api.
Who fills a value explains how
the author sets each of these.
Signer-owned inputs (owner: "signer") are collected from the signer at
signing, so they are never required of a caller; signer_required says
whether the signer must fill one.
Positioned text boxes of a PDF template (they carry the slot they belong to
and has_default) follow the author's Filled by choice:
| Filled by | owner | required | What happens |
|---|---|---|---|
| Sender / API | sender | true, unless the author made it optional or gave it a default | The value you send is fixed document content. Every signer sees it as text, never as an input, from their first view of the document, in any signing order, and no signer can change it. Each value is printed into the sealed PDF exactly once. |
| Signer (every box until an author chooses) | signer | false | The signer of that box's slot types it. You may pre-fill it; the signer can edit it. |
A Sender / API box's slot says only which signer's field set carries it,
not who sees it. So a fact every signer shares — the company that both
representatives sign for — is one box, placed once, and one key you send
once. A per-signer copy (_s2) is for a value that differs per signer, such
as signing.place_s2: each representative's own place of signing. See
Several signers.
Either way signer_required is false on a text box: a signer may leave an
empty one empty — it never blocks signing. A default value the author set
prints (Sender / API) or pre-fills the box (Signer) when you omit the key;
sending "" replaces it. A required token that sits inside a conditional
section whose condition is currently false is not required either; it will
never be rendered. A condition on a date field compares days, so
15.03.1990 and 1990-03-15 decide it the same way.
Auto-filled tokens (auto_filled: true) are the platform's: on a direct
instantiate the recipient tokens come from recipients[] and date_today
is the send date, while the sender_* letterhead tokens stay blank unless you
supply them in field_values (a review: true instance fills them from your
workspace letterhead). A value you supply always wins over the platform's.
Read the flag, don't re-derive it: required: true on the published schema is
the whole contract.
How dates and times print
The template's author picks, on each date or time placeholder, how it prints
in the document. GET /v1/templates/{id} shows the pick read-only on the
entry. It never changes what you send, what is stored or what is echoed back:
// GET /v1/templates/{id} → field_values[]
{ "key": "start_date", "kind": "scalar", "owner": "api", "required": true,
"label": "Start date", "type": "date", "source": "api",
"date_format": "mdy_slash", "lang": "en" }
// send "15.03.2027" or "2027-03-15" — stored and echoed "2027-03-15",
// printed 03/15/2027date_format | Prints |
|---|---|
dmy_dot | 15.03.1990 |
dmy_slash | 15/03/1990 |
mdy_slash | 03/15/1990 |
long | written out in lang: 15 March 1990 (en), 15. März 1990 (de), 15 mars 1990 (fr), 15 marzo 1990 (it), 15 de marzo de 1990 (es), 15 maart 1990 (nl) |
time_format | Prints |
|---|---|
h24 | 14:30 |
h12 | 2:30 PM |
Absent means the author never picked one:
- a date prints
15.03.1990. So does aYYYY-MM-DDvalue in an entry the author left untyped (it readstype: "text"), unless the author chose Text as the placeholder's type, which prints what you sent; - a time prints
14:30; date_todayis written out. It is the day (UTC) the document was created, not the day anyone signs;- a written-out date without
langis spelled in the document's language (the language its signature lines were written in), else the call'slocale, else English. A stagedgeneratehas nolocale.
The author's language is recorded with every date or time placeholder, so
lang can also appear next to a numeric format or a time, where it changes
nothing.
A date entry in signing_fields is stamped with the date that signer
signs on, taken in the workspace owner's time zone. It may carry a
date_format (and a lang for a written-out one). Without one it is stamped
in your workspace's house format: 03/15/1990 when the workspace's country
(Settings → Company) is the United States, 15.03.1990 for every other. A
written-out stamp without lang uses the signing request's locale.
A positioned text box prints the value you sent for it (stored as
YYYY-MM-DD or HH:MM) in its format, whether it is a Sender / API box or
a Signer box you pre-filled. Whatever the signer types into a Signer box on
the sign page is printed exactly as typed, so 03/04/1990 is never re-read as
another day.
Conditions and formulas read the stored value (1990-03-15, 14:30), never
the printed one, so a condition on a date compares days whatever the format.
How values are treated
- enum — either spelling is accepted: the published display label
(
"Gold") or the underlying column key ("gold"). - max length — a value longer than a field's cap is silently abbreviated
(
…) to fit the space the template allocates. It is not an error. - boolean / agree —
true/yesandfalse/no(any case) becomeYes/No; a localized spelling (Ja,Oui, …) is kept as sent. - phone — normalised to the format the template renders.
- date / time — normalised to
YYYY-MM-DD/ 24-hourHH:MM; the document prints them in the placeholder's format. - collections — an array of row objects; unknown columns earn an
unknown_columnwarning and are dropped.
The normalised values are what gets stored and what the document is printed
from, so read field_values_echoed on the response if you want to see exactly
what landed.
Prepare a fill in your app
An app that fills templates for its users asks them for the data before
it calls: read GET /v1/templates/{id}, ask for every required input, dry
run with validate_only, offer what missing_optional lists, then send the
same body. The whole flow, with the answers at each step, is in
Prepare a fill in your app.
The legacy path https://www.letssign.now/api/templates/{id}/instantiate on the web
origin still works as a deprecated alias. New integrations should use the api.wesign.now URL above.
Several signers
A template numbers its signer positions as slots, from 1, and a request
assigns one recipient to each slot in recipients[].
- PDF template: slot n is the n-th recipient in the editor's recipient list, counted from the top. A recipient you add goes to the end; moving or removing one renumbers the others, and a recipient's boxes always stay with that recipient.
- Rich-text template: every signature line belongs to a signer (Signer 1, Signer 2, …); Add signer and the Signers panel set how many there are.
- Locking publishes the slots with the version, so a caller pinned to a version never sees them move.
Every slot that has boxes needs a recipient (400 missing_recipients, with
the missing slots in meta.slots), and every recipient's slot needs a
signature or initials box (422 signer_without_signature).
A rich-text template may also give a slot a role (a name such as
"Representative 1") and pin it to a signer, in the editor's Signers
panel. A PDF template made in the app has neither today: its slots carry no
role (signer.role is null) and no pin.
| Pin | Over the API |
|---|---|
| An email address | The slot's recipient must have that address, on instantiate and on confirm: 400 pinned-slot-mismatch, with the slot. |
| Me (the sender) | Not checked with an API key, because there is no signed-in sender: address the slot as your workflow needs. A confirm on the review page checks it against the reviewer's own address. |
GET /v1/templates/{id} publishes the slot numbers only
("recipients": [{ "slot": 1 }, { "slot": 2 }]): neither the role nor a pin
is in the schema. The template's page in the app shows both. The role appears
in the invitation email and as signer.role on signing_request.sent.
Example: two representatives, in order
"Power of attorney (company)", version 3 (its schema is under
Get a template): slot 1 is the first representative and
slot 2 the second. The company's name and tax ID are
Sender / API boxes placed once, on slot 1. Each representative has a Signer
box for their own place of signing: signing.place on slot 1,
signing.place_s2 on slot 2.
// POST https://api.wesign.now/v1/templates/{id}/instantiate
{
"recipients": [
{ "slot": 1, "email": "anna@example.ch", "name": "Anna Muster",
"job_title": "Geschäftsführerin", "company": "Muster AG" },
{ "slot": 2, "email": "ben@example.ch", "name": "Ben Keller",
"job_title": "Mitglied des Verwaltungsrates", "company": "Muster AG" }
],
"field_values": {
"company.legal_name": "Muster AG",
"company.tax_id": "DE123456789-00001",
"signing.place": "Zürich" // pre-fills Anna's box; she can change it
},
"signing_mode": "sequential",
"metadata": { "case_id": "ZT-2026-0142" },
"locale": "de"
}HTTP/1.1 200 OK
{
"document_id": "…",
"metadata": { "case_id": "ZT-2026-0142" },
"template_version": 3,
"signing_mode": "sequential",
"recipients": [
{
"slot": 1, "email": "anna@example.ch",
"signing_request_id": "…", "signing_url": "https://yourco.letssign.now/de/sign/…",
"emailed": true,
"require_sms_verification": false, "phone_masked": null, "sms_verified_at": null,
"sms_gate": "before_sign", "signature_level": "SES", "reference": null,
"company": "Muster AG", "job_title": "Geschäftsführerin"
},
{
"slot": 2, "email": "ben@example.ch",
"signing_request_id": "…", "signing_url": "https://yourco.letssign.now/de/sign/…",
"emailed": false, // queued: invited once slot 1 has signed
"require_sms_verification": false, "phone_masked": null, "sms_verified_at": null,
"sms_gate": "before_sign", "signature_level": "SES", "reference": null,
"company": "Muster AG", "job_title": "Mitglied des Verwaltungsrates"
}
],
"field_values_echoed": {
"company.legal_name": "Muster AG",
"company.tax_id": "DE123456789-00001",
"signing.place": "Zürich"
},
"warnings": []
}- Order. Slot 1 is invited now. Slot 2 is
queuedand invited automatically when slot 1 has signed. Itssigning_urlalready works: opened before its turn, it shows a waiting notice instead of the document. - Sender values. The company name and tax ID sit on slot 1's boxes, yet
both representatives see them from their first view, and both seals cover
them. Sending them once is enough; there is no
company.legal_name_s2. - Signer values. Nothing was sent for
signing.place_s2, so Ben types his own place while signing, or leaves it empty. A dry run of this body lists it inmissing_optionalwith"slot": 2. - Who signs for whom. Each representative's caption reads
Anna Muster · Geschäftsführerin · Muster AG · anna@example.ch, and the audit certificate (attached to the completion emails, and fromGET /v1/documents/{id}/audit-trail) lists the function and company under each signature, "stated by the sender".
Dry run
Add "validate_only": true to instantiate or generate to run every check
without creating anything: no document, no signing request, no email, no SMS.
No Idempotency-Key is read or stored, so the real call can reuse the key you
were going to send. A dry run costs one request against the
rate limit.
| Answer | When |
|---|---|
200, "ok": true | The real call would go through. |
200, "ok": false | instantiate only: the body is fine, but the real call would be refused because the workspace's monthly document cap is reached or its SMS allowance cannot cover the SMS recipients. problems says so. |
422 template_input_invalid | A value is missing or wrong: the body the real call would return, plus missing_optional. |
Any other 4xx | Exactly the refusal the real call would give — a missing recipient, a slot without a signature box, a pinned slot, an unlocked template. |
// POST https://api.wesign.now/v1/templates/{id}/instantiate
{
"recipients": [{ "slot": 1, "email": "anna@example.ch", "name": "Anna Muster" }],
"field_values": { "person.full_name": "Anna Muster", "address.full": "Bahnhofstrasse 1, 8001 Zürich" },
"validate_only": true
}
// → 200
{
"ok": true, "template_id": "…", "template_version": 2,
"warnings": [], "problems": [], "missing_optional": []
}Neither the cap nor the allowance is a fault in the body, so a shortfall
comes back as a 200 with ok: false — the cap as
{ "code": "tier_required", "message": "…", "meta": { "tier", "cap", "used" } }
(reported with review: true too), the allowance like this:
{
"ok": false, "template_id": "…", "template_version": 2, "warnings": [],
"problems": [{
"field": "recipients",
"code": "sms_allowance_exhausted",
"message": "The real call would be refused with 402 sms_allowance_exhausted: 1 signer must verify by SMS, the workspace has 0 of 300 SMS codes left this month (resets 2026-10-01T00:00:00.000Z).",
"sms_allowance": { "monthly": 300, "used": 300, "remaining": 0, "resets_at": "2026-10-01T00:00:00.000Z" },
"required": 1
}],
"missing_optional": []
}Read ok, not only the status code.
missing_optional comes with a dry run's 200 and its
422 template_input_invalid: the inputs you may still supply and left empty
(absent, or ""). That is a signer box you could pre-fill (otherwise the
signer is asked, or it stays empty) or a Sender / API input the author made
optional or gave a default (has_default: true, the default then prints).
Each entry carries what your app needs to ask its user for it: key,
label, owner, type, standard, and where known example, pattern
(a hint, never enforced), labels, slot, has_default and the print format
(date_format, time_format, lang). It lists scalars only — never a
platform token, a table or an input inside a hidden conditional section —
in schema order. Nothing in it ever makes ok false, a required input is
never listed there (it is a problems entry), and a real call's 422 does
not carry the list. A full example is in
Prepare a fill in your app.
On instantiate a dry run checks, in this order: the key (and its IP
allowlist), the Enterprise entitlement, the rate limit, the body — recipients
strict, the SMS and sms_gate rules, value types —, metadata, that the
template exists and is locked, the pinned version, that every slot with
fields has a recipient, that every recipient has a signature field, slot pins,
the full field contract (required keys, types, dates, enums, conditional
sections), the interactive-table rule, the workspace's monthly document
cap and its SMS allowance. It does not check the Idempotency-Key, the PDF render (the real call
answers 502 render_failed if it fails), whether a phone
number is a reachable mobile, or email delivery.
On generate it checks the key, entitlement, rate limit, body, metadata,
the locked template and the field contract. Its 200 is
{ "ok": true, "template_id", "template_version", "warnings", "missing_optional" }
— no problems, because every refusal there is a 4xx. It never requires or
lists a positioned box, since generate does not print them. A template with
no content to render still passes the dry run while the real call answers
409 not_rendered.
Review before sending
Sometimes the machine should not have the last word: a colleague needs to read
the filled contract, correct a recipient's mobile number, or simply approve it.
Add "review": true and the instantiate call stages the document instead of
sending it.
{
"recipients": [{ "slot": 1, "email": "anna@example.ch", "name": "Anna Muster" }],
"field_values": { "person.full_name": "Anna Muster", "address.full": "Bahnhofstrasse 1, 8001 Zürich" },
"metadata": { "case_id": "ZT-2026-0142" },
"review": true
}HTTP/1.1 201 Created
{
"document_id": "…",
"metadata": { "case_id": "ZT-2026-0142" },
"template_id": "…",
"template_version": 2,
"status": "staged",
"signing_mode": "parallel", // echoed, with the defaults applied
"send_emails": true,
"expires_in_days": 14,
"review_url": "https://www.letssign.now/en/documents/…/review",
"review_expires_at": "2026-10-08T09:14:00.000Z",
"recipients": [
{
"slot": 1, "email": "anna@example.ch", "signing_request_id": "…",
"require_sms_verification": false, "phone_masked": null, "sms_verified_at": null,
"sms_gate": "before_sign", "signature_level": "SES", "reference": null,
"company": null, "job_title": null
}
],
"field_values_echoed": { "person.full_name": "Anna Muster", "address.full": "Bahnhofstrasse 1, 8001 Zürich" },
"warnings": []
}Each staged recipient already reads back the SMS factor it was staged with, so a missing factor shows before anyone confirms.
What a staged instance means:
- The document and one signing request per recipient exist, and the document shows up in the app as Awaiting review.
- Nothing is emailed and nothing is texted. The signing tokens exist but are inert — opening one shows a waiting notice instead of the document.
- There is deliberately no
signing_urlin the 201. Links only become real once a confirm releases them, and publishing them earlier would invite you to distribute links that do not work. review_urlopens the review page in the app. It is a page for people, not an API URL: whoever opens it signs in as a member of your workspace, and your API key does not open it. It is in the call'slocale;GET /v1/documents/{id}reports the same page asreview.url, always in English (/en/).- The window is 14 days from staging (
review_expires_at). See Expiry.
review: true respects Idempotency-Key exactly like an ordinary call: the
same key with the same body replays the same 201 instead of staging twice.
What a reviewer can change
On the review page, while the window is open:
| What | Where | Detail |
|---|---|---|
| Recipients | The recipient list on the page | Name, email (not on a slot the template pins: "Fixed by the template"), mobile number, delivery channel (Email, SMS or Email + SMS), the SMS code before signing, and the signing order (In order or Everyone at once). Also the signing deadline and whether invitation emails go out. |
| Body and values of a rich-text instance | Edit content | The document's text and its Sender / API values, in the template editor. The edits change this document only, never the template. Confirm renders the PDF from them. |
| Sender / API values of a PDF instance | Edit values (shown when the template has a Sender / API box) | A form with just those values. Confirm copies them onto the signing fields before anyone can open the document, so every signer sees, and every seal prints, the reviewed value. |
A signer's box and a platform token are shown read-only; the reviewer cannot
retype them. Confirm checks the values again: a required value left empty is
422 template_input_invalid.
Over the API, confirm changes recipients (their
company and job_title included), the deadline and send_emails — it
takes no values. A field_values key in
its body is ignored and named in warnings[]. To correct a value without the
review page, discard the instance and instantiate again with the right value.
Expiry
- After 14 days the review page offers only Discard, and
confirmanswers410 review_expired. - A daily cleanup then discards the instance: its recipients are withdrawn
and were never contacted, and
template_instance.discardedfires with"reason": "expired". - A workspace that no longer has the Enterprise template entitlement sees the review page in discard-only mode, so it can still clean up.
Confirm and discard
POST https://api.wesign.now/v1/documents/{id}/confirm
Authorization: Bearer wsk_live_…
Content-Type: application/jsonEvery field of the body is optional — an empty body or {} sends exactly
what was staged. Pass recipients to override, addressing each row by its
signing_request_id from the 201; only the fields you send change:
{
"recipients": [
{
"signing_request_id": "…",
"name": "Anna Muster-Keller",
"email": "anna.keller@example.ch",
"phone_e164": "+41791234567", // null clears it
"channel": "both", // "email" | "sms" | "both" — invitation channel
"require_sms_verification": true, // SMS code before signing (AES)
"sms_gate": "before_view", // "before_sign" | "before_view"
"signing_order": 1, // sequential position, 1…20
"reference": "person-8812", // null clears it
"job_title": "Geschäftsführerin", // null or "" clears it
"company": "Muster AG" // null or "" clears it
}
],
"send_emails": true,
"expires_in_days": 14 // 1…180; default: the instance's own
}The confirm body follows the same rules as the create calls:
phone_e164is the phone field, as everywhere else.phoneis still accepted as a deprecated alias until 2026-12-31; each use adds adeprecated_fieldentry towarnings[]. Both with different values:400 conflicting_fields(meta.recipients: [{ index, fields }]).- Recipient objects are strict: an unknown key is a
400 unknown_recipient_field(meta.recipients,meta.accepted). Unknown top-level keys —field_valuesincluded — are ignored and named inwarnings[]. companyandjob_titlefollow the signer rules;null, or a value that is blank once trimmed, clears the one staged at instantiate, and leaving the key out keeps it.- A body that is not valid JSON is a
400 invalid_request— never read as "confirm unchanged". - The SMS rules are checked on each recipient as edited (a phone stored
at instantiate counts): SMS without a phone, an SMS channel without a phone,
or
before_viewwithout SMS is a422 invalid_recipientsnaming the field. - The recipients this confirm releases must fit the workspace's SMS
allowance, or it answers
402 sms_allowance_exhausted. - A refused confirm writes nothing (
400,402,409,422): the instance stays staged exactly as it was, and you retry with a corrected body. One narrow exception: a discard (or the review expiry) that lands while the confirm renders the document answers409 discarded/not_stagedafter your edits were saved — on an instance that can no longer be confirmed, so they have no effect.
HTTP/1.1 200 OK
{
"ok": true,
"kind": "esign",
"status": "sent",
"already_confirmed": false,
"re_rendered": true,
"document_id": "…",
"expires_at": "2026-10-08T11:02:00.000Z",
"recipients": [
{
"signing_request_id": "…", "slot": 1, "email": "anna.keller@example.ch",
"status": "pending", "signing_url": "https://yourco.letssign.now/en/sign/…",
"emailed": true, "sms_sent": true,
"require_sms_verification": true, "phone_masked": "•••• •••• 4567",
"sms_verified_at": null, "sms_gate": "before_view",
"signature_level": "AES", "reference": "person-8812",
"company": "Muster AG", "job_title": "Geschäftsführerin"
}
],
"warnings": []
}sms_sent is the invitation SMS of channel sms / both (it texts
the signing link; with before_view it does not name the document) — not
the verification code, which the signer requests on the page.
- Retry-safe. A second confirm answers
200with"already_confirmed": trueand"recipients": [], and sends nothing. Retry a timeout without fear of double-sending. - Sequential documents promote only the first cohort. Later recipients come
back with
"status": "queued"and are released automatically as each previous signer finishes — the same behaviour a sequential send has always had. Each release emitssigning_request.sentwith"source": "sequential_release". "send_emails": falsesends no invitation email — to the first cohort now or to a later recipient when their turn comes; you still get asigning_urlper recipient and distribute them yourself. It does not stop the invitation SMS of a recipient whosechannelissmsorboth. Absent, the value staged with the instance applies.- The signing deadline restarts at confirm time from the instance's
expires_in_days. - Confirming a rich-text instance re-renders the PDF from the current content
and values first (
re_rendered: true). If the render fails you get502 render_failedand nothing is sent — the instance stays staged and you retry. - Confirming a PDF instance first copies its Sender / API values, as reviewed, onto the signing fields that carry them, before any recipient is released.
- The staged values are checked again at confirm. Positioned text boxes are
checked against the template version the instance was staged from, so a
template re-locked while the instance waited cannot block its confirm with a
field the staging call was never asked for. A generated file
(
kind: "file") never requires a positioned field, asgeneratenever did.
Throw the instance away instead:
POST https://api.wesign.now/v1/documents/{id}/discard
Authorization: Bearer wsk_live_…{
"ok": true, "status": "discarded", "kind": "esign",
"document_id": "…", "already_discarded": false,
"withdrawn": 1, "deleted": false
}Discard takes no body and withdraws every staged recipient. It has no
Enterprise gate — cleanup must work even after a downgrade — and it is
idempotent ("already_discarded": true on a repeat). It works on an expired
instance the daily cleanup has not reached yet.
Confirm errors
| Status | Code | Meaning |
|---|---|---|
400 | invalid_request | Not JSON, or a field failed its type (the first problem in error) — a company or job_title that is too long or holds a control character included. |
400 | unknown_recipient_field | A recipient carried a key confirm does not accept (meta.recipients, meta.accepted). |
400 | conflicting_fields | phone_e164 and its deprecated alias phone disagree. |
400 | pinned-slot-mismatch | A recipient of a slot the template pins was re-addressed (slot). |
402 | enterprise-required | The workspace has no template entitlement. |
402 | sms_allowance_exhausted | The SMS allowance cannot cover the SMS recipients this confirm releases (sms_allowance, required). |
404 | not_found | Unknown id, or a document of another workspace. |
409 | not_staged | The document is not awaiting review. |
409 | discarded | The instance was discarded, by hand or at expiry. |
409 | interactive-table-requires-sequential | An unresolved interactive table with parallel recipients. |
410 | review_expired | The 14-day window closed. Discard it and create a new instance. |
422 | invalid_recipients | Bad email, non-E.164 phone, SMS without a number, before_view without SMS, a duplicate signing_order, or a signing_request_id that is not a staged recipient of this document. problems[] names each row and field. |
422 | template_input_invalid | A required value is empty or a stored value fails its type — for example one blanked during review. On confirm the body is { error, code, problems } only. |
502 | render_failed | The PDF could not be rendered. Nothing was sent; retry. |
502 | update_failed | Saving the recipient edits or the reviewed values failed. Nothing was sent; retry. |
502 | confirm_failed | Releasing the recipients (or finalizing a file) failed. Retry: a repeated confirm never sends twice. |
A repeated confirm is not an error: it answers 200 with
"already_confirmed": true.
Discard errors
| Status | Code | Meaning |
|---|---|---|
404 | not_found | Unknown id, or a document of another workspace. |
409 | already_confirmed | The instance was confirmed; it can no longer be discarded. |
409 | not_staged | The document is not a staged instance. |
502 | discard_failed | The recipients could not be withdrawn (or the file deleted). Retry. |
A repeated discard answers 200 with "already_discarded": true. The
shared errors — 401 for the key, 429 for the rate limit —
apply to both endpoints.
Generate (no signature)
Need the filled PDF without a signing step — an invoice, an order
confirmation, a letter? generate fills the template and returns the PDF bytes
directly. No recipients, no signing request.
POST https://api.wesign.now/v1/templates/{id}/generate
Authorization: Bearer wsk_live_…
Content-Type: application/json{
"field_values": {
"client_name": "Acme AG",
"line_items": [
{ "description": "Design sprint", "qty": "1", "unit_price": "900.00", "amount": "900.00" }
]
},
"filename": "invoice-1042.pdf" // optional
}| Field | Type | Description |
|---|---|---|
field_values | object | As on instantiate. |
metadata | object | Validated on every call; stored only with review: true, which creates a document to carry it. |
filename | string ≤ 200 | Name of the file; default the template's name. Letters and digits of any script, _, . and - are kept; spaces and other characters become _; .pdf is appended. The PDF answer's Content-Disposition carries it as filename*=UTF-8''… (and an ASCII filename fallback). |
locale | "en" | "de" | "fr" | "it" | "nl" | "es" | Default en. The language of the review_url and docs links, and on a plain generate the fallback language of a written-out date. |
review | boolean | Stage the file for a human — see Generated files with review. |
validate_only | boolean | Dry run — see Dry run. |
Responds 200 with Content-Type: application/pdf (the document as an
attachment). The stream has no JSON body for warnings[], so ignored
top-level keys are named in the X-Unknown-Fields response header
(comma-separated, percent-encoded; absent when there are none) — from
2026-12-31 they are a 400 unknown_field on this call too. The template must
be locked. field_values are validated and
length-capped exactly like instantiate, with one difference: generate never
requires a positioned text field of a PDF template (not even a Sender / API
one) and never lists one in missing_optional, because it does not print
them. A value you send for one is still type-checked. This is the API behind the editor's
“Generate via API” option; “Generate & sign via API” uses instantiate.
Generated files with review
generate also takes "review": true. Instead of streaming bytes it stores the
document and hands back a review link, so a human can read the invoice before it
counts as final:
HTTP/1.1 201 Created
{
"document_id": "…",
"metadata": { "case_id": "ZT-2026-0142" },
"template_id": "…",
"template_version": 3,
"status": "staged",
"kind": "file",
"filename": "invoice-1042.pdf",
"review_url": "https://www.letssign.now/en/documents/…/review",
"review_expires_at": "2026-10-08T09:14:00.000Z",
"warnings": []
}There are no recipients, so confirm finalizes rather than sends:
POST https://api.wesign.now/v1/documents/{id}/confirm
{
"ok": true, "kind": "file", "status": "finalized",
"already_confirmed": false, "re_rendered": true,
"document_id": "…", "sha256": "…", "pages": 2,
"download_url": "https://api.wesign.now/v1/documents/…/pdf",
"warnings": []
}Fetch the bytes any time afterwards with
GET https://api.wesign.now/v1/documents/{id}/pdf and your API key —
download_url is exactly that URL and takes the same bearer. discard
deletes the staged file outright ("deleted": true); an unconfirmed one is
deleted automatically once the 14-day window has closed. On the review page a
rich-text file can be corrected with Edit content; a file generated from a
PDF template has nothing to edit.
A generate with review: true fills the platform tokens date_today and
sender_* from your workspace letterhead, because a staged document is a real
document in your account. A plain generate leaves them blank, exactly as it
always has. A staged file keeps no
locale, so a written-out date whose placeholder and document name no
language is spelled in English in the review copy and in the confirmed file
alike; a plain generate uses the call's locale for it.
Staging a generated file needs the review storage columns. Until they are in
place the call answers 503 { "code": "review_unavailable" } and persists
nothing; a plain generate is unaffected either way.
Webhooks
Staging fires its own events, so an integration can follow an instance without
polling. Subscribe in Settings → API → Webhooks or via
POST /v1/hooks.
| Event | When |
|---|---|
template_instance.staged | An instantiate or generate with review: true created the instance. |
template_instance.confirmed | A confirm released the invitations (or finalized a file). |
template_instance.discarded | Discarded by hand, or expired — reason is "discarded" or "expired". |
Every payload carries document_id, template_id, kind ("esign" or
"file") and, on the staged event, review_url and review_expires_at, so a
document-scoped callback_url receives them too.
A confirm additionally fires the ordinary
signing_request.sent for the first recipient it released,
with "source": "template_instance" — the same event a direct instantiate
fires ("source": "v1_api"), so nothing downstream needs a special case. A
later recipient of a sequential document gets its own sent, with
"source": "sequential_release", when its turn comes.
From there on every recipient produces the ordinary signer events —
viewed, declined, withdrawn, sms_sent, sms_failed,
sms_verified (each with signer.reference,
signer.company and signer.job_title), signed (the same signer block)
and expired — and every one of them carries the document's metadata.
