API reference
All current folio endpoints in one place.
When API_KEY is set, every route requires the
X-Api-Key header, including /health and
/metrics.
Authentication
All routes are protected by a static API key when the
API_KEY environment variable is set. Clients must send the key in
the X-Api-Key request header. When API_KEY is unset,
auth is skipped for local development.
curl -X POST http://localhost:8080/pdf/generate \
-H "Content-Type: application/json" \
-H "X-Api-Key: your-secret-key-here" \
-d '{"html":"<html><body><h1>Hello</h1></body></html>"}'
/health
Returns service liveness. Also responds to HEAD /health.
{ "status": "ok" }
curl -s http://localhost:8080/health | jq .
/metrics
Returns Prometheus text-format metrics.
curl -s http://localhost:8080/metrics
| Metric | Type | Description |
|---|---|---|
pdf_generation_duration_ms |
histogram | Wall-clock time with buckets at 100–10000 ms |
pdf_size_bytes |
histogram | Output size with buckets at 10 KB–10 MB |
pdf_generation_requests_total |
counter | Labeled status="success" and status="error" |
/pdf/generate
Render a PDF from HTML or by navigating to a URL. Provide html
or url — the two fields are mutually exclusive.
Request body (HTML)
{
"html": "<html><body><h1>Hello</h1></body></html>",
"css": "body { font-family: Arial, sans-serif; }",
"paper": { "size": "A4", "orientation": "portrait" },
"options": {
"margin": { "top": "20mm", "right": "15mm", "bottom": "20mm", "left": "15mm" },
"scale": 1.0,
"printBackground": false,
"headerTemplate": "<div style=\"font-size:10px;text-align:center;\">My Header</div>",
"footerTemplate": "<div style=\"font-size:10px;text-align:right;\">Page <span class=\"pageNumber\"></span> of <span class=\"totalPages\"></span></div>"
},
"stream": false
}
Request body (URL)
{
"url": "https://example.com",
"cookies": [{ "name": "session", "value": "abc123", "domain": "example.com" }],
"extraHeaders": { "Authorization": "Bearer my-token" },
"paper": { "size": "A4" },
"stream": false
}
| Field | Type | Required | Description |
|---|---|---|---|
html | string | one of html/url | HTML to render |
url | string | one of html/url | URL to navigate to and render |
css | string | no | Extra CSS injected after the page loads |
cookies | array | no | Cookies set before navigation (each with name, value, domain) |
extraHeaders | object | no | HTTP headers sent with every request on the page |
paper.size | string | no | A4, A3, Letter, Legal, Tabloid |
paper.orientation | string | no | portrait or landscape |
options.margin | object | no | top, right, bottom, left in CSS units |
options.scale | number | no | 0.1–2.0 |
options.printBackground | boolean | no | Include CSS backgrounds |
options.headerTemplate | string | no | HTML header displayed on every page |
options.footerTemplate | string | no | HTML footer with page-number placeholders |
stream | boolean | no | true returns binary, false returns a stored PDF response |
Response when stream: false
{ "statusCode": 200, "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "url": "https://s3.amazonaws.com/...?X-Amz-Signature=..." } }
Response when stream: true
Content-Type: application/pdfContent-Disposition: attachment; filename="document.pdf"
Simple HTML to PDF:
curl -s -X POST http://localhost:8080/pdf/generate \
-H "Content-Type: application/json" \
-d '{"html": "<html><body><h1>Hello</h1></body></html>"}' | jq .
URL to PDF:
curl -s -X POST http://localhost:8080/pdf/generate \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com"}' | jq .
URL with authentication (cookies + headers):
curl -s -X POST http://localhost:8080/pdf/generate \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com",
"cookies": [{"name": "session", "value": "abc123", "domain": "example.com"}],
"extraHeaders": {"Authorization": "Bearer my-token"}
}' | jq .
Styled HTML with CSS, paper size, margins, and header/footer:
curl -s -X POST http://localhost:8080/pdf/generate \
-H "Content-Type: application/json" \
-d '{
"html": "<html><body><h1>Invoice</h1></body></html>",
"css": "body { font-family: Helvetica, sans-serif; }",
"paper": {"size": "A4"},
"options": {
"margin": {"top": "20mm", "bottom": "20mm", "left": "15mm", "right": "15mm"},
"printBackground": true,
"headerTemplate": "<div style=\"font-size:8px;text-align:center;\">ACME Corp</div>",
"footerTemplate": "<div style=\"font-size:8px;text-align:center;\">Page <span class=\"pageNumber\"></span> / <span class=\"totalPages\"></span></div>"
}
}' | jq .
Stream binary PDF to a file:
curl -s -X POST http://localhost:8080/pdf/generate \
-H "Content-Type: application/json" \
-d '{"html": "<html><body><h1>Hello</h1></body></html>", "stream": true}' \
-o output.pdf
/pdf/:id
Returns a fresh presigned URL for a previously generated PDF.
{ "statusCode": 200, "data": { "url": "https://s3.amazonaws.com/...?X-Amz-Signature=..." } }
Missing PDFs return 404.
curl -s http://localhost:8080/pdf/550e8400-e29b-41d4-a716-446655440000 | jq .
/pdf/:id
Permanently deletes a PDF from S3.
HTTP 204 No Content
curl -s -o /dev/null -w "%{http_code}" -X DELETE \
http://localhost:8080/pdf/550e8400-e29b-41d4-a716-446655440000
PDF operations
Stored PDF in, stored PDF or binary out
/pdf/merge
Merge two or more existing PDFs in the order supplied.
Request body
{
"ids": ["550e8400-e29b-41d4-a716-446655440000", "6ba7b810-9dad-11d1-80b4-00c04fd430c8"],
"stream": false
}
| Field | Type | Required | Description |
|---|---|---|---|
ids | UUID[] | yes | Ordered list of source IDs, minimum 2 and maximum 20 |
stream | boolean | no | true returns binary, false returns a stored PDF response |
Response when stream: false
{ "statusCode": 200, "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "url": "https://s3.amazonaws.com/...?X-Amz-Signature=..." } }
Response when stream: true
Content-Type: application/pdfContent-Disposition: attachment; filename="merged.pdf"
curl -s -X POST http://localhost:8080/pdf/merge \
-H "Content-Type: application/json" \
-d '{"ids": ["ID_1", "ID_2"]}' | jq .
/pdf/split
Extract a subset of pages from an existing PDF.
Request body
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"pages": "1-3,5,7-",
"stream": false
}
| Field | Type | Required | Description |
|---|---|---|---|
id | UUID | yes | Source PDF ID |
pages | string | yes | Page range like 1-3, 1,3,5, or 2- |
stream | boolean | no | true returns binary, false returns a stored PDF response |
Response when stream: false
{ "statusCode": 200, "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "url": "https://s3.amazonaws.com/...?X-Amz-Signature=..." } }
Response when stream: true
Content-Type: application/pdfContent-Disposition: attachment; filename="split.pdf"
Invalid expressions and page ranges that resolve to no valid pages return
400. Missing PDFs return 404.
curl -s -X POST http://localhost:8080/pdf/split \
-H "Content-Type: application/json" \
-d '{"id": "PDF_ID", "pages": "1-3,5"}' | jq .
/pdf/compress
Reduce the size of an existing PDF. Uses Ghostscript when
GHOSTSCRIPT_PATH is set, otherwise falls back to a
pdf-lib re-save.
Request body
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"stream": false
}
| Field | Type | Required | Description |
|---|---|---|---|
id | UUID | yes | Source PDF ID |
stream | boolean | no | true returns binary, false returns a stored PDF response |
Response when stream: false
{ "statusCode": 200, "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "url": "https://s3.amazonaws.com/...?X-Amz-Signature=..." } }
Response when stream: true
Content-Type: application/pdfContent-Disposition: attachment; filename="compressed.pdf"
curl -s -X POST http://localhost:8080/pdf/compress \
-H "Content-Type: application/json" \
-d '{"id": "PDF_ID"}' | jq .
/pdf/pdfa
Convert an existing PDF to PDF/A for archival use. This route is registered
only when GHOSTSCRIPT_PATH is set.
Request body
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"conformance": "2b",
"stream": false
}
| Field | Type | Required | Description |
|---|---|---|---|
id | UUID | yes | Source PDF ID |
conformance | 1b | 2b | 3b | no | PDF/A conformance level, default 2b |
stream | boolean | no | true returns binary, false returns a stored PDF response |
Response when stream: false
{ "statusCode": 200, "data": { "id": "550e8400-e29b-41d4-a716-446655440000", "url": "https://s3.amazonaws.com/...?X-Amz-Signature=..." } }
Response when stream: true
Content-Type: application/pdfContent-Disposition: attachment; filename="pdfa.pdf"
curl -s -X POST http://localhost:8080/pdf/pdfa \
-H "Content-Type: application/json" \
-d '{"id": "PDF_ID", "conformance": "2b"}' | jq .