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:

  1. Read the contract: GET /v1/templates/{id}.
  2. Ask your user for every required input, and for the others as your product likes.
  3. Dry run: the same body with "validate_only": true. Fix problems, offer missing_optional.
  4. 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_ID

The 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 from owner.
  • owner — api and sender are your side. signer is 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 fills date_today and the recipient_* tokens. The sender_* letterhead tokens stay blank on a direct instantiate unless you send them; see which fields you must supply.
  • labels, label — the input's name: labels[yourUsersLanguage] when present, else label.
  • type, options, max_length — the input control. Treat a type you do not know as text. A longer value is shortened with …, not refused.
  • example, pattern — placeholder text and a shape hint. The API never enforces pattern: 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 as YYYY-MM-DD, times as HH:MM or h: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_id is 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 as YYYY-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 }
  ]
}
  • problems block the call, and every problem comes back at once. Each carries the input's label and a readable message, so you can show it to your user; branch on code, never on the message. Fix them and dry-run again.
  • missing_optional never 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 known example, pattern, labels, slot, has_default and the print format. Platform tokens, tables and inputs in a hidden conditional section are never listed. Send a value and its entry goes away.
  • warnings never block. An unknown_field warning is a key the template does not have: usually a spelling mismatch between your keys and the template's.
  • docs links 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 byownerrequiredWhat signers see
Sender / APIsendertrue, unless the author made it optional or gave it a defaultYour 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)signerfalseThe 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.date is stamped by the platform when a signer signs; you never send it.
  • Custom keys are just as valid. standard: false means 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.