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:
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:
/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:
To get base64 in JSON instead — the shape hadron-server embeds in a
GraphQL response — send Accept: application/json or add
?format=base64:
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):
or
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.