> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vumasign.com/llms.txt
> Use this file to discover all available pages before exploring further.

# One of a template’s documents, filled in with your values, as it would be sealed.

> SEE A BINDING BEFORE YOU SEND IT. Takes the `values` and `subjects` you would send to `POST /api/v1/envelopes` for this template and answers with one of its documents as a PDF, every value drawn exactly as the sealed copy would draw it — a value split across printed boxes one character per cell, a date in the form the field prints, the option of a question ticked, long text wrapped in its box — and PREVIEW across every page. Nothing is stored, sent, recorded or billed, and there is no envelope and nobody to sign.

⚠️ A PREVIEW THAT RENDERS IS ONE THE SEND WOULD ACCEPT. The values are judged by the same check as `POST /api/v1/templates/{templateId}/validate-values` and create, and the first one create would refuse is refused here with create’s code and create’s message — ask validate-values for every entry’s verdict at once. A `template_etag` that is no longer current is refused `template_changed`, as create refuses it. The send’s own checks on the answers it would fix are asked too, of every document of the template, and refused with the send’s code and sentence: a locked, required box left empty (`locked_answer_missing`), or a locked answer its own box refuses (`locked_answer_unusable`) — the template’s own fixed answers included. It also refuses, with `cannot_be_drawn`, a value too long for its box even at the smallest size the sealed copy prints: create accepts that value, and the envelope would then fail to seal after every signature.

⚠️ THERE ARE NO RECIPIENTS, SO EVERY ROLE IS DRAWN AS IF IT IS ON THE ENVELOPE, held by one person — an optional role included, so you see the whole form. For the send’s checks your `values` stand in for the recipients: an optional role is on the envelope when one of them fills one of its boxes, and left out otherwise, as a send leaves out an optional role nobody is named for; a left-out role’s boxes are drawn but not checked, and leaving out a role whose box decides another role’s box is refused as the send refuses it. A title or company box filled from a recipient’s details is neither drawn nor checked here.

Boxes nobody gave a value stay blank, except an answer the template fixes itself. Boxes filled by signing — a signature, initials, the date of signing, the signer’s name and email — show whose they are, as `Signature: Employee`.

`document` is the 1-based position of the document in `documents` on `GET /api/v1/templates/{templateId}`, and is required when the template holds more than one.

⚠️ IT IS A RENDER, AND IT SHARES A CEILING. Each preview that renders counts against your organisation’s per-minute ceiling on renders, which draft renders (`GET /api/v1/envelopes/{envelopeId}/documents/{position}/draft`) count against too; past it the answer is `rate_limited` with `Retry-After`. ⚠️ EVERY REQUEST PAST THE CREDENTIAL AND BODY CHECKS IS COUNTED, before the template is read — one refused for a value or a stale etag, and a `rate_limited` one, included. To check values without spending the ceiling, use validate-values, which renders nothing.

⚠️ IT NEEDS `documents:read` AS WELL AS `templates:read` (`x-additional-required-scopes`): what it answers with is the template’s document itself, which `templates:read` alone never grants. A key missing either is refused `insufficient_scope`.

A test key and a live key get the same answer: a template has no environment. It is a POST only because the body can be large, and takes no `Idempotency-Key`.



## OpenAPI

