Filling a form

To fill a form, send a POST request to:

https://philforms.com/api/v1/engine/fill/{form_id}

The body is a JSON object with your answers, keyed by parameter name. The answer is the filled PDF (application/pdf).

Send values the way you already have them

You don't need to format anything for the paper. PhilForms does that for you:

You send · The form gets; 2026-01-15, 01/15/2026 or January 15, 2026 · The date in the form's own boxes, in the order that form uses; 123-456-789-000 · One digit per TIN box, with the branch code when the form has room for it; ₱12,500.00 or 12500 · Pesos and centavos, lined up at the printed decimal point; +63 917 123 4567 · The mobile number spread across its boxes

If a value can't fit on the paper, you get an error that names the limit. PhilForms never cuts a value short without telling you.

Options

These are all optional.

Option · Where it goes · What it does; style · In the body · Ink colour, letter case and size. Example: { "ink": "blue", "case": "upper" }. Ink can be blue, black or a colour like #1A3D7C. Size is 5 to 24.; profile_id · In the body · Fills any blank boxes from a saved profile, such as your TIN and address.; option · In the body · Switches the automatic help on or off: arithmetic, rounding, capital letters, copies, saved details, how names are read, and fields left blank. See Switch the automatic help.; ?require=a,b · In the address · Refuses to fill unless these parameters are present.; ?strict=1 · In the address · Refuses keys the form doesn't know, instead of ignoring them.; ?revise=<document_id> · In the address · Edits a PDF you already made: same form, within 7 days, same filer. Free. See Credits.

What comes back

Besides the PDF, the response has headers your code can read:

Header · Meaning; X-Document-Id · The id of this document. The verify page reports the same id.; X-Fields-Filled · How many of your parameters were used.; X-Annex-Count · How many long answers went to annex pages at the end of the PDF. Only some forms allow this; their fields say is_annex: true.; X-Layout · adjusted when table rows were resized. You asked for it with "layout": { "row": "fit" }. Only forms whose agency publishes an editable original allow it. Other forms answer layout_not_allowed. The PDF says at the foot of the page that it is not the published proportions.; X-Boxes-Filled · How many printed boxes received ink (a TIN fills one box per digit).; X-Unknown-Keys · Keys you sent that this form doesn't have. They were ignored. Only sent when there are some. A key that looks like a typo of a real one is refused instead — see below.; X-Core-Fields · The core fields X-Is-Complete is judged on: the fields the form marks required, or else the filer's ID, name and address.; X-Fields-Missing · Core fields you left blank, such as the filer's TIN. Only sent when one is blank.; X-Is-Complete · true when every core field is filled. It does not mean every box is filled: page totals, payment details, dates and signatures can still be blank.; X-Missing-Count · How many of the form's fields are blank, core or not, including signatures, photos and empty tables. Tick boxes left unticked don't count.; X-Fields-Blank · The first 20 of those blank fields, by name. A check (/engine/check) lists them all under missing.; X-Credit-Cost · Credits this call used: 1 for a new PDF, 0 for an edit or a test key.; X-Credit-Remaining · Only sent when the PDF was paid with a prepaid credit: how many prepaid credits are left.; X-Revise-Of, X-Revise-Remaining · On an edit: the first document, and how many free edits it has left.; X-Quota-Used, X-Quota-Limit · Credits used this month, and your plan's credits.; X-Copy-Count · How many copies of the form this PDF holds: 1, or more when your rows ran past what a table prints (see below).

More rows than the form prints

A table prints a fixed number of rows: the SSS R-3 holds 15 members a page, the PhilHealth RF-1 10 employees. On these two forms, send all your rows in one fill. You get one PDF with as many copies of the form as the rows need, filled the way the paper is filed:

- the rows in the order you sent them, 15 (or 10) to a copy; - the employer's details on every copy; - PAGE 1 OF 3, PAGE 2 OF 3… on the R-3; - each copy's page totals added up from that copy's rows ("TOTALS FOR THIS PAGE (to be filled out on every page)", the RF-1's SUBTOTAL), and the RF-1's employees per page; - the grand total and payment details where the form says: on the R-3 the last page only ("Fill out the Grand Total and Payment Details of the last page of the R-3 only", its Instructions 7), on the RF-1 every page ("GRAND TOTAL … To be accomplished on every page").

