Skip to content

PDF service HTTP API

hadrontool-pdf is a stateless capability tool that renders Markdown → PDF and extracts PDF → Markdown text. It holds no database, no keys, and no access-control logic — hadron-server authenticates, decrypts, and loads the node, then calls this service for the heavy render.

Internal service

This service is deployed internal-only: it lives on the platform's private network and is reached by hadron-server by service name (e.g. http://hadrontool-pdf:8080). It is not exposed to the public internet, so you won't call these endpoints directly. This page documents the contract for operators and for the server code that calls it.

Authentication

All /convert/* routes require a shared bearer token:

Authorization: Bearer $PDF_SERVICE_TOKEN

The token is matched in constant time. When PDF_SERVICE_TOKEN is unset the gate is a pass-through — but that is development only: in production the service refuses to start without the token set. A missing or wrong token returns 401:

{ "error": "unauthorized" }

/healthz, /readyz, and /convert/info do not require the token.

Endpoints

Method · Path In Out
POST /convert/markdown-to-pdf { markdown, options? } application/pdf bytes, or { pdfBase64, bytes }
POST /convert/pdf-to-markdown raw application/pdf, or { pdfBase64 } { markdown, pageCount, info }
GET /convert/info capabilities (no secrets)
GET /healthz liveness
GET /readyz readiness + whether Chromium is warm

POST /convert/markdown-to-pdf

Renders Markdown (GitHub-flavored, with syntax highlighting) to a PDF.

Request body (JSON):

{
  "markdown": "# Title\n\nBody **text**.",   // required, 1–5,000,000 chars
  "options": {                                // all optional
    "title": "My document",                   // ≤ 500 chars; <title> + filename
    "format": "A4",                            // A4 | A3 | A5 | Letter | Legal | Tabloid
    "landscape": false,
    "printBackground": true,                   // print code-block backgrounds etc.
    "margin": {                                // CSS length strings
      "top": "20mm", "right": "18mm",
      "bottom": "20mm", "left": "18mm"
    },
    "css": "h1 { color: navy }"                // appended after the print stylesheet, ≤ 100,000 chars
  }
}

Every options field has a default (shown above), so options may be omitted entirely.

Response. By default the service returns the raw PDF:

200 OK
Content-Type: application/pdf
Content-Disposition: inline; filename="my-document.pdf"

To get base64 in JSON instead — the shape hadron-server embeds in a GraphQL response — send Accept: application/json or add ?format=base64:

{ "pdfBase64": "JVBERi0xLj…", "bytes": 20481 }

POST /convert/pdf-to-markdown

Extracts Markdown from a PDF's text layer (v1). Accepts either a raw PDF body (with Content-Type: application/pdf) or a JSON-wrapped base64 string (with Content-Type: application/json):

POST /convert/pdf-to-markdown
Content-Type: application/pdf

%PDF-1.7…(binary)…

or

{ "pdfBase64": "JVBERi0xLj…" }

The raw PDF body or the decoded base64 content must begin with the %PDF- header (corresponding to the JVBERi0 base64 prefix) or the request is rejected with 400.

Response:

{
  "markdown": "# Heading\n\nParagraph…\n\n---\n\nPage two…",
  "pageCount": 2,
  "info": { "title": "From PDF metadata", "author": "If present" }
}

Pages are separated by a --- rule. Headings are inferred from font size and paragraphs from vertical gaps.

v1 extraction limits

Extraction reads the text layer only. Scanned or image-only PDFs (no text layer) yield no text — there is no OCR. Tables are not reconstructed perfectly. A rendered PDF is a presentation output, not a round-trippable portable file, so do not expect a byte-identical Markdown ⇄ PDF round trip.

GET /convert/info

Reports capabilities without exposing secrets:

{
  "service": "hadrontool-pdf",
  "directions": ["markdown-to-pdf", "pdf-to-markdown"],
  "allowRemote": false,
  "authRequired": true
}

GET /healthz · GET /readyz

// GET /healthz  — liveness
{ "status": "ok" }

// GET /readyz   — readiness; browserWarm is false during cold start
{ "status": "ok", "browserWarm": true }

Errors

All errors are JSON with an error string.

Status When Body
400 Schema validation failed (Zod) { "error": "invalid request", "details": [ … ] }
400 Empty body, bad base64, or missing %PDF- header { "error": "<message>" }
401 Missing/invalid bearer token { "error": "unauthorized" }
404 Unknown route { "error": "not found" }
413 Request body exceeds MAX_BODY_SIZE { "error": "<message>" }
500 Unexpected render/extract failure { "error": "internal error" }

Limits & configuration

These are set by the service's environment:

Setting Env var Default Notes
Service token PDF_SERVICE_TOKEN Required in production; gates /convert/*.
Max request body MAX_BODY_SIZE 10mb Caps both the JSON body and the raw PDF upload.
Allow remote fetch PDF_ALLOW_REMOTE false Whether the headless browser may fetch remote http(s) resources.
Port PORT 8080

The markdown field is additionally capped at 5,000,000 characters and options.css at 100,000 characters by request validation.

See also

  • Capability tools — why this service is a separate stateless process, and the Portal "Download as PDF" flow that drives it.