````yaml /openapi.json post /api/v1/templates/{templateId}/preview
openapi: 3.1.0
info:
  title: Vumasign API
  version: 0.2.0
  description: >-
    The Vumasign API. South African e-signature: templates, the addresses an

    integration writes values to, an idempotent send, and webhooks.


    **This document describes what exists today and nothing else.** It is not a

    roadmap, and there is deliberately no entry here for anything that has not

    shipped — a spec you can generate a client from and then discover was

    aspirational is worse than no spec at all.


    ## Authentication

        Authorization: Bearer vsk_live_…

    One header, one step. Keys are minted in Settings. `vsk_live_` and
    `vsk_test_`

    are different keys; the prefix is stored in clear so a leaked key is
    greppable

    and traceable to an organisation, while the secret is stored only as a hash.


    ## Errors


    Every refusal the API returns deliberately, at every status, is

    `{ "error": { "code", "message" } }`, and such a response is a failure **if

    and only if** it carries an `error` key — that test is correct even for a

    client that ignores the status line. An unexpected server failure may not
    have

    this shape: treat a 5xx without an `error` key as an `internal_error`, which

    is retryable. `code` is the

    contract and is stable forever; `message` is prose for a developer reading a

    log and may be reworded in any release.


    The root of this document carries **`x-error-codes`**: the closed enum, with

    `status`, `retryable`, `backoff`, `emitted` and `guidance` for each

    code. Branch on that rather than on our prose.


    ## Rate limits


    **600 requests a minute on any paid plan,

    60 on the free plan — and a test key

    gets the same number as a live one.** Per key, not per organisation, so a
    batch

    job and an interactive integration can be given a key each and neither can

    starve the other.


    The ceiling follows the PLAN rather than the kind of key, because the plan
    is

    what says whether this is production traffic or somebody evaluating us. On
    the

    API plan it is sized so that no honest workload meets it: a thousand
    envelopes

    with a status read each is two thousand requests, which is three and a half

    minutes rather than seventeen. The sandbox deliberately gets the *same*
    number

    rather than a larger one — an integration that passes in rehearsal is then
    an

    integration that passes live, and your `429` handling is exercised where

    meeting a `429` is free.


    Every successful authenticated response carries `RateLimit-Limit`,

    `RateLimit-Remaining` and `RateLimit-Reset` (seconds until the window

    closes, at most 60). Read them on your successes and you will never need to
    be

    refused. A 429 carries them too, and additionally carries `Retry-After`,
    which

    you should honour. A 401 carries none of them; a 403 may not, depending on

    whether it was decided before the request was counted. Each operation's

    responses say which others do.


    ⚠️ **A refused request still counts.** Retrying inside the window pushes the

    counter higher rather than holding it at the ceiling — which is why the
    backoff

    in `x-error-codes` is exponential and why `Retry-After` is worth reading

    rather than guessing at.


    ⚠️ The limits are **per minute**, and the window is fixed rather than
    sliding.

    Spending a whole window at the end of one minute and a whole window at the
    start

    of the next is possible; it is bounded at two windows' worth, which is a
    burst

    rather than an incident.


    ## Webhooks


    Register an endpoint with `POST /api/v1/webhooks`; the response carries the

    signing secret, **once**. The root of this document carries
    **`x-webhooks`**:

    the body we POST, the signature recipe (including the two mistakes that make
    a

    verifier look correct and be worthless), the published retry ladder, and the

    auto-disable rule. Read that before writing a receiver.


    ## What is not here yet


    No organisation-level email exists, so an auto-disabled webhook endpoint is

    discovered by polling rather than by being told. There is no way to change
    an

    envelope after it is created and before it is sent — `PATCH
    /v1/envelopes/{id}`

    is designed and not built — so a draft is created complete or not at all.
    Both

    are stated because a gap named is a gap somebody can plan around.


    ⚠️ **The rate limit above counts authenticated requests only.** A request

    carrying no key, or a key we cannot recognise, is refused before any counter
    is

    touched — a per-key ceiling cannot defend against a caller who has not
    presented

    a key, and pretending otherwise would be a promise we could not keep. That
    half

    belongs to the edge rather than to this API.


    (This section has twice described as missing something that now exists: the

    `uri` on every webhook body, which was `null` until

    `GET /api/v1/envelopes/{envelopeId}` gave it somewhere to point, and the
    rate

    limiter, which nothing enforced until the per-key ceiling above. Both were
    named

    here as gaps first, which is the point of naming them.)
servers:
  - url: https://app.vumasign.com
security:
  - ApiKey: []
tags:
  - name: Templates
    description: >-
      A template is a document with its signing roles and field placements
      authored once. Every envelope is made from one, or from a one-off pack.
  - name: Documents
    description: >-
      The pages of a template’s documents, served as PDF so that a client which
      has to show the paper can do so without rendering it itself.
  - name: Envelopes
    description: >-
      An envelope is one sending of a document to named people. Creating one is
      not sending it: a draft emails nobody until it is sent.
  - name: Webhooks
    description: >-
      Endpoints we POST signed events to when an envelope changes, with the
      health of each endpoint and a secret you can rotate without a gap.
  - name: Standing authorisations
    description: >-
      A grantor’s signed, brand-wide authority for your organisation to apply
      their signature to documents you send under that brand. Requested here; it
      takes effect only when the grantor signs the mandate and it is sealed. See
      /concepts/standing-authorisations.
  - name: Specification
    description: >-
      This document, served without a credential so that anybody can read what
      the API does before they hold a key.
