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_ID

One 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

PropertyMeaning
id, name, descriptiondescription is null when the author wrote none.
statusdraft, locked or archived.
versionThe current version. instantiate can pin an older locked one with version.
pagesPage count of the template's PDF.
content_kindrichtext (written in the template editor, with {{placeholders}}) or pdf (an uploaded PDF with positioned boxes).
instantiabletrue exactly when status is locked.
updated_atLast 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.

PropertyOnMeaning
keyevery entryThe key to use in field_values.
kindevery entryscalar (send a string) or collection (send an array of row objects).
ownerevery entryWho supplies the value: api, sender or signer. See Who fills a value.
requiredevery entrytrue: the call fails without it (422, problem code field_required). This flag is the whole contract — see Which fields you must supply.
auto_filledplatform tokenstrue on date_today, sender_*, recipient_*, first_name and last_name: the platform fills them, so they are never required.
signer_requiredsigner-owned entries, positioned boxesWhether the signer must answer. Information only, never a requirement on you. Always false on a positioned text box.
has_defaultpositioned boxesThe author set a default value. Omit the key and the default prints (Sender / API) or pre-fills the box (Signer).
slotpositioned boxesThe slot whose field set the box belongs to. Placeholders and tables have none.
labelscalarsDisplay name: your field registry's label, else the author's, else the standard name's, else the key made readable.
typescalarstext, multiline, email, phone, number, currency, date, time, enum, boolean, agree, country… Treat a type you do not know as text.
options, option_keysenum entriesThe allowed values as display labels and as keys. Either spelling is accepted.
max_lengthwhere your registry sets oneA longer value is shortened with …, not refused.
examplewhere knownYour registry's example value, else the standard name's.
standardevery entrytrue when the key, or its per-signer base (signing.place_s2 → signing.place), is a standard name. false never means refused.
labelsstandard names; custom keys where the template records its languageThe name in en, de, fr, it, es, nl. Never a machine translation.
patternsix standard namesA regular expression for the usual shape. A hint: the API never enforces it.
descriptionstandard namesOne English sentence saying what the value is.
date_format, time_format, langdate and time entries the author formattedHow the value prints, never how you send it — see How dates and times print.
modecollectionsapi: 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.
columnscollections[{ "key", "label" }]: the cell keys of each row object.
sourceevery entryLegacy. 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

FieldTypeDescription
recipientsRecipient[], 1–20Required. One per signer slot — see Recipients.
field_valuesobjectThe values to fill — see The field contract.
metadataobjectYour 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_daysint 1..180Default 14.
signing_mode"parallel" | "sequential"Default parallel. Sequential signs in slot order.
versionint ≥ 1Pin a locked template version; default the current one.
send_emailsbooleanDefault 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.
reviewbooleanStage for a human instead of sending — see Review before sending.
validate_onlybooleanDry 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

FieldTypeDescription
slotint 1..20Required. The template's signer slot, from 1 — see Several signers. Every slot that has fields needs a recipient (400 missing_recipients, meta.slots).
emailstringRequired. 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).
namestringDisplay name; fills {{recipient_name}} tokens.
phone_e164stringE.164 mobile number. Required with require_sms_verification: true.
require_sms_verificationbooleanSMS 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.
referencestring ≤ 200Your 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.
companystring ≤ 200The 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_titlestring ≤ 120The 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 slotMeaningWhat you send
roleThe author's name for the slot ("Landlord", "Client"). The invitation and the signing page show it.Nothing; it is informational.
pinned: "email" and pinned_emailThe 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:

CallStatusBody
Direct send200document_id, metadata, template_version, signing_mode, recipients[] (each with signing_url and emailed), field_values_echoed, warnings.
review: true201The 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: true200ok, template_id, template_version, warnings, problems, missing_optional. Nothing is created. See Dry run.

The field contract

RuleDetail
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 stringsEvery 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.
BooleansCase-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.
DatesDay 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).
TimesHH: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 keysA 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 requiredOne 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.

