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>"}'
GET /health

Returns service liveness. Also responds to HEAD /health.

{ "status": "ok" }
Example
curl -s http://localhost:8080/health | jq .
GET /metrics

Returns Prometheus text-format metrics.

Example
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"
POST /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
htmlstringone of html/urlHTML to render
urlstringone of html/urlURL to navigate to and render
cssstringnoExtra CSS injected after the page loads
cookiesarraynoCookies set before navigation (each with name, value, domain)
extraHeadersobjectnoHTTP headers sent with every request on the page
paper.sizestringnoA4, A3, Letter, Legal, Tabloid
paper.orientationstringnoportrait or landscape
options.marginobjectnotop, right, bottom, left in CSS units
options.scalenumberno0.1–2.0
options.printBackgroundbooleannoInclude CSS backgrounds
options.headerTemplatestringnoHTML header displayed on every page
options.footerTemplatestringnoHTML footer with page-number placeholders
streambooleannotrue 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/pdf
  • Content-Disposition: attachment; filename="document.pdf"
Examples

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
GET /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.

Example
curl -s http://localhost:8080/pdf/550e8400-e29b-41d4-a716-446655440000 | jq .
DELETE /pdf/:id

Permanently deletes a PDF from S3.

HTTP 204 No Content
Example
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

POST /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
idsUUID[]yesOrdered list of source IDs, minimum 2 and maximum 20
streambooleannotrue 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/pdf
  • Content-Disposition: attachment; filename="merged.pdf"
Example
curl -s -X POST http://localhost:8080/pdf/merge \
  -H "Content-Type: application/json" \
  -d '{"ids": ["ID_1", "ID_2"]}' | jq .
POST /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
idUUIDyesSource PDF ID
pagesstringyesPage range like 1-3, 1,3,5, or 2-
streambooleannotrue 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/pdf
  • Content-Disposition: attachment; filename="split.pdf"

Invalid expressions and page ranges that resolve to no valid pages return 400. Missing PDFs return 404.

Example
curl -s -X POST http://localhost:8080/pdf/split \
  -H "Content-Type: application/json" \
  -d '{"id": "PDF_ID", "pages": "1-3,5"}' | jq .
POST /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
idUUIDyesSource PDF ID
streambooleannotrue 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/pdf
  • Content-Disposition: attachment; filename="compressed.pdf"
Example
curl -s -X POST http://localhost:8080/pdf/compress \
  -H "Content-Type: application/json" \
  -d '{"id": "PDF_ID"}' | jq .
POST /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
idUUIDyesSource PDF ID
conformance1b | 2b | 3bnoPDF/A conformance level, default 2b
streambooleannotrue 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/pdf
  • Content-Disposition: attachment; filename="pdfa.pdf"
Example
curl -s -X POST http://localhost:8080/pdf/pdfa \
  -H "Content-Type: application/json" \
  -d '{"id": "PDF_ID", "conformance": "2b"}' | jq .