# Templates

> Create reusable document templates with signer roles, fields, and merge variables

Source: https://docs.korala.ai/guides/templates

---

Templates define a reusable document layout: signer roles, field positions, and merge variables. Generate ready-to-sign documents from them without reconfiguring fields each time.

In the dashboard, open **Templates → My templates**. Open **Templates →
Standard forms** to browse the forms library, or choose **New Template →
Use standard form**.

## How Templates Work

1. **Upload** a PDF or DOCX file as the base document
2. **Define signer roles** (e.g., "Client", "Approver"), placeholders you map to real people when you create a document
3. **Place fields** on the template and assign each to a signer role
4. **Create documents** from the template by mapping real signers to roles

```mermaid
flowchart LR
    A[Upload PDF/DOCX] --> B[Add Signer Roles]
    B --> C[Place Fields]
    C --> D[Create Documents]
    D --> E[Send for Signing]
```

## Creating a Template

Template creation follows the same two-step upload flow as documents:

    ```typescript
    import { KoralaClient } from '@korala/api-client';
    import fs from 'fs';

    const korala = new KoralaClient({
      apiKeyId: 'your-api-key-id',
      apiSecret: 'your-api-secret',
    });

    // Step 1: Get upload URL
    const { templateId, uploadUrl, key } = await korala.templates.createUploadUrl({
      filename: 'nda-template.pdf',
      contentType: 'application/pdf',
    });

    // Step 2: Upload the file
    const pdfBuffer = fs.readFileSync('nda-template.pdf');
    await fetch(uploadUrl, {
      method: 'PUT',
      body: pdfBuffer,
      headers: { 'Content-Type': 'application/pdf' },
    });

    // Step 3: Confirm and set metadata
    const template = await korala.templates.confirmUpload(templateId, {
      name: 'NDA Template',
      description: 'Standard non-disclosure agreement',
    });

    console.log(`Template created: ${template.id}`);
    ```
    ```bash
    # Step 1: Get upload URL
    UPLOAD_RESPONSE=$(api_request "POST" "/api/v1/templates/upload-url" \
      '{"filename":"nda-template.pdf","contentType":"application/pdf"}')

    TEMPLATE_ID=$(echo $UPLOAD_RESPONSE | jq -r '.templateId')
    UPLOAD_URL=$(echo $UPLOAD_RESPONSE | jq -r '.uploadUrl')

    # Step 2: Upload the file
    curl -X PUT "$UPLOAD_URL" \
      -H "Content-Type: application/pdf" \
      --data-binary @nda-template.pdf

    # Step 3: Confirm upload
    api_request "POST" "/api/v1/templates/${TEMPLATE_ID}/confirm" \
      '{"name":"NDA Template","description":"Standard non-disclosure agreement"}'
    ```

Templates support both PDF and DOCX files. Korala converts DOCX files to PDF for preview and detects `{{placeholder}}` merge fields.

## Signer Roles

Signer roles are named placeholders that define *who* needs to sign, without specifying the actual person. When you create a document from a template, you map real signers to these roles.

### Adding Roles

    ```typescript
    // Add roles with signing order
    const client = await korala.templates.addSignerRole(templateId, {
      name: 'Client',
      signingOrder: 1,
    });

    const approver = await korala.templates.addSignerRole(templateId, {
      name: 'Approver',
      signingOrder: 2,
    });
    ```
    ```bash
    # Add first role
    api_request "POST" "/api/v1/templates/${TEMPLATE_ID}/signer-roles" \
      '{"name":"Client","signingOrder":1}'

    # Add second role
    api_request "POST" "/api/v1/templates/${TEMPLATE_ID}/signer-roles" \
      '{"name":"Approver","signingOrder":2}'
    ```

### Managing Roles

```typescript
// List all roles
const roles = await korala.templates.getSignerRoles(templateId);

// Update a role
await korala.templates.updateSignerRole(templateId, roleId, {
  name: 'Company Representative',
  signingOrder: 2,
});

// Delete a role
await korala.templates.deleteSignerRole(templateId, roleId);
```