paths:
  /api/v1/templates/{templateId}/preview:
    post:
      tags:
        - Templates
      summary: >-
        One of a template’s documents, filled in with your values, as it would
        be sealed.
      description: >-
        SEE A BINDING BEFORE YOU SEND IT. Takes the `values` and `subjects` you
        would send to `POST /api/v1/envelopes` for this template and answers
        with one of its documents as a PDF, every value drawn exactly as the
        sealed copy would draw it — a value split across printed boxes one
        character per cell, a date in the form the field prints, the option of a
        question ticked, long text wrapped in its box — and PREVIEW across every
        page. Nothing is stored, sent, recorded or billed, and there is no
        envelope and nobody to sign.


        ⚠️ A PREVIEW THAT RENDERS IS ONE THE SEND WOULD ACCEPT. The values are
        judged by the same check as `POST
        /api/v1/templates/{templateId}/validate-values` and create, and the
        first one create would refuse is refused here with create’s code and
        create’s message — ask validate-values for every entry’s verdict at
        once. A `template_etag` that is no longer current is refused
        `template_changed`, as create refuses it. The send’s own checks on the
        answers it would fix are asked too, of every document of the template,
        and refused with the send’s code and sentence: a locked, required box
        left empty (`locked_answer_missing`), or a locked answer its own box
        refuses (`locked_answer_unusable`) — the template’s own fixed answers
        included. It also refuses, with `cannot_be_drawn`, a value too long for
        its box even at the smallest size the sealed copy prints: create accepts
        that value, and the envelope would then fail to seal after every
        signature.


        ⚠️ THERE ARE NO RECIPIENTS, SO EVERY ROLE IS DRAWN AS IF IT IS ON THE
        ENVELOPE, held by one person — an optional role included, so you see the
        whole form. For the send’s checks your `values` stand in for the
        recipients: an optional role is on the envelope when one of them fills
        one of its boxes, and left out otherwise, as a send leaves out an
        optional role nobody is named for; a left-out role’s boxes are drawn but
        not checked, and leaving out a role whose box decides another role’s box
        is refused as the send refuses it. A title or company box filled from a
        recipient’s details is neither drawn nor checked here.


        Boxes nobody gave a value stay blank, except an answer the template
        fixes itself. Boxes filled by signing — a signature, initials, the date
        of signing, the signer’s name and email — show whose they are, as
        `Signature: Employee`.


        `document` is the 1-based position of the document in `documents` on
        `GET /api/v1/templates/{templateId}`, and is required when the template
        holds more than one.


        ⚠️ IT IS A RENDER, AND IT SHARES A CEILING. Each preview that renders
        counts against your organisation’s per-minute ceiling on renders, which
        draft renders (`GET
        /api/v1/envelopes/{envelopeId}/documents/{position}/draft`) count
        against too; past it the answer is `rate_limited` with `Retry-After`. ⚠️
        EVERY REQUEST PAST THE CREDENTIAL AND BODY CHECKS IS COUNTED, before the
        template is read — one refused for a value or a stale etag, and a
        `rate_limited` one, included. To check values without spending the
        ceiling, use validate-values, which renders nothing.


        ⚠️ IT NEEDS `documents:read` AS WELL AS `templates:read`
        (`x-additional-required-scopes`): what it answers with is the template’s
        document itself, which `templates:read` alone never grants. A key
        missing either is refused `insufficient_scope`.


        A test key and a live key get the same answer: a template has no
        environment. It is a POST only because the body can be large, and takes
        no `Idempotency-Key`.
      operationId: previewTemplate
      parameters:
        - name: templateId
          in: path
          required: true
          description: The template’s `id`, as returned by the list endpoint.
          schema:
            type: string
            format: uuid
            examples:
              - 01960000-0000-4000-8000-0000000007e1
            description: A template id.
      requestBody:
        required: true
        description: >-
          The `values` and `subjects` you would send to create, and optionally
          `template_etag` and `document`.
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/TemplatePreviewRequest'
              description: The values to draw.
            examples:
              example:
                summary: A made-up employee, against the template as it was read
                value:
                  values:
                    - subject: employee
                      key: staff.full_name
                      value: Thandi Mokoena
                  template_etag: >-
                    "53d359af89429682ddcbf8915f330bd61d2e40fb48f6bc94a76010bea48bba1f"
                  document: 1
      responses:
        '200':
          description: >-
            The document, filled in and marked PREVIEW on every page.
            `Content-Disposition: attachment; filename="<template name>
            (preview).pdf"` — with ` (document 2 of 3)` after the name for a
            template of several — and `Cache-Control: no-store`.
          content:
            application/pdf:
              schema:
                type: string
                format: binary
                description: >-
                  The PDF bytes. ⚠️ NOT JSON, like the draft render: every
                  non-2xx is still the same `Error` envelope in
                  `application/json`.
          headers:
            RateLimit-Limit:
              description: >-
                How many requests this key may make per minute: 600 on any paid
                plan and 60 on the free plan, the same in both environments. Per
                key, not per organisation — a second key has its own allowance.
                ⚠️ READ IT RATHER THAN ASSUMING IT: it is the number the
                database applied to this request, so it is right even when this
                description is out of date.
              schema:
                type: integer
                description: Requests permitted in the current minute.
            RateLimit-Remaining:
              description: >-
                How many of them are left, this one already counted. Zero on the
                request that is refused, and zero on every further request of
                that minute — ⚠️ a refused request still counts.
              schema:
                type: integer
                description: Requests remaining in the current minute.
            RateLimit-Reset:
              description: >-
                Seconds until the current minute closes and the allowance
                returns. At most 60, which is what makes the published
                exponential backoff converge in about six doublings rather than
                eleven.
              schema:
                type: integer
                description: Seconds until the window resets.
        '400':
          description: >-
            `invalid_request` — The request is malformed, or breaks a rule about
            its own shape: a body that is not JSON or does not match the
            operation’s schema (a missing or mistyped property, a value out of
            range), a document that is not a PDF or a Word file we can read, a
            text tag that cannot be placed, an `Idempotency-Key` that is present
            but not a valid key, a query parameter outside its range, or a
            cursor that names nothing. The `message` says which, and for a body
            names the property path (`recipients[0].email`) — there is no
            separate `param` field. Fix the request; retrying it unchanged will
            fail identically.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
                description: The refusal.
        '401':
          description: >-
            `unauthenticated` — The credential was absent, unreadable, unknown,
            wrong or revoked — this answer is deliberately identical for all of
            them, so it cannot be used to probe which keys exist. Retrying the
            same request changes nothing. Check the key in Settings or issue a
            new one.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
                description: The refusal.
        '403':
          description: >-
            `forbidden` — The key is valid and the organisation’s plan does not
            grant what this operation does. Nothing about the credential needs
            to change, and it is deliberately MORE specific than 401 because the
            caller has already proved they hold the key. TWO SITUATIONS PRODUCE
            IT. (1) THE PLAN EXCLUDES THE OPERATION: `POST /api/v1/templates` on
            the Free plan, which does not include authoring templates. A
            template that already exists stays readable at `GET
            /api/v1/templates`; authoring one needs a plan that includes
            templates. (2) WE CANNOT ESTABLISH WHAT THE PLAN INCLUDES, on any
            operation, so nothing is granted. That one is ours rather than
            yours, an ordinary account does not meet it, and it means ask us.


            `insufficient_scope` — This key is scoped and does not hold the
            scope this operation requires — `x-required-scope` on the operation
            names it, and the `WWW-Authenticate` header on the refusal repeats
            it (RFC 6750 §3.1). ⚠️ Retrying cannot help and neither can editing
            the key: scopes are fixed when a key is minted and no endpoint or
            screen can widen them. Issue a new key with the access it needs. A
            key created before scopes existed carries an empty scope list, which
            means EVERY capability, so this refusal cannot reach an integration
            that was already working.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
                description: The refusal.
        '404':
          description: >-
            `not_found` — No such resource — or none this key’s organisation can
            see, or one that has been archived, or an id that is not a uuid.
            Those four are one answer on purpose: "it exists but is not yours"
            is itself a disclosure. Do not retry; check the id against the list
            endpoint.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
                description: The refusal.
        '412':
          description: >-
            `template_changed` — The template is not the version you checked:
            the `If-Match` on a rename, or the `template_etag` on an envelope
            create, names an etag that is no longer current. Nothing was written
            or created. Read the template again (`GET
            /api/v1/templates/{templateId}`; a rename’s refusal also carries the
            current `ETag`), look at what changed — a rename can swap which box
            an address fills — and send again with the new etag.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
                description: The refusal.
        '422':
          description: >-
            `value_subject_unknown` — A value names a subject the template does
            not declare. The subjects are in the template response; `null` is
            the subject for a field belonging to nobody in particular.


            `value_address_unknown` — The `(subject, key)` pair addresses no
            field of this template. ⚠️ This is the refusal that most often means
            the template has UNADDRESSED boxes rather than that you mistyped:
            `data_key` is null on any field the sender never gave an address to.
            Check the template response before blaming the value.


            `value_option_unknown` — A value for a question names an option it
            does not have. The message lists the question’s options as `{value,
            label}`; send one of those values (`options[].value` on the question
            in `GET /api/v1/templates/{templateId}`). On a rename, an
            `option_value` outside the options a core `choice` key fixes.
            Nothing was created or renamed.


            `value_option_locked` — A value for a question disagrees with an
            option the template locks (`questions[].options[].locked` in `GET
            /api/v1/templates/{templateId}`). Either it names an option locked
            `cleared`, which the sent document leaves unticked whatever a value
            says; or it leaves out an option locked `ticked`, which the document
            ticks anyway, and with it the question would tick more than
            `max_selected`. A locked-ticked option left out is otherwise added
            for you. The message names the option and says what to send: every
            option locked `ticked`, none locked `cleared`, and open options
            within the bounds. Not raised for an entry that sets `locked`, which
            replaces the template’s locks on every box. Nothing was created.


            `value_selection_out_of_bounds` — A value for a question ticks fewer
            options than its `min_selected` or more than its `max_selected`,
            counting every option the template locks `ticked` whether the value
            names it or not. The message states the bounds and the count. Send
            an array of option values within them — `[]` for none, where
            `min_selected` is 0.


            `value_not_writable` — The value cannot be written at this address.
            A field there is one nobody may fill in — a `date_signed`, a
            `signer_name`, a signature: those are written by the server or by
            the signer. Or a dropdown there does not offer the value. Or a lone
            tickbox there is given something other than `"true"`, `"false"` or
            `""`. Or the value is an array, which only a question takes. Or, on
            a rename, a key is given to a field nobody may prefill.


            `value_not_lockable` — The address names a `signer_title` or a
            `signer_company`, and `locked` cannot ride on a value for one. ⚠️
            THE VALUE ITSELF IS FINE — send the same triple without `locked` and
            it is accepted. What is refused is FIXING the answer on the field: a
            title is a fact about whoever fills the slot, so it is seeded per
            recipient from the `job_title` and `company` you give on the
            recipient, while a locked answer is one value on one box — assert it
            and every party holding that role signs under the same title. Set
            `job_title` / `company` on each recipient instead. (A sender may
            still tick “Signer cannot change” on such a field in the editor:
            there the lock says *the roster’s value is final* and carries no
            answer of its own.)


            `cannot_be_drawn` — A value, a name or a title contains something
            the sealer cannot draw into the PDF, or a value has more characters
            than the printed boxes of a field split across several sets of them
            (`Field.continuations`). Asked BEFORE the send rather than
            discovered after everybody has signed. The message names which
            string, or for a split field which boxes, and never quotes the
            value.


            `locked_answer_missing` — A field on this envelope is locked,
            required and carries no answer — three statements that cannot all be
            honoured. A locked answer is the sender’s, so the signer is refused
            if they supply one and the completeness check does not ask them for
            it; once sent, nobody could fill the box and it would seal blank. ⚠️
            THE LOCK MAY NOT HAVE COME FROM YOUR REQUEST — it can be authored on
            the template — so the message names the FIELD rather than a
            `values[i]` path. Send a value for that address, send `{"value": "",
            "locked": false}` to hand it back to the signer, or have the
            template make it optional. ⚠️ THE SAME CODE ALSO MEANS A QUESTION
            NOBODY LEFT COULD ANSWER. Grouped tickboxes — a Race or Gender
            question — are never individually `required`; the question’s own
            `min_selected` is what makes it compulsory. Lock every option of
            such a question with no tick in any of them and it asks for more
            answers than remain within anybody’s reach, which is the same
            unsatisfiable sentence and refuses the same way. There the message
            names the QUESTION, and the remedies are to tick one of the locked
            options, unlock one, or lower the question’s minimum.


            `locked_answer_unusable` — A field on this envelope carries a locked
            answer the field itself will not accept: a value its validation rule
            rejects, one its input format cannot carry, one longer than the
            printed cells of a comb or holding a character one of those cells
            does not accept, a `date` that is not a calendar day, or a dropdown
            answer that is not among the options it offers. ⚠️ A `date` DRAWN AS
            PRINTED CELLS IS MEASURED IN ITS OWN SPELLING: it holds the digits
            its boxes print — `15092026`, not `2026-09-15` — and those digits
            must still name a day that exists, read in the order the field’s
            label states (`Date of birth (DD/MM/YYYY)`). ⚠️ THIS IS NOT
            `locked_answer_missing` — that one is a box with NO answer. This is
            a box with an answer nobody can use: a locked answer is the
            sender’s, the signer may not change it, and their very first save
            would be refused over it on an envelope nobody can edit any more.
            Refused here so that it is still a draft. ⚠️ THE ANSWER MAY NOT HAVE
            COME FROM YOUR REQUEST — a lock and its value can be authored on the
            template, and a rule can be tightened after the answer was fixed —
            so the message names the FIELD rather than a `values[i]` path. Send
            a value that satisfies the field, send `{"locked": false}` to hand
            the box back to the signer, or change the rule on the template. ⚠️
            THE SAME CODE ALSO MEANS A QUESTION THE SENDER ANSWERED MORE TIMES
            THAN IT PERMITS, AND THEN NO FIELD RULE IS BROKEN AT ALL. Grouped
            tickboxes — a Marital status question at `max_selected: 1` — can
            each carry a perfectly good locked tick while together they assert
            more than the question allows; every one of them would seal, side by
            side, on a question that says fewer can be true, and the signer may
            clear none of them. There the message names the QUESTION rather than
            a field, and sending a field-valid value resolves nothing because
            every value already is one. The remedies are to untick one of the
            locked options, to unlock one so the signer chooses, or to raise the
            question’s `max_selected`. ⚠️ AND THAT LAST ONE IS NOT SOMETHING YOU
            CAN SEND: `max_selected` is published by `GET
            /api/v1/templates/{templateId}` and NO ENDPOINT HERE SETS IT — a
            question’s bounds are authored on the template, and its `id` is a
            join key within that response rather than an address this API
            accepts. ⚠️ A QUESTION IS ANSWERED AT ITS OWN ADDRESS: its boxes
            have none. Send the option value you mean to the question’s
            `data_key` — `{"key": "person.marital_status", "value": "married",
            "locked": true}` — or an array of option values for a question that
            takes several, and only those boxes are ticked and locked. If this
            refusal names a question you sent several options to, send fewer.


            `recipient_role_missing` — A required role that has fields on it was
            given nobody to fill them. Every required role the template declares
            with work to do needs a recipient; an optional role
            (`Role.presence`) may be left out, and is, with its fields. From a
            send, it also means somebody who signs or approves on a draft
            prepared in the dashboard has no name or email yet (their `name` or
            `email` reads `null`): add them in the dashboard first.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
                description: The refusal.
        '429':
          description: >-
            `rate_limited` — This key has spent its minute. The limit is per KEY
            and per minute, and it follows your PLAN rather than the kind of key
            — 600 requests a minute on every paid plan, 60 on the free plan, and
            the sandbox gets the same number as production so that an
            integration which passes in rehearsal passes live. Every successful
            authenticated response, and this one, carries `RateLimit-Limit`,
            `RateLimit-Remaining` and `RateLimit-Reset` (seconds), so a client
            can slow down before it is made to. THREE OPERATIONS ALSO ANSWER IT
            FOR A REASON OF THEIR OWN. `POST /api/v1/envelopes/batches` costs
            one request per row and is refused WHOLE when its rows do not fit
            what is left of the key’s minute — `RateLimit-Remaining` says how
            many would. `GET
            /api/v1/envelopes/{envelopeId}/documents/{position}/draft` has its
            own per-envelope and per-organisation ceilings on renders each
            minute, and the `message` says which one refused; `POST
            /api/v1/templates/{templateId}/preview` shares the per-organisation
            one. There `Retry-After` is that ceiling’s, while the `RateLimit-*`
            headers still describe the key. `Retry-After` on this refusal is at
            most 60 and is read from the stored window rather than computed, so
            two of our instances refusing the same key quote the same instant.
            ⚠️ A REFUSED REQUEST STILL COUNTS: hammering a limit you have
            already exceeded pushes the counter higher rather than holding it.
            Honour `Retry-After`. If one key genuinely needs more throughput,
            issue a second one in Settings — the ceiling is a fairness and
            blast-radius control, not a commercial meter.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
                description: The refusal.
          headers:
            RateLimit-Limit:
              description: >-
                How many requests this key may make per minute: 600 on any paid
                plan and 60 on the free plan, the same in both environments. Per
                key, not per organisation — a second key has its own allowance.
                ⚠️ READ IT RATHER THAN ASSUMING IT: it is the number the
                database applied to this request, so it is right even when this
                description is out of date.
              schema:
                type: integer
                description: Requests permitted in the current minute.
            RateLimit-Remaining:
              description: >-
                How many of them are left, this one already counted. Zero on the
                request that is refused, and zero on every further request of
                that minute — ⚠️ a refused request still counts.
              schema:
                type: integer
                description: Requests remaining in the current minute.
            RateLimit-Reset:
              description: >-
                Seconds until the current minute closes and the allowance
                returns. At most 60, which is what makes the published
                exponential backoff converge in about six doublings rather than
                eleven.
              schema:
                type: integer
                description: Seconds until the window resets.
            Retry-After:
              description: >-
                Seconds to wait before retrying, from the stored end of the
                window. Always present on a 429, and never more than 60. On the
                per-key limit it equals `RateLimit-Reset`; a narrower limit,
                such as the draft render allowance, has a window of its own and
                may ask for a different wait. `x-error-codes` tells you to
                honour it; this is it.
              schema:
                type: integer
                description: Seconds to wait before retrying.
        '500':
          description: >-
            `internal_error` — Ours, not yours. ⚠️ RETRY WITH THE SAME
            `Idempotency-Key` YOU SENT THE FIRST TIME, on any operation that
            takes one — that is what makes a retry safe here, and sending a NEW
            key would create a second envelope or send the same one twice. The
            safe GETs in this document can be retried freely. (This entry used
            to say every operation here was a GET; the write endpoints have
            since shipped, and the reasoning moved with them.)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
                description: The refusal.
        '503':
          description: >-
            `service_unavailable` — Ours, and briefly. Something this operation
            depends on was not answering, or was already doing as much as it
            will at once. What it was depends on the operation: on a create, the
            service that renders a Word document to a PDF, or the isolated
            service every uploaded document is read in, when it is starting up,
            at capacity, or did not answer in time; on `GET
            .../documents/{position}/draft` and `POST
            /api/v1/templates/{templateId}/preview`, the service that renders
            the draft or the preview itself; on the two template-document reads
            (`GET /api/v1/documents/{documentId}/pages/{pageNumber}` and
            `.../content`), that same isolated service, which says so with
            `Retry-After`. ⚠️ THE DISTINCTION FROM 400 IS THE WHOLE POINT: your
            request is fine and your document is fine, so do not go and re-save
            it. On a create, nothing was created and no allowance was spent. On
            an operation that takes an `Idempotency-Key`, retry the identical
            request with the SAME one — a completed key replays, so a retry can
            never make a second envelope or a second template even if the first
            attempt got further than this answer suggests. A new key could. ⚠️
            ON THE DRAFT AND THE PREVIEW THE ATTEMPT WAS ALREADY PAID FOR: the
            render is charged to the per-minute draft ceilings (the envelope’s
            and the organisation’s, or the organisation’s alone) before the
            renderer is asked, and a render that then fails is not refunded.
            There is no key to reuse, because there is nothing to replay — so
            retry on the backoff above rather than at once, or an outage of ours
            becomes a `rate_limited` of yours.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
                description: The refusal.
          headers:
            RateLimit-Limit:
              description: >-
                How many requests this key may make per minute: 600 on any paid
                plan and 60 on the free plan, the same in both environments. Per
                key, not per organisation — a second key has its own allowance.
                ⚠️ READ IT RATHER THAN ASSUMING IT: it is the number the
                database applied to this request, so it is right even when this
                description is out of date.
              schema:
                type: integer
                description: Requests permitted in the current minute.
            RateLimit-Remaining:
              description: >-
                How many of them are left, this one already counted. Zero on the
                request that is refused, and zero on every further request of
                that minute — ⚠️ a refused request still counts.
              schema:
                type: integer
                description: Requests remaining in the current minute.
            RateLimit-Reset:
              description: >-
                Seconds until the current minute closes and the allowance
                returns. At most 60, which is what makes the published
                exponential backoff converge in about six doublings rather than
                eleven.
              schema:
                type: integer
                description: Seconds until the window resets.
      security:
        - ApiKey: []
