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
- Upload a PDF or DOCX file as the base document
- Define signer roles (e.g., “Client”, “Approver”), placeholders you map to real people when you create a document
- Place fields on the template and assign each to a signer role
- Create documents from the template by mapping real signers to roles
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}`);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,
});Managing Roles
// 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',
});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.
// 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:
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 returns400. editableapplies totext,date, andcheckboxfields; setting it on signature or initials fields returns400.
Managing Fields
// 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.
// 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:
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
anchorOffsetXmoves the field right, negative left. - Positive
anchorOffsetYmoves 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 insideStandby:. SetanchorMatchWholeWord: trueto 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: -1to 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
pdftotextstill extracts it. hideAnchor: truedraws 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:
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, 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:
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:
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:
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 survivesA 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: '[email protected]',
},
// Or key by role name instead of ID
Approver: {
name: 'Bob Johnson',
email: '[email protected]',
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'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).
The created document is in draft status. Send it for signing 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:
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
// 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:
// 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 for scope, archive behavior, request limits, and API and SDK examples.