How to Add Page Numbers and Headers with PDFShift
Add repeating headers, footers, and page numbers to PDFShift PDFs with runnable examples and fixes for layout and font issues.
To add page numbers and a repeating header with PDFShift, send a header object in your JSON request. Put {{ page }} in its source for the current page number and {{ total }} for the document’s page count. Set height to reserve room, and use start_at if the header should begin after page one. The footer uses the same configuration pattern. PDFShift’s official guide documents these fields and variables.
Quick start with Node.js
This complete example uses Node.js 18 or later, which provides fetch. Set PDFSHIFT_API_KEY in the environment, save the file as generate.mjs, then run node generate.mjs. It sends a URL as the document source and writes the returned PDF bytes to disk.
import { writeFile } from 'node:fs/promises';
const apiKey = process.env.PDFSHIFT_API_KEY;
if (!apiKey) throw new Error('Set PDFSHIFT_API_KEY before running this script.');
const params = {
source: 'https://example.com',
header: {
source: `<div style="font: 10px Arial, sans-serif; text-align: right; color: #444">Page {{ page }} of {{ total }}</div>`,
height: '12mm'
}
};
const response = await fetch('https://api.pdfshift.io/v3/convert/pdf', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': apiKey
},
body: JSON.stringify(params)
});
if (!response.ok) {
const detail = await response.text();
throw new Error(`PDFShift returned HTTP ${response.status}: ${detail}`);
}
const pdf = Buffer.from(await response.arrayBuffer());
await writeFile('result.pdf', pdf);
console.log(`Saved result.pdf (${pdf.length} bytes)`);
The request pattern is a POST to https://api.pdfshift.io/v3/convert/pdf with JSON and the X-API-Key header. See PDFShift’s documentation for the full API reference and any options beyond the header fields discussed here.
Configure the header and page numbering
Header fields
| Field | Purpose | Notes |
|---|---|---|
source |
Header content as a URL or raw HTML. | For a page counter, include {{ page }} and optionally {{ total }} in this content. |
height |
Space reserved for the header. | Pixels are the default unit. You can also use mm, cm, or in, for example 10mm. Choose a value that fits the rendered header. |
start_at |
First page that displays the header. | The default is page one. Set it to 2 to begin on the second page. |
Use the matching footer object for a footer; it follows the same pattern and accepts the same variables. The variables documented for header and footer source are:
{{ title }}: document title{{ url }}: document URL{{ page }}: current page number{{ total }}: total page count{{ date }}: current date, formatted by PDFShift as a date and time
Keep the HTML compact and explicit. For example, a left-aligned document title and right-aligned page count can share a flex row:
<div style="display:flex; justify-content:space-between; width:100%; font:10px Arial,sans-serif">
<span>{{ title }}</span>
<span>Page {{ page }} of {{ total }}</span>
</div>
Check the output PDF to confirm variable replacement, spacing, and page totals. Use the documented variables as written, including the braces.
Start the header after the cover
To omit the header from page one, set start_at to 2. The value identifies the first page on which the header appears; it does not change the page numbering variable. For example, the second page should still report its actual page number.
{
"source": "https://example.com",
"header": {
"source": "<div style='text-align:right'>Page {{ page }} of {{ total }}</div>",
"height": "12mm",
"start_at": 2
}
}
Complete cURL example
Use --data-binary to send a JSON request body. Replace the placeholder with your API key. The response is binary PDF data, so redirect standard output to a file.
curl --fail-with-body \
-X POST 'https://api.pdfshift.io/v3/convert/pdf' \
-H 'X-API-Key: YOUR_PDFSHIFT_API_KEY' \
-H 'Content-Type: application/json' \
--data-binary '{"source":"https://example.com","header":{"source":"<div style=\"text-align:right;font:10px Arial\">Page {{ page }} of {{ total }}</div>","height":"12mm"}}' \
-o result.pdf
If the request fails, --fail-with-body makes curl return an error while retaining the response body for diagnosis. Avoid putting a real key in shell history or committed scripts; use a secret manager or environment-based script in production.
Complete Python example
Install the HTTP client with python -m pip install requests. Set PDFSHIFT_API_KEY, then run this script. It checks the HTTP response before saving the PDF bytes.
import os
import requests
api_key = os.environ.get("PDFSHIFT_API_KEY")
if not api_key:
raise RuntimeError("Set PDFSHIFT_API_KEY before running this script.")
payload = {
"source": "https://example.com",
"header": {
"source": '<div style="text-align:right;font:10px Arial,sans-serif">Page {{ page }} of {{ total }}</div>',
"height": "12mm",
},
}
response = requests.post(
"https://api.pdfshift.io/v3/convert/pdf",
headers={"X-API-Key": api_key},
json=payload,
timeout=120,
)
if not response.ok:
raise RuntimeError(f"PDFShift returned HTTP {response.status_code}: {response.text}")
with open("result.pdf", "wb") as pdf_file:
pdf_file.write(response.content)
print(f"Saved result.pdf ({len(response.content)} bytes)")
HTML input and a styled header
The source can be a page URL or raw HTML. When sending HTML, keep your body content in that field and define the header separately. Here is a JSON payload shape for raw HTML input:
{
"source": "<html><body><h1>Quarterly report</h1><p>Report content...</p></body></html>",
"header": {
"source": "<div style='border-bottom:1px solid #bbb;padding-bottom:4px;font:10px Arial'>Quarterly report · Page {{ page }} of {{ total }}</div>",
"height": "14mm"
},
"footer": {
"source": "<div style='text-align:center;font:9px Arial;color:#666'>Confidential</div>",
"height": "10mm"
}
}
For a URL source, the header and footer can still contain raw HTML. Their styles and assets have a separate constraint: they must be available within the header/footer content itself. PDFShift says network-loaded CSS, JavaScript, and fonts will not load there. Use inline styles and embed required assets as Base64. This keeps header rendering independent of network availability.
Custom fonts in headers and footers
For a custom font, Base64-encode the font file and include the font definition in both the main document and the header or footer, then apply that font in each location. PDFShift reports successful testing with TrueType and WOFF2 fonts. When the body is supplied by URL, PDFShift’s help article describes putting the font in the request’s CSS so it is used in the page as well as embedding the font data in the header/footer. See PDFShift’s custom-font instructions.
For example, the relevant CSS shape is:
@font-face {
font-family: 'ReportFont';
src: url('data:font/woff2;base64,BASE64_FONT_DATA') format('woff2');
}
.header { font-family: 'ReportFont', sans-serif; }
Include equivalent font-face data in the body document or body CSS and in the header/footer source. Replace BASE64_FONT_DATA with the actual encoded font bytes; it is a placeholder, not a usable font. Embedding a large font increases request size, so subset or use a suitable smaller font file where practical.
Fix pagination shifts and first-page overflow
Headers and footers need reserved page space. A common surprise occurs when a header starts on page two while page one already fills the printable height: the reserved margin may still affect page one and push the last content onto page two. PDFShift documents the first-page margin override as the fix. For a later-starting header, add:
{
"css": "@page:first { margin-top: 0 }"
}
For a later-starting footer, use @page:first { margin-bottom: 0 }. If your document already defines margins, adapt the values instead of blindly setting them to zero. If both header and footer are present, account for both top and bottom space. The exact adjustment depends on the existing page layout. PDFShift’s troubleshooting article explains the margin behavior and these CSS patterns.
For headers displayed on every page, ensure the body content does not overlap the reserved header area. Increase the header height if it clips, then review the resulting content area and page breaks. Too little space clips or crowds header content; too much space can move body content onto additional pages.
Troubleshooting
| Symptom | Likely cause | What to change |
|---|---|---|
| Page placeholders appear literally. | The placeholder is missing or altered, or it is outside the header/footer source. | Put {{ page }} or {{ total }} directly in the header/footer source string and inspect the generated PDF. |
| Header is missing on the first page. | start_at is set to a later page. |
Remove start_at or set it to 1 if the header should start on page one. |
| Header overlaps body content. | The reserved header height is insufficient for the rendered content. | Increase height and verify the body’s top margin and page boundaries. |
| Content moves to another page or page one leaks onto page two. | Header/footer margins add space to a page that was already full, particularly with a later start_at. |
Review the first page’s available height and use the appropriate @page:first margin override, adapted to existing margins. |
| Header styles or external font do not appear. | Header/footer rendering cannot fetch external CSS, JavaScript, or fonts. | Inline the CSS and Base64-embed required assets. For custom fonts, include and use the font in both body and header/footer. |
| Output file is empty, corrupt, or contains an error response. | The request failed but the client saved its response as if it were a PDF. | Check HTTP status and response body before writing bytes; confirm the key, JSON syntax, endpoint, and source URL. |
| PDF generation takes longer than expected. | The source page or its assets may require network loads and rendering work. | Keep header resources self-contained, avoid unnecessary remote assets, and check that the source URL is reachable by the conversion service. |
Performance, reliability, and cost considerations
- Keep resources local to the header. Inline CSS and embed fonts or images that the header needs. This avoids depending on remote resource fetches that PDFShift says are unavailable in the header/footer context.
- Choose a realistic height. A concise one-line header needs less reserved space than a multi-line branded header. Validate both a short document and the longest expected output.
- Handle conversion errors explicitly. Check HTTP status and response content before treating the result as a PDF. Set a client timeout appropriate to your application and retry only transient failures with bounded backoff; avoid unbounded retries that can duplicate cost or work.
- Protect credentials. Keep the API key on the server side and out of browser code, public repositories, and logs.
- Budget for document complexity. The research dossier provides no conversion-price or timing basis for this specific configuration. Check PDFShift’s current plan and API terms for your usage rather than assuming a fixed per-document cost or conversion time.
Or skip the browser setup
If your actual task is capturing a web page as an image, ScreenshotNeo is a website screenshot API and MCP server for developers. It does not add page numbers to PDFShift output; use the PDFShift examples above when you need a paginated PDF. For a screenshot, one GET request can return PNG, JPEG, WebP, or PDF. See the ScreenshotNeo API documentation for options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
ScreenshotNeo removes cookie banners, popups, and chat widgets before the shot. Bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Sign up for ScreenshotNeo and get 1,000 free screenshots a month, with no card.
FAQ
Can I put a page number in both the header and footer?
Yes. Add the page variable to the source of each configured section where you want it to appear.
Can the header begin on a selected page?
Yes. Set start_at to that page number; it defaults to the first page.
Does the header height use only pixels?
No. Pixels are the default, and PDFShift also documents millimeters, centimeters, and inches.
Can I use a custom font in a header?
Yes. Embed its Base64 data and include/use the font in both the main document and the header or footer. PDFShift reports successful tests with TrueType and WOFF2.


