Developer API

PDF API: fill and parse PDF forms

Two REST endpoints. /parse turns a PDF into JSON: its form fields, page count, metadata and text. /fill writes values into those fields and returns the finished PDF, flattened if you want. Base URL https://fillable.ca.

Get a test API key

Free tier: 100 requests per day. Upgrade on the API plan for production volume.

What it is for

The API is built for one job: putting data into existing PDF forms, and reading it back out. Typical uses:

  • Client intake. A law, accounting or immigration practice collects answers in its own system and generates the completed PDF form for signature.
  • Onboarding packets. HR fills direct deposit, tax and policy acknowledgement forms from the employee record instead of asking people to type the same details three times.
  • Data capture. Completed fillable PDFs come back by email; /parse pulls the field values into JSON so nobody retypes them.
  • Batch generation. One template, hundreds of records, one call per PDF.

It is not a PDF rendering engine. It does not create PDFs from HTML, convert file formats, or add form fields to a flat document. For those, use the browser tools: HTML to PDF, Word to PDF, and Make a PDF fillable.

How a typical integration works

  1. Prepare the template once. Start from a PDF that already has AcroForm fields (most government and HR forms do). If yours is flat, add fields with Make a PDF fillable and download the result.
  2. Discover the field names. Send the template to /parse. Save the fields array; the name values are the keys you will fill.
  3. Map your data. Build an object of field name to value. Strings for text fields, booleans for checkboxes, the option label for dropdowns and radio groups.
  4. Fill. Send the template and the map to /fill. Check skipped for names that did not match, then decode pdf from base64 and store or send it.
  5. Flatten or not. With flatten true (the default) the values become part of the page and cannot be edited. Set it to false when the recipient still needs to complete or correct fields.

Authentication and limits

Send your key as a bearer token on every request. The API also accepts the same key in an x-api-key header.

Authorization: Bearer fk_live_your_key

Every key gets 100 requests per calendar day (UTC). Each response includes a remaining count, and the 101st request returns 429 until the day rolls over. Keys generated on this page are not tied to an account; keys generated after you sign up show up in your dashboard. For production volume, subscribe to the API plan.

POST /api/v1/parse

Extract form fields, page count, metadata and text from a PDF. Send the PDF as base64 in JSON, or as multipart form-data in a file field.

curl -X POST https://fillable.ca/api/v1/parse \
  -H "Authorization: Bearer fk_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{"pdf":"<base64-encoded-pdf>"}'

Multipart form:

curl -X POST https://fillable.ca/api/v1/parse \
  -H "Authorization: Bearer fk_live_your_key" \
  -F "file=@intake-form.pdf"

Response:

{
  "pageCount": 12,
  "title": "Client Intake Form",
  "author": null,
  "fieldCount": 18,
  "fields": [
    { "name": "full_name", "type": "TextField", "value": "" },
    { "name": "agree", "type": "CheckBox", "value": false },
    { "name": "province", "type": "Other" }
  ],
  "text": ["Page 1 text ...", "Page 2 text ..."],
  "remaining": 99
}
KeyMeaning
pageCountNumber of pages.
title, authorDocument metadata, or null when the PDF has none.
fieldCount, fieldsEvery form field with its name, its type (TextField, CheckBox, or Other for dropdowns, radio groups and signatures) and its current value where readable.
textOne string per page, whitespace collapsed. Best effort: null for scanned or image-only PDFs.
remainingRequests left today for this key.

POST /api/v1/fill

Fill a PDF’s form fields and get the completed PDF back as base64. JSON only.

curl -X POST https://fillable.ca/api/v1/fill \
  -H "Authorization: Bearer fk_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "pdf": "<base64-encoded-pdf>",
    "fields": { "full_name": "Jane Smith", "agree": true, "province": "ON" },
    "flatten": true
  }'

Response:

{
  "applied": ["full_name", "agree", "province"],
  "skipped": [],
  "remaining": 98,
  "pdf": "<base64-encoded-filled-pdf>"
}
Field typeSendResult
Text fieldAny string (numbers are converted to strings)Applied
Checkboxtrue or falseChecked or unchecked
Dropdown, radio groupThe option label as a stringApplied if the option exists, otherwise skipped
Signature, button, unknown nameAnythingListed in skipped; the rest of the fill still succeeds