## Template Fields

Fields define where signers need to fill in information. Each field is positioned on a specific page and assigned to a signer role.

### Adding Fields

    ```typescript
    // Add a signature field for the Client role
    await korala.templates.addField(templateId, {
      signerRoleId: clientRoleId,
      fieldType: 'signature',
      pageNumber: 1,
      xPosition: 100,
      yPosition: 650,
      width: 200,
      height: 50,
    });

    // Add a date field
    await korala.templates.addField(templateId, {
      signerRoleId: clientRoleId,
      fieldType: 'date',
      pageNumber: 1,
      xPosition: 350,
      yPosition: 650,
      width: 150,
      height: 30,
    });

    // Add a text field with a merge variable
    await korala.templates.addField(templateId, {
      signerRoleId: clientRoleId,
      fieldType: 'text',
      pageNumber: 1,
      xPosition: 100,
      yPosition: 200,
      width: 300,
      height: 30,
      mergeFieldName: 'company_name',
      defaultValue: 'Acme Corp',
    });
    ```
    ```bash
    api_request "POST" "/api/v1/templates/${TEMPLATE_ID}/fields" \
      '{
        "signerRoleId": "'$CLIENT_ROLE_ID'",
        "fieldType": "signature",
        "pageNumber": 1,
        "xPosition": 100,
        "yPosition": 650,
        "width": 200,
        "height": 50
      }'
    ```

### Field Types

| Type | Description |
|------|-------------|
| `signature` | Drawn or uploaded signature image |
| `initials` | Signer's initials |
| `date` | Date value |
| `text` | Free-text input |
| `checkbox` | Boolean checkbox |

### Signer-Fillable Fields

