Prepare a fill in your app
Read a template's contract, ask your user for what it needs, dry-run, then send. Required fields, missing_optional and standard field names.
An app that fills templates for its users asks them for the data before it calls the API. Everything it needs is in one published contract per template: which values are mandatory, who fills each one, what to call it in your user's language, and what a value usually looks like. A dry run then says what is still wrong and what could still be filled.
Four steps:
- Read the contract:
GET /v1/templates/{id}. - Ask your user for every
requiredinput, and for the others as your product likes. - Dry run: the same body with
"validate_only": true. Fixproblems, offermissing_optional. - Send the same body without
validate_only.
The template must be locked (instantiable: true). Reading the contract
needs only an API key; instantiate and generate,
dry runs included, also need the Enterprise template entitlement
(402 enterprise-required without it).
1. Read the contract
curl -H "Authorization: Bearer $WSK_KEY" \
https://api.wesign.now/v1/templates/$TEMPLATE_IDThe answer holds the template (version, instantiable), its recipient
slots (recipients[]), the inputs (field_values[]) and the boxes the
signers sign or date (signing_fields[]). The "Power of attorney (company)"
PDF template, version 3, has two slots — two representatives of one company —
and four inputs:
{
"template": { "id": "…", "name": "Power of attorney (company)", "status": "locked", "version": 3, "instantiable": true, … },
"recipients": [{ "slot": 1 }, { "slot": 2 }],
"field_values": [
// Two text boxes the author set to "Filled by: Sender / API", on slot 1.
// Every signer sees them; you send each once.
{
"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.)", … },
"description": "The company's tax number, e.g. the German W-IdNr DE123456789-00001. The format depends on the country."
},
// One "Filled by: Signer" box per representative; you may pre-fill it
{
"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", … // as signing.place
}
],
"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 }
]
}Every property is defined under Templates → Get a template. What your app does with the ones that shape a form:
required— ask for it, or take it from your own data: the call fails without it. This flag is the whole contract; never re-derive it fromowner.owner—apiandsenderare your side.signeris the signer's box: you may pre-fill it, and the signer can change it. See Who fills what.auto_filled— skip it: the platform fillsdate_todayand therecipient_*tokens. Thesender_*letterhead tokens stay blank on a directinstantiateunless you send them; see which fields you must supply.labels,label— the input's name:labels[yourUsersLanguage]when present, elselabel.type,options,max_length— the input control. Treat a type you do not know astext. A longer value is shortened with…, not refused.example,pattern— placeholder text and a shape hint. The API never enforcespattern: warn, do not block.slot— which recipient a positioned box belongs to. The schema has no role names, so label a per-signer input yourself ("Place of signing · signer 2").has_default— the author's default prints (or pre-fills the signer's box) when you omit the key. Sending""replaces it.date_format,time_format,lang— how the value prints, never how you send it: dates go day first or asYYYY-MM-DD, times asHH:MMorh:MM AM/PM.kind: "collection"— a table: send an array of row objects.source— legacy; ignore it. A Sender / API box usually reads"source": "lead", which says nothing about who fills it (why).
A custom key has labels only when the template itself labels it and
records the language it is written in (a rich-text template records it on its
signature lines). A PDF template does not, so use label there. labels
never holds a machine translation, nor your field registry's label (which
label prefers), because the registry does not say what language it is in.
GET /v1/templates/{id} describes the template's current version. If you pin
version on instantiate, send it on the dry run too: the dry run then
checks the positioned fields of that version.
2. Ask your user
Build the form from field_values:
const res = await fetch(`https://api.wesign.now/v1/templates/${id}`, {
headers: { Authorization: `Bearer ${process.env.WSK_KEY}` },
})
const { recipients, field_values } = await res.json()
const inputs = field_values.filter((f) => !f.auto_filled)
const mustAsk = inputs.filter((f) => f.required) // cannot send without these
const mayAsk = inputs.filter((f) => !f.required) // optional, signer boxes included
const name = (f) => f.labels?.[userLang] ?? f.label ?? f.key- Ask for every required input, unless your own data already holds it.
For a standard key it usually does:
company.tax_idis the same datum in every template. - Ask for optional inputs as your product likes. An input owned by the signer can be left out: the signer then sees an empty box, or the author's default, and may leave it empty. Text never blocks signing.
- Ask for one recipient per slot in
recipients: every slot that has fields needs one (400 missing_recipients). Here that is two representatives, slot 1 and slot 2. - Ask for a shared Sender / API value once, even though its box sits on one slot: every signer sees it.
- Send every value as a string. Dates day first (
15.03.1990) or asYYYY-MM-DD, never month first. Booleans as"yes"/"no". The full rules are in the field contract.
A required input inside a conditional section is only demanded while your values show that section. The dry run applies the conditions, so let it decide.
3. Dry run
Send the body you are about to send, with "validate_only": true:
POST https://api.wesign.now/v1/templates/{id}/instantiate
Authorization: Bearer wsk_live_…
Content-Type: application/json
{
"recipients": [
{ "slot": 1, "email": "anna@example.ch", "name": "Anna Muster" },
{ "slot": 2, "email": "ben@example.ch", "name": "Ben Keller" }
],
"field_values": { "company.legal_name": "Muster AG" },
"signing_mode": "sequential",
"metadata": { "case_id": "ZT-2026-0142" },
"validate_only": true
}Nothing is created, and no Idempotency-Key is read. With a value missing,
the answer is the same 422 the real call would give, plus the inputs you
could still add:
HTTP/1.1 422 Unprocessable Entity
{
"error": "1 problem with this request.",
"code": "template_input_invalid",
"template_id": "…", "template_name": "Power of attorney (company)", "template_version": 3,
"problems": [
{ "field": "company.tax_id", "label": "Company tax ID", "code": "field_required",
"message": "\"Company tax ID\" is required by this template but was not supplied." }
],
"warnings": [],
"docs": "https://www.letssign.now/en/settings/api",
"missing_optional": [
{ "key": "signing.place", "label": "Place of signing", "owner": "signer",
"type": "text", "standard": true, "example": "Zürich",
"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" },
"slot": 1 },
{ "key": "signing.place_s2", "label": "Place of signing", "owner": "signer",
"type": "text", "standard": true, "example": "Zürich",
"labels": { … },
"slot": 2 }
]
}problemsblock the call, and every problem comes back at once. Each carries the input'slabeland a readablemessage, so you can show it to your user; branch oncode, never on the message. Fix them and dry-run again.missing_optionalnever blocks. It lists the inputs you may still supply and left empty (absent, or""), in schema order: a signer's box you could pre-fill (here each representative's place of signing, which they would otherwise type) or an input the author made optional or gave a default (has_default: true). Each entry carries what you need to ask for it:key,label,owner,type,standard, and where knownexample,pattern,labels,slot,has_defaultand the print format. Platform tokens, tables and inputs in a hidden conditional section are never listed. Send a value and its entry goes away.warningsnever block. Anunknown_fieldwarning is a key the template does not have: usually a spelling mismatch between your keys and the template's.docslinks to the Settings → API page of the app, for a signed-in member of your workspace — useful to a person, not to your code.
Once every problem is fixed, the dry run answers 200:
{
"ok": true, "template_id": "…", "template_version": 3,
"warnings": [], "problems": [],
"missing_optional": [ /* what you could still send, as above */ ]
}Read ok, not only the status: it is false (still 200) when the real call
would be refused for the workspace's monthly document cap or its SMS
allowance, which problems then explains. Any other refusal (a missing recipient, a slot without a signature
box) comes back exactly as the real call would give it. A dry run counts
against the rate limit like any request. What it does
and does not check is listed under Dry run.
4. Send
Send the same body without validate_only, with an
Idempotency-Key so a retry never sends twice:
POST https://api.wesign.now/v1/templates/{id}/instantiate
Authorization: Bearer wsk_live_…
Idempotency-Key: poa-ZT-2026-0142
Content-Type: application/json
{
"recipients": [
{ "slot": 1, "email": "anna@example.ch", "name": "Anna Muster" },
{ "slot": 2, "email": "ben@example.ch", "name": "Ben Keller" }
],
"field_values": {
"company.legal_name": "Muster AG",
"company.tax_id": "DE123456789-00001",
"signing.place": "Zürich"
},
"signing_mode": "sequential",
"metadata": { "case_id": "ZT-2026-0142" }
}The 200 carries a signing_url per recipient and field_values_echoed, the
values as stored (dates as YYYY-MM-DD). Slot 1 is invited now; slot 2 when
slot 1 has signed. The full answer is under
Several signers. Add "review": true to
stage the document for a colleague instead of sending it; see
Review before sending.
Who fills what
Every input has an owner. On a rich-text template the author's choice of
who fills the template (People fill or API fills) sets it per
placeholder; see
Who fills a value. On a PDF
template the author picks Filled by on each text box:
| Filled by | owner | required | What signers see |
|---|---|---|---|
| Sender / API | sender | true, unless the author made it optional or gave it a default | Your value (or the author's default), printed as fixed text. Every signer sees it from their first view, in any signing order, whichever slot carries the box. No input, and nothing a signer sends changes it. An optional one left empty prints nothing. |
| Signer (every box until an author chooses) | signer | false | The signer of the box's slot finds a box with your value (or the author's default) already in it, and may change it or leave it empty. |
signer_required is always false on a text box: an empty one never blocks
signing. Filled by can only change while the template is unlocked; locking it
again publishes a new version, and a call that pins version keeps the
contract of the version it names.
Standard names
Templates may name their inputs with the platform's standard names: English dotted keys anchored to international vocabularies (schema.org, ISO 3166-1, ITU-T E.164, ETSI EN 319 142 / PAdES, the Swiss UID and AHV registers):
person.first_name, person.last_name, person.full_name,
person.date_of_birth, person.tax_id, person.ch_ahv_number,
company.legal_name, company.uid, company.tax_id, company.vat_id,
address.street, address.postal_code, address.city,
address.country, address.full, contact.email, contact.phone,
signing.place, signing.date.
- Map your data to them once. Every template that uses them then fills without a per-template mapping. The meaning, anchor, type and example of each are in Fields & placeholders.
- A second signer's copy carries the per-signer suffix (
signing.place_s2) and is standard too. A template uses a copy only for a value that differs per signer; a value every signer shares has one key. signing.dateis stamped by the platform when a signer signs; you never send it.- Custom keys are just as valid.
standard: falsemeans only that the key is the template's own (mandate_number). It is never refused, and it is filled exactly like a standard one.
Generate
POST /v1/templates/{id}/generate takes "validate_only": true too and
answers with missing_optional in the same way. It never requires a
positioned text box of a PDF template, not even a Sender / API one, and never
lists one, because generate does not print them. A value you send for one
is still type-checked. Its dry-run 200 has no problems: ok is always
true there, and every refusal is a 4xx.
For template authors
The contract your integrators read is what you set in the editor:
- Name each field. The key box suggests the standard names as you type, with a Standard badge, and fills in the label in your language. Any other valid key works too.
- Choose Filled by on each text box of a PDF template, in the field inspector while you edit the template (on a phone: Field settings) or in the template's field list while it is a draft. Sender / API makes the value required for API callers unless you switch on Optional for the sender or give it a default.
- Place a shared fact once. A Sender / API box shows to every signer, whichever slot carries it; add a per-signer copy only for a value that differs per signer.
- Lock the template. Its contract is then fixed for that version, and
GET /v1/templates/{id}serves it.
When you send from the app instead of the API, Quick send asks you for the Sender / API values. A Present (form) link has no sending step: a Sender / API box prints its default there, and one without a default becomes a box the link's visitor fills in.