flatten defaults to true. A flattened PDF keeps the values as page content, which is what you want for a final copy. Send false to keep the fields live.

Code examples

Node (18 or later, no dependencies):

import { readFile, writeFile } from "node:fs/promises";

const key = process.env.FILLABLE_KEY;
const pdf = (await readFile("intake-form.pdf")).toString("base64");

// 1. Discover the field names
const parsed = await fetch("https://fillable.ca/api/v1/parse", {
  method: "POST",
  headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
  body: JSON.stringify({ pdf }),
}).then((r) => r.json());
console.log(parsed.fields.map((f) => f.name));

// 2. Fill and flatten
const filled = await fetch("https://fillable.ca/api/v1/fill", {
  method: "POST",
  headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
  body: JSON.stringify({ pdf, fields: { full_name: "Jane Smith", agree: true }, flatten: true }),
}).then((r) => r.json());

if (filled.skipped?.length) console.warn("Not applied:", filled.skipped);
await writeFile("intake-form-filled.pdf", Buffer.from(filled.pdf, "base64"));

Python (requests):

import base64, os, requests

key = os.environ["FILLABLE_KEY"]
headers = {"Authorization": f"Bearer {key}"}

with open("intake-form.pdf", "rb") as f:
    pdf_b64 = base64.b64encode(f.read()).decode()

parsed = requests.post("https://fillable.ca/api/v1/parse",
                       headers=headers, json={"pdf": pdf_b64}).json()
print([f["name"] for f in parsed["fields"]])

filled = requests.post("https://fillable.ca/api/v1/fill", headers=headers,
                       json={"pdf": pdf_b64,
                             "fields": {"full_name": "Jane Smith", "agree": True},
                             "flatten": True}).json()

with open("intake-form-filled.pdf", "wb") as out:
    out.write(base64.b64decode(filled["pdf"]))

Errors

  • 400 bad request: missing pdf, missing fields, invalid JSON, or no file in a multipart upload. The error string says which.
  • 401 missing or invalid API key.
  • 422 the PDF could not be opened or processed. Common causes: not a PDF, corrupted file, or a file that needs repair. Run it through Repair PDF and retry.
  • 429 daily limit reached for this key.

Encrypted PDFs are opened with encryption ignored, so owner-password-protected forms usually work. A file that needs a user password to open will fail with 422; remove the password first with Unlock PDF if you have the right to.

Privacy and handling

  • PDFs are processed in memory and are not stored. Nothing about the document content is logged.
  • The only stored record is the per-key daily request count.
  • All traffic is over HTTPS. Keys are shown once at creation; if you lose one, generate a new one.
  • Keep keys server-side. Do not embed them in browser code or mobile apps.

Notes

  • Field names come from the PDF’s form. Use /parse first to discover them, then /fill.
  • Field names are case sensitive and can contain dots or spaces; send them exactly as returned.
  • Need higher limits or a signed agreement? See the API plan or email info@fourcents.ca.

Frequently asked questions

Do you store the PDFs I send?

No. Both endpoints load the PDF in memory, do the work, return the result, and discard it. The only thing recorded is a per-key request count for the daily limit.

What counts as a request?

Every call to /parse or /fill counts once, whether it succeeds or returns an error after authentication. The remaining field in each response shows how many calls are left for the day.

How do I find the field names in my PDF?

Call /parse first. The fields array lists every form field by name and type. Use those exact names as keys in the fields object you send to /fill.

My PDF has no form fields. Can the API add them?

Not through the API. Use Make a PDF fillable in the browser to add fields to a flat PDF, then use the API to fill the result.

Why does text come back null?

Text extraction is best effort. Scanned or image-only PDFs have no text layer, so text is null. OCR is not part of the API; the browser OCR tool handles scans one at a time.

Can I send a multipart upload instead of base64?

For /parse, yes: send the PDF in a file field. /fill takes JSON only, because the field values travel with it.

What is the size limit?

Send everyday documents. Each request has a 30-second processing window, so very large or complex PDFs can time out with a 422. Split big files first if you need to.

How do I get a production key?

Generate a key below to start. Keys created from an account are listed in your dashboard. Higher limits are part of the API plan on the pricing page.