Template fields work without merge variables. Omit `mergeFieldName` and
`defaultValue`, and each document generated from the template carries an
**empty** field for the signer to fill in, in the signing UI or the embedded
iframe. Use this to collect information your application doesn't have,
instead of asking those questions in your own forms. See
[Collecting Information from Signers](https://docs.korala.ai/guides/fields#collecting-information-from-signers).

```typescript
// The signer types this in during signing, no variable needed
await korala.templates.addField(templateId, {
  signerRoleId: clientRoleId,
  fieldType: 'text',
  label: 'Phone number', // prompt shown to the signer
  pageNumber: 1,
  xPosition: 100,
  yPosition: 250,
  width: 200,
  height: 30,
});
```

How a generated document treats each template field:

| Template field | Generated document field |
|----------------|--------------------------|
| Matching `variables` value at generation | Prefilled and **locked** (signer can't edit) |
| No variable, `defaultValue` set | Prefilled with the default and locked |
| Prefilled with `editable: true` | Prefilled with the value as a **suggestion**; the signer can change it |
| No variable, no default | Empty; the signer fills it in the signing UI |
| `checkbox` default without `editable` | Ignored; the checkbox stays empty for the signer |

Assign signer-fillable fields to a signer role. Fields are required by
default, so the signer must fill them in before completing signing.

### Editable Defaults

Set `editable: true` on a `text`, `date`, or `checkbox` field when a
prefilled value is your best guess rather than a fact. The generated field
shows the value, and the signer can correct it in the signing UI or the
embedded iframe before signing:

```typescript
await korala.templates.addField(templateId, {
  signerRoleId: clientRoleId,
  fieldType: 'text',
  label: 'Company name',
  defaultValue: 'Acme Corp', // shown as a suggestion
  editable: true, // the signer can change it
  pageNumber: 1,
  xPosition: 100,
  yPosition: 300,
  width: 200,
  height: 30,
});
```

Editable-default semantics:

- An untouched default counts as complete: the signer can sign without
  changing it, and the value is stamped into the final PDF.
- The signer can change the value any number of times before signing; once
  they sign, the values are final.
- Editable checkboxes toggle. Clearing a **required** checkbox blocks
  signing until the signer checks it again.
- Checkbox defaults require `editable: true`. A locked pre-checked box would
  satisfy a required consent checkbox without signer action, so Korala
  ignores non-editable checkbox defaults.
- Without `editable`, prefilled values stay read-only. The API enforces
  this: filling a locked field returns `400`.
- `editable` applies to `text`, `date`, and `checkbox` fields; setting it on
  signature or initials fields returns `400`.

### Managing Fields

```typescript
// List all fields
const fields = await korala.templates.getFields(templateId);

// Update field position or assigned role
await korala.templates.updateField(templateId, fieldId, {
  xPosition: 150,
  yPosition: 700,
  signerRoleId: newRoleId,
});

// Delete a field
await korala.templates.deleteField(templateId, fieldId);
```

## DOCX Merge Fields

When you upload a DOCX template, Korala detects `{{placeholder}}` merge fields in the document text and substitutes your variable values when generating documents.

```typescript
// Get detected merge fields
const mergeFields = await korala.templates.getMergeFields(templateId);

console.log(mergeFields);
// [
//   { name: 'company_name', occurrences: 3 },
//   { name: 'effective_date', occurrences: 1 },
//   { name: 'client_address', occurrences: 2 },
// ]
```

Merge fields are only available for DOCX templates. PDF templates use positioned fields instead.

## Anchor Placement (DOCX)

Absolute `xPosition`/`yPosition` coordinates are fragile on DOCX templates: when
Korala substitutes merge fields, the document reflows and repaginates, so a
coordinate captured against the preview may no longer line up. **Anchor
placement** fixes this: the field attaches to a literal string in the document
body and lands wherever that string renders, regardless of reflow or page
count. This is the same model as DocuSign "anchor tabs".

Set `placement: 'anchor'` and provide an `anchorText`:

```typescript
await korala.templates.addField(templateId, {
  signerRoleId: fundLeadRole.id,
  fieldType: 'signature',
  placement: 'anchor',
  anchorText: 'Fund Lead Signature:',
  anchorPosition: 'right', // place the field to the right of the matched text
  anchorOffsetX: 8,
  width: 200,
  height: 50,
});
```

| Property | Type | Description |
|----------|------|-------------|
| `placement` | enum | `coordinate` (default) or `anchor` |
| `anchorText` | string | Literal string to locate in the rendered document. Must not contain `{{ }}`. |
| `anchorPosition` | enum | Where the field sits relative to each match: `replace`, `left`, `right`, `above`, `below` |
| `anchorOffsetX` / `anchorOffsetY` | number | Offset in points applied after resolution |
| `hideAnchor` | boolean | White-out a visible marker after resolving it |
| `anchorMatchWholeWord` | boolean | Match only at word boundaries. Off by default |

### What `anchorPosition` measures from

Korala locates the anchor with `pdftotext -bbox`, which reports each match's
**glyph bounding box** — font ascent to descent — not its baseline. Positions
attach to that box:

| `anchorPosition` | Result |
|------------------|--------|
| `replace` | Field top-left at the box top-left |
| `above` | Field **bottom** on the box **top** |
| `below` | Field **top** on the box **bottom** |
| `left` | Field **right edge** on the box **left edge**, tops aligned |
| `right` | Field **left edge** on the box **right edge**, tops aligned |

This matters most when anchoring to a ruled line made of underscores.
Underscore glyphs sit at the bottom of their box, so the box top is roughly one
ascender — about 11pt at a 12pt font — above the rule you can see. Anchoring
`above` such a line places the field that much higher than expected; correct it
with `anchorOffsetY`.

Korala applies the offsets last, in the top-left coordinate space the rest of
the API uses:

- Positive `anchorOffsetX` moves the field **right**, negative **left**.
- Positive `anchorOffsetY` moves the field **down**, negative **up**.

Key behaviors:

- **Each occurrence gets a field.** If the anchor string appears three times,
  you get three fields. Use a unique marker if you want exactly one.
- **Matching is substring by default.** `By:` also matches inside `Standby:`.
  Set `anchorMatchWholeWord: true` to match only at word boundaries; it is off
  by default so that tightening it never silently drops a field that resolves
  today.
- **Coordinate fields still work.** Mix anchor and coordinate fields freely. For
  coordinate fields on documents whose page count varies, set `pageNumber: -1` to
  target the last page.

### Hidden markers stay in the text layer

Both ways of hiding an injected marker hide it without removing it:

- White text in the Word document renders invisible, and `pdftotext` still
  extracts it.
- `hideAnchor: true` draws an opaque white rectangle over the resolved match
  and leaves the text run underneath alone.

The anchor string therefore survives into the signed PDF, where `pdftotext`,
copy-paste, in-viewer search and any indexer still surface it:

```bash
pdftotext executed-agreement.pdf - | head
KoralaSigFundLead
KoralaNameFundLead
...
```

For agreements you archive or send to counterparties, do not inject markers.
Korala cannot strip one from the output. Anchor on text that **already belongs
in the document** — a caption like `Fund Lead Signature:` — or use
[bookmark placement](#bookmark-placement-docx), which carries a position
without carrying text and is the better answer when the document repeats a
signature block.

Korala resolves anchor placement on each generation, so fields stay correct as
merge content changes, with no preview clicking.

## Bookmark Placement (DOCX)

Anchor placement needs the anchor string to be unique. A multi-party agreement
usually repeats its signature block verbatim:

```
THE BUYER:
{{buyerLegalName}}
By: ___________  Name: ___________  Title: ___________

THE SELLER:
{{sellerLegalName}}
By: ___________  Name: ___________  Title: ___________
```

`By:` and `Name:` appear once per block and nothing static tells the blocks
apart, so anchoring means injecting a unique marker into each one — and a
marker, hidden or not, stays in the executed PDF.

**Bookmark placement** avoids that. A Word bookmark carries a name and a
position but no text, so it survives conversion as a PDF destination and leaves
nothing behind:

```typescript
await korala.templates.addField(templateId, {
  signerRoleId: sellerRole.id,
  fieldType: 'signature',
  placement: 'bookmark',
  bookmarkName: 'SigSeller',
  anchorOffsetY: -4,
  width: 200,
  height: 24,
});
```

Insert the bookmark in Word with **Insert → Bookmark**, at the point where the
field belongs. Word keeps bookmark names unique within a document, so a
bookmark always resolves to exactly one position — there is no "which
occurrence" question to answer.

| Property | Type | Description |
|----------|------|-------------|
| `placement` | enum | `bookmark` |
| `bookmarkName` | string | Word bookmark whose position locates the field. Required |
| `anchorPosition` | enum | Where the field sits relative to the bookmark point |
| `anchorOffsetX` / `anchorOffsetY` | number | Offset in points, same convention as anchors |

### Geometry

A bookmark has no glyph box, so it resolves to a **point**, and the field
attaches to it as though it were a zero-width anchor at that spot. The point
lands on the top of the line's glyph box — the same y a text anchor on that line
resolves to — so offsets calibrated against a text anchor transfer unchanged:

| `anchorPosition` | Result |
|------------------|--------|
| `replace` | Field top-left on the point |
| `above` | Field **bottom** on the point |
| `left` | Field **right edge** on the point |
| `below` / `right` | Same as `replace` — there is no far edge to attach to |

A bookmark placed mid-line resolves to that inline position, so you can put a
field on the `Name:` blank rather than at the start of the line. Bookmarks
inside table cells work too, which is how most signature blocks are laid out.

### Naming

Use **letters, digits and hyphens**. The converter rewrites every other
character to a hex code, so a bookmark named `Sig_Seller` is stored in the PDF
as `Sig5FSeller`. Korala translates the name you pass, so
`bookmarkName: 'Sig_Seller'` resolves correctly.

The rewrite is not reversible, though, and two names can collapse onto one. A
document containing both `Sig_Seller` and `Sig5FSeller` converts to a single
destination, and a field bound to either lands wherever the survivor is —
which on a signature page means the wrong party's line. `getBookmarks` reports
any such clash:

```typescript
const { bookmarks, ambiguous } = await korala.templates.getBookmarks(templateId);
// ambiguous: [{ key: 'Sig5FSeller', names: ['Sig_Seller', 'Sig5FSeller'] }]
```

Sticking to letters, digits and hyphens makes the rewrite a no-op and the clash
impossible.

Korala does not validate names beyond requiring one. Real documents carry
bookmarks that break Word's documented rules — a Google Docs export leaves
`_heading=h.gjdgxs` behind — and rejecting those would block templates that render
correctly.

### Seeing what resolved

Bookmarks hide in Word unless you enable **Show bookmarks** in Advanced
options, and never appear in the PDF at all. When a field doesn't show up, list
what the rendered document exposes:

```typescript
const { bookmarks, missing, ambiguous } = await korala.templates.getBookmarks(templateId);
// bookmarks: [{ name: 'SigSeller', pageNumber: 1, x: 72, y: 139.3 }, ...]
// missing:   ['_Ref418700706']
```

`bookmarks` is every destination in the rendered template, including ones Word
added on its own for cross-references and tables of contents, each shown in the
form the converter rewrote it to.

This renders the template as authored, with merge tokens left unsubstituted, so
read the coordinates as "where the bookmark sits in the blank template" rather
than where a field will land in a generated document. A bookmark inside content
that a merge can drop is still listed here.

`missing` is the more important half. It lists names declared in the DOCX that
reached no destination, which the bookmark list alone cannot show you: a field
bound to such a name places nothing, and everything else still looks correct.
Treat one of your own bookmarks appearing here as a template setup failure, the
same way you would treat an undetected merge token.

**Put a bookmark after a Word field code in the same paragraph and the
converter drops it.** This is the one that bites legal templates: a clause
containing a cross-reference (`REF`), a page number (`PAGE`) or any other
field, ending with the bookmark that some other cross-reference points at. Put
your bookmark before the field, or in a paragraph that has none.

```
paragraph text … REF field … BOOKMARK     bookmark is lost
paragraph text … BOOKMARK … REF field     bookmark survives
BOOKMARK … paragraph text … REF field     bookmark survives
```

A field in a different paragraph does not affect it, and an unterminated field
behaves the same as a complete one. We measured this with `REF`, `PAGE`, `TOC`,
`HYPERLINK`, `SEQ` and `DATE`; every one of them loses a bookmark that follows
it, so treat it as applying to fields generally rather than to a list.

The other cause we can reproduce is a `bookmarkEnd` that appears before its
`bookmarkStart` in `word/document.xml`. These all survive conversion, so none
of them explains a loss when you go looking: duplicate ids, duplicate names,
zero-length bookmarks, bookmarks inside tracked deletions or hyperlinks, a
bookmark at a paragraph end, before a section break, at the end of a table
cell, and several distinct names sharing one position.

The list is empirical, so check `missing` rather than reasoning about which
constructs are safe.

Bookmark placement is built for DOCX templates, where you control the source
document and can insert bookmarks. An uploaded PDF works only if it already
carries named destinations, which one exported from Word sometimes does — call
`getBookmarks` to find out. Otherwise PDF templates use anchor or coordinate
placement.

## Creating Documents from Templates

`signers` is an **object keyed by role**, not an array. Each key is a signer
role ID or the role's name (matched case-insensitively), and each value is the
real person filling that role:

    ```typescript
    const document = await korala.templates.createDocument(templateId, {
      name: 'NDA — Acme Corp',
      signers: {
        [clientRoleId]: {
          name: 'Jane Smith',
          email: 'jane@acme.com',
        },
        // Or key by role name instead of ID
        Approver: {
          name: 'Bob Johnson',
          email: 'bob@company.com',
          externalId: 'user_456', // optional, addresses this signer later
        },
      },
      variables: {
        company_name: 'Acme Corp',
        effective_date: '2024-03-01',
        client_address: '123 Main St',
      },
    });

    console.log(`Document created: ${document.id}`);
    console.log(`Status: ${document.status}`); // 'draft'
    ```
    ```bash
    api_request "POST" "/api/v1/templates/${TEMPLATE_ID}/create-document" \
      '{
        "name": "NDA — Acme Corp",
        "signers": {
          "'$CLIENT_ROLE_ID'": {"name": "Jane Smith", "email": "jane@acme.com"},
          "Approver": {"name": "Bob Johnson", "email": "bob@company.com"}
        },
        "variables": {
          "company_name": "Acme Corp",
          "effective_date": "2024-03-01"
        }
      }'
    ```

Every role on the template must be assigned, or the call fails with
`Missing signer assignment for role "..."`. A template with exactly one role
and exactly one signer entry auto-maps, whatever the key is named.

The same person can fill several roles — assign the same name and email to
each. That produces one signer per role, so give them distinct `externalId`
values if you plan to address them individually later (see
[Bulk Signing](https://docs.korala.ai/guides/bulk-signing#when-one-person-holds-several-roles)).

The created document is in `draft` status. [Send it for signing](https://docs.korala.ai/guides/documents#sending-for-signatures) to start the signing workflow.

## Generating PDFs Without Signing

Generate a filled PDF from a template without starting a signing workflow. Useful for previews or non-signing document generation:

```typescript
const result = await korala.templates.generate(templateId, {
  variables: {
    company_name: 'Acme Corp',
    effective_date: '2024-03-01',
  },
});

console.log(`Download: ${result.url}`); // expires after 1 hour (result.expiresAt)
```

## Managing Templates

```typescript
// List templates (paginated: { data, total, page, limit, totalPages })
const { data: templates, totalPages } = await korala.templates.list({
  page: 1,
  limit: 20,
});

// Only sandbox templates (created with a test-mode API key)
const testTemplates = await korala.templates.list({ sandbox: true });

// Search by name or description — case-insensitive substring match.
// Combines with the mode filter and pagination; `total` counts every match,
// not just the page you asked for.
const w8 = await korala.templates.list({ search: 'W-8BEN', limit: 20 });

// Get template with full details (roles + fields)
const template = await korala.templates.get(templateId);
console.log(`Roles: ${template.signerRoles.length}`);
console.log(`Fields: ${template.fields.length}`);

// Update name, description, or active status
await korala.templates.update(templateId, {
  name: 'Updated NDA Template',
  description: 'Revised for 2024',
  isActive: true,
});

// Deactivate a template
await korala.templates.update(templateId, { isActive: false });

// Relabel a template as sandbox (test) or live. Templates created before the
// sandbox flag existed all read as live, so this is how you correct them.
// A test-mode API key can only set this to true.
await korala.templates.update(templateId, { sandbox: true });

// Delete a template
await korala.templates.delete(templateId);

// Duplicate a template — copies the file, signer roles and fields.
// The copy is created in the mode of the key making the call, so a live key
// duplicating a sandbox template gives you a live one to promote.
const copy = await korala.templates.duplicate(templateId, {
  name: 'NDA Template (production)',
});

// Or state the mode explicitly (a test-mode key cannot ask for a live copy)
await korala.templates.duplicate(templateId, { sandbox: true });
```

## Replacing the Template File

Replace the underlying PDF/DOCX without losing signer roles or field positions:

```typescript
// Step 1: Get replacement upload URL
const { uploadUrl, key } = await korala.templates.replaceFile(templateId, {
  filename: 'nda-v2.pdf',
  contentType: 'application/pdf',
});

// Step 2: Upload new file
await fetch(uploadUrl, {
  method: 'PUT',
  body: fs.readFileSync('nda-v2.pdf'),
  headers: { 'Content-Type': 'application/pdf' },
});

// Step 3: Confirm replacement
await korala.templates.confirmReplaceFile(templateId, { key });
```

After replacing a template file, verify that existing field positions still align with the new document layout.

## Organize with collections

Templates can belong to several optional collections. Supply `collectionIds`
when requesting an upload URL. Generated documents use their own explicit
destinations without inheriting template memberships. Remove all memberships
before changing a template’s mode.

See [Collections](https://docs.korala.ai/guides/collections) for scope, archive behavior, request
limits, and API and SDK examples.
