Get your API key
How to add Bates numbering to a PDF (free, programmatic)

How to add Bates numbering to a PDF (free, programmatic)

Add Bates numbering to a PDF: stamp sequential legal page identifiers with a prefix, fixed digit count, and start value using pdf-lib in Node.js, step by step.

10 min read

Bates numbering is a sequential identifier stamped on every page of a document, usually formatted as a prefix plus a zero-padded number like ABC000001 in the bottom-right corner. The fastest free way to add it to an existing PDF is the open-source pdf-lib library in Node.js: load the file, loop over the pages, and draw a formatted label on each one. It runs locally, works on scanned and text PDFs, and costs nothing per document. If you are generating the PDF from HTML instead of stamping a finished file, PDF4.dev can print the same label in a repeating footer.

This guide covers the format rules, the pdf-lib code to stamp one file, how to continue numbering across multiple files, and how to make the stamp permanent.

What is Bates numbering and who uses it?

Bates numbering is a method for assigning a unique, sequential identifier to each page of a document set. The name comes from the Bates Automatic Numbering Machine patented in the 1890s. Today it is a standard practice in legal discovery, where every page produced to opposing counsel must be individually referenceable.

Each Bates number is unique across the entire production, not just within one file. A 500-page production carries numbers 1 through 500 even when the pages are split across ten separate PDFs. This is the core difference from ordinary page numbers, which reset to 1 at the start of each file.

The format is a prefix plus a zero-padded sequence. The prefix identifies the case, party, or custodian (for example SMITH or DEF). The number is padded to a fixed width so labels sort correctly as strings and align visually. Six or seven digits is the common choice, giving room for up to a million pages.

Fields outside the legal industry use the same idea. Accounting teams stamp invoice batches, medical records departments number patient files, and audit teams tag evidence bundles. Any workflow that needs a durable, unique page reference across a document set benefits from Bates-style numbering.

Bates numbering vs page numbers: what is the difference?

Bates numbers are globally unique across a production and never reset, while page numbers describe position within a single file and reset per document. The table below shows the practical differences.

PropertyPage numbersBates numbering
ScopeOne fileEntire production (many files)
Resets per fileYesNo
PrefixRareStandard (case or party ID)
Zero-paddingOptionalStandard (fixed width)
Typical positionFooter center or cornerBottom-right corner
Primary useReading navigationLegal reference, discovery
UniquenessPer documentAcross the whole set

If you only need reading navigation inside one document, add ordinary page numbers with the free add page numbers tool or the approach in our page numbers guide. Use Bates numbering when a number must uniquely identify a page across an entire set of files, such as a discovery production or an audit bundle.

How do I add Bates numbering to a PDF with pdf-lib?

Add Bates numbering by loading the PDF with pdf-lib, looping over every page, and drawing a formatted label in the bottom-right corner. The label combines a prefix, the current counter, and zero-padding to a fixed width. pdf-lib is open source and runs in Node.js, the browser, or Deno, so no file leaves your machine.

Install the library first:

npm install pdf-lib

The function below stamps a single PDF. It takes the input bytes, a prefix, a starting number, and a digit count, then returns the stamped bytes plus the next available number so you can chain files.

import { PDFDocument, StandardFonts, rgb } from "pdf-lib";
 
interface BatesOptions {
  prefix: string;      // e.g. "ABC"
  start: number;       // first Bates number, e.g. 1
  digits: number;      // zero-pad width, e.g. 6 -> 000001
  fontSize?: number;   // default 9
  margin?: number;     // points from page edge, default 24
}
 
export async function stampBates(
  input: Uint8Array,
  opts: BatesOptions
): Promise<{ bytes: Uint8Array; nextNumber: number }> {
  const { prefix, start, digits, fontSize = 9, margin = 24 } = opts;
  const pdf = await PDFDocument.load(input);
  const font = await pdf.embedFont(StandardFonts.Helvetica);
  const pages = pdf.getPages();
 
  let counter = start;
  for (const page of pages) {
    const label = `${prefix}${String(counter).padStart(digits, "0")}`;
    const textWidth = font.widthOfTextAtSize(label, fontSize);
    const { width } = page.getSize();
 
    page.drawText(label, {
      x: width - margin - textWidth,
      y: margin,
      size: fontSize,
      font,
      color: rgb(0, 0, 0),
    });
    counter += 1;
  }
 
  const bytes = await pdf.save();
  return { bytes, nextNumber: counter };
}

Run it against a file and write the result to disk:

import { readFile, writeFile } from "node:fs/promises";
 
const input = await readFile("production.pdf");
const { bytes } = await stampBates(new Uint8Array(input), {
  prefix: "ABC",
  start: 1,
  digits: 6,
});
await writeFile("production-bates.pdf", bytes);

The padStart call is what produces ABC000001 instead of ABC1. Fixed width keeps every label the same length, which is what makes a stack of stamped pages line up and sort correctly.

How do I continue Bates numbering across multiple PDF files?

Continue Bates numbering across files by carrying a running counter between them: start the next file at the number returned by the previous one. Because Bates numbers are unique across a whole production, the counter must never reset when you move to a new file.

The stampBates function above already returns nextNumber, the first unused value after a file. Feed that value into the next call as the new start:

import { readFile, writeFile } from "node:fs/promises";
 
const files = ["part-1.pdf", "part-2.pdf", "part-3.pdf"];
let counter = 1;
 