Leave page_number, page_count and the page totals out: each copy has its own, so a value sent once is refused (per_copy_value). The whole PDF is one document (one X-Document-Id, one seal) and costs one credit, however many copies it holds. X-Copy-Count says how many; the free check answers copy_count. On every other form, more rows than it prints is refused (too_many_rows) — PhilForms does not guess how a form continues. The R-3's contribution cells print whole pesos: send 1350, not 1350.00.

The PNG pictures in one request come to at most 8 megapixels, and in one batch to at most 24, with a picture used many times counted once. A picture repeated on every copy of a long fill has its own size limit, and the refusal says it. Both refuse with bad_image.

A test key, a trial or an unpaid plan holds at most 200 pages in one PDF, a refusal with too_many_rows. Long unpaid PDFs draw two at a time, and a 429 means the line is full, so try again in a minute. A paid key prints as many copies as the form numbers.

Sources: SSS R-3, PhilHealth RF-1.

Let the form do its own arithmetic

Add ?compute=1 to /api/v1/engine/check/{form_id} or /api/v1/engine/fill/{form_id} and the lines the form itself says how to work out are filled for you — only where you left them blank. For example, on BIR 1701Q at the 8% rate, sending the quarter's sales fills Items 49, 51, 53 and 54 (8% of Item 53) and carries the tax due to Part III. Each rule is the printed instruction on the form ("Sum of Items 47 and 48", "Item 53 x 8% Tax Rate"); the graduated income tax comes from the tax table printed on 1701Q itself (Table 1 for 2018–2022, Table 2 from 2023, by the return's year); a line the form does not spell out stays yours. The check response lists what was computed under computed; a fill names the lines in the X-Computed header. Limits the form prints come back under warning ({ field, message, source }) — for example the 8% rate on 1701Q is only for up to ₱3,000,000 — and so does a total you typed that the form's own arithmetic contradicts (is_mismatch: true, e.g. 1601-EQ Item 19 against its rows); nothing is changed for them. Warnings come back with or without ?compute=1: in the check's warning, and as the field names in a fill's X-Formula-Warning header. On 1701Q, computed lines are rounded to the peso, as the form instructs ("49 centavos or less drop down; 50 or more round up"), and the ₱250,000 reduction (Item 52) is filled unless the ATC is II016 (mixed income). Covered today: BIR 1700, 1701, 1701A, 1701Q, 1601-C, 1601-EQ, 2316, 2550Q, 2551Q and 0605.

curl -X POST "https://philforms.com/api/v1/engine/check/bir-1701q?compute=1" -H "Content-Type: application/json" \ -d '{ "tax_rate_option": "eight_percent", "eight_percent_sales_revenue_receipt_fee_taxpayer": "650000" }' # → "computed": { "eight_percent_tax_due_taxpayer": "52000.00", "tax_due_taxpayer": "52000.00", … }

Or send "option": { "is_compute": true } in the body instead of ?compute=1. "is_compute": false turns it off, even when your account turned it on.

Switch the automatic help

Every fill gets some help by default: the form works out its totals, capital letters on forms that ask for them, a box the form repeats copies the first one, and saved details fill blanks with titles dropped from names. Each of these can be turned off or tuned with the option body key. It works on check, fill, batch (one option for every row) and profile apply.

Key · Values · What it does; is_compute · true / false · The form's own arithmetic. Off by default in the API, on in the app.; rounding · form / centavo · form drops centavos where the form says so. centavo always keeps two decimals.; is_warning · true / false · Warnings for limits the form prints.; case · form / upper / as_is · Capital letters only where the form asks, always, or never. style.case still wins.; is_copy · true / false · A box the form repeats (the TIN on page 2) copies the first one.; is_tick_choice · true / false · Saved details tick sex, civil status and citizenship.; is_split_address · true / false · A saved one-line address fills street, barangay, city and ZIP boxes.; is_split_name · true / false · A saved LAST, FIRST MIDDLE fills last, first and middle name boxes, and saved parts fill a one-box name.; is_org_detail · true / false · A workflow run inside an organization fills the employer or payor boxes a row left blank from the organization's saved details. Send it in the run body's option.; is_client_party · true / false · Certificates issued for a client (POST /api/v1/clients/:id/certificates) take that client's TIN, name and address as the payor or employer unless you send them.; name_title · saved / all / keep · Titles (Ms., Mr., Atty.) are dropped from saved names, also from names you send, or kept.; name_title_word · list of words · More words that count as a title, such as ["Kgg.", "Capt."].; name_particle · join / split · join keeps "Dela Cruz" as one surname.; name_particle_word · list of words · More surname particles.; name_order · last_first / first_last · How a one-box name is written from saved parts: DELA CRUZ, JUAN SANTOS or Juan Santos Dela Cruz.; field · { "<name>": { … } } · Per field: is_off (never filled, even if sent), is_compute, is_autofill (saved details), is_copy.