codeCause
field_requiredA required input was missing, empty, or an empty collection.
invalid_shapeA scalar got rows, or a collection got a single value.
invalid_booleanNot a yes/no answer.
invalid_agreeA consent checkbox was not confirmed.
invalid_enumNot one of the options — the allowed list comes back in allowed.
invalid_emailNot a valid email address.
invalid_numberNot a number.
invalid_dateNot 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_timeNot 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_ibanNot a valid IBAN.
invalid_vatNot 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 byownerrequiredWhat happens
Sender / APIsendertrue, unless the author made it optional or gave it a defaultThe 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)signerfalseThe 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/2027
date_formatPrints
dmy_dot15.03.1990
dmy_slash15/03/1990
mdy_slash03/15/1990
longwritten 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_formatPrints
h2414:30
h122:30 PM

Absent means the author never picked one:

  • a date prints 15.03.1990. So does a YYYY-MM-DD value in an entry the author left untyped (it reads type: "text"), unless the author chose Text as the placeholder's type, which prints what you sent;
  • a time prints 14:30;
  • date_today is written out. It is the day (UTC) the document was created, not the day anyone signs;
  • a written-out date without lang is spelled in the document's language (the language its signature lines were written in), else the call's locale, else English. A staged generate has no locale.

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/yes and false/no (any case) become Yes / 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-hour HH:MM; the document prints them in the placeholder's format.
  • collections — an array of row objects; unknown columns earn an unknown_column warning 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.

PinOver the API
An email addressThe 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 queued and invited automatically when slot 1 has signed. Its signing_url already 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 in missing_optional with "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 from GET /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.

AnswerWhen
200, "ok": trueThe real call would go through.
200, "ok": falseinstantiate 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_invalidA value is missing or wrong: the body the real call would return, plus missing_optional.
Any other 4xxExactly 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_url in 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_url opens 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's locale; GET /v1/documents/{id} reports the same page as review.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:

WhatWhereDetail
RecipientsThe recipient list on the pageName, 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 instanceEdit contentThe 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 instanceEdit 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 confirm answers 410 review_expired.
  • A daily cleanup then discards the instance: its recipients are withdrawn and were never contacted, and template_instance.discarded fires 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/json

Every 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_e164 is the phone field, as everywhere else. phone is still accepted as a deprecated alias until 2026-12-31; each use adds a deprecated_field entry to warnings[]. 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_values included — are ignored and named in warnings[].
  • company and job_title follow 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_view without SMS is a 422 invalid_recipients naming 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 answers 409 discarded / not_staged after 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 200 with "already_confirmed": true and "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 emits signing_request.sent with "source": "sequential_release".
  • "send_emails": false sends no invitation email — to the first cohort now or to a later recipient when their turn comes; you still get a signing_url per recipient and distribute them yourself. It does not stop the invitation SMS of a recipient whose channel is sms or both. 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 get 502 render_failed and 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, as generate never 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

StatusCodeMeaning
400invalid_requestNot 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.
400unknown_recipient_fieldA recipient carried a key confirm does not accept (meta.recipients, meta.accepted).
400conflicting_fieldsphone_e164 and its deprecated alias phone disagree.
400pinned-slot-mismatchA recipient of a slot the template pins was re-addressed (slot).
402enterprise-requiredThe workspace has no template entitlement.
402sms_allowance_exhaustedThe SMS allowance cannot cover the SMS recipients this confirm releases (sms_allowance, required).
404not_foundUnknown id, or a document of another workspace.
409not_stagedThe document is not awaiting review.
409discardedThe instance was discarded, by hand or at expiry.
409interactive-table-requires-sequentialAn unresolved interactive table with parallel recipients.
410review_expiredThe 14-day window closed. Discard it and create a new instance.
422invalid_recipientsBad 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.
422template_input_invalidA 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.
502render_failedThe PDF could not be rendered. Nothing was sent; retry.
502update_failedSaving the recipient edits or the reviewed values failed. Nothing was sent; retry.
502confirm_failedReleasing 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

StatusCodeMeaning
404not_foundUnknown id, or a document of another workspace.
409already_confirmedThe instance was confirmed; it can no longer be discarded.
409not_stagedThe document is not a staged instance.
502discard_failedThe 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
}
FieldTypeDescription
field_valuesobjectAs on instantiate.
metadataobjectValidated on every call; stored only with review: true, which creates a document to carry it.
filenamestring ≤ 200Name 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.
reviewbooleanStage the file for a human — see Generated files with review.
validate_onlybooleanDry 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.

EventWhen
template_instance.stagedAn instantiate or generate with review: true created the instance.
template_instance.confirmedA confirm released the invitations (or finalized a file).
template_instance.discardedDiscarded 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.