Skip to Content
GuidesFillable PDFs

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 maxLength is 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 PDF

The 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 becomes groupRequired rather 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:

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 form

Template 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:

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:

  1. Create fields yourself from the form-fields response. 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.
  2. 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 acroFieldName link. 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.

// 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 pasting 2125550123 produces (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 accept abc1234xyz. 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 required flag (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: true on the group’s members and signing is blocked until one is selected, with a field-labelled error. Keep the members individually required: 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:

  1. Linked values are written through the PDF’s form fields
  2. Signatures and unlinked fields are stamped at their coordinates
  3. The whole form is flattened: values become page content, and no interactive field remains
  4. 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:

FormEmbedded fieldsNotes
W-923SSN/EIN comb boxes, classification checkboxes
W-8BEN23Includes an embedded signature widget
W-8BEN-E1798 pages of checkboxes and text fields
W-8IMY1488 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.

Last updated on