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

# The document as it stands, mid-flight, stamped DRAFT.

> THE DOCUMENT WITH EVERY ANSWER SO FAR ON IT, WHILE IT IS STILL OUT FOR SIGNATURE. The operation above serves the document as originally sent and, once sealing has landed, the executed copy — and between those two moments there was nothing to fetch. This is that gap: the pages carry every signature, initial and answer of every party who has already signed or approved, every page is stamped DRAFT, and one page is appended listing who is still outstanding and what each of them is waiting to do.

⚠️ IT ANSWERS `application/pdf` DIRECTLY — THERE IS NO `url`, AND THE DIFFERENCE IS FORCED RATHER THAN CHOSEN. The operation above returns a short-lived `url` because the bytes it serves are at rest and a credential is how you reach them. A draft is rendered while you wait and is never stored anywhere, so there is nothing for a credential to point at. `Content-Disposition: attachment` carries a filename built from the envelope’s title.

⚠️ IT IS A SNAPSHOT AND IT IS NOT EVIDENCE. Nothing is stored, nothing is recorded and no seal is applied, so the bytes attest to nothing about themselves — the appended page says so in as many words. Two calls a second apart can differ, because a party may have signed in between. The artefact that IS evidence is the sealed copy, and it is served by the operation above once `envelope.sealed` has fired.

## What it costs

⚠️ A DRAFT IS A FULL RENDER AND IS RATE LIMITED SEPARATELY FROM YOUR KEY’S MINUTE. Two ceilings apply, both per minute: one on the ENVELOPE, so a single record page cannot spend your whole allowance, and one on the ORGANISATION, shared by every envelope and every key you hold. `rate_limited` names which of the two refused and `Retry-After` says how long to wait. ⚠️ THE `RateLimit-*` HEADERS DO **NOT** DESCRIBE THESE BUCKETS — they mean here exactly what they mean on every other operation in this API, which is your key’s own per-minute allowance. Nothing is billed either way: a draft consumes no envelope and no document.

⚠️ A RENDER THAT FAILS HAS STILL SPENT ITS PLACE. The charge is taken before the renderer is asked, so a `service_unavailable` from this operation has counted against both ceilings, and it is not refunded — the counter only ever goes up, by design, so that nothing can talk the limiter out of what it has seen. An `internal_error` may or may not have been charged: some arise before the charge is taken (including a failure to take it) and some after, and the answer does not say which — so budget for it as though it counted. Retry a `service_unavailable` on its published backoff, not in a loop.

## When there is no draft to take

`draft_not_available` (422) covers both of them and the `message` says which. An envelope still in `draft` has asked nobody to do anything, so the render would be your original with nothing written on it. A document that has already SEALED has a final executed copy, which is strictly better than a draft of it. Either way the next request is the same one: `GET /api/v1/envelopes/{envelopeId}/documents`.

⚠️ A `voided`, `declined` OR `expired` ENVELOPE STILL RENDERS. None of those ever completes and none ever seals, so a draft is the only copy of them that will ever exist — and a contract that died part-signed is exactly the thing somebody needs to file.

## The rest of the rules

`position` is 1-based, matching what a sender is shown — "Document 2 of 3" — and ordinarily there is exactly one.

⚠️ `documents:read`, NOT `envelopes:read`. A key that may read an envelope's status is not thereby a key that may fetch what it holds, and a draft is what it holds more than the original is, because a draft carries other parties’ signatures.

⚠️ A `vsk_test_` KEY MAY READ THE STATUS OF ANY ENVELOPE IN ITS ORGANISATION BUT MAY ONLY DRAFT A SANDBOX ONE — `test_key_cannot_read_documents` otherwise. A part-signed real contract is still a real contract.

⚠️ NO `Idempotency-Key`. Nothing is created, so there is nothing to replay; retrying is a second render and spends the ceilings above.

