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

# Seeing a template

> The pages of a template's documents as PDF, and how each field's rect lands on them.

export const testKeyPrefix = "vsk_test_";

export const documentContentDisposition = "attachment; filename=\"document.pdf\"";

export const documentBusyCode = "service_unavailable";

export const documentTooComplexCode = "document_too_complex";

export const documentFaultCode = "internal_error";

export const documentNotFoundCode = "not_found";

export const documentReadScope = "documents:read";

[`GET /api/v1/templates/{templateId}`](/api-reference/get-template) says where every box on a
template is. Two endpoints serve what those boxes are drawn on: the pages themselves, as PDF. With
both, a person or a model can do what a person filling in the form does, and read the page to
decide which of your values belongs in which box. That matters on real forms, where a field is
named `checklist.17` and "ID or passport number" is printed beside several boxes on one page.

## The join

A template holds one or more documents, and each field says which document and which page it is on.

| Step | Where it comes from |
| - | - |
| The template's documents | `documents[].id` on the template |
| The page a field is on | `fields[].document_id` and `fields[].page` |
| The size of that page | the entry of `pages` with the same `document_id` and `page`: `width` and `height`, in points, already rotated |
| Where the field is | `fields[].rect`: `x`, `y`, `w` and `h` as fractions of that page, measured from its top-left corner, `y` downward |

`(document_id, page)` is the key throughout. A page number alone is not enough, because a template
whose pack holds several documents has a page 1 in each.

## One page, or the whole document

* [`GET /api/v1/documents/{documentId}/pages/{pageNumber}`](/api-reference/get-document-page)
  serves one page as a PDF. It carries that page and the fonts and images it uses, so it is usually
  far smaller than the whole document, and it is the one to use when you need a single page.
* [`GET /api/v1/documents/{documentId}/content`](/api-reference/get-document-content) serves every
  page of the document, in order, as one PDF, with
  <code>Content-Disposition: {documentContentDisposition}</code>.

Both need a key holding <code>{documentReadScope}</code> (or a key with full access). A test key
(<code>{testKeyPrefix}</code>) reads them exactly as a live key does: templates belong to the
organisation, not to an environment. Both serve only a document that a template of your
organisation holds, and only while that template is not archived.

For a template uploaded as a Word file, both serve the PDF Vumasign made from it, which is what
every `rect` is measured against. The Word file itself is never served here.

## What you get is a viewing copy

The PDF is rebuilt, not the file as it was uploaded: Vumasign makes the copy once, when the
document is uploaded (or, for a document uploaded before copies were made, the first time it is
asked for), and serves that copy every time after. It keeps each page's content and the
appearance saved with each annotation it keeps, so field boxes draw as they were saved, and it does
nothing. A box saved without an appearance of its own draws empty, and content the original keeps
on a hidden layer is shown, because layers are not carried. Fonts and images come across as they
are drawn, so open the copy in an up-to-date viewer, as you would any PDF.

* **Kept, as drawn:** the page's content, and annotations of these kinds only: `Text`, `Link`, `FreeText`, `Line`, `Square`, `Circle`, `Polygon`, `PolyLine`, `Highlight`, `Underline`, `Squiggly`, `StrikeOut`, `Stamp`, `Caret`, `Ink`, `Watermark`, `Widget`. Each keeps its position, its appearance and its plain properties, and nothing it could do.
* **Links:** a link keeps a go-to to a page that is in the same copy, or an address beginning `http://`, `https://` or `mailto:`. Any other link is still drawn and does nothing.
* **Removed:** everything else. That includes document-level scripts, the open action, embedded files, the uploader’s document metadata, the form itself, every page and annotation action, and annotations of any other kind, file attachments, sounds, movies, screens, rich media and 3D among them.

The form is not fillable. To put values into the boxes, send them as `values` when you
[create an envelope](/api-reference/create-envelope).

⚠️ **Upload blank forms.** A form that was filled in before it was uploaded still shows what was
typed into it on the pages these endpoints serve.

## Drawing the fields on a page

