Ask Anvil

Answers to questions about automating PDFs, e-signatures, Webforms, and other paperwork problems.
PDFs
Categories

How do I generate W-2 PDFs from payroll data?

Two different jobs get called "generating W-2s." One is producing the employee copies (Copies B, C, and 2) that each worker receives. The other is Copy A, the version that goes to the Social Security Administration. Only the first is a PDF generation problem. The second is a filing problem, and the IRS is explicit that you cannot print Copy A from a downloaded PDF and mail it in.

Fill a W-2 template with payroll data

Upload the W-2 as a PDF template, give each box a field alias, then send your payroll row as JSON. With Anvil's PDF filling API, the call returns the filled PDF as binary bytes:

import fs from 'fs'
import Anvil from '@anvilco/anvil'

const anvilClient = new Anvil({ apiKey: process.env.ANVIL_API_KEY })
const pdfTemplateID = 'YOUR_PDF_TEMPLATE_ID'

const { statusCode, data } = await anvilClient.fillPDF(pdfTemplateID, {
  title: 'W-2 Employee Copy',
  fontSize: 9,
  data: {
    employeeSsn: '123-45-6789',
    employerEin: '12-3456789',
    wagesTipsOtherComp: 84000.0,
    federalIncomeTaxWithheld: 11200.5,
    socialSecurityWages: 84000.0,
    socialSecurityTaxWithheld: 5208.0,
  },
})

console.log(statusCode) // => 200

// The response is binary. Save it with no encoding, or the file will be corrupt.
fs.writeFileSync('w2-employee-copy.pdf', data, { encoding: null })

The keys inside data are the field aliases on your template, not the IRS box numbers, so name them once and reuse them every year. Anything you send that has no matching field is ignored, which means a payroll export with extra columns will not blow up the request.

One PDF per employee

Pass an array to data and the template is filled once per element, returning a single merged PDF. That is what you want for a print run:

const { data } = await anvilClient.fillPDF(pdfTemplateID, {
  data: payrollRows.map((row) => ({
    employeeSsn: row.ssn,
    employerEin: row.ein,
    wagesTipsOtherComp: row.grossWages,
    federalIncomeTaxWithheld: row.fedWithheld,
  })),
})

If you need a separate file per employee (for a portal download, for example), skip the array and call fillPDF once per payroll row instead.

Two things that will bite you

Copy A is not a printable PDF. The IRS General Instructions for Forms W-2 and W-3 say: "Do not print Copy A of Forms W-2, W-3, W-2c, or W-3c from IRS.gov and then file them with the SSA. The SSA accepts only e-filed reports and the official red-ink versions (or approved substitute versions) of these forms." Copies 1, B, C, 2, and D are the ones you can generate and hand out. Note too that Copy A must carry the complete SSN and EIN, so no truncation there.

You are probably required to e-file. If you must file at least 10 information returns for the year (W-2s plus 1099s and the other listed forms, added together), all of them have to be filed electronically. The SSA's Business Services Online lets you create up to 50 Forms W-2 at a time in the browser, or upload an EFW2-format file for bigger runs. Your PDF pipeline feeds the employees; BSO or your payroll provider feeds the SSA.

Keep those paths separate in your code and the year-end job stays boring: one function that turns payroll rows into employee-facing PDFs, and one that hands the same rows to whatever files with the SSA.

Back to All Questions

The fastest way to build software for documents

Anvil Document SDK is a comprehensive toolbox for product teams launching document flows where PDF filling, signing, and complex conditional scenarios are necessary.
Explore Anvil
Anvil Webforms