curl -X POST "https://philforms.com/api/v1/engine/fill/bir-1701q" -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \ -d '{ "tax_rate_option": "eight_percent", "eight_percent_sales_revenue_receipt_fee_taxpayer": "650000", "option": { "is_compute": true, "rounding": "centavo", "field": { "spouse_tin": { "is_off": true } } } }'

Unknown keys and values are refused with a bad_option issue, never ignored. GET /api/v1/fill-option lists every option with its choices, default and a plain description.

The same switches are on the Filling tab of your account (a default for every form) and under Form settings on each form's fill page (one form, field by field). A request made with your account's key follows them. Later wins: the default, then your account, then the form's setting, then the request's option.

When something goes wrong

Every refusal is JSON with the same two keys on top: error, a sentence for a person, and code, a fixed word for your code. Branch on code; the sentence may be reworded, a code is never renamed. Most also carry a hint with the next step. A refused value adds the issue list, which tells you exactly what to fix:

{ "error": "Invalid values for this form", "code": "invalid_value", "issue": [{ "field": "payee_tin", "label": "Taxpayer Identification Number (TIN)", "code": "has_letter", "message": "\"12A-456-789\" has letters — expected a 9-digit TIN, optionally followed by a 3–5 digit branch code (NNN-NNN-NNN-NNNNN)", "expected": "a 9-digit TIN, optionally followed by a 3–5 digit branch code (NNN-NNN-NNN-NNNNN)" }], "hint": "GET /api/v1/parameters/bir-2307" }

Status · code · What it means; 400 · invalid_value · A value doesn't fit the form. The issue list says which (each issue has its own code, such as has_letter).; 400 · bad_request, bad_json, row_limit_exceeded, bad_date_window · The request itself is wrong: no parameters, a body that isn't JSON, more batch rows than your plan takes, a deadline window that isn't real dates or runs past 3,700 days.; 401 · key_required, invalid_key · The key is missing, or unknown / revoked.; 402 · quota_exceeded · This month's credits can't pay for it. A batch says credit_needed and credit_left.; 402 · test_key_limit · A test key's 500 PDFs this month are used.; 403 · test_key_not_allowed · Your organization's data policy does not allow test keys or Practice.; 402 · plan_required · A later form of a pack needs a paid plan, the trial or a prepaid credit.; 403 · forbidden, email_unverified, test_key_values, test_key_write, session_required, password_required, two_factor_required_by_org · This key, role or policy may not do it. A test key changes nothing saved.; 404 · form_not_found · There is no form with that id — the same answer from fill, check and batch. Search GET /api/v1/engine/forms?q=….; 404 · job_not_found, not_found, endpoint_not_found · No such job / thing / API route.; 409 · conflict · A ?revise that isn't an edit of the same form and filer, or a job that is still running. Nothing is charged.; 410 · gone · A deleted job's ZIP, or an expired link.; 422 · invalid_value · From POST /api/v1/engine/check/{form_id} only: is_valid: false with the same issue list. A valid check is 200.; 422 · idempotency_conflict · The Idempotency-Key was already used with a different body.; 429 · rate_limited, daily_form_limit, login_wait, login_busy · Too many requests for now, too many different forms today on an unpaid tier, or too many wrong passwords on this account.; 429 · too_many_jobs, job_storage_full · One account runs 20 batches at a time and keeps 10 GB of finished ZIPs. Wait for a batch to finish, or download and delete older ones.; 500 / 503 · internal_error, unavailable · Our side — retry.; 503 · document_busy, document_too_slow · A generated document is waiting too long, or took too long to draw. It says Retry-After. Try again, or send less text.

GET /api/v1/openapi.json lists every code in the Error schema.

Tip: run a free check first (POST /api/v1/engine/check/{form_id}, same body, no key). It returns the same issue list without making a PDF.

Rate limits

Every limited endpoint sends RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset (seconds) on each answer, and Retry-After with a 429. Limits are a fixed window per endpoint, per client address (an IPv6 address counts as its whole /64): two keys on one server share them, and one key used from two servers has two. They are not per key or per account.

Endpoint · Limit; POST /api/v1/engine/fill/{id} · 300 a minute; POST /api/v1/engine/check/{id} · 120 a minute; POST /api/v1/engine/batch/{id} · 30 a minute (each up to your plan's batch_row_max rows and 2 GB of PDFs; rows past 2 GB are refused in the job and cost nothing); GET /api/v1/engine/jobs/{id} · 240 a minute; GET /api/v1/engine/jobs, GET …/jobs/{id}/errors · 120 a minute; GET …/jobs/{id}/download, DELETE /api/v1/engine/jobs/{id} · 60 a minute; GET /api/v1/parameters/{id}, GET /api/v1/engine/forms · 120 a minute; POST /api/v1/documents/{id}, its /batch, POST /api/v1/documents/custom · 20 a minute, batches 5 a minute, and 500 documents a day per network; POST /api/v1/verify · 60 a minute; POST /api/v1/engine/scan/{id}, /engine/ingest/{id}, /engine/extract · 10 a minute; POST /api/v1/recipes/{id}/run, /workflows/{id}/run, /schedules · 30 a minute; POST /api/v1/webhooks · 30 a minute; …/{id}/test 10 a minute

Reading a form's field list (GET /api/v1/parameters/:id) counts as opening that form; send your key so it counts against your account. An account whose email is not confirmed has the same budget as a visitor without one. Unpaid tiers (no key, test keys, Free and the trial) also have a budget of different forms a day, counted per address and per account: 12 without an account, 30 for a test key, 40 on Free or the trial, 60 for everyone behind one address. Past it a 429 with code: "daily_form_limit" says when it resets. Paid plans have no form budget.

Example dates and amounts in a field list can differ by a digit between keys and addresses. Each is valid for its field; treat them as samples, not fixed test data.

Idempotency-Key

Send an Idempotency-Key header (any unique string up to 200 characters — your own id for the request) on POST /api/v1/engine/fill/{id} or POST /api/v1/engine/batch/{id}, and a retry can never make a second document or a second charge:

- The same key with the same body within 24 hours replays the first answer, with Idempotent-Replayed: true: the same PDF bytes and the same X-Document-Id for a fill, the same job for a batch. - Two requests with the same key that arrive at the same moment: the second waits for the first to finish, then replays it. - The same key with a different body is refused with 422 and code: "idempotency_conflict". Use a new key for a new request.

curl -X POST https://philforms.com/api/v1/engine/fill/bir-2307 -H "Authorization: Bearer $KEY" \ -H "Content-Type: application/json" -H "Idempotency-Key: payroll-2026-03-emp-1042" \ -d '{ "payee_tin": "123-456-789-000", "payee_name": "DELA CRUZ, JUAN" }' -o 2307.pdf

Retrying a fill

A retry after a timeout does not charge you twice. Sending the same values for the same form again in the same month is a replay: it costs nothing, and the response says which document it repeats in X-Replay-Of (the new copy has its own X-Document-Id). A key inside a table row that the form does not have is listed in X-Unknown-Keys as table[0].key; add ?strict=1 to have the call refused instead.

A key that is almost a real name — employees for the R-3's member table, ss_numbers for ss_number, payeeTin for payee_tin, a one- or two-letter typo — is refused with a 400 (and is_valid: false from a check). Its issue has code: "unknown_parameter" and did_you_mean with the name to use. Rename it and retry, or send "ignore_unknown_column": true in the body (or ?ignore_unknown_column=1) to leave unknown keys out and fill the rest.

This guide as markdown