Your API key stays on your server. The server asks Vumasign for the template and the page and
hands them to your own front end; the browser renders the page with
[pdf.js](https://mozilla.github.io/pdf.js/) and outlines each field on it, labelled with its
`data_key`.

On the server (Node, with no dependencies):

```js theme={null}
import { createServer } from "node:http";

const API = "https://app.vumasign.com/api/v1";
const auth = { Authorization: `Bearer ${process.env.VUMASIGN_KEY}` };

// GET /template/{templateId}            -> the template, as JSON
// GET /page/{documentId}/{pageNumber}   -> one page, as PDF
createServer(async (req, res) => {
  // Put your own sign-in check here: these are your organisation's documents.
  const [, kind, id, page] = req.url.split("/");
  const upstream =
    kind === "template" ? `${API}/templates/${id}`
    : kind === "page" ? `${API}/documents/${id}/pages/${page}`
    : null;
  if (!upstream) return res.writeHead(404).end();
  const answer = await fetch(upstream, { headers: auth });
  res.writeHead(answer.status, {
    "content-type": answer.headers.get("content-type") ?? "application/octet-stream",
    "cache-control": "private, no-store",
  });
  res.end(Buffer.from(await answer.arrayBuffer()));
}).listen(3000);
```

In the browser, from your own origin:

```js theme={null}
import * as pdfjs from "pdfjs-dist";

pdfjs.GlobalWorkerOptions.workerSrc = new URL("pdfjs-dist/build/pdf.worker.min.mjs", import.meta.url).href;

export async function drawFields(templateId, documentId, pageNumber, container) {
  const template = await (await fetch(`/template/${templateId}`)).json();
  const response = await fetch(`/page/${documentId}/${pageNumber}`);
  if (!response.ok) throw new Error((await response.json()).error.message);
  const pdf = await pdfjs.getDocument({ data: await response.arrayBuffer() }).promise;
  const page = await pdf.getPage(1); // the response holds one page

  const viewport = page.getViewport({ scale: 1.5 });
  const canvas = document.createElement("canvas");
  canvas.width = viewport.width;
  canvas.height = viewport.height;
  await page.render({ canvasContext: canvas.getContext("2d"), viewport }).promise;

  // Check the render against the size the API measured the rects against.
  const size = template.pages.find((p) => p.document_id === documentId && p.page === pageNumber);
  const ratio = (viewport.width / viewport.height) / (size.width / size.height);
  if (Math.abs(ratio - 1) > 0.01) throw new Error("the renderer did not honour the page rotation");

  const context = canvas.getContext("2d");
  context.strokeStyle = "#d00";
  context.fillStyle = "#d00";
  context.font = "12px sans-serif";
  for (const field of template.fields) {
    if (field.document_id !== documentId || field.page !== pageNumber) continue;
    const x = field.rect.x * viewport.width;
    const y = field.rect.y * viewport.height;
    const w = field.rect.w * viewport.width;
    const h = field.rect.h * viewport.height;
    context.strokeRect(x, y, w, h);
    context.fillText(field.data_key ?? field.label ?? field.type, x, y - 2);
  }

  container.append(canvas);
}
```

Because `rect` is a fraction of the page as displayed, it is multiplied by the size of the
rendered page, whatever scale you render at. `pages[].rotation` is information, not an
instruction: `width`, `height` and every `rect` already describe the page as it is displayed, so
do not rotate them again.

## Refusals

Every reason you cannot have a document is the same refusal, <code>{documentNotFoundCode}</code>:
an id that names nothing, another organisation's document, a document no live template of yours
holds, and an id that is not a uuid. "This exists but is not yours" would itself be a disclosure.
The page endpoint also answers <code>{documentNotFoundCode}</code> for a page number the document
does not have, and that one message says how many pages there are, because by then the document
has been found under your key. <code>{documentFaultCode}</code> means the stored file could not be
produced or was not the document recorded; it is ours, not yours.

Every uploaded PDF is read in an isolated service, one document at a time, with a fixed memory
limit and a fixed time limit. A document that needs more than either is refused at upload with
<code>{documentTooComplexCode}</code>, and nothing is stored. A document stored before uploads
were checked this way can meet the same refusal here, the first time its copy is made; its
template, with every field and where it is, is still readable. While that service is starting up,
at capacity, or slow to answer, these endpoints answer <code>{documentBusyCode}</code> with
`Retry-After`; retry after that many seconds.

| Code | HTTP | Retry? | What it means |
| - | - | - | - |
| `unauthenticated` | 401 | no | 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. |
| `forbidden` | 403 | no | 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` | 403 | no | 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. |
| `not_found` | 404 | no | 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. |
| `document_too_complex` | 422 | no | The document needs more memory or time to read than one document is allowed, what reading it produced is larger than one document may produce, or it could not be read through at all. Every uploaded PDF is read in an isolated service, one document at a time, under a fixed memory limit and a fixed time limit, and this one went past one of them; the message says which. Because nothing else was running there, the same document fails the same way. On an upload (`POST /api/v1/templates`, `POST /api/v1/envelopes/one-off`) nothing was stored or created; printing the document to a new PDF usually makes it simpler. On `GET /api/v1/documents/{documentId}/pages/{pageNumber}` or `.../content`, the document was stored before uploads were checked this way and its viewing copy cannot be made; the template, with every field and its position, is still readable from `GET /api/v1/templates/{templateId}`. Retrying gives the same answer. |
| `rate_limited` | 429 | Exponential from 1s, doubling, with jitter. Honour `Retry-After` when present. | 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. |
| `internal_error` | 500 | Exponential from 1s, at most three attempts, then stop and alert a human. | 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.) |
| `service_unavailable` | 503 | Honour `Retry-After` when the answer carries one; otherwise exponential from 5s, doubling, for a few minutes. Nothing was created. | Ours, and briefly. Something this operation depends on was not answering, or was already doing as much as it will at once. What it was depends on the operation: on a create, the service that renders a Word document to a PDF, or the isolated service every uploaded document is read in, when it is starting up, at capacity, or did not answer in time; on `GET .../documents/{position}/draft`, the service that renders the draft itself; on the two template-document reads (`GET /api/v1/documents/{documentId}/pages/{pageNumber}` and `.../content`), that same isolated service, which says so with `Retry-After`. ⚠️ THE DISTINCTION FROM 400 IS THE WHOLE POINT: your request is fine and your document is fine, so do not go and re-save it. On a create, nothing was created and no allowance was spent. On an operation that takes an `Idempotency-Key`, retry the identical request with the SAME one — a completed key replays, so a retry can never make a second envelope or a second template even if the first attempt got further than this answer suggests. A new key could. ⚠️ ON THE DRAFT 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. |


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