Fillable PDFs
Many documents you collect signatures on were created by someone else as fillable PDFs: government forms (IRS W-9, W-8 family), insurance applications, bank onboarding packets, HR paperwork. These files carry an embedded form (AcroForm) that declares every field: name, type, page, exact position, option list, and length limit.
Korala reads that declaration and writes values back through it. You never guess coordinates for a form the file already describes, and filled output renders the way the form’s author intended: an SSN lands one digit per box, a checked box gets the form’s own checkmark.
Korala can import the embedded fields for you — for the first signer on a document, or straight onto a template — so nobody transcribes 23 boxes by hand. In the dashboard that happens automatically on upload; over the API it is opt-in (autoImportFormFields: true), because integrations usually provision their own roles and fields.
This guide covers that automatic path, then the general flow for when you want control: discover the embedded fields, link Korala fields to them, prefill from your data, and collect signatures. The IRS W-9 is the worked example.
The problem this solves
On a flat PDF, Korala places values at coordinates you choose. That works when you design the layout. It breaks down on forms you don’t control:
- Positions must be measured by hand and re-measured when the form’s author ships a new revision
- Comb fields (one character per box) render as squished text when stamped at a coordinate
- The file’s own fields stay interactive underneath your stamped values, so a completed form stays editable in any PDF viewer
Linked filling fixes all three: geometry comes from the file, values go through the real fields, and completion flattens the form so nothing stays editable.
How filling works
Whether a value goes through the form or is drawn at coordinates is decided per field, by one property: acroFieldName. A field without one is stamped at its coordinates, the same as on any flat PDF. A field with one is written through the PDF’s embedded form field at completion:
- Comb fields (SSN/EIN digit boxes) distribute characters one per cell
- Checkboxes render the form’s native checkmark
- The field’s
maxLengthis imported and enforced: over-long fills are rejected at entry, never silently cut - An empty value clears anything the uploaded file shipped in that field
- After filling, Korala flattens the entire form: every field becomes permanent page content, and the signed document is no longer fillable in any PDF viewer. Documents produced by
POST /templates/:id/generate(no signing) keep their form live instead, so recipients can continue filling them outside Korala.
Signature fields are the exception: PDFs rarely declare usable signature widgets, so signatures are always placed as images at the field’s coordinates. Detection still reads the widget’s position for you.
Imported fields arrive linked, which is what you want almost always — it is the only way comb boxes, native checkmarks and length limits render correctly. To opt one field out, clear its link and keep everything else about it:
api_request "PATCH" "/api/v1/documents/${DOCUMENT_ID}/fields/${FIELD_ID}" \
'{ "acroFieldName": null }'
# Templates: PATCH /api/v1/templates/${TEMPLATE_ID}/fields/${FIELD_ID}The box keeps the position, size and styling the form declared; only the way the value gets onto the page changes. There is no document-wide or generation-time switch, and deliberately so: turning linking off wholesale would lose exactly the rendering the form defines, and a value the form rejects (a dropdown option that isn’t in the list, a kind mismatch) already falls back to stamping on its own.
Automatic import
When you confirm an upload, Korala scans the PDF and reports how many interactive fields it carries:
const document = await korala.documents.confirmUpload(documentId, {
name: 'W-9 — Jane Q. Taxpayer',
autoImportFormFields: true,
});
document.formFieldsDetected; // 23 for the IRS W-9, 0 for a flat PDFThe count is always recorded. Whether the fields are also placed follows
autoImportFormFields: documents created from the dashboard default to true;
documents created with an API key default to false, so an integration that
places its own fields is never surprised by a second set. Pass the flag at
confirm time to override either default.
Adding the first signer to that document places all of them, assigned to that signer:
api_request "POST" "/api/v1/documents/${DOCUMENT_ID}/signers" \
'{ "name": "Jane Q. Taxpayer", "email": "[email protected]" }'
api_request "GET" "/api/v1/documents/${DOCUMENT_ID}/fields"
# 23 fields, each already linked to its embedded field, positioned exactly,
# typed (text/checkbox/dropdown), length-limited, labelled from the form's
# tooltip or the text printed beside it, and styled with the form's own
# point size, color and alignment.What the import decides for you:
-
How the value is drawn: the point size, color, alignment and font the form declares. A form naming a base-14 face (Helvetica, Times, Courier and their bold/italic variants) is honored exactly; anything else falls back to the platform font, as does any value the face cannot encode — a Cyrillic name never gets dropped to match a typeface.
-
Multiline boxes: a multiline form field gives the signer a multi-line editor (Enter inserts a break, ⌘/Ctrl+Enter confirms), keeps the breaks when written through the form, and wraps to the box from the top when drawn as an overlay.
-
Types come from the form:
/Tx→ text, checkbox and radio widgets → checkbox,/Ch→ dropdown with its option list,/Sig→ signature. A text field whose label asks for a date becomes a date field, which auto-fills at signing. -
Radio options share a
groupKey, so checking one clears its siblings; a required radio group becomesgroupRequiredrather than requiring every option. -
Read-only fields (computed or locked by the form) are skipped — a signer could never change them.
-
Signature widgets are placed as ordinary signature fields: a drawn signature is an image, which cannot be written through a PDF form field.
Fields are ordinary Korala fields once imported: move, relabel, reassign or delete them like any other.
Controlling the import
autoImportFormFields at confirm time decides it either way — API-key
documents need true to import at all, and a dashboard upload can pass
false when you place fields yourself:
await korala.documents.confirmUpload(documentId, {
name: 'W-9 — Jane Q. Taxpayer',
autoImportFormFields: false,
});Or run the import yourself — to assign the form to a signer other than the first, or to redo it after edits:
TypeScript
const fields = await korala.documents.importFormFields(documentId, {
signerId: secondSigner.id,
replaceExisting: true, // clear existing fields first
});Documents generated from a template are never auto-imported: their fields come from the template — which imports the same way.
Templates
Confirming a fillable base PDF can place its fields on the template as well, so every document generated from it inherits them. The same surface defaults apply — dashboard templates import automatically, API-key templates opt in:
const template = await korala.templates.confirmUpload(templateId, {
name: 'IRS W-9',
autoImportFormFields: true,
});
template.formFieldsDetected; // 23
const fields = await korala.templates.getFields(templateId);
// 23 template fields, linked, positioned and styled from the formTemplate fields are assigned to the template’s first signer role. A template field with no role places nothing at generation, so a template that has no roles yet gets one named Signer — rename it, or create your roles before confirming the upload and the import uses the first by signing order.
autoImportFormFields at confirm time decides it either way, and POST /templates/{id}/form-fields/import ({ signerRoleId, replaceExisting }) runs the import on demand.
await korala.templates.importFormFields(templateId, {
signerRoleId: taxpayerRole.id,
replaceExisting: true,
});A template that already carries fields — a catalog clone, a duplicate, or one you built by hand — is never touched by the automatic import.
From there, bind merge fields to the imported fields (PATCH /templates/{id}/fields/{fieldId} with a mergeFieldName) and every generated document fills them through the PDF’s own form.
Multi-party forms
A fillable PDF says where every box is but never who fills it, so an import puts the whole form on the first signer. That is right for a W-9 and wrong for a lease or an I-9. Ask the document which parties it names:
const parties = await korala.documents.getFormFieldParties(documentId);
// [
// { key: 'employee', name: 'Employee', pages: [1], fieldCount: 18,
// evidence: '6 fields on page 1 name the employee', signerIds: ['...'] },
// { key: 'employer', name: 'Employer', pages: [2], fieldCount: 12,
// evidence: '4 fields on page 2 name the employer', signerIds: ['...'] },
// ]The parties come from the words the form authored — a field’s tooltip and its
name, never the prose printed beside it — so evidence is there to be checked
against the document rather than trusted. English, German, French, Spanish,
Italian, Dutch, Polish, Portuguese and Czech are recognised.
Where a form names nobody this can read, source comes back as layout
instead of named: the form is cut along its widest breaks into as many parts
as there are signers, unlabelled, for you to assign. A single-signer document
returns [] — there is nothing to divide it between.
Hand each section to the signer who fills it:
await korala.documents.getFormFieldParties(documentId);
await korala.documents.assignFormFieldParties(documentId, {
assignments: [{ party: 'employer', signerId: hrSigner.id }],
});Send the party keys, never field ids: the split is re-read on the server, so a
field added or removed since you fetched the parties cannot be missed or moved
by a stale list. Templates work the same way with signerRoleId in place of
signerId. In the dashboard this appears as a dialog when the editor opens on
a form with more than one party.
File size
Every plan carries an upload ceiling — 25 MB on the free plan, 250 MB and up on paid ones — returned with the upload URL so you can check before transferring:
const { uploadUrl, maxUploadBytes } = await korala.documents.createUploadUrl({
filename: 'w9.pdf',
contentType: 'application/pdf',
});Where the ceiling is enforced, confirming a file above it returns 413 and the upload is discarded — for DOCX templates too, checked on the uploaded file before it is converted, and skipping the confirm step does not get around it. Where it is not enforced, an upload above the ceiling is accepted and behaves in every other way like one below it: its form is read and its fields are imported as normal.
If your files are legitimately larger than your plan allows, the ceiling can be raised for your organization without changing plans — ask support. Large files that are within it are read by a background worker rather than in the request: formFieldsDetected stays null and the fields appear shortly after, so a hundred-page form never holds up the API. POST /form-fields/import returns an empty list in that case, having queued the work — poll GET /documents/{id}/fields for the result.
Discover the embedded fields
Upload the PDF as a document or template, then list its form fields:
TypeScript
const formFields = await korala.documents.getFormFields(documentId);
// W-9 excerpt:
// [
// { name: 'topmostSubform[0].Page1[0].f1_01[0]',
// kind: 'text', pageNumber: 1,
// xPosition: 59, yPosition: 118, width: 517, height: 14 },
// { name: 'topmostSubform[0].Page1[0].f1_11[0]',
// kind: 'text', maxLength: 3, ... }, // first SSN box group
// { name: '...c1_1[0]', kind: 'checkbox', ... },
// ...
// ]Each entry gives you the exact geometry and the name to link against. kind is one of text, checkbox, radio, dropdown, or signature. Radio groups return one entry per option, all sharing the group’s name, each with its own optionValue. Dropdowns and list boxes include their options; create them as dropdown fields with that list, and the signer picks from it. Fills outside the option list are rejected. A flat PDF returns an empty list.
Each entry also reports what the form declares about the field’s appearance and behavior: label (its tooltip), required, readOnly, comb, multiline, and the fontSize, fontFamily, textColor and alignment it draws values with. A fontSize is absent when the form auto-sizes text to the box.
Besides the automatic import above, you have two ways to turn these into Korala fields:
- Create fields yourself from the
form-fieldsresponse. You control the mapping, labels, and signer assignment. Best for API integrations that know the form, and the way to link a template’s fields. - Run AI field detection. When the PDF has embedded form fields, detection proposes them directly with confidence 100 instead of guessing from layout, and accepted proposals keep the
acroFieldNamelink. Nearby page text becomes the proposal’s context. Skip this on a document that already imported its form — detection would propose the same fields a second time.
Worked example: a W-9 template
Create the template from the official PDF once, link its fields, then generate a document per recipient. The same pattern applies to any fillable form.
TypeScript
// Line 1: name (prefilled from your data, signer can correct it)
await korala.templates.addField(templateId, {
signerRoleId: taxpayerRole.id,
fieldType: 'text',
pageNumber: 1,
xPosition: 59, yPosition: 118, width: 517, height: 14,
label: 'Name of entity/individual',
mergeFieldName: 'legal_name',
editable: true,
acroFieldName: 'topmostSubform[0].Page1[0].f1_01[0]',
});
// Tax classification: Individual/sole proprietor checkbox
await korala.templates.addField(templateId, {
signerRoleId: taxpayerRole.id,
fieldType: 'checkbox',
pageNumber: 1,
xPosition: 73, yPosition: 180, width: 8, height: 8,
label: 'Individual/sole proprietor',
required: false,
acroFieldName: 'topmostSubform[0].Page1[0].Boxes3a-b_ReadOrder[0].c1_1[0]',
});
// SSN boxes (comb fields, 3-2-4)
await korala.templates.addField(templateId, {
signerRoleId: taxpayerRole.id,
fieldType: 'text',
pageNumber: 1,
xPosition: 418, yPosition: 372, width: 43, height: 24,
label: 'SSN (first 3 digits)',
mergeFieldName: 'ssn_part1',
editable: true,
acroFieldName: 'topmostSubform[0].Page1[0].f1_11[0]',
});
// Signature and date: no acroFieldName. The signature is stamped as an
// image; the date field fills with the signing date when tapped.
await korala.templates.addField(templateId, {
signerRoleId: taxpayerRole.id,
fieldType: 'signature',
pageNumber: 1,
xPosition: 110, yPosition: 645, width: 200, height: 24,
});
await korala.templates.addField(templateId, {
signerRoleId: taxpayerRole.id,
fieldType: 'date',
pageNumber: 1,
xPosition: 430, yPosition: 645, width: 120, height: 24,
});
// Generate one document per recipient, prefilled from your data
const doc = await korala.templates.createDocument(templateId, {
name: 'W-9 — Jane Q. Taxpayer',
signers: {
[taxpayerRole.id]: { email: '[email protected]', name: 'Jane Q. Taxpayer' },
},
variables: {
legal_name: 'Jane Q. Taxpayer',
ssn_part1: '123',
},
});Set editable: true on prefilled fields so the signer can correct your data before signing. Prefilled checkbox defaults only apply to editable fields; see Collecting Information from Signers.
Fill-time validation
Tax forms carry values that must be exactly right, so fields validate at fill time, in the signing surface and through the API alike:
maxLength(text fields): imported automatically from AcroForm comb fields (the SSN digit boxes on a W-9 arrive with their limits set), or set it yourself. A fill beyond the limit returns a 400 with a signer-readable message.inputMask(single-line text fields): use#for digit positions and literal separators everywhere else. With(###) ###-####, typing or pasting2125550123produces(212) 555-0123. Korala rejects incomplete or unformatted values at fill time. You can configure the mask in the dashboard field editor.validationPattern+validationMessage(text fields): a regex the value must match, with your own error text. Patterns that nest quantifiers (like(a+)+) are rejected at creation, and every match runs under a hard timeout, so a bad pattern can never stall signing. Anchor patterns with^and$; an unanchored pattern matches anywhere in the value, so\d{4}would acceptabc1234xyz. Example, a US phone number:
{
"fieldType": "text",
"label": "Phone number",
"maxLength": 14,
"inputMask": "(###) ###-####",
"validationPattern": "^\\(\\d{3}\\) \\d{3}-\\d{4}$",
"validationMessage": "Enter a 10-digit US phone number"
}- Dropdown options: fills outside the option list are rejected.
- Required: proposal acceptance takes a
requiredflag (and the dashboard review panel has a matching toggle), so a 23-field W-9 doesn’t force the signer through every optional line.
The same rules apply to bulk-sign overrides and template prefills: invalid sender data fails loudly at generation instead of stamping a wrong value onto a signed form.
Choose-one field groups
Many tax-form choices are “pick exactly one”: a W-9’s federal tax classification, a W-8BEN-E’s Chapter 3 and Chapter 4 status. Korala models these as a field group — a set of checkbox fields sharing a groupKey:
- Exclusivity. Checking one member un-checks the signer’s other members of the same
groupKey, in the signing surface and through the API — even when each member fills a different PDF checkbox. - At least one required. Set
groupRequired: trueon the group’s members and signing is blocked until one is selected, with a field-labelled error. Keep the members individuallyrequired: false(a required option would strand the document).
[
{ "fieldType": "checkbox", "label": "Individual/sole proprietor",
"acroFieldName": "topmostSubform[0].Page1[0].Boxes3a-b_ReadOrder[0].c1_1[0]",
"groupKey": "federal_tax_classification", "groupRequired": true, "required": false },
{ "fieldType": "checkbox", "label": "C corporation",
"acroFieldName": "topmostSubform[0].Page1[0].Boxes3a-b_ReadOrder[0].c1_1[1]",
"groupKey": "federal_tax_classification", "groupRequired": true, "required": false }
]This works whether the options are a true PDF radio group (one field, many widgets) or, as on the actual W-9, seven independent checkboxes with distinct fill targets. Acrobat’s exclusivity there comes from a script layer that PDF processors discard, so groupKey supplies it. For a true radio group, detection sets groupKey for you automatically; for independent checkboxes, assign the same groupKey to each member.
Assign every member of a group to the same signer: a group is one logical choice for one person.
What’s still on you to validate: “exactly one” beyond “at least one” (uncheck-and-recheck already keeps it to one via exclusivity), conditional-required (“LLC code only if the LLC box is checked”), and “complete SSN or complete EIN” (group-required asserts one member is filled, not that a whole TIN is complete; pair it with maxLength/validationPattern per box).
What the signed output looks like
When the last signer completes:
- Linked values are written through the PDF’s form fields
- Signatures and unlinked fields are stamped at their coordinates
- The whole form is flattened: values become page content, and no interactive field remains
- Korala applies its cryptographic signature and an RFC 3161 timestamp
Verify it yourself: open the signed PDF in any viewer and try to click a field. There is nothing to click. Extract the text and your values are in the page content.
Example: IRS tax forms
Any PDF with an AcroForm works. Korala’s pipeline is verified against the current IRS revisions of:
| Form | Embedded fields | Notes |
|---|---|---|
| W-9 | 23 | SSN/EIN comb boxes, classification checkboxes |
| W-8BEN | 23 | Includes an embedded signature widget |
| W-8BEN-E | 179 | 8 pages of checkboxes and text fields |
| W-8IMY | 148 | 8 pages |
These forms are XFA hybrids. Korala uses the AcroForm layer and discards the XFA layer, which is the standard behavior for PDF processors other than Acrobat.