Endpoint reference
POST /api/page-numbers
Stamp a page number on each page of a PDF. The format takes {n} and {total} tokens. Returns the numbered PDF as application/pdf.
Request
Method: POST. URL: https://pdfops.dev/api/page-numbers. Body: multipart/form-data.
| Field | Type | Required | Description |
|---|---|---|---|
pdf | File (PDF binary) | Yes | Exactly one PDF, up to 4 MB and 2,000 pages. The whole request must stay under 4.5 MB, Vercel's platform request limit. |
format | String | No (default {n} / {total}) | Label template, up to 100 characters. {n} is the page number, {total} is the page count of the whole PDF. Standard Helvetica only. |
position | String | No (default bottom-center) | One of bottom-center, bottom-right, bottom-left, top-center, top-right, top-left. |
size | Number | No (default 10) | Font size in points, 4 to 72. |
start | Integer | No (default 1) | The number shown on page 1 of the document, 0 to 100000. |
margin | Number | No (default 24) | Distance in points from the page edge as displayed, 0 to 200. |
pages | String | No | Page list such as 1-3,5 (see Page lists). Default: all pages. |
password | String | No | Opens a PDF that needs a user password. Not needed for owner-locked files with an empty user password. |
The optional X-API-Key header works as on /api/merge: no header is anonymous, 100 requests per IP per month; a valid key uses your tier's quota; an invalid or revoked key returns 401 invalid_api_key. A successful (2xx) call counts 1 request. Check consumption with GET /api/usage.
Page lists
The pages value is a comma list of items: N (one page), A-B (an inclusive range) or A- (page A through the last page). Pages are 1-based. Spaces around items and around the dash are ignored. last and negative numbers are not supported. A page past the end of the PDF, a reversed range, an empty item or an empty value fails with invalid_pages.
Response
On success: 200 OK, Content-Type: application/pdf, body is the numbered PDF.
On any failure: 4xx or 500, Content-Type: application/json, body is { "error": "<code>", "details": "<explanation>" }.
Error codes
| Error code | Status | Trigger |
|---|---|---|
invalid_multipart | 400 | Request body could not be parsed as multipart/form-data. |
missing_pdf | 400 | The request does not have exactly one pdf file field. |
pdf_too_large | 400 | The pdf file is over 4 MB. |
invalid_pdf | 400 | The PDF could not be parsed (corrupt, or not a PDF). |
encrypted_pdf | 400 | The PDF needs a password we were not given. Owner-locked files with an empty user password (most government blanks) open on their own; for a real user password send it as the password form field. |
decompressed_too_large | 400 | Object streams inside the PDF decompress to over 32 MB. |
too_many_pages | 400 | The PDF has over 2,000 pages. |
format_too_long | 400 | format is over 100 characters. |
unsupported_characters | 400 | format has a character that standard Helvetica cannot draw. details names the first one. |
invalid_parameter | 400 | position, size, start or margin is invalid or out of range. details names it. |
invalid_pages | 400 | pages is empty, malformed or out of range. details says which item failed. |
save_failed | 500 | PDF serialization failed. Retryable. |
request_too_large | 413 | The whole request body is over 4.5 MB (Vercel's platform request limit). Rejected before anything is parsed. |
invalid_api_key | 401 | An X-API-Key header was sent but the key is unknown or revoked. No fallback to the anonymous IP cap. Get a fresh key at /pricing. |
rate_limited (anonymous) | 429 | No X-API-Key header and the per-IP limit (100/month) is exhausted. Retry-After gives seconds until next calendar month. |
rate_limited (keyed) | 429 | Your key's tier quota (Free 250 / Indie 4,000 / Pro 25,000 per month) is exhausted. Retry-After gives seconds until next calendar month. |
Example
curl -X POST https://pdfops.dev/api/page-numbers \
-F "pdf=@report.pdf" \
-F "format=Page {n} of {total}" \
-F "position=bottom-right" \
-o numbered.pdf \
-H "X-API-Key: pdfops_live_..." # optional, raises your quota to your tier
Keyless calls work: 100 requests per IP per month with no key.
Using this for real? A free API key raises the keyless 100 calls per IP to 250 requests a month, with no card.
Code examples
Have an API key? Add an X-API-Key header to any request. It is optional, and it raises your quota from 100/IP/month to your tier's.
JavaScript (Node.js 22+) — built-in fetch + FormData
import { readFile, writeFile } from 'node:fs/promises';
const fd = new FormData();
fd.append('pdf', new Blob([await readFile('report.pdf')], { type: 'application/pdf' }), 'report.pdf');
fd.append('format', 'Page {n} of {total}');
fd.append('position', 'bottom-right');
const res = await fetch('https://pdfops.dev/api/page-numbers', {
method: 'POST',
headers: { 'X-API-Key': process.env.PDFOPS_API_KEY }, // optional
body: fd,
});
if (!res.ok) {
const err = await res.json();
throw new Error(`PDFops page-numbers failed: ${err.error}: ${err.details}`);
}
await writeFile('numbered.pdf', Buffer.from(await res.arrayBuffer()));
Behavioral notes
- Numbers follow the whole document. Page i shows
start + i - 1, even whenpagesselects only some pages.{total}is always the full page count. - The label is placed on the page as displayed. It is upright for any page size and any existing page rotation. The margin is measured from the edge of the visible area: the CropBox when a page has one, else the media box.
- The output is deterministic. The modification date and producer are pinned, and an existing creation date is kept.
- Limits: 4 MB per PDF, 2,000 pages, 4.5 MB per request (Vercel's platform limit).
FAQ
How do I skip the cover page?
Send pages=2-. Page 2 then shows 2, because numbers follow the whole document. To show 1 on page 2, send start=0.
Can I change the font or colour?
No. Numbers are drawn in dark grey standard Helvetica. You can change the size, position, margin and label text.
Related: Add a text watermark · Join PDFs · Set title and author.
Get a free API key. Keyless calls get 100 requests per IP per month; a free key raises that to 250, with no card. Enter your email and the key arrives in your inbox.