⚠️ FOUR SITUATIONS ANSWER WITH THE SAME 404, the disclosure rule this namespace uses everywhere: no such envelope anywhere; one belonging to another organisation; an id that is not a uuid; and a `position` this envelope has no document at. "It exists but is not yours" is itself the secret.



## OpenAPI

````yaml /openapi.json get /api/v1/envelopes/{envelopeId}/documents/{position}/draft
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/envelopes/{envelopeId}/documents/{position}/draft:
    get:
      tags:
        - Envelopes
      summary: The document as it stands, mid-flight, stamped DRAFT.
      description: >-
        THE DOCUMENT WITH EVERY ANSWER SO FAR ON IT, WHILE IT IS STILL OUT FOR
        SIGNATURE. The operation above serves the document as originally sent
        and, once sealing has landed, the executed copy — and between those two
        moments there was nothing to fetch. This is that gap: the pages carry
        every signature, initial and answer of every party who has already
        signed or approved, every page is stamped DRAFT, and one page is
        appended listing who is still outstanding and what each of them is
        waiting to do.


        ⚠️ IT ANSWERS `application/pdf` DIRECTLY — THERE IS NO `url`, AND THE
        DIFFERENCE IS FORCED RATHER THAN CHOSEN. The operation above returns a
        short-lived `url` because the bytes it serves are at rest and a
        credential is how you reach them. A draft is rendered while you wait and
        is never stored anywhere, so there is nothing for a credential to point
        at. `Content-Disposition: attachment` carries a filename built from the
        envelope’s title.


        ⚠️ IT IS A SNAPSHOT AND IT IS NOT EVIDENCE. Nothing is stored, nothing
        is recorded and no seal is applied, so the bytes attest to nothing about
        themselves — the appended page says so in as many words. Two calls a
        second apart can differ, because a party may have signed in between. The
        artefact that IS evidence is the sealed copy, and it is served by the
        operation above once `envelope.sealed` has fired.


        ## What it costs


        ⚠️ A DRAFT IS A FULL RENDER AND IS RATE LIMITED SEPARATELY FROM YOUR
        KEY’S MINUTE. Two ceilings apply, both per minute: one on the ENVELOPE,
        so a single record page cannot spend your whole allowance, and one on
        the ORGANISATION, shared by every envelope and every key you hold.
        `rate_limited` names which of the two refused and `Retry-After` says how
        long to wait. ⚠️ THE `RateLimit-*` HEADERS DO **NOT** DESCRIBE THESE
        BUCKETS — they mean here exactly what they mean on every other operation
        in this API, which is your key’s own per-minute allowance. Nothing is
        billed either way: a draft consumes no envelope and no document.


        ⚠️ A RENDER THAT FAILS HAS STILL SPENT ITS PLACE. The charge is taken
        before the renderer is asked, so a `service_unavailable` from this
        operation has counted against both ceilings, and it is not refunded —
        the counter only ever goes up, by design, so that nothing can talk the
        limiter out of what it has seen. An `internal_error` may or may not have
        been charged: some arise before the charge is taken (including a failure
        to take it) and some after, and the answer does not say which — so
        budget for it as though it counted. Retry a `service_unavailable` on its
        published backoff, not in a loop.


        ## When there is no draft to take


        `draft_not_available` (422) covers both of them and the `message` says
        which. An envelope still in `draft` has asked nobody to do anything, so
        the render would be your original with nothing written on it. A document
        that has already SEALED has a final executed copy, which is strictly
        better than a draft of it. Either way the next request is the same one:
        `GET /api/v1/envelopes/{envelopeId}/documents`.


        ⚠️ A `voided`, `declined` OR `expired` ENVELOPE STILL RENDERS. None of
        those ever completes and none ever seals, so a draft is the only copy of
        them that will ever exist — and a contract that died part-signed is
        exactly the thing somebody needs to file.


        ## The rest of the rules


        `position` is 1-based, matching what a sender is shown — "Document 2 of
        3" — and ordinarily there is exactly one.


        ⚠️ `documents:read`, NOT `envelopes:read`. A key that may read an
        envelope's status is not thereby a key that may fetch what it holds, and
        a draft is what it holds more than the original is, because a draft
        carries other parties’ signatures.


        ⚠️ A `vsk_test_` KEY MAY READ THE STATUS OF ANY ENVELOPE IN ITS
        ORGANISATION BUT MAY ONLY DRAFT A SANDBOX ONE —
        `test_key_cannot_read_documents` otherwise. A part-signed real contract
        is still a real contract.


        ⚠️ NO `Idempotency-Key`. Nothing is created, so there is nothing to
        replay; retrying is a second render and spends the ceilings above.


        ⚠️ FOUR SITUATIONS ANSWER WITH THE SAME 404, the disclosure rule this
        namespace uses everywhere: no such envelope anywhere; one belonging to
        another organisation; an id that is not a uuid; and a `position` this
        envelope has no document at. "It exists but is not yours" is itself the
        secret.
      operationId: getEnvelopeDocumentDraft
      parameters:
        - name: envelopeId
          in: path
          required: true
          description: The envelope’s `id`, as returned by `POST /api/v1/envelopes`.
          schema:
            type: string
            format: uuid
            examples:
              - 01960000-0000-4000-8000-0000000000e5
            description: An envelope id.
        - name: position
          in: path
          required: true
          description: >-
            Which of the envelope’s documents, 1-based, as
            `documents[].position` reports it from `GET
            /api/v1/envelopes/{envelopeId}/documents`. ⚠️ There is one spelling
            of each position: `01`, `1.0` and ` 1` are refused rather than read
            as 1, so that one document does not answer to five URLs.
          schema:
            type: integer
            minimum: 1
            examples:
              - 1
            description: Which document of the envelope.
      responses:
        '200':
          description: >-
            The document as it stands. `Content-Disposition: attachment;
            filename="<title> (draft).pdf"`, and `Cache-Control: no-store` — a
            draft stops being true the moment the next party acts, so nothing
            may keep a copy of this response.
          content:
            application/pdf:
              schema:
                type: string
                format: binary
                description: >-
                  The PDF bytes. ⚠️ NOT JSON — like `GET
                  /api/v1/documents/{id}/pages/{n}`, this operation’s success
                  body is not a resource. Its refusals still are: every non-2xx
                  is 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.
        '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.


            `test_key_cannot_read_documents` — A `vsk_test_` key asked `GET
            /api/v1/envelopes/{id}/documents`, or a draft render of one of them,
            for an envelope that is not a sandbox envelope. A test key may read
            the STATUS of any envelope in its organisation — that emails nobody
            and spends no allowance — but the documents endpoint hands over the
            bytes of a signed contract, and a sandbox is meant to be rehearsable
            without ever touching something real, and a draft is those bytes
            with the answers so far drawn on. Fetch this envelope’s documents
            with a live key, or fetch the documents of an envelope a test key
            created.
          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.
        '422':
          description: >-
            `draft_not_available` — There is no draft of this envelope to
            render, and the `message` says which of the two reasons it is.
            Either it is still a DRAFT — nobody has been asked to do anything,
            so the render would be your original document with nothing written
            on it — or it has already SEALED, and the final executed copy
            exists. ⚠️ ONE CODE FOR BOTH BECAUSE THE NEXT REQUEST IS THE SAME
            ONE: `GET /api/v1/envelopes/{envelopeId}/documents` serves the
            original in the first case and the sealed copy in the second. What
            differs is what you do with it — send the envelope, or take the
            executed contract — and that is what the sentence tells you.
            Retrying cannot help in either case: a sealed envelope never
            unseals, and a draft stays a draft until somebody sends it. ⚠️ A
            VOIDED, DECLINED OR EXPIRED envelope is NOT refused here — a
            contract that died part-signed is exactly the thing this endpoint
            exists to hand you.
          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:
    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
  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.

````