Skip to Content
GuidesTemplates

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

Creating a Template

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

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

// 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

// 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

TypeDescription
signatureDrawn or uploaded signature image
initialsSigner’s initials
dateDate value
textFree-text input
checkboxBoolean 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 fieldGenerated document field
Matching variables value at generationPrefilled and locked (signer can’t edit)
No variable, defaultValue setPrefilled with the default and locked
Prefilled with editable: truePrefilled with the value as a suggestion; the signer can change it
No variable, no defaultEmpty; the signer fills it in the signing UI
checkbox default without editableIgnored; 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 returns 400.
  • editable applies to text, date, and checkbox fields; setting it on signature or initials fields returns 400.

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, });
PropertyTypeDescription
placementenumcoordinate (default) or anchor
anchorTextstringLiteral string to locate in the rendered document. Must not contain {{ }}.
anchorPositionenumWhere the field sits relative to each match: replace, left, right, above, below
anchorOffsetX / anchorOffsetYnumberOffset in points applied after resolution
hideAnchorbooleanWhite-out a visible marker after resolving it
anchorMatchWholeWordbooleanMatch 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:

anchorPositionResult
replaceField top-left at the box top-left
aboveField bottom on the box top
belowField top on the box bottom
leftField right edge on the box left edge, tops aligned
rightField 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:

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.

PropertyTypeDescription
placementenumbookmark
bookmarkNamestringWord bookmark whose position locates the field. Required
anchorPositionenumWhere the field sits relative to the bookmark point
anchorOffsetX / anchorOffsetYnumberOffset 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:

anchorPositionResult
replaceField top-left on the point
aboveField bottom on the point
leftField right edge on the point
below / rightSame 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 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:

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.

Last updated on