Payroll run → payslip per employee

Payroll day takes forty minutes, and one bad row means the whole run again.

Trigger → artefact

The pay cycle closesa payslip PDF per employee, each with its own numbers

A payslip is not a hard document to design. Producing one for every employee, every month, where each has different earnings, different deductions and a different number of lines — that is the part that gets built in a hurry by someone who did not want to build it.

You POST one record per employee. The document does its own arithmetic, paginates itself, and comes back as bytes. If a row is missing a field the render fails loudly and names the field, so a bad row is a 400 you can retry, not a blank space nobody notices until an employee does.

Built with the HTML API. Need a designed image or certificate instead? Use the Canvas API — same data, same free year.

A payslip PDF rendered by PixlDocs, showing employer details, earnings and deductions tables and net pay
Rendered by the template this page calls — not a mockup.

The whole integration

One call. Named fields. No SDK to install.

the call your product makesbash
# the call your payroll job makes, once per employee
curl https://api.pixldocs.com/v1/render \
  -H "Authorization: Bearer $PIXLDOCS_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "template_id": "9f7b8380-710e-433a-a4c9-4ecafc986d35",
    "format": "pdf",
    "data": {
      "employee_name": "A. Fischer",
      "pay_period": "July 2026",
      "pay_date": "2026-07-31",
      "earnings": [
        { "component": "Basic", "current": 60000, "ytd": 240000 }
      ]
    }
  }' --output payslip.pdf

# pay day: submit up to 100 at a time, poll the ids
JOBS=$(curl -s https://api.pixldocs.com/v1/renders/batch \
  -H "Authorization: Bearer $PIXLDOCS_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "items": [
      { "template_id": "9f7b8380-710e-433a-a4c9-4ecafc986d35", "format": "pdf",
        "data": { "employee_name": "A. Fischer", "pay_period": "July 2026", "pay_date": "2026-07-31",
                  "earnings": [ { "component": "Basic", "current": 60000, "ytd": 240000 } ] } },
      { "template_id": "9f7b8380-710e-433a-a4c9-4ecafc986d35", "format": "pdf",
        "data": { "employee_name": "R. Iyer", "pay_period": "July 2026", "pay_date": "2026-07-31",
                  "earnings": [ { "component": "Basic", "current": 74000, "ytd": 296000 } ] } }
    ]
  }')
# 202 { "jobs": [ { "id": "…", "status": "queued" }, … ] } — one id per employee

# poll an id until status is "succeeded"; result_url is the finished PDF
curl -s https://api.pixldocs.com/v1/renders/$(echo "$JOBS" | jq -r '.jobs[0].id') \
  -H "Authorization: Bearer $PIXLDOCS_KEY"

This exact block was run against our API before it was published — both calls, verbatim: the single render returns 200 application/pdf in about 1.3s, the batch returns 202 with one id per employee. The template id is real and public, so you can run it with no account.

Measured, not estimated

1,000 payslips submitted
45.6s to accept the whole batch
last document finished
423.6s — about 7 minutes
sustained throughput
142 documents a minute
failures
none — 1,000 of 1,000 delivered
pushed to 50 concurrent
11 refusals carrying Retry-After, 0 bare 5xx

One real burst against our staging environment, not a projection. When we are full we refuse with a Retry-After header rather than timing out — a queue you can back off against is worth more on pay day than a number we cannot hold.

The contract

Exactly what you send, exactly what you get.

The payslip template takes these named keys. Nothing about layout, fonts or page breaks appears in your code.

Payslip

9f7b8380-710e-433a-a4c9-4ecafc986d35

Fields · 28

company_namecompany_addresscompany_emailcompany_tax_idpayslip_nopay_periodperiod_startperiod_endpay_dateemployee_nameemployee_codeemployee_titleemployee_departmentemployee_locationdate_joinedemployee_tax_idemployee_pf_nobank_namebank_account_maskedpayment_modecurrency_codecurrency_localedays_in_period #days_paid #days_unpaid #leave_balancenotesfooter_note

Rows · earnings[]

component · current · ytd

Send as many as you have. Pagination, running totals and the carried-forward header are the renderer's problem, not yours.

Rows · deductions[]

component · current · ytd

Send as many as you have. Pagination, running totals and the carried-forward header are the renderer's problem, not yours.

Rows · employer_contributions[]

component · current · ytd

Send as many as you have. Pagination, running totals and the carried-forward header are the renderer's problem, not yours.

What you would ask next.

What happens to the other 999 when one row is bad?

Nothing. Each render is independent and each carries its own id. A row missing a required field returns 400 with the field named, and no credit is used for it — you fix that row and resubmit that row.

Ten thousand employees, not one thousand?

We have measured 1,000. At the throughput we measured that is roughly 75 minutes, but we have not run it, so treat it as arithmetic rather than a promise. If that is your monthly shape, tell us and we will run it before you commit.

Our payslip looks nothing like yours.

Describe yours in a sentence and you get a working template with named fields — measured at about 30 seconds. The document above was generated that way.

Your system already knows when the document is owed.

Free to start · No credit card required