PDF generation API

HTML or a zipped web app to PDF or PNG, PDFs merged, a PDF to JPEG images. The rendering service that Numsolar's own PDF offers use.

Key only. This API is closed by default and is not self-serve. Numsolar opens it for a partner by agreement: write to contact@numsolar.com. To make offer PDFs in Numsolar itself you do not need it: upload a PDF template.
Endpoint What it does
GET /api/pdf_generation Health check: the render tools that are installed and the busy render slots.
POST /api/pdf_generation/html-to-pdf One HTML page to a PDF.
POST /api/pdf_generation/html-to-pngs One HTML page to PNG images (one per page, or one full-page image).
POST /api/pdf_generation/zip-to-pdf A zipped web app (for example a PDF template) to a PDF.
POST /api/pdf_generation/zip-to-pngs A zipped web app to PNG images.
POST /api/pdf_generation/merge-pdfs Several PDFs to one PDF, in the given order.
POST /api/pdf_generation/pdf-to-images/convert A PDF to one JPEG per page, plus small previews.

Authentication

Send the key in the x-api-key header on every call. The key is checked before the body is read. A missing or wrong key gets 403 {"message": "Forbidden"}. After 10 wrong keys or bad bodies in a minute from one IP, every call of that IP gets 429 until the minute ends. Bodies are JSON, at most 20 MB. There is no CORS: call it from your server.

Files in and out

  • Input files (url, pdfUrl, urls[]) are public https links (port 443) that the server downloads. A link to a private, local or cloud-metadata address is refused; each redirect is checked again (at most 3); a file can be at most 100 MB. A refused or failed download gets 400.
  • Results always go to a new folder pdf-generation-api/<id>/. Of resultPath only the file name is kept; resultDir and outputS3Dir are replaced by the new folder. A call never writes to or over an existing file. The answer gives the stored path (s3_path, imagePaths) and a signed url that works for 1 hour.
  • A page may load its own files, data: URLs and Google Fonts. Other requests from the page are blocked.

Endpoints

GET /api/pdf_generation

Health check: the render tools that are installed and the busy render slots.

200 OK (503 when a tool is missing)
{
  "ok": true,
  "checks": {"node": true, "render_script": true, "chromium": true, "pdftoppm": true, "pdftk": true},
  "in_use": {"chromium": 0, "tools": 0}
}

POST /api/pdf_generation/html-to-pdf

One HTML page to a PDF.

Field Type Description
htmlrequired string The whole page.
resultPathrequired string Where to store the PDF.
awaitForSelector string A CSS selector to wait for before printing (at most 10 s), e.g. html.offer-ready.
data object JSON the page can fetch from http://request.data/.
settings object Render settings passed to the page.

The page size comes from the page's CSS (@page); without it the PDF is A4. An element #pageformat ({"width", "height"}) and #documentmetadata (title, author, subject, keywords, creator) set the size and the PDF metadata.

pdf_html_to_pdf.request.json
{
  "html": "<!doctype html><html><body><h1>Offer 2026-101</h1></body></html>",
  "resultPath": "partner-acme/offers/2026-101.pdf"
}
curl
curl -s -X POST 'https://numsolar.com/api/pdf_generation/html-to-pdf' -H 'content-type: application/json' -H 'x-api-key: YOUR_KEY' \
  -d @pdf_html_to_pdf.request.json
200 OK
{
  "s3_path": "partner-acme/offers/2026-101.pdf",
  "url": "https://numsolar.com/storage/s/<signed token>/2026-101.pdf"
}

POST /api/pdf_generation/html-to-pngs

One HTML page to PNG images (one per page, or one full-page image).

As html-to-pdf, with resultDir instead of resultPath and page (true: one PNG per page, screenshot-page-1.png, …; false: one screenshot-page-full.png). The answer is a list of {"s3_path", "url"}.

POST /api/pdf_generation/zip-to-pdf

A zipped web app (for example a PDF template) to a PDF.

url (the zip), archivePath (the page in the zip, e.g. dist/index.html) and resultPath; plus the optional fields of html-to-pdf. A PDF template zip renders this way.

POST /api/pdf_generation/zip-to-pngs

A zipped web app to PNG images.

As zip-to-pdf, with resultDir and page as in html-to-pngs.

POST /api/pdf_generation/merge-pdfs

Several PDFs to one PDF, in the given order.

urls (at least one PDF link, in order) and resultPath. The answer is {"s3_path", "url"}.

POST /api/pdf_generation/pdf-to-images/convert

A PDF to one JPEG per page, plus small previews.

pdfUrl and outputS3Dir. The answer is {"fileId", "imagePaths", "previewPaths", "pageCount", "ts"}: one JPEG per page (2000 px on the long side) and a preview of each (within 300 × 300 px). Pages count from 0.

Status codes

Errors are {"error": "…"} (403: {"message": "Forbidden"}).

StatusRetry-AfterWhen
403 – The x-api-key header is missing or wrong (body {"message": "Forbidden"}).
429 seconds until the minute ends 10 failed calls (a wrong key, a body that is not valid JSON or too large) from your IP this minute. Every call of that IP gets 429 until the minute ends.
400 – A required field is missing or wrong, the JSON is not valid, or the content type is not JSON. Also: an input link that is not a public https link, a download that failed, or a zip that is not valid or has unsafe paths (Could not download an input file. Use a public https link.).
413 – The body is larger than 20 MB.
503 30 No render slot came free in time. Try again later.
504 – The download, the wait and the render took longer than 30 s (120 s for pdf-to-images).
500 – The render failed. error tells why; a page failure also has log (the page's console).

The server renders 2 pages at a time and runs 4 PDF tools at a time; a call waits for a free slot until its deadline.