components:
  schemas:
    TemplatePreviewRequest:
      type: object
      description: >-
        The `values` and `subjects` you would send to `POST /api/v1/envelopes`,
        exactly as you would send them, and, optionally, the `template_etag` you
        checked them against and which `document` to draw.
      properties:
        values:
          type: array
          description: >-
            Optional. Values to prefill, as triples. Omitted means none. ⚠️ A
            signature, a `date_signed`, a `signer_name` and a `signer_email` may
            not be prefilled — they are acts, or they are written by this
            system.
          items:
            $ref: '#/components/schemas/ValueInput'
            description: One value at one address.
        subjects:
          type: object
          additionalProperties:
            description: What this envelope says about one subject.
            oneOf:
              - type: boolean
                description: '`true` if this subject is on the envelope, `false` if not.'
              - type: string
                enum:
                  - ask
                description: >-
                  Ask the signer, at the start of signing, whether this person
                  is on the form.
          examples:
            - co_tenant: true
              surety: false
            - co_tenant: ask
          description: >-
            Optional. WHO THIS ENVELOPE IS ABOUT, as a map from a subject `key`
            the template declares to `true` (on this envelope), `false` (not),
            or `"ask"`: the signer who holds that person’s section is asked
            first, in a required question, and their answer decides. While it is
            open the person reads `present: false` and `awaiting_signer: true`
            on the envelope. Two cases are settled at send instead, with nobody
            asked: a person no recipient holds a box for is left out, and a
            person a recipient signs for in a role standing for the same person
            is included. A field addressed to a subject that is not on the
            envelope is hidden from the signer and not required.


            A subject you do not name takes the template’s own rule: a
            `required` subject is on every envelope, and a `conditional` one is
            absent and the signer is never asked about them, whatever the
            template’s Ask, Include or Leave out says (that choice applies to
            envelopes sent from the dashboard). Omitting `subjects` altogether
            opens the same sections as naming nobody — but see the next
            paragraph, because the two are not the same request. A key the
            template does not declare is refused, and so are `false` and `"ask"`
            for a `required` subject — all `invalid_request`, and nothing is
            created.


            ⚠️ IT SETS WHAT THE DOCUMENT OPENS WITH, NOT WHAT IT MUST STAY. The
            signer can switch a `conditional` subject on or off on the signing
            page, in either direction, and every such change is recorded in the
            envelope’s audit history.


            ⚠️ NAME AS `true` OR `"ask"` EVERY `conditional` SUBJECT YOU
            PREFILL. A value for a subject you ask about is shown if the signer
            answers Yes, and cleared if they answer No. A value in `values` for
            a `conditional` subject you did not name as present or asked is
            handled in one of two ways, depending on whether `subjects` is in
            the request at all:


            - **`subjects` sent** (even `{}`): the subject is absent — by
            `false`, or by the template’s rule when unnamed — so the value
            contradicts the request and is refused with `invalid_request`.
            Nothing is created.

            - **`subjects` omitted**: the value is accepted and stored, but the
            subject is absent by the template’s rule when the envelope is sent,
            so the prefilled section opens HIDDEN until the signer switches it
            on.


            See [Subjects](https://docs.vumasign.com/concepts/subjects).
        template_etag:
          type: string
          minLength: 1
          maxLength: 200
          description: >-
            Optional. The `ETag` of the template as you checked it — from `GET
            /api/v1/templates/{templateId}` or the `template_etag` of
            `validate-values` — with or without its quotes. When it is no longer
            current the preview is refused with `template_changed`, exactly as
            create would refuse the send.
        document:
          type: integer
          minimum: 1
          description: >-
            Which of the template’s documents to draw: its 1-based position in
            `documents` on `GET /api/v1/templates/{templateId}`. Required when
            the template holds more than one; otherwise the only one is drawn.
    Error:
      type: object
      description: >-
        The body of every refusal the API returns deliberately, at every status,
        from every endpoint. Such a response is a failure if and only if it
        carries an `error` key — that test is correct even for a client that
        ignores the status line, which is a thing real clients do. An unexpected
        server failure may not have this shape: treat a 5xx without an `error`
        key as an `internal_error`, which is retryable.
      properties:
        error:
          type: object
          description: The refusal. Anything added in future is added INSIDE this object.
          properties:
            code:
              type: string
              description: >-
                A stable snake_case identifier that will never change meaning or
                spelling. This is the contract; branch on it. See
                `x-error-codes` at the root of this document for retry semantics
                per code.
              enum:
                - unauthenticated
                - forbidden
                - insufficient_scope
                - not_found
                - invalid_request
                - rate_limited
                - internal_error
                - service_unavailable
                - idempotency_key_required
                - idempotency_key_reused
                - request_in_progress
                - template_archived
                - template_unusable
                - template_has_no_fields
                - template_changed
                - precondition_required
                - field_unknown
                - naming_rule_broken
                - naming_run_not_ready
                - naming_run_failed
                - proposal_changed
                - decision_not_allowed
                - witness_is_permanent
                - recipient_role_unknown
                - recipient_role_missing
                - recipient_role_duplicated
                - recipient_step_partial
                - step_with_signing_mode
                - value_subject_unknown
                - value_address_unknown
                - value_not_writable
                - value_option_unknown
                - value_option_locked
                - value_selection_out_of_bounds
                - value_not_lockable
                - locked_answer_missing
                - locked_answer_unusable
                - cannot_be_drawn
                - send_allowance_exhausted
                - test_key_cannot_send
                - test_key_cannot_read_documents
                - test_key_cannot_edit_templates
                - document_too_complex
                - test_key_cannot_mint
                - webhook_url_invalid
                - webhook_events_invalid
                - webhook_brand_unknown
                - webhook_limit_reached
                - api_key_owner_removed
                - envelope_sender_removed
                - recipient_not_embedded
                - recipient_not_yet_turn
                - recipient_cannot_sign
                - envelope_not_sent
                - envelope_not_draft
                - brand_has_no_embed_origins
                - recipient_cannot_be_embedded
                - recipient_verification_unavailable
                - recipient_mobile_unused
                - recipient_verification_not_allowed
                - batch_interrupted
                - brand_unknown
                - draft_not_available
                - standing_authorisations_not_enabled
                - authorisation_text_rejected
                - authorisation_validity_invalid
                - authorisation_not_revocable
                - authorisation_not_pending
                - authorisation_resend_limited
                - authorisation_not_renewable
                - authorisation_already_renewed
                - authorisation_unknown
                - authorisation_not_granted
                - authorisation_revoked
                - authorisation_expired
                - authorisation_not_yet_valid
                - authorisation_invalidated
                - authorisation_brand_mismatch
                - authorisation_environment_mismatch
                - authorisation_role_is_witness
                - authorisation_role_not_signing
                - authorisation_recipient_altered
                - authorisation_used_twice
                - authorisation_grantor_twice
                - authorisation_field_unsupported
                - authorisation_value_forbidden
                - value_for_left_out_role
                - authorisation_value_missing
                - authorisation_value_unlocked
                - authorisation_mark_too_small
                - authorisation_mark_unplaced
                - authorisation_without_signer
                - authorisation_mark_unavailable
                - authorisation_expires_during_batch
            message:
              type: string
              description: >-
                ⚠️ PROSE FOR A DEVELOPER READING A LOG, and NOT part of the
                contract. It may be reworded in any release; anything that
                parses it is broken by design.
          required:
            - code
            - message
      required:
        - error
    ValueInput:
      type: object
      description: One value, at one address. See the note above about triples.
      required:
        - subject
        - key
        - value
      properties:
        subject:
          type:
            - string
            - 'null'
          examples:
            - tenant
          description: >-
            A `key` from the template’s `subjects` — WHOSE datum this is — or
            `null` for a field belonging to nobody in particular. Required as a
            property: `null` and "I did not say" must not be two spellings of
            one thing.
        key:
          type: string
          examples:
            - staff.full_name
          description: >-
            The field’s `data_key`, or a question’s, exactly as the template
            endpoint reports it. A box of a question has no address of its own:
            its question’s is the one to send.
        value:
          oneOf:
            - type: string
              maxLength: 10000
              examples:
                - Thandi Mokoena
              description: >-
                What to write. It lands in EVERY field at this address, which is
                the point. For a question, one of its option values
                (`classic_saver`), which ticks the box it names and clears the
                others; `""` names none. For a lone tickbox, `"true"` ticks it
                and `"false"` or `""` leaves it clear — anything else is
                refused.
            - type: array
              maxItems: 200
              items:
                type: string
                maxLength: 10000
                description: One of the question’s option values.
              examples:
                - - classic_saver
                  - gap_cover
              description: >-
                For a QUESTION only: the option values to tick, each once, as
                many as its `min_selected` to `max_selected` allow — `[]` for
                none. The bounds count every option the template locks `ticked`
                (`questions[].options[].locked`), named or not: one left out is
                added when the bounds still hold with it. Naming an option
                locked `cleared` is refused. Neither applies with `locked` set,
                which replaces the template’s locks on every box. At any other
                address an array is refused.
          description: >-
            What to write. An address that matches nothing is refused rather
            than dropped. A string, or — for a question — an array of its option
            values (template field naming PR 6).
        locked:
          type: boolean
          examples:
            - true
          description: >-
            Optional. `true` fixes this answer: the signer reads it and cannot
            change it, and a submission that tries is refused. Use it when your
            own records are the authority and a signed form contradicting them
            would help nobody.


            ⚠️ IT RIDES ON THE VALUE ON PURPOSE, so you cannot lock an address
            without saying in the same breath what it is locked TO. To hand a
            field back to the signer, send `{"value": "", "locked": false}`: the
            empty string already means *clear this*.


            ⚠️ `{"value": "", "locked": true}` IS ACCEPTED, and it means "this
            box stays empty and the signer may not fill it". It is the spelling
            a grouped tickbox needs — five locked, empty options of a Race
            question, each saying *not this one* — so it is not refused. Earlier
            versions of this text said it could not be expressed at all; that
            was wrong.


            ⚠️ A LOCKED ANSWER SURVIVES BEING HIDDEN AND SHOWN AGAIN. A signer
            can make a section disappear — by answering the question a condition
            reads, or by declaring that a `subject` is not on the document — and
            the answers under it are removed, so nothing unasked is printed. A
            locked answer is yours rather than theirs, so it is kept separately
            and comes back the moment the section does. You do not have to
            re-send it, and no gesture available to the signer can destroy it.


            ⚠️ ON A TICKBOX, AN EMPTY VALUE IS RECORDED AS `false`. The two
            already mean the same thing to everything that reads or draws a
            tickbox; storing one spelling is what keeps *not this one* a single
            sentence. Every other type keeps exactly what you sent.


            ⚠️ OMITTING IT LEAVES THE FIELD AS THE TEMPLATE HAD IT, which is not
            the same as sending `false`. A sender can lock a field in the
            editor; absence respects that, `false` overrides it.


            ⚠️ AN ADDRESS MAY NAME SEVERAL BOXES AND THIS LOCKS ALL OF THEM —
            the same rule `value` follows. Two shapes of empty lock refuse the
            SEND with `locked_answer_missing`, because nobody left could supply
            the answer: a field that is locked, `required` and carries no value;
            and a QUESTION whose `min_selected` is out of reach because its
            options are locked and unticked. An optional, ungrouped box that is
            locked and empty is neither, and seals blank as it says.


            ⚠️ AND NEITHER REFUSAL LOOKS AT A BOX ABOUT SOMEBODY THIS ENVELOPE
            IS NOT ABOUT. A field carrying a `subject` you declared absent in
            `subjects` — or one the template marks optional and nobody declared
            — is hidden from the signer and cannot block the document, so a lock
            on it moved no demand and there is nothing to refuse. This is what
            lets one template carry a locked spouse section and still send to
            somebody who has no spouse: `subjects` decides who the envelope is
            about, and these refusals are about the people it IS about.
  securitySchemes:
    ApiKey:
      type: http
      scheme: bearer
      description: >-
        `Authorization: Bearer vsk_live_…`. Chosen over a bespoke `X-API-KEY`
        header because every client, proxy and log-redaction rule already knows
        this one. What a key may DO is its scopes — see `x-scopes` at the root
        of this document and `x-required-scope` on each operation. The scope
        list is not written here because OpenAPI reserves a requirement’s scope
        array for `oauth2` and `openIdConnect` and requires it to be empty for
        an `http` scheme.

````

This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.