Automation and integrations
Filling one form at a time is fine for a person. A business usually needs hundreds. This page shows the ways to do that, from the simplest to the most flexible.
If you want to... · Use; Fill one PDF per row of a spreadsheet · Google Sheets; Connect Zapier, Make, n8n or Power Automate · Hooks; Send many rows from your own code · Batch; Have PDFPhile pull from your system every day · Schedules
Hooks
A hook is a private web address for one form. Anything that sends values to it gets the filled PDF back. Because the address itself is the secret, the tool you connect never needs your API key.
Create one under Account, then Automations. Starter plans include 3 hooks and Pro plans 25.
curl -X POST 'https://pdfphile.com/api/v1/hook/hk_...' \ -H 'Content-Type: application/json' \ -d '{"taxpayer_tin":"123-456-789-000","taxpayer_name":"DELA CRUZ, JUAN"}' \ -o filled.pdf
- Send one set of values and you get one PDF back. - Send a list (or { "row": [...] }; the older rows also works) and you get a job. Its ZIP file is ready to download when it finishes, and it is also sent to the hook's webhook if you set one. - A hook can hold preset values, like your company's TIN and address. They fill in under every row, and a blank cell never replaces them. - Pause a hook and it stops answering. Delete it and the address stops working for good.
How to connect the common tools:
Tool · What to set up; Zapier · Add the action "Webhooks by Zapier", choose POST, paste the hook address, and choose JSON.; Make · Add "HTTP, Make a request", choose POST and a JSON body.; n8n · Add an "HTTP Request" node, choose POST and JSON, and set the response format to File.; Power Automate · Add an "HTTP" action. Or import our connector file for ready-made actions.
Google Sheets
Fill one PDF for each row, saved straight to Google Drive.
1. Download the form's column template: https://pdfphile.com/api/v1/engine/forms/{form_id}/template.csv. Row 1 has the field names. Row 2 has the printed labels and starts with #, so it is skipped. 2. Import it into a sheet. Fill in one row per person or filing. 3. In the sheet, open Extensions, then Apps Script. Paste in pdfphile-sheets.gs and save. 4. In Project Settings, add a Script Property called PDFPHILE_HOOK_URL with your hook address. Add PDFPHILE_FOLDER_ID too if you want the PDFs in a particular Drive folder. 5. Reload the sheet. A new PDFPhile menu appears. Choose "Fill selected rows".
Each row then shows its status and a link to its PDF in Drive. You can also turn on "Fill when a Google Form is submitted" to make a PDF for every new form response.
Batch
From your own code, send up to 1,000 rows at once and get one ZIP file back:
curl -X POST https://pdfphile.com/api/v1/engine/batch/bir-2307 \ -H "Authorization: Bearer $PDFPHILE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"row":[{"payee_tin":"123-456-789-000"},{"payee_tin":"987-654-321-000"}],"file_name_field":"payee_tin"}'
You get a job back right away. Check on it with GET /api/v1/engine/jobs/{id}, then download the ZIP from GET /api/v1/engine/jobs/{id}/download. The ZIP has every sealed PDF plus a manifest.json listing them. manifest.json is { "job_id", "form_id", "row": [ … ] }, one entry per row you sent (counted from 1). Each says what it cost and whether it is complete: { "row": 1, "file": "…pdf", "document_id": "…", "credit_cost": 1, "is_complete": true, "missing_core": [] }. Each PDF costs 1 of your plan's monthly credits (test keys: 0). A refused row costs nothing, and its manifest entry carries the same issue list as a refused fill, plus credit_cost: 0. missing_core has the same shape as in a check: [{ "field", "label" }]. Prepaid credits are not spent by a batch. A row that could not be filled does not stop the others: the job ends completed_with_error, and row_error in the job tells you which rows and why ({ "row": 3, "error": "…" }) without downloading anything. GET /api/v1/engine/jobs lists your latest jobs.
Retrying after a timeout? Send an Idempotency-Key header (any unique string, e.g. your own batch id). A repeat of the same key within 24 hours returns the job the first request started (200, Idempotent-Replayed: true) instead of filling — and billing — the rows again.
Schedules
A schedule makes PDFPhile fetch rows from your system on a timer and fill them. Create one with POST /api/v1/schedules and:
- form_id: the form to fill. - daily_at: a time like "18:00" (Manila time), or interval_minute for every N minutes (5 or more). - source: where to get the rows, for example { "type": "url", "url": "https://yourapp.example/rows" }. That address may return a JSON list of rows or a CSV file (a published Google Sheet works). Add "header": { "Authorization": "Bearer …" } if it needs a key. - webhook_url (optional): where to send the finished ZIP.
For your safety, schedules never call addresses on a private network.
Your own column names
Your sheet probably says TIN No. where the form says payee_tin. Save the match once and every batch, schedule and recipe with those columns fills by itself:
curl -X PUT https://pdfphile.com/api/v1/mappings/bir-2307 \ -H "Authorization: Bearer $PDFPHILE_API_KEY" -H "Content-Type: application/json" \ -d '{"header":["TIN No.","Full Name","Amount"],"mapping":[{"header":"TIN No.","parameter":"payee_tin"},{"header":"Full Name","parameter":"payee_name"}]}'
The answer lists unmapped_column (columns nothing uses) and unfed_parameter (fields no column fills). Not sure which column is which? POST /api/v1/mappings/bir-2307/propose with {"header": [...]} suggests a match and saves it. Only the column names are looked at, never your values. The next file with the same columns uses the saved match and nothing is asked again.
Recipes
A recipe is one small JSON file that says where the rows come from, which column fills which field, what is the same on every form, and where to send the result. A person or an AI agent can read and edit it.
{ "name": "2307 from the payables sheet", "form_id": "bir-2307", "source": { "type": "url", "url": "https://docs.google.com/spreadsheets/d/…/export?format=csv" }, "mapping": [{ "header": "TIN No.", "parameter": "payee_tin" }, { "header": "Full Name", "parameter": "payee_name" }], "join": [{ "parameter": "payee_registered_address", "header": ["Street", "City"], "separator": ", " }], "constant": { "payor_tin": "123456789000", "payor_name": "ACME TRADING CORP." }, "transform": [{ "parameter": "payee_tin", "step": ["digit"] }, { "parameter": "period_from", "step": ["date:DD/MM/YYYY"] }], "file_name_field": "payee_name", "deliver": { "webhook_url": "https://hooks.example.com/pdfphile" } }
Save it with POST /api/v1/recipes. Then:
- POST /api/v1/recipes/{id}/run?dry=1 shows mapped_column, unmapped_column and row_count without filling anything or using a credit. - POST /api/v1/recipes/{id}/run makes a batch job (a ZIP, and a webhook if you gave one). Send {"row": [...]} or {"csv": "…"} to use other rows than the source.
A recipe gives the same result every time. Run it twice on the same rows on the same day and you get the same PDFs, byte for byte. To date the documents yourself, add "as_of": "2026-09-30". Leave out mapping and the recipe uses your saved column names.
Steps you can use in transform: trim, upper, lower, collapse_space, digit, number, date (or date:DD/MM/YYYY for the order your sheet writes dates in), zip, default:<value>, prefix:<text>, slice:<start>,<end>.
Signatures and photos by link
A signature, thumbmark or ID photo can be a link to the image in your own storage instead of the image itself:
{ "payee_signature": { "url": "https://storage.example.com/signatures/123.png" } }
PDFPhile fetches it when it fills the form. It must be a PNG or JPEG of 4 MB or less on the public internet. A link to a private network is refused, and the error names the field.
Accounting and payroll exports
Already keep your books in Xero or QuickBooks Online, or run payroll in Sprout? Send the export and get rows ready for a batch:
- xero-bill-2307: Xero bills (plus the contacts export for each supplier's TIN) → one BIR 2307 per supplier for a quarter. - qbo-vendor-2307: QuickBooks Online Transaction List by Vendor (plus the Vendor Contact List) → one BIR 2307 per supplier. - sprout-alphalist-2316: the Sprout Payroll annual alphalist → one BIR 2316 per employee.
curl -X POST https://pdfphile.com/api/v1/accounting-sources/xero-bill-2307/rows \ -H "Authorization: Bearer $PDFPHILE_API_KEY" -H "Content-Type: application/json" \ -d '{"csv":"…bills export…","lookup_csv":"…contacts export…","quarter":"2026-Q3","rate":0.10,"atc":"WC010","constant":{"payor_tin":"123456789000"}}'
Amounts are added up month by month for the quarter. skipped lists every line that was left out and why. GET /api/v1/accounting-sources lists the exports and the vendor page that documents each one's columns.
Zapier and n8n
The PDFPhile Zapier app and n8n node have the same three parts:
- Fill a form: one row of values in, the sealed PDF out. - Run a recipe: start one of your recipes. - Job finished: starts your Zap or workflow when a batch, schedule or recipe job is done. It signs up for the batch.completed event (POST /api/v1/webhooks) and signs off when you turn it off.
Both use your API key. Neither is listed in the Zapier or n8n directories yet; until then, the hooks above work with any of these tools.
Webhooks
A webhook is a message PDFPhile sends to your server when something finishes. Every webhook is a POST with this header:
X-PDFPhile-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256(secret, t + "." + body)>
Your webhook secret is on your account page. To be sure a message really came from PDFPhile, compute the same signature on your side and compare. Ignore any message whose time t is more than five minutes old.