> ## 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.

# Upload a PDF or a Word document and get a template with its fields already placed.

> THE ENDPOINT THIS API EXISTS FOR. Send a document and a list of roles; get back a template whose signature boxes, date fields, ID-number combs and marital-status questions are already on the page, each carrying the `id` you need to address it. Nobody opens an editor.

⚠️ **FIRST, CHECK THIS IS THE RESOURCE YOU WANT.** A template is a LIBRARY ENTRY — a kind of document you will send again, which stays in `GET /api/v1/templates` and on the Templates screen until it is archived. If what you are sending exists ONCE — an offer of employment to one candidate, a Letter of Authority — you want `POST /api/v1/envelopes/one-off` instead. It takes the same document and the SAME TEXT TAGS, does the same detection, sends the same envelope, and creates no template: `template_id` comes back `null`.

⚠️ THIS FORK WAS NOT WRITTEN DOWN HERE AND IT COST A CUSTOMER A ROW PER SEND. The one-off endpoint explains itself perfectly and an integrator reading THIS page had no reason to go and find it — so a product owner sent one offer letter and watched it appear in his template library. The paragraph below about documents that differ per employee is what pointed them here, and it was right about the TAGS and silent about the RESOURCE.

⚠️ TWO WAYS FIELDS GET PLACED, AND THE DOCUMENT CHOOSES. If your document carries TEXT TAGS, they win and detection does not run. Otherwise the page is measured.

**Text tags** — write the field into the document where it goes, and it arrives placed and already assigned to the right signer:

```
<<sig:Main Member>>                      a signature for the role named
<<text:Witness:Home address>>            a labelled text box
<<title:Main Member>>                    the signer’s job title, prefilled
<<datesigned:Main Member>>               the date they signed, stamped
<<text:Witness:optional>>                a box they may leave empty
<<text:Witness:validate(email)>>         checked as an email address
<<sig:Main Member:dimension(60x15mm)>>   say the size, do not pad the tag
<<text:Employee:key(employee_id)>>       the api name a value addresses
```

Types: `sig`, `signature`, `initial`, `text`, `dateinput`, `check`, `checkbox`, `title`, `company`, `name`, `email`, `datesigned`. `<<sig:Main Member>>` names the role EXACTLY and case-sensitively; `<<sig:signerN>>` names it BY POSITION — `signer1` is the first entry in your `roles` array — for a generator that does not know your roster. SEVERAL DIRECTIVES may follow the role, in any order: `optional`, `validate(<type>)`, `dimension(WxH mm|pt)` and `key(<name>)` — which sets the field’s `data_key`, stored lower-cased, so address it in lower case when you prefill. A segment shaped like `name(...)` is never a label: it is a directive this parser knows, or the whole request is refused. The first plain word or phrase becomes the field’s label, which is why a field labelled literally “optional” cannot be expressed by a tag. What a tag still cannot set is a comb, a per-character input shape, a condition, a dropdown’s options or a `validate(regex)` pattern — a pattern would have to survive being typed into a document — so those belong to a field you edit in the editor afterwards.

⚠️ TAGS ARE WHAT YOU WANT WHEN EVERY DOCUMENT DIFFERS. A contract generated per employee — conditional clauses, a page count that varies — cannot reuse one template and has nothing detectable on it either. Tags travel with the text, so they land correctly however the document reflows.

⚠️ BUT THAT SAYS NOTHING ABOUT **WHICH ENDPOINT**, and this paragraph used to be read as though it did. `POST /api/v1/envelopes/one-off` accepts the identical tags. If each generated contract goes to one person and is never sent again, send it THERE: every one you create here is a permanent library row, and a hundred offers of employment is a hundred of them. Create a template here when the document is a KIND you will send repeatedly — even if its text is merged per recipient.

⚠️ A TAG THAT CANNOT BE PLACED REFUSES THE WHOLE REQUEST with 400 `invalid_request`, naming every bad tag and its page. A tag naming a role you did not declare is the common one. Nothing is created — silently dropping the tag would lose a signature field, and leaving it as text would print `<<sig:Main Member>>` on a document somebody signs.

⚠️ **WHO A FIELD IS ABOUT** — `subject(dependant_2)`. Declare the people the document asks about in `subjects`, then address a field to one of them. A field addressed to somebody who is not on a given envelope is **hidden and not required, automatically**, with no condition to write:

```
<<text:Employee:subject(dependant_2):Surname>>
```

The argument is the subject’s KEY, not its label — the key is stable and the label is prose somebody may rename. An undeclared key refuses the whole request, naming the ones the document does declare, exactly as an unknown role does.

⚠️ AND THE SUBJECT MUST BE `conditional` FOR ANY OF IT TO MATTER. A `required` subject is on every envelope and can never be absent, which is the safe default; `conditional` is the one the caller or the signer decides about. A medical-aid application declaring `spouse` and `dependant_1..3` as conditional asks for none of them until somebody says that person exists — which is the whole point.

⚠️ **WHERE THE FIELD LANDS**, which this reference did not say until a customer had to work it out from a screenshot. The field’s LEFT EDGE is where the tag starts, and the field is **centred vertically on the tag’s line** — so it straddles the line you typed on, reaching about half its height above and half below the middle of the tag’s text, whatever type it is. Its HEIGHT comes from its type. Its WIDTH is the tag’s own printed width or the type’s minimum, whichever is larger. `dimension()` replaces both, and is placed the same way.

⚠️ SO WRITE THE TAG ON THE LINE **ABOVE** A RULE, not on the rule and not under it. Signature blocks are drawn with underscores, and there is nowhere on a row of underscores to put a tag without breaking the rule you drew:

```
<<sig:Employee>>            <<datesigned:Employee>>
______________________      ______________________
Employee name               Date
```

The signature, 40pt tall, then straddles its rule. A shorter field — a date, a text box, a signed date — is centred on the same line but reaches less far, so at ordinary line spacing it sits just above its rule rather than across it; `dimension()` sets an exact size when you want it to reach the rule. A tag on the row BELOW a rule puts the field below it, beside whatever caption is there — which is a common way to end up with a date under the line instead of on it.

Sizes by tag: `sig` or `signature` 40pt tall, at least 120pt wide; `initial` 40pt tall, at least 40pt wide; `dateinput` 18pt tall, at least 80pt wide; `text` 18pt tall, at least 80pt wide; `check` or `checkbox` 14pt tall, at least 14pt wide; `title` 15.2pt tall, at least 131pt wide; `company` 15.2pt tall, at least 131pt wide; `name` 15.2pt tall, at least 131pt wide; `email` 15.2pt tall, at least 166.6pt wide; `datesigned` 15.2pt tall, at least 89pt wide.

⚠️ AND THE TAG STAYS VISIBLE unless you write it in white. Nothing here rewrites your PDF, so a tag in black ink prints on the sealed document. Extraction finds white text perfectly well.

⚠️ AND THE DELIMITER IS `<< >>` BECAUSE YOUR DOCUMENT IS PROBABLY GENERATED. Every `{`-based templating engine destroys a `{{ }}` tag before we ever see the document — Mustache and Handlebars delete it SILENTLY, so the upload succeeds and reports no tags, and docxtemplater throws. `<< >>` is untouched by all three, so your merge fields (`{employee.full_name}`, `{#clause}…{/}`) and your tags coexist with nothing to reconfigure. A `{{type:Role}}` tag is refused by name rather than ignored, as is Dropbox Sign’s `[sig|req|signer1]`, so neither can be uploaded silently and printed on a signed document.

✅ AND YOU NO LONGER HAVE TO RENDER IT TO PDF YOURSELF. `file` takes a Word `.docx` as well as a PDF — decided by the BYTES, so there is no `content_type` to set and nothing to rename — and a docxtemplater or Word pipeline therefore has one step FEWER than it used to: write the tags into the document and upload the `.docx`. We render it, and the tags survive that render, which is the whole reason they are text.

A Word document (`.docx`) is accepted and converted to PDF on our side. The template is over the converted PDF: that is what page numbers, field coordinates and every later `GET` refer to, and it is what a signer sees. The `.docx` you sent is retained unchanged and is not what anybody signs.

⚠️ THE OLDER `.doc` IS NOT THE SAME FORMAT AND IS NOT ACCEPTED — it is refused by its bytes, with a sentence saying to Save As a `.docx`.

**Detection**, when there are no tags, is the same code the editor’s “Find fields” button runs, not a second implementation — so a template authored here and one authored by a person pressing that button have their fields in the same places, with the same masks and the same inferred types, because it is the same function. It infers position but not ownership, so every detected field goes to your first non-`cc` role, for you to reassign.

⚠️ A DOCUMENT WITH NOTHING DETECTABLE IN IT IS NOT AN ERROR. A flat scan with no text layer yields no fields, and this still answers 201 with a template holding its roles, its document and no fields — which is the thing you then place fields on. `fields` being empty is how you tell.

The document travels base64-encoded inside the JSON body rather than as `multipart/form-data`, so one media type and one HTTP client serve every endpoint of this API. See `file` for what that costs and what it bounds.

A `vsk_test_` key may author templates, deliberately: a template is a draft, nothing reaches a signer until an envelope is created AND sent, and iterating on where the boxes land is what a sandbox is for. Templates carry no sandbox/live distinction of their own.



## OpenAPI

````yaml /openapi.json post /api/v1/templates
openapi: 3.1.0
info:
  title: Vumasign API
  version: 0.1.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: 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:
    post:
      tags:
        - Templates
      summary: >-
        Upload a PDF or a Word document and get a template with its fields
        already placed.
      description: >-
        THE ENDPOINT THIS API EXISTS FOR. Send a document and a list of roles;
        get back a template whose signature boxes, date fields, ID-number combs
        and marital-status questions are already on the page, each carrying the
        `id` you need to address it. Nobody opens an editor.


        ⚠️ **FIRST, CHECK THIS IS THE RESOURCE YOU WANT.** A template is a
        LIBRARY ENTRY — a kind of document you will send again, which stays in
        `GET /api/v1/templates` and on the Templates screen until it is
        archived. If what you are sending exists ONCE — an offer of employment
        to one candidate, a Letter of Authority — you want `POST
        /api/v1/envelopes/one-off` instead. It takes the same document and the
        SAME TEXT TAGS, does the same detection, sends the same envelope, and
        creates no template: `template_id` comes back `null`.


        ⚠️ THIS FORK WAS NOT WRITTEN DOWN HERE AND IT COST A CUSTOMER A ROW PER
        SEND. The one-off endpoint explains itself perfectly and an integrator
        reading THIS page had no reason to go and find it — so a product owner
        sent one offer letter and watched it appear in his template library. The
        paragraph below about documents that differ per employee is what pointed
        them here, and it was right about the TAGS and silent about the
        RESOURCE.


        ⚠️ TWO WAYS FIELDS GET PLACED, AND THE DOCUMENT CHOOSES. If your
        document carries TEXT TAGS, they win and detection does not run.
        Otherwise the page is measured.


        **Text tags** — write the field into the document where it goes, and it
        arrives placed and already assigned to the right signer:


        ```

        <<sig:Main Member>>                      a signature for the role named

        <<text:Witness:Home address>>            a labelled text box

        <<title:Main Member>>                    the signer’s job title,
        prefilled

        <<datesigned:Main Member>>               the date they signed, stamped

        <<text:Witness:optional>>                a box they may leave empty

        <<text:Witness:validate(email)>>         checked as an email address

        <<sig:Main Member:dimension(60x15mm)>>   say the size, do not pad the
        tag

        <<text:Employee:key(employee_id)>>       the api name a value addresses

        ```


        Types: `sig`, `signature`, `initial`, `text`, `dateinput`, `check`,
        `checkbox`, `title`, `company`, `name`, `email`, `datesigned`.
        `<<sig:Main Member>>` names the role EXACTLY and case-sensitively;
        `<<sig:signerN>>` names it BY POSITION — `signer1` is the first entry in
        your `roles` array — for a generator that does not know your roster.
        SEVERAL DIRECTIVES may follow the role, in any order: `optional`,
        `validate(<type>)`, `dimension(WxH mm|pt)` and `key(<name>)` — which
        sets the field’s `data_key`, stored lower-cased, so address it in lower
        case when you prefill. A segment shaped like `name(...)` is never a
        label: it is a directive this parser knows, or the whole request is
        refused. The first plain word or phrase becomes the field’s label, which
        is why a field labelled literally “optional” cannot be expressed by a
        tag. What a tag still cannot set is a comb, a per-character input shape,
        a condition, a dropdown’s options or a `validate(regex)` pattern — a
        pattern would have to survive being typed into a document — so those
        belong to a field you edit in the editor afterwards.


        ⚠️ TAGS ARE WHAT YOU WANT WHEN EVERY DOCUMENT DIFFERS. A contract
        generated per employee — conditional clauses, a page count that varies —
        cannot reuse one template and has nothing detectable on it either. Tags
        travel with the text, so they land correctly however the document
        reflows.


        ⚠️ BUT THAT SAYS NOTHING ABOUT **WHICH ENDPOINT**, and this paragraph
        used to be read as though it did. `POST /api/v1/envelopes/one-off`
        accepts the identical tags. If each generated contract goes to one
        person and is never sent again, send it THERE: every one you create here
        is a permanent library row, and a hundred offers of employment is a
        hundred of them. Create a template here when the document is a KIND you
        will send repeatedly — even if its text is merged per recipient.


        ⚠️ A TAG THAT CANNOT BE PLACED REFUSES THE WHOLE REQUEST with 400
        `invalid_request`, naming every bad tag and its page. A tag naming a
        role you did not declare is the common one. Nothing is created —
        silently dropping the tag would lose a signature field, and leaving it
        as text would print `<<sig:Main Member>>` on a document somebody signs.


        ⚠️ **WHO A FIELD IS ABOUT** — `subject(dependant_2)`. Declare the people
        the document asks about in `subjects`, then address a field to one of
        them. A field addressed to somebody who is not on a given envelope is
        **hidden and not required, automatically**, with no condition to write:


        ```

        <<text:Employee:subject(dependant_2):Surname>>

        ```


        The argument is the subject’s KEY, not its label — the key is stable and
        the label is prose somebody may rename. An undeclared key refuses the
        whole request, naming the ones the document does declare, exactly as an
        unknown role does.


        ⚠️ AND THE SUBJECT MUST BE `conditional` FOR ANY OF IT TO MATTER. A
        `required` subject is on every envelope and can never be absent, which
        is the safe default; `conditional` is the one the caller or the signer
        decides about. A medical-aid application declaring `spouse` and
        `dependant_1..3` as conditional asks for none of them until somebody
        says that person exists — which is the whole point.


        ⚠️ **WHERE THE FIELD LANDS**, which this reference did not say until a
        customer had to work it out from a screenshot. The field’s LEFT EDGE is
        where the tag starts, and the field is **centred vertically on the tag’s
        line** — so it straddles the line you typed on, reaching about half its
        height above and half below the middle of the tag’s text, whatever type
        it is. Its HEIGHT comes from its type. Its WIDTH is the tag’s own
        printed width or the type’s minimum, whichever is larger. `dimension()`
        replaces both, and is placed the same way.


        ⚠️ SO WRITE THE TAG ON THE LINE **ABOVE** A RULE, not on the rule and
        not under it. Signature blocks are drawn with underscores, and there is
        nowhere on a row of underscores to put a tag without breaking the rule
        you drew:


        ```

        <<sig:Employee>>            <<datesigned:Employee>>

        ______________________      ______________________

        Employee name               Date

        ```


        The signature, 40pt tall, then straddles its rule. A shorter field — a
        date, a text box, a signed date — is centred on the same line but
        reaches less far, so at ordinary line spacing it sits just above its
        rule rather than across it; `dimension()` sets an exact size when you
        want it to reach the rule. A tag on the row BELOW a rule puts the field
        below it, beside whatever caption is there — which is a common way to
        end up with a date under the line instead of on it.


        Sizes by tag: `sig` or `signature` 40pt tall, at least 120pt wide;
        `initial` 40pt tall, at least 40pt wide; `dateinput` 18pt tall, at least
        80pt wide; `text` 18pt tall, at least 80pt wide; `check` or `checkbox`
        14pt tall, at least 14pt wide; `title` 15.2pt tall, at least 131pt wide;
        `company` 15.2pt tall, at least 131pt wide; `name` 15.2pt tall, at least
        131pt wide; `email` 15.2pt tall, at least 166.6pt wide; `datesigned`
        15.2pt tall, at least 89pt wide.


        ⚠️ AND THE TAG STAYS VISIBLE unless you write it in white. Nothing here
        rewrites your PDF, so a tag in black ink prints on the sealed document.
        Extraction finds white text perfectly well.


        ⚠️ AND THE DELIMITER IS `<< >>` BECAUSE YOUR DOCUMENT IS PROBABLY
        GENERATED. Every `{`-based templating engine destroys a `{{ }}` tag
        before we ever see the document — Mustache and Handlebars delete it
        SILENTLY, so the upload succeeds and reports no tags, and docxtemplater
        throws. `<< >>` is untouched by all three, so your merge fields
        (`{employee.full_name}`, `{#clause}…{/}`) and your tags coexist with
        nothing to reconfigure. A `{{type:Role}}` tag is refused by name rather
        than ignored, as is Dropbox Sign’s `[sig|req|signer1]`, so neither can
        be uploaded silently and printed on a signed document.


        ✅ AND YOU NO LONGER HAVE TO RENDER IT TO PDF YOURSELF. `file` takes a
        Word `.docx` as well as a PDF — decided by the BYTES, so there is no
        `content_type` to set and nothing to rename — and a docxtemplater or
        Word pipeline therefore has one step FEWER than it used to: write the
        tags into the document and upload the `.docx`. We render it, and the
        tags survive that render, which is the whole reason they are text.


        A Word document (`.docx`) is accepted and converted to PDF on our side.
        The template is over the converted PDF: that is what page numbers, field
        coordinates and every later `GET` refer to, and it is what a signer
        sees. The `.docx` you sent is retained unchanged and is not what anybody
        signs.


        ⚠️ THE OLDER `.doc` IS NOT THE SAME FORMAT AND IS NOT ACCEPTED — it is
        refused by its bytes, with a sentence saying to Save As a `.docx`.


        **Detection**, when there are no tags, is the same code the editor’s
        “Find fields” button runs, not a second implementation — so a template
        authored here and one authored by a person pressing that button have
        their fields in the same places, with the same masks and the same
        inferred types, because it is the same function. It infers position but
        not ownership, so every detected field goes to your first non-`cc` role,
        for you to reassign.


        ⚠️ A DOCUMENT WITH NOTHING DETECTABLE IN IT IS NOT AN ERROR. A flat scan
        with no text layer yields no fields, and this still answers 201 with a
        template holding its roles, its document and no fields — which is the
        thing you then place fields on. `fields` being empty is how you tell.


        The document travels base64-encoded inside the JSON body rather than as
        `multipart/form-data`, so one media type and one HTTP client serve every
        endpoint of this API. See `file` for what that costs and what it bounds.


        A `vsk_test_` key may author templates, deliberately: a template is a
        draft, nothing reaches a signer until an envelope is created AND sent,
        and iterating on where the boxes land is what a sandbox is for.
        Templates carry no sandbox/live distinction of their own.
      operationId: createTemplate
      parameters:
        - name: Idempotency-Key
          in: header
          required: true
          description: >-
            Any string that identifies this request; a UUID is the usual choice,
            and at most 255 characters. ⚠️ REQUIRED, because an upload is the
            request most likely to lose its answer — the body is the largest
            this API accepts and the reply comes after the document has been
            parsed twice. Retrying with the same key returns the same template
            rather than authoring a second one from the same document.


            ⚠️ AND IT IS ANSWERED CONSERVATIVELY IN ONE CASE. If a previous
            request under this key stopped partway and this API cannot establish
            whether it created a template, you are told so with
            `request_in_progress` and asked to look — it will not guess, because
            a wrong guess is a second template. Call `GET /api/v1/templates`; if
            it is not there, retry with a NEW key.
          schema:
            type: string
            examples:
              - 01960000-0000-4000-8000-00000000ffff
            description: The caller’s own request identifier.
      requestBody:
        required: true
        description: The document, base64-encoded, and who signs it.
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateTemplateRequest'
              description: The document and its roles.
            examples:
              example:
                summary: A one-page PDF whose tag places the one field
                value:
                  file: >-
                    JVBERi0xLjcKJYGBgYEKCjUgMCBvYmoKPDwKL0ZpbHRlciAvRmxhdGVEZWNvZGUKL0xlbmd0aCAyMDcKPj4Kc3RyZWFtCnicjY9BS0MxEITv+ytyFsRNNjubQCm8vubhwYuQP1BKLRV7qIi/331VD5UWJATCzsx+mROtOnGYz/ueHh53b5+7j8N2c29cSy5spYaI0F8o5dCfKJ6tMYCD+e1HWmTF2hgjJqtYQ9EsJ4Zgml+WECGWl6G/Ur+j1umZTreo1aMoSVFCjFepcOlMhakzRyc1JzhXhv8xEjMX1lpwk6HfzWSU0TLUimUZLnrqzLv2BxOfJQzu8Nap4E8qVRmw8p3VtZ+8Tr95V5u0ix5fVGNcUwplbmRzdHJlYW0KZW5kb2JqCgo2IDAgb2JqCjw8Ci9GaWx0ZXIgL0ZsYXRlRGVjb2RlCi9UeXBlIC9PYmpTdG0KL04gNAovRmlyc3QgMjAKL0xlbmd0aCAyNjMKPj4Kc3RyZWFtCnicZVDRSgMxEHzPV+wP2E1i7i4HpdCWVkFEaYUK4kN6F46UkkgvJ/XvzcbTUiQv2Z3Zmd0RwEGCUnALlQYFouQwnTJ8+fqwgM+msz3DB9f28JZQDht4Z7gMg48g2GzGLtylieYYOvYzBILI14x18JHhdtjHXFJTMFyY3hICeG+Pnza6xjBc+Sa0zneAO+fnvne/jWtFsiLDk6V9siNubB+GU5NWIF5Wps+f+E3Fa600r3Sdjs4jF6yulCy1LEr9H5Occ82LWpcjllbB16f9wTbZgsrVOd5to4l2bFDv0bbOLMI5JcjTK+piIjVoJSZpg5Tm3PsQKd+crI/pFqqKMe0k8Q0eU3JYCmVuZHN0cmVhbQplbmRvYmoKCjcgMCBvYmoKPDwKL1NpemUgOAovUm9vdCAyIDAgUgovRmlsdGVyIC9GbGF0ZURlY29kZQovVHlwZSAvWFJlZgovTGVuZ3RoIDM5Ci9XIFsgMSAyIDIgXQovSW5kZXggWyAwIDggXQo+PgpzdHJlYW0KeJwVxLERADAIA7G3c0edMbIw8xKsQsCMKUhKTkdckN7mhg9QkQL4CmVuZHN0cmVhbQplbmRvYmoKCnN0YXJ0eHJlZgo2NjEKJSVFT0Y=
                  roles:
                    - Employee
                  subjects:
                    - key: employee
                      label: Employee
                  filename: employment-contract.pdf
                  name: Employment contract
                  routing_types:
                    - sign
                  signing_mode: parallel
      responses:
        '201':
          description: >-
            The template, in exactly the shape `GET
            /api/v1/templates/{templateId}` returns — same fields, same ids,
            same everything, because it is read back through the same function
            rather than assembled separately. `Idempotency-Replayed` says
            whether this request authored it (`false`) or is being shown an
            earlier one’s result (`true`); the status is 201 either way,
            deliberately, so a client branching on it behaves identically on a
            retry.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Template'
                description: The template, with every field’s id.
              examples:
                example:
                  summary: The template, with one detected field
                  value:
                    id: 01960000-0000-4000-8000-0000000007e1
                    name: Employment contract
                    created_at: '2026-09-01T08:30:00.000Z'
                    documents:
                      - id: 01960000-0000-4000-8000-00000000d0c5
                        page_count: 1
                    pages:
                      - document_id: 01960000-0000-4000-8000-00000000d0c5
                        page: 1
                        width: 595.28
                        height: 841.89
                        rotation: 0
                    roles:
                      - name: Employee
                        routing_type: sign
                    subjects:
                      - key: employee
                        label: Employee
                    questions: []
                    fields:
                      - id: 01960000-0000-4000-8000-00000000f1e1
                        type: text
                        label: Full name
                        required: true
                        role: Employee
                        document_id: 01960000-0000-4000-8000-00000000d0c5
                        page: 1
                        subject: employee
                        data_key: full_name
                        question_id: null
                        options: null
                        rect:
                          x: 0.1007929
                          'y': 0.2107045
                          w: 0.5288973
                          h: 0.0213805
          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.


            `idempotency_key_required` — Send `Idempotency-Key: <uuid>` — this
            operation takes one and the request carried none. A distinct code
            from `invalid_request` because the remedy is to ADD a header, and an
            integrator reading it in a log should not have to work that out. (A
            key that is present but malformed is `invalid_request`, and its
            `message` names the header.)
          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.


            `api_key_owner_removed` — The person who created this key has left
            the organisation, and everything the key creates or sends — an
            envelope, a batch, a template — is attributed to that person, who
            must still be a member. The key is not revoked and its reads still
            work, and so does managing webhooks; mint a new key from a current
            member and use that.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
                description: The refusal.
        '409':
          description: >-
            `idempotency_key_reused` — This key was already used for a DIFFERENT
            request. ⚠️ The request itself may be perfectly valid — what
            conflicts is the key against state we already hold, which is what
            409 means and why it is not a 400. Mint a new key. Do not retry with
            this one; it will conflict forever.


            `request_in_progress` — A request carrying this key has not
            finished. We deliberately do not block waiting for it — that would
            hold a connection across a send that calls an email provider N
            times. Retry the identical request; when the first one lands you
            will get its result, replayed.
          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. TWO 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; 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. Two services can produce it and which
            one it was depends on the operation: on a create, the service that
            renders a Word document to a PDF, so it is reachable only by sending
            a `.docx` and never by sending one that is already a PDF; on `GET
            .../documents/{position}/draft`, the service that renders the draft
            itself. ⚠️ 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 READ THE ATTEMPT WAS ALREADY PAID FOR: the render is
            charged to the envelope’s and the organisation’s per-minute draft
            ceilings 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:
    CreateTemplateRequest:
      type: object
      description: One document — a PDF or a Word `.docx` — and who signs it.
      properties:
        file:
          type: string
          examples:
            - JVBERi0xLjcKJeLjz9MKMSAwIG9iago8PC9UeXBlL0NhdGFsb2c+PgplbmRvYmoK
          description: >-
            ⚠️ THE WHOLE DOCUMENT, BASE64-ENCODED, AND THE EXAMPLE ABOVE IS A
            TRUNCATED STUB — replace it with your own document’s bytes or the
            request is refused as unreadable. A PDF or a Word `.docx`; the BYTES
            decide which, so there is no `content_type` field to get wrong, and
            a `.docx` is converted to PDF on our side. Standard alphabet only:
            no `data:` prefix, no whitespace, no line wrapping, and not the
            URL-safe `-`/`_` variant. Base64 is about a third larger than the
            file it carries, and the whole request body is bounded at 8388608
            bytes — so roughly 6291456 bytes of document, whatever its format or
            page count.
        roles:
          type: array
          description: >-
            Who signs it, in order, at least one and at most six. ⚠️ THE ORDER
            IS LOAD-BEARING: it is a role’s position in the recipient list and
            its colour in the editor, and — when `signing_mode` is `sequential`
            — the order people are actually asked. Every field detected in the
            document is allocated to the first role that is not a `cc`, because
            a page cannot say which of three signers writes in a box; reassign
            them in the editor afterwards. Each comes back with an `id`, but
            nothing accepts it as input today — see `Field.id`.
          items:
            type: string
            examples:
              - Employee
            description: One role’s name.
        filename:
          type: string
          examples:
            - employment-contract.pdf
          description: >-
            Optional. Used only to NAME the template when `name` is absent,
            exactly as the upload box suggests a name from the file you dropped
            on it. It never reaches storage — the object key is the content
            hash.
        name:
          type: string
          examples:
            - Employment contract
          description: >-
            Optional. What to call the template. Absent or blank falls back to
            `filename`, and then to “Untitled document”. ⚠️ A name that is SENT
            and cannot be stored is refused, while a name that is not sent is
            not — a template’s name is editable afterwards, and refusing an
            upload over one would be a wall in front of the thing this endpoint
            exists to make free.
        routing_types:
          type: array
          description: >-
            Optional. One entry per role, in the same order. ⚠️ IF YOU SEND IT
            AT ALL IT MUST HAVE EXACTLY ONE ENTRY PER ROLE — a short list is
            refused rather than padded, because from an API caller a short list
            is far more likely to be an off-by-one than an intention. Omit it
            entirely and every role signs. A `cc` is never asked to fill
            anything in, which is why detected fields skip past one. ⚠️ IT IS
            SENT THE FINISHED COPY ONLY WHEN WE SEND THE COMPLETION NOTICES —
            `completion_delivery: vumasign`, the default for an envelope sent
            from the dashboard. Under `integrator`, the default for an envelope
            sent with an API key, nobody is sent anything at completion, a `cc`
            included: telling them is yours.
          items:
            type: string
            enum:
              - sign
              - approve
              - cc
            examples:
              - sign
            description: What this role is asked to do.
        signing_mode:
          type: string
          enum:
            - parallel
            - sequential
          examples:
            - parallel
          description: >-
            Optional, default `parallel` — everybody is asked at once.
            `sequential` asks them in the order `roles` is written, each
            invitation going out only when the one before it is finished.
        subjects:
          type: array
          maxItems: 200
          description: >-
            Optional. The people this document asks about, so that a
            `subject(...)` tag can address a field to one of them. Omitted means
            none, and every `subject(...)` tag is then refused. At most 200. Two
            subjects with the same `key`, or with labels that differ only in
            case, are refused.
          items:
            type: object
            description: One person the document asks about.
            properties:
              key:
                type: string
                minLength: 1
                maxLength: 100
                pattern: ^[a-z0-9_]+$
                examples:
                  - employee
                description: >-
                  The subject’s machine name, and the name a `subject(...)` text
                  tag addresses it by: lower-case `a`–`z`, `0`–`9` and `_` only,
                  at most 100 characters — for example `spouse` or
                  `dependant_1`. Anything else is refused with
                  `invalid_request`, and the message names the key that would be
                  accepted. ⚠️ IT IS NOT LOWER-CASED FOR YOU: `{"key":
                  "Spouse"}` is refused, not stored as `spouse`, because fields
                  and `values[].subject` refer to a key byte for byte. A tag is
                  case-insensitive, so `subject(Spouse)` names the key `spouse`.
                  Matches `Subject.key` and `Field.subject`.
              label:
                type: string
                minLength: 1
                examples:
                  - Employee
                description: >-
                  What the sender calls this person. Stored in Unicode NFC: a
                  label sent decomposed is composed, and that composed form is
                  what is checked, stored and returned. Not blank (whitespace
                  alone is refused), and at most 200 UTF-16 code units once
                  composed, as JavaScript counts string length: most characters
                  count as one, some (outside the Basic Multilingual Plane)
                  count as two. No `maxLength` is declared, because JSON Schema
                  counts code points and would admit a label the server refuses.
              presence:
                type: string
                enum:
                  - required
                  - conditional
                examples:
                  - conditional
                description: >-
                  Optional, default `required` — the subject is on every
                  envelope. `conditional` is what lets a subject be absent from
                  an envelope, and a field addressed to an absent subject is
                  hidden and not required.
            required:
              - key
              - label
      required:
        - file
        - roles
    Template:
      type: object
      description: >-
        One template in full: its documents, its roles, its subjects, its
        questions and every field placement. This is the response an integrator
        reads once, by hand, to learn the ADDRESSES their values must be written
        to.
      properties:
        id:
          type: string
          format: uuid
          description: The template’s id. A bare uuid, no prefix.
        name:
          type: string
          description: What the sender called it.
        created_at:
          type: string
          format: date-time
          description: ISO 8601, UTC. Also the order this list is in — newest first.
        documents:
          type: array
          description: The pack, in the order it is stacked.
          items:
            $ref: '#/components/schemas/Document'
            description: One document of the pack.
        pages:
          type: array
          description: >-
            ⚠️ THE DENOMINATOR FOR EVERY `Field.rect`, and a client doing field
            matching cannot skip it: a rectangle of fractions is dimensionless
            without the page it is a fraction of. Join on `(document_id, page)`,
            the same pair `fields` is joined by.


            Every page of every document of the pack, in the pack’s order and
            then by page number — the order a signer scrolls.
          items:
            $ref: '#/components/schemas/Page'
            description: 'One page: its size in points and its rotation.'
        roles:
          type: array
          description: The signing roles, in routing order.
          items:
            $ref: '#/components/schemas/Role'
            description: One role.
        subjects:
          type: array
          description: The people whose data this template collects.
          items:
            $ref: '#/components/schemas/Subject'
            description: One subject.
        questions:
          type: array
          description: The tickbox questions and how many answers each takes.
          items:
            $ref: '#/components/schemas/Question'
            description: One question.
        fields:
          type: array
          description: >-
            ⚠️ A FLAT LIST, NOT A MAP KEYED BY ADDRESS. Several fields sharing
            one `(subject, data_key)` pair is NORMAL — real forms ask for an ID
            number on the application and again on the declaration, and initials
            in the footer of all nine pages. A value supplied for an address
            means it for every box that asks.
          items:
            $ref: '#/components/schemas/Field'
            description: One field placement.
      required:
        - id
        - name
        - created_at
        - documents
        - pages
        - roles
        - subjects
        - questions
        - fields
    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
                - recipient_role_unknown
                - recipient_role_missing
                - recipient_role_duplicated
                - value_subject_unknown
                - value_address_unknown
                - value_not_writable
                - 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_mint
                - webhook_url_invalid
                - webhook_events_invalid
                - webhook_brand_unknown
                - webhook_limit_reached
                - api_key_owner_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
                - batch_interrupted
                - brand_unknown
                - draft_not_available
            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
    Document:
      type: object
      description: >-
        One document of a template’s pack. Present so that a field’s `page`
        means something: the pack renders as one continuous column, so page 1
        occurs once per document. Listed in the order the pack is stacked, which
        is the order a signer scrolls.
      properties:
        id:
          type: string
          format: uuid
          description: >-
            The document’s id. ⚠️ ALSO THE `documentId` OF `GET
            /api/v1/documents/{documentId}/pages/{pageNumber}`, which is where
            you fetch the page itself to look at.
        page_count:
          type: integer
          description: How many pages this document contributes to the pack.
      required:
        - id
        - page_count
    Page:
      type: object
      description: >-
        One page of one of the template’s documents: how big it is and which way
        up it is. ⚠️ EVERY `Field.rect` IS A FRACTION OF THIS — a rectangle
        without its page is dimensionless, so a client that ignores this array
        cannot tell a portrait page from a landscape one and cannot check its
        own render against ours.
      properties:
        document_id:
          type: string
          format: uuid
          description: >-
            Which document of the pack. Matches `Document.id` and
            `Field.document_id`.
        page:
          type: integer
          description: >-
            1-indexed, WITHIN `document_id` and not within the pack. Matches
            `Field.page`, and is the `pageNumber` of `GET
            /api/v1/documents/{documentId}/pages/{pageNumber}`.
        width:
          type: number
          description: >-
            The visible width in PDF points (1/72 inch). ⚠️ ALREADY ROTATED —
            this is the width of the page as it is drawn and as you will see it.
            A4 portrait is 595.28; the same page at `rotation: 90` reports
            841.89 here.
          examples:
            - 595.28
        height:
          type: number
          description: The visible height in PDF points. Already rotated, like `width`.
          examples:
            - 841.89
        rotation:
          type: integer
          examples:
            - 0
          description: >-
            One of 0, 90, 180 or 270. ⚠️ INFORMATION, NOT AN INSTRUCTION.
            `width`, `height` and every `Field.rect` on this page are ALREADY
            expressed against the rotated page. A client that rotates the
            geometry again because this says 90 places every field wrongly, and
            the result looks entirely plausible.


            It is published for the two things it is genuinely needed for:
            telling a page that is landscape because it was rotated from one
            that was authored landscape, and checking that whatever produced
            your image honoured the rotation at all — if your image’s aspect
            ratio disagrees with `width`/`height`, your renderer is what is
            wrong.
      required:
        - document_id
        - page
        - width
        - height
        - rotation
    Role:
      type: object
      description: >-
        A signing role, addressed by NAME and never by position. A positional
        address breaks the moment a sender reorders a template; the array
        arrives in routing order, so nothing is lost by omitting the index.
      properties:
        name:
          type: string
          description: What the sender called this role. Matches `Field.role`.
        routing_type:
          type: string
          description: What this role does with the document.
          enum:
            - sign
            - approve
            - cc
      required:
        - name
        - routing_type
    Subject:
      type: object
      description: >-
        A subject: WHOSE data a field holds, as opposed to who fills it in. Half
        of every field address — `first_name` under the subject `spouse` is a
        different datum from `first_name` under the subject `main_member`.
      properties:
        key:
          type: string
          description: >-
            The machine half of the address — letters, digits and underscores
            only, so it can never contain a dot. Matches `Field.subject`.
        label:
          type: string
          description: >-
            What the sender called this person. This is what an integrator
            matches against their own record of the same human.
      required:
        - key
        - label
    Question:
      type: object
      description: >-
        ⚠️ A QUESTION, WHICH IS WHAT A SET OF TICKBOXES MEANS. A caller
        supplying a value for "Race" has to know the question takes exactly one
        answer, and no individual tickbox can tell them: `required` is per
        option, so a "required" Race question would mean every race must be
        ticked. The bounds here are the answer.
      properties:
        id:
          type: string
          format: uuid
          description: >-
            ⚠️ A JOIN KEY WITHIN THIS RESPONSE, NOT AN ADDRESS. Nothing in the
            API accepts it as input. It exists because a question has no natural
            key — two questions on one form legitimately called "Other" are a
            form, not a mistake — so joining fields to questions by name would
            merge them.
        name:
          type: string
          description: 'What a signer reads above the boxes: "Race", "Marital status".'
        min_selected:
          type: integer
          description: 0 makes the question optional. `>= 1` is what "required" means here.
        max_selected:
          type: integer
          description: >-
            1 makes the question exclusive. ⚠️ It is NOT always 1: "tick any
            that apply, at least one" is `(1, n)`. Never 0 — a question that
            takes no answers is not a question.
      required:
        - id
        - name
        - min_selected
        - max_selected
    Field:
      type: object
      description: >-
        One placement on the paper. ⚠️ ONE ARRAY WITH A `type` DISCRIMINATOR,
        not one array per type: DocuSign has 37 typed tab arrays, so adding a
        type is a breaking change there and a generic client must enumerate 37
        keys.
      properties:
        id:
          type: string
          format: uuid
          description: >-
            ⚠️ THE HANDLE FOR EXACTLY ONE FIELD, AND NOT A SECOND ADDRESS.
            `subject` + `data_key` is the address and it names a SET of fields
            on purpose — one `main_member.id_number` is meant to fill the box on
            the application, the box on the declaration and the initials in nine
            footers. Write values to the ADDRESS by default. Use this id when
            you need to name ONE of several boxes the address cannot tell apart,
            or to keep a durable reference to a particular box. The two can
            never name the same target, so there is no precedence to learn: an
            id is one field, an address is a set. ⚠️ NOTHING ACCEPTS IT AS INPUT
            TODAY — `values` on `POST /api/v1/envelopes` is addressed by
            `subject` and `key`, and there is no id-keyed form. ⚠️ IT IS STABLE
            across ordinary editing — the sender may move, retype, relabel and
            readdress a box, or delete its neighbours, and this value does not
            change — so it is safe to store. ⚠️ IT NAMES A TEMPLATE FIELD:
            sending a template COPIES its fields onto the envelope, and those
            copies are their own rows with their own ids.
        type:
          type: string
          description: >-
            What kind of box this is. This vocabulary is `FIELD_TYPES` verbatim
            and the database CHECK constraint’s, not a translation of either.
          enum:
            - signature
            - initial
            - text
            - date
            - checkbox
            - dropdown
            - attachment
            - date_signed
            - signer_name
            - signer_email
            - signer_title
            - signer_company
        label:
          type:
            - string
            - 'null'
          description: >-
            What is printed beside the box. Null for a box the sender never
            named.
        required:
          type: boolean
          description: >-
            ⚠️ ALWAYS FALSE ON A GROUPED OPTION — requiredness lives on the
            question, so read `min_selected` there instead. This flag is the
            answer for independent boxes only.
        role:
          type: string
          description: WHO fills this in. Matches `Role.name`.
        document_id:
          type: string
          format: uuid
          description: Which document of the pack this box is on. Matches `Document.id`.
        page:
          type: integer
          description: 1-indexed, WITHIN `document_id` and not within the pack.
        subject:
          type:
            - string
            - 'null'
          description: >-
            WHOSE datum this is — a `Subject.key` — or null for the signer’s
            own. Half of the address.
        data_key:
          type:
            - string
            - 'null'
          description: >-
            ⚠️ THE OTHER HALF OF THE ADDRESS, AND NULL IS THE MOST USEFUL THING
            THIS ENDPOINT SAYS. Null means this box has no address at all and an
            integration CANNOT write to it. Find those here, against your own
            data model, rather than by watching a send leave them blank. ⚠️ It
            may contain a dot and any Unicode letter — South Africa has eleven
            official languages and a Sesotho label is not a malformed address —
            which is why this is never composed with `subject` into one dotted
            string.
        question_id:
          type:
            - string
            - 'null'
          description: >-
            The `Question.id` this box is an option of, or null for an
            independent one.
        options:
          type:
            - array
            - 'null'
          items:
            type: string
            description: >-
              One option, exactly as a signer sees it and exactly as it is
              sealed.
          description: >-
            WHAT A `dropdown` OFFERS, in the order the signer reads them. Null
            on every other type. An option is ONE STRING — what is shown, what
            you write back, and what is sealed onto the document; there is no
            separate value and label, so the string on the certificate cannot
            disagree with the string in the database. ⚠️ A value you write to a
            dropdown MUST be one of these exactly: anything else is refused. ⚠️
            Null on a dropdown created before options existed — such a field
            cannot be answered at all, by you or by a signer, and has to be
            given a list in the editor.
        rect:
          $ref: '#/components/schemas/Rect'
          description: >-
            ⚠️ WHERE THE BOX IS ON THE PAGE, AND THE REASON THIS ENDPOINT IS
            USABLE BY SOMETHING THAT CAN SEE. `label` says what is printed
            beside the box and `data_key` says what may be written into it; on a
            real form neither is enough. A nine-page medical application carries
            284 fields named `checklist.17` and `fullwidth.42`, with OCR-derived
            labels, where "ID or passport number" appears four times on one page
            and only the POSITION says which dependant it belongs to.


            Fetch the page it is on from `GET
            /api/v1/documents/{document_id}/pages/{page}`, render it, and these
            four fractions land on the box. ⚠️ Read `Rect` before using them:
            origin top-left, Y downward, measured against the page AFTER
            rotation.
      required:
        - id
        - type
        - label
        - required
        - role
        - document_id
        - page
        - subject
        - data_key
        - question_id
        - options
        - rect
    Rect:
      type: object
      description: >-
        ⚠️ WHERE THE BOX IS. **Fractions of the VISIBLE page. Origin TOP-LEFT. Y
        increases DOWNWARD. `x`/`y` are the box’s TOP-LEFT corner.**


        To place it on an image of the page you rendered at any size:

        `left = x * imageWidth`, `top = y * imageHeight`, `width = w *
        imageWidth`, `height = h * imageHeight`. That arithmetic is correct at
        every resolution, which is the reason these are fractions and not
        points.


        ⚠️ "VISIBLE" MEANS AFTER ROTATION. A page with `rotation: 90` is
        measured as it is drawn and as it is seen, not as it is stored — so
        these fractions are already right for the image in front of you and must
        not be rotated again. Join `Page` on `(document_id, page)` for the
        dimensions they are fractions of.


        ⚠️ TOP-LEFT IS NOT PDF’S CONVENTION. PDF measures from the bottom-left
        upward. This API does not, because what you are matching against is an
        image, and every image and every vision model is top-left origin. If
        your fields come out mirrored vertically, this is the paragraph you
        needed.
      properties:
        x:
          type: number
          description: >-
            The LEFT edge, as a fraction of the visible page width. 0 is the
            left edge of the page, 1 the right.
          examples:
            - 0.1024
        'y':
          type: number
          description: >-
            ⚠️ The TOP edge, as a fraction of the visible page height, measured
            DOWNWARD. 0 is the top of the page, 1 the bottom.
          examples:
            - 0.2153
        w:
          type: number
          description: Width, as a fraction of the visible page width. Always positive.
          examples:
            - 0.3201
        h:
          type: number
          description: Height, as a fraction of the visible page height. Always positive.
          examples:
            - 0.0261
      required:
        - x
        - 'y'
        - w
        - h
  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.

````