for (const name of files) {
  const input = await readFile(name);
  const { bytes, nextNumber } = await stampBates(new Uint8Array(input), {
    prefix: "ABC",
    start: counter,
    digits: 6,
  });
  await writeFile(name.replace(".pdf", "-bates.pdf"), bytes);
  counter = nextNumber;
}

If part-1.pdf has 40 pages, its labels run ABC000001 to ABC000040, and part-2.pdf picks up at ABC000041. The counter never resets, so the whole set stays uniquely numbered.

An alternative is to merge the files into one PDF first, then stamp the combined document in a single pass. This removes the counter-passing step entirely because there is only one file to number. Merge the parts with the free merge PDF tool or the code in our merge PDF guide, then run stampBates once on the result.

How do I add a confidentiality label alongside the Bates number?

Add a confidentiality designation by drawing a second text string on the bottom-left of each page while you draw the Bates number on the bottom-right. Legal productions often pair a Bates number with a status label such as CONFIDENTIAL or ATTORNEYS EYES ONLY.

Extend the loop to draw both labels:

const status = "CONFIDENTIAL";
for (const page of pages) {
  const label = `${prefix}${String(counter).padStart(digits, "0")}`;
  const textWidth = font.widthOfTextAtSize(label, fontSize);
  const { width } = page.getSize();
 
  // Bates number, bottom-right
  page.drawText(label, {
    x: width - margin - textWidth,
    y: margin,
    size: fontSize,
    font,
    color: rgb(0, 0, 0),
  });
 
  // Confidentiality label, bottom-left
  page.drawText(status, {
    x: margin,
    y: margin,
    size: fontSize,
    font,
    color: rgb(0.7, 0, 0),
  });
  counter += 1;
}

Keep both labels in the bottom margin so they do not overlap body text. If a document is being redacted before production, apply redactions first and flatten them, then add the Bates stamp last. Our permanent redaction guide explains why redaction must be destructive rather than a drawn black box.

How do I make the Bates stamp permanent?

Make the stamp permanent by flattening the PDF after numbering, which merges the drawn text into the page content so a viewer cannot select or delete it. A drawn text object added by pdf-lib is part of the page content stream, so it is not a form field or annotation and is already hard to remove. Flattening removes any remaining interactive layers and guarantees the label prints identically everywhere.

The behavior differences are summarized below.

StepRemovable in a viewer?Prints reliably?When to use
Drawn text (default)Hard, part of content streamYesMost Bates workflows
Flatten after stampingNoYesCourt filings, final production
Form-field overlayYes, editableSometimesAvoid for Bates

Never place a Bates number in an editable form field or a note annotation, because a recipient could change or delete it. Draw the text directly and flatten. You can flatten in code or with the free flatten PDF tool; see the flatten PDF guide for the details.

Can I generate Bates-style labels when creating the PDF from HTML?

Yes. If you produce the PDF from HTML rather than stamping a finished file, output the label in a repeating footer instead of drawing it per page afterward. PDF4.dev renders HTML with headless Chromium and repeats a footer component on every page, so a Bates-style label can be part of the template.

This path fits documents you generate on the fly, such as report bundles or invoice batches, where you control the source HTML. The label is data, not a post-processing step:

const res = await fetch("https://pdf4.dev/api/v1/render", {
  method: "POST",
  headers: {
    Authorization: "Bearer p4_live_xxx",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    template_id: "discovery-bundle",
    data: { case_prefix: "ABC", start_number: 1 },
  }),
});

Use HTML footers when you own the generation step and want the label baked in from the start. Use pdf-lib when you receive a finished PDF from someone else and need to stamp numbers onto it. Both produce the same visible result: a unique, sequential identifier on every page. You can try HTML to PDF rendering with the free Html To PdfTry it free tool before wiring up the API.

Common issues with Bates numbering

The most frequent problems are labels overlapping body content, inconsistent widths, and numbering that resets between files. Each has a direct fix.

  • Label sits on top of text. Increase the margin value so the number stays inside the page margin, or reduce fontSize. The bottom-right corner at a 24-point margin clears most document layouts.
  • Numbers are different lengths. Use padStart with a fixed digits value. Without zero-padding, ABC1 and ABC100 have different widths and misalign.
  • Numbering resets across files. Pass the returned nextNumber into the next file as start, or merge all files into one PDF before stamping.
  • Rotated pages stamp in the wrong spot. pdf-lib draws relative to the unrotated coordinate system. For pages with a /Rotate value, read the rotation and adjust the x and y position, or normalize rotation before stamping.
  • Stamp can be removed. Flatten the PDF after numbering so the text merges into the page content stream.

For a broader overview of programmatic PDF editing with pdf-lib, see our complete guide to PDF manipulation.

Summary

Bates numbering stamps a unique, sequential identifier on every page of a document set, formatted as a prefix plus a zero-padded number like ABC000001. For an existing PDF, loop over the pages with pdf-lib and draw the label in the bottom-right corner, carrying a running counter across files so numbers never reset. Flatten the result to make the stamp permanent. When you generate the PDF from HTML, print the same label from a repeating footer with PDF4.dev instead of stamping afterward.

Free tools mentioned:

Number PdfTry it freeMerge PdfTry it freeWatermark PdfTry it free

Start generating PDFs

Build PDF templates with a visual editor. Render them via API from any language in ~300ms.