Log in Sign up
Docs/Documents/HTML to PDF

HTML to PDF v1.0 2 credits / call

GET|POST /api/v1/html-to-pdf

Render HTML into a paginated PDF — page size, margins, running headers/footers, metadata and password protection.

40 free/month Test keys supported OpenAPI spec Try it in the console
At a glance
Base path
/api/v1/html-to-pdf
Methods
GET or POST — parameters are read identically from the query string or a JSON body
Auth
X-Api-Key header (never Authorization: Bearer)
Price
2–8 credits per call, by endpoint · about $0.0020 at 1,000 credits/USD
Free tier
40 credits, refilled every month — drawn down before your paid balance
Test mode
Supported — ksty_test_ keys call this API free of charge
Rate limit
60 calls/minute per key by default, raisable per key

HTML to PDF API

Turns HTML into a real, paginated PDF. Send the markup with html, or a url to fetch, and you get back a document with the page size, margins, running headers and footers you asked for — the things a browser's print dialog does, but as an API call you can make from a server with no browser on it.

Rendering is synchronous: one request in, one finished PDF out. There is no job to poll and no file to clean up afterwards.

Your first call

POST /api/v1/html-to-pdf
X-Api-Key: ksty_test_...
Content-Type: application/json

{"html": "<h1>Invoice #1042</h1><p>Thanks for your business.</p>", "filename": "invoice-1042.pdf"}
{
  "filename": "invoice-1042.pdf",
  "content_type": "application/pdf",
  "bytes": 12894,
  "pages": 1,
  "page_size": "A4",
  "orientation": "portrait",
  "encrypted": false,
  "assets_blocked": [],
  "pdf_base64": "JVBERi0xLjQKJeLj..."
}

Getting the file, not JSON

The default JSON response carries the document in pdf_base64 — decode it and write it to disk:

$pdf = base64_decode($response['pdf_base64']);
file_put_contents($response['filename'], $pdf);

If you would rather have the bytes straight, send "output": "binary" (or an Accept: application/pdf header) and the response body is the PDF, with Content-Type: application/pdf, a Content-Disposition naming your file, and X-Pdf-Pages / X-Pdf-Bytes headers:

curl -X POST https://ksty.ch/api/v1/html-to-pdf \
  -H "X-Api-Key: ksty_live_..." -H "Content-Type: application/json" \
  -d '{"html":"<h1>Hi</h1>","output":"binary"}' -o out.pdf

Errors are always JSON, even in binary mode — check the content type or the status code before writing the body to a file.

Page setup

  • page_sizeA3, A4 (default), A5, Letter, Legal, Tabloid or Ledger.
  • orientationportrait (default) or landscape.
  • margin — millimetres. A single number sets all four sides; an object sets them individually, and any side you leave out keeps its default ({"top":16,"right":15,"bottom":16,"left":15}).
{"html": "…", "page_size": "Letter", "orientation": "landscape", "margin": {"top": 25, "bottom": 25}}

Headers, footers and page numbers

header_html and footer_html are small HTML fragments repeated on every page, laid out inside the top and bottom margins. Four placeholders are substituted as the document paginates:

Placeholder Becomes
{PAGENO} the current page number
{nb} the total page count
{DATE j-m-Y} today's date, in any PHP date format
{PAGENO}/{nb} the usual "3/12" pairing
{
  "html": "<h1>Quarterly report</h1>…",
  "header_html": "<div style='text-align:right;font-size:9pt;color:#888'>Acme Inc — Q3</div>",
  "footer_html": "<div style='text-align:center;font-size:9pt'>Page {PAGENO} of {nb}</div>"
}

Give a header or footer room to sit in — with the default 16 mm top margin there is space for one line. If your header wraps onto two, raise margin.top to match or the body text will start underneath it.

Styling

Inline style attributes and <style> blocks in your HTML both work, and css adds a stylesheet on top of them — handy when the markup comes from somewhere you don't control:

{"html": "<table>…</table>", "css": "body{font-family:sans-serif} table{width:100%;border-collapse:collapse} td{border:1px solid #ddd;padding:6px}"}

Supported CSS is print-oriented: the box model, tables, colours, borders, backgrounds, fonts, and the page-break properties (page-break-before, page-break-after, page-break-inside: avoid) — including <div style="page-break-after:always"></div> to force a new page. Flexbox and CSS grid are not supported; use tables or floats for print layout.

Assets: what can and cannot be loaded

Because you supply the markup, you also supply every URL in it — so the renderer is strict about what it will go and fetch:

  • Local files are never read. A src pointing at a path on the gateway's disk resolves to nothing.
  • Remote URLs are off by default. Pass remote_assets: true to allow http(s) images and stylesheets. Each one is then checked (public addresses only, redirects re-checked, 5 s timeout, 5 MB each, 25 per document).
  • data: URIs always work. They are already in your payload — nothing is fetched.

Anything refused is skipped, not fatal: the PDF still renders and every skipped asset is listed in assets_blocked with the reason, so a missing logo is something you can see rather than guess at.

{"assets_blocked": [{"url": "/var/www/logo.png", "reason": "local file access is not permitted"}]}

For a logo or a signature image, embedding it as a data: URI is both faster and more reliable than a URL — nothing to fetch, nothing to time out.

Relative paths need a base

A page written to be served from a web root refers to its assets relatively — assets/css/main.css, images/pic01.jpg. Those are meaningless on their own, so base_url says where they live:

{"html": "…<link rel=\"stylesheet\" href=\"assets/css/main.css\">…", "base_url": "https://example.com/site/", "remote_assets": true}

Every relative path is then resolved against it (https://example.com/site/assets/css/main.css) and fetched under the rules above. When you use url, base_url defaults to that URL, so a fetched page's own assets resolve without you saying anything. Without a base, relative assets resolve to nothing and the document renders as unstyled text — which is the usual reason a saved-from-the-browser HTML file comes back looking bare.

Rendering a live page

url fetches a page and renders it. The fetch is a single guarded GET (public http(s) only, ≤3 redirects each re-checked, 8 s timeout, 3 MB cap) and a failure costs you nothing.

{"url": "https://example.com", "remote_assets": true}

Note that a page fetched this way is usually not styled the way you see it in a browser: its CSS and images are external, so they only load with remote_assets: true, and anything drawn by JavaScript after load was never in the HTML at all. For pages that render client-side, get the final HTML yourself and pass it as html.

Document properties and protection

title, author and subject populate the PDF's info panel. password encrypts the document — readers prompt for it on open, and printing and copying stay allowed:

{"html": "…", "title": "Payslip — March", "author": "Acme Payroll", "password": "s3cret"}

Lost passwords cannot be recovered. The document is encrypted with the password you send and never stored, so re-rendering is the only way back in.

Merging several documents into one PDF

documents takes a list and renders it as a single PDF, in order, with a page break between each:

{
  "documents": [
    {"html": "<h1>Cover</h1>"},
    {"html": "<h1>Statement</h1><table>…</table>", "css": "table{width:100%}"},
    {"url": "https://example.com/terms"}
  ],
  "footer_html": "<div style='text-align:center'>Page {PAGENO} of {nb}</div>"
}

Each entry takes the same html / url pair as a single call, plus an optional css; a bare string is shorthand for {"html": "…"}. The response adds a documents count.

The reason to merge here rather than make three calls and stitch the results yourself is pagination: {PAGENO} and {nb} run continuously across the whole set, so page 4 of 9 really says "4 of 9". Page setup, margins, headers and footers are shared — once merged there is one document, so one cascade, and a per-document css is appended to the shared stylesheet rather than scoped to its section.

Up to 10 documents per call, and the combined HTML still has to fit the 2 MB input budget.

Page images

html-to-pdf.image renders the document exactly as the PDF endpoints do, then returns one page as a PNG — for thumbnails, previews in your own UI, or anywhere shipping a PDF viewer is more trouble than it is worth:

POST /api/v1/html-to-pdf.image
{"html": "<h1>Invoice #1042</h1>", "page": 1, "width": 900}
{"page":1,"pages":1,"width":900,"height":1273,"bytes":139014,"content_type":"image/png","image_base64":"iVBORw0KGgo…"}
  • page — which page, 1-based (default 1). Asking for a page the document does not have is a free invalid_request, and the error tells you how many there are.
  • width — 80 to 2000 px; the height follows the page's aspect ratio.
  • output: "binary" streams the raw PNG, same as it does for the PDF.

It costs 3 credits rather than 2: the document is rendered and rasterised.

Page images need a PDF-capable ImageMagick on the server. Where that is missing — or where its policy.xml disables the PDF coder, which is a common hardening default — the endpoint answers preview_unavailable (503) and does not charge you. The PDF endpoints are unaffected, so a 503 here is never a sign that rendering itself is broken.

Rendering it exactly as the browser draws it

The default engine is mPDF: fast, pure PHP, print-oriented — and, as the Limits say, no JavaScript and no flexbox or grid. When you need the PDF to look like the page looks in a browser, html-to-pdf.browser renders through headless Chrome instead:

POST /api/v1/html-to-pdf.browser
{"html": "<div style='display:grid;grid-template-columns:1fr 1fr'>…</div>", "wait_ms": 250}

Everything a browser does, it does: flexbox and grid, gradients, box-shadow, custom properties, and your scripts run before the page is printed. The response is identical in shape to the mPDF one, with "engine": "chrome" so you can tell them apart.

It costs 8 credits rather than 2, because a browser process is started for the call. Expect roughly 1–2 seconds.

  • wait_ms — extra settle time for scripts that finish after load (default 250, max 3000). Raise it if a chart or a font renders a moment late.
  • header_html / footer_html work here too: the same {PAGENO} / {nb} / {DATE …} placeholders are translated to Chrome's own template syntax, so one syntax works on both engines.

This engine has no network access, by design. Your JavaScript runs inside a real browser, so an engine that could fetch would be able to read internal services and paint the response into the PDF you get back. Chrome is therefore launched with all outbound requests failing, including to localhost. Everything must be in the payload: inline <style>, the css parameter, and data: URIs for images. remote_assets and base_url do nothing here — an external stylesheet or image simply will not load. url mode still works, because the gateway fetches that page itself (guarded) and hands Chrome the markup.

Which engine to reach for:

html-to-pdf (mPDF) html-to-pdf.browser (Chrome)
Cost 2 credits 8 credits
Speed ~0.3–1 s ~1–2 s
Flexbox / grid no yes
JavaScript no yes
Remote images / CSS opt-in via remote_assets never — inline or data: only
Best for invoices, statements, reports dashboards, marketing pages, anything designed for screen

Endpoints

  • html-to-pdf — render and return the document.
  • html-to-pdf.info — render exactly the same document and return only the facts about it (pages, bytes, assets_blocked) with no payload. Useful while you iterate on a template and only want to know "does this still fit on one page?" without moving a megabyte of base64 around. It costs the same, because the same rendering work happens.
  • html-to-pdf.image — render, then return one page as a PNG (3 credits).
  • html-to-pdf.browser — render through headless Chrome, exactly as a browser draws it (8 credits).

Limits

  • html up to 2 MB (or 2 MB combined across documents), css up to 200 KB, 10 documents per merge.
  • max_pages (default 50, maximum 200) refuses an over-long document rather than returning it — an unbounded template is usually a bug, and you are not charged when it trips.
  • No JavaScript, no flexbox and no CSS grid on the default engine — it is a print renderer, not a browser. Use html-to-pdf.browser when you need those.

Errors

Every error is JSON, and the ones you can cause by accident are free — invalid_request, payload_too_large, fetch_failed, render_failed, too_many_pages, preview_unavailable and engine_unavailable all return 0 credits charged. You pay for documents you receive.

When to use it

  • Invoices, receipts, payslips and contracts generated from a template you already render as HTML.
  • Reports and statements that need real page numbers and a running header.
  • Statement packs: a cover, a body and standing terms merged into one correctly-paginated file.
  • Turning a rendered email or article into an archivable file.
  • Thumbnails of any of the above, via .image, without a PDF viewer in your stack.

Endpoints

GETPOST /api/v1/html-to-pdf 2 credits

Render HTML (or a fetched URL) into a PDF document

The default engine is mPDF — pure PHP, no browser process, so the call is a single synchronous request with nothing to poll and nothing to clean up. It handles the CSS that print layouts actually use: page size and margins, running headers and footers, tables, floats, absolute positioning, web fonts and page breaks.

What it does not do is run JavaScript or lay out flexbox and grid. If your document depends on either, use .browser instead and pay for the browser process.

Parameters
ParameterTypeDescription
html optional string The HTML to render (required unless url or documents is given)
url optional string An http(s) URL to fetch and render instead of passing html
documents optional array Merge several documents into one PDF, in order: [{"html":…}|{"url":…}, …] (max 10)
css optional string Extra CSS applied on top of the document's own styles
base_url optional string Base URL that relative asset paths resolve against (defaults to url when fetching)
page_size optional string Paper size One of: A4A3A5LetterLegalTabloidLedger Defaults to A4
orientation optional string Page orientation One of: portraitlandscape Defaults to portrait
margin optional object Page margins in mm — a single number for all sides, or {top,right,bottom,left}
header_html optional string HTML repeated at the top of every page ({PAGENO}, {nb}, {DATE j-m-Y})
footer_html optional string HTML repeated at the bottom of every page ({PAGENO}, {nb}, {DATE j-m-Y})
title optional string PDF document title
author optional string PDF document author
subject optional string PDF document subject
filename optional string Filename returned in the response / Content-Disposition Defaults to document.pdf
password optional string Encrypt the PDF; readers prompt for this password on open
remote_assets optional boolean Allow http(s) images and stylesheets to be fetched Defaults to false
max_pages optional integer Refuse the document if it renders to more pages than this (max 200) Defaults to 50
output optional string json (base64 payload) or binary (raw PDF body) One of: jsonbinary Defaults to json
disposition optional string Content-Disposition for binary output One of: attachmentinline Defaults to attachment
Request
curl -X POST "https://ksty.ch/api/v1/html-to-pdf" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"html":"<h1>Invoice #1042</h1><p>Thanks for your business.</p>","page_size":"A4","orientation":"portrait","footer_html":"<div style=\"text-align:center;font-size:9pt\">Page {PAGENO} of {nb}</div>","filename":"invoice-1042.pdf","max_pages":50,"output":"json"}'
<?php
$ch = curl_init('https://ksty.ch/api/v1/html-to-pdf');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        'X-Api-Key: YOUR_API_KEY',
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => '{    "html": "<h1>Invoice #1042</h1><p>Thanks for your business.</p>",    "page_size": "A4",    "orientation": "portrait",    "footer_html": "<div style=\"text-align:center;font-size:9pt\">Page {PAGENO} of {nb}</div>",    "filename": "invoice-1042.pdf",    "max_pages": 50,    "output": "json"}',
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
const response = await fetch('https://ksty.ch/api/v1/html-to-pdf', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_API_KEY',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    "html": "<h1>Invoice #1042</h1><p>Thanks for your business.</p>",
    "page_size": "A4",
    "orientation": "portrait",
    "footer_html": "<div style=\"text-align:center;font-size:9pt\">Page {PAGENO} of {nb}</div>",
    "filename": "invoice-1042.pdf",
    "max_pages": 50,
    "output": "json"
})
});
const data = await response.json();
import requests

response = requests.post(
    'https://ksty.ch/api/v1/html-to-pdf',
    headers={'X-Api-Key': 'YOUR_API_KEY'},
    json={
    "html": "<h1>Invoice #1042</h1><p>Thanks for your business.</p>",
    "page_size": "A4",
    "orientation": "portrait",
    "footer_html": "<div style=\"text-align:center;font-size:9pt\">Page {PAGENO} of {nb}</div>",
    "filename": "invoice-1042.pdf",
    "max_pages": 50,
    "output": "json"
},
)
data = response.json()
Response 200 OK
{
    "filename": "document.pdf",
    "content_type": "application/pdf",
    "bytes": 12894,
    "pages": 1,
    "page_size": "A4",
    "orientation": "portrait",
    "encrypted": false,
    "assets_blocked": [],
    "pdf_base64": "JVBERi0xLjQK…"
}
Response fields
FieldTypeDescription
filename string The name you asked for, or document.pdf. Also used in Content-Disposition when output=binary.
content_type string Always application/pdf.
bytes integer Size of the finished document, before base64 encoding.
pages integer Pages in the finished document — useful for billing your own users, or for checking a document is the length you expected.
page_size string The paper size that was applied.
orientation string portrait or landscape, as applied.
encrypted boolean true when a password was set, so readers will prompt on open.
assets_blocked array Asset URLs the guard refused, with the reason. This is a successful response, not an error — the document rendered without them. Check this array if an image is missing: local paths are always refused, and remote URLs need remote_assets: true.
pdf_base64 string The document, base64-encoded. Absent when output=binary, where the response body is the PDF itself.
GETPOST /api/v1/html-to-pdf.info 2 credits

Render the document and return only its facts (pages, bytes) — no payload

Parameters
ParameterTypeDescription
html optional string The HTML to render (required unless url or documents is given)
url optional string An http(s) URL to fetch and render instead of passing html
documents optional array Merge several documents into one PDF, in order (max 10)
css optional string Extra CSS applied on top of the document's own styles
base_url optional string Base URL that relative asset paths resolve against (defaults to url when fetching)
page_size optional string Paper size One of: A4A3A5LetterLegalTabloidLedger Defaults to A4
orientation optional string Page orientation One of: portraitlandscape Defaults to portrait
margin optional object Page margins in mm — a single number for all sides, or {top,right,bottom,left}
header_html optional string HTML repeated at the top of every page ({PAGENO}, {nb}, {DATE j-m-Y})
footer_html optional string HTML repeated at the bottom of every page ({PAGENO}, {nb}, {DATE j-m-Y})
remote_assets optional boolean Allow http(s) images and stylesheets to be fetched Defaults to false
Request
curl -X POST "https://ksty.ch/api/v1/html-to-pdf.info" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"html":"<h1>Quarterly report</h1><p>Section one.</p>","page_size":"A4","orientation":"portrait"}'
<?php
$ch = curl_init('https://ksty.ch/api/v1/html-to-pdf.info');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        'X-Api-Key: YOUR_API_KEY',
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => '{    "html": "<h1>Quarterly report</h1><p>Section one.</p>",    "page_size": "A4",    "orientation": "portrait"}',
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
const response = await fetch('https://ksty.ch/api/v1/html-to-pdf.info', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_API_KEY',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    "html": "<h1>Quarterly report</h1><p>Section one.</p>",
    "page_size": "A4",
    "orientation": "portrait"
})
});
const data = await response.json();
import requests

response = requests.post(
    'https://ksty.ch/api/v1/html-to-pdf.info',
    headers={'X-Api-Key': 'YOUR_API_KEY'},
    json={
    "html": "<h1>Quarterly report</h1><p>Section one.</p>",
    "page_size": "A4",
    "orientation": "portrait"
},
)
data = response.json()
Response 200 OK
{
    "pages": 3,
    "bytes": 48210,
    "page_size": "A4",
    "orientation": "portrait",
    "assets_blocked": []
}
Response fields
FieldTypeDescription
pages integer Pages the document renders to.
bytes integer Size the finished PDF would be.
page_size string The paper size that was applied.
orientation string portrait or landscape, as applied.
assets_blocked array Assets the guard refused — the cheapest way to find out an image will be missing before you generate the real document.
GETPOST /api/v1/html-to-pdf.image 3 credits

Render the document and return one page as a PNG image

Parameters
ParameterTypeDescription
html optional string The HTML to render (required unless url or documents is given)
url optional string An http(s) URL to fetch and render instead of passing html
documents optional array Merge several documents first, then image a page of the result (max 10)
css optional string Extra CSS applied on top of the document's own styles
base_url optional string Base URL that relative asset paths resolve against (defaults to url when fetching)
page optional integer Which page to rasterise, 1-based Defaults to 1
width optional integer Image width in pixels, 80–2000 (height follows the page ratio) Defaults to 900
page_size optional string Paper size One of: A4A3A5LetterLegalTabloidLedger Defaults to A4
orientation optional string Page orientation One of: portraitlandscape Defaults to portrait
margin optional object Page margins in mm — a single number for all sides, or {top,right,bottom,left}
header_html optional string HTML repeated at the top of every page
footer_html optional string HTML repeated at the bottom of every page
remote_assets optional boolean Allow http(s) images and stylesheets to be fetched Defaults to false
output optional string json (base64 payload) or binary (raw PNG body) One of: jsonbinary Defaults to json
Request
curl -X POST "https://ksty.ch/api/v1/html-to-pdf.image" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"html":"<h1>Invoice #1042</h1><p>Thanks for your business.</p>","page":1,"width":900,"page_size":"A4","orientation":"portrait","output":"json"}'
<?php
$ch = curl_init('https://ksty.ch/api/v1/html-to-pdf.image');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        'X-Api-Key: YOUR_API_KEY',
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => '{    "html": "<h1>Invoice #1042</h1><p>Thanks for your business.</p>",    "page": 1,    "width": 900,    "page_size": "A4",    "orientation": "portrait",    "output": "json"}',
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
const response = await fetch('https://ksty.ch/api/v1/html-to-pdf.image', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_API_KEY',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    "html": "<h1>Invoice #1042</h1><p>Thanks for your business.</p>",
    "page": 1,
    "width": 900,
    "page_size": "A4",
    "orientation": "portrait",
    "output": "json"
})
});
const data = await response.json();
import requests

response = requests.post(
    'https://ksty.ch/api/v1/html-to-pdf.image',
    headers={'X-Api-Key': 'YOUR_API_KEY'},
    json={
    "html": "<h1>Invoice #1042</h1><p>Thanks for your business.</p>",
    "page": 1,
    "width": 900,
    "page_size": "A4",
    "orientation": "portrait",
    "output": "json"
},
)
data = response.json()
Response 200 OK
{
    "page": 1,
    "pages": 3,
    "width": 900,
    "height": 1273,
    "bytes": 139014,
    "content_type": "image/png",
    "image_base64": "iVBORw0KGgo…"
}
Response fields
FieldTypeDescription
page integer Which page was rasterised (1-based).
pages integer Total pages in the document, so you can loop to image the rest.
width integer Image width in pixels, as requested.
height integer Height in pixels, derived from the page aspect ratio — you do not set it.
bytes integer PNG size before base64 encoding.
content_type string Always image/png.
image_base64 string The PNG, base64-encoded. Absent when output=binary, where the body is the image itself.
GETPOST /api/v1/html-to-pdf.browser 8 credits

Render through headless Chrome — the page exactly as a browser draws it

Renders in real headless Chrome, so flexbox, grid, web fonts and JavaScript all work and the output matches what you see when you print the page from a browser. It costs 8 credits rather than 2 because a browser process is started for the call.

No network access, at all. Chrome runs with egress blocked, so every asset must be inline or a data: URI — a remote <img> or stylesheet simply will not load, and remote_assets does not apply here. This is deliberate: your JavaScript executes inside that browser, and an engine that could reach the network would be able to read internal addresses and paint the results into the PDF you receive.

Use the default endpoint when your document is print-CSS only; use this one when it genuinely needs a browser engine.

Parameters
ParameterTypeDescription
html optional string The HTML to render (required unless url or documents is given)
url optional string An http(s) URL to fetch and render instead of passing html
documents optional array Merge several documents into one PDF, in order (max 10)
css optional string Extra CSS applied on top of the document's own styles
page_size optional string Paper size One of: A4A3A5LetterLegalTabloidLedger Defaults to A4
orientation optional string Page orientation One of: portraitlandscape Defaults to portrait
margin optional object Page margins in mm — a single number for all sides, or {top,right,bottom,left}
header_html optional string HTML repeated at the top of every page ({PAGENO}, {nb}, {DATE j-m-Y})
footer_html optional string HTML repeated at the bottom of every page ({PAGENO}, {nb}, {DATE j-m-Y})
wait_ms optional integer Extra settle time in ms for scripts that finish after load (max 3000) Defaults to 250
filename optional string Filename returned in the response / Content-Disposition Defaults to document.pdf
max_pages optional integer Refuse the document if it renders to more pages than this (max 200) Defaults to 50
output optional string json (base64 payload) or binary (raw PDF body) One of: jsonbinary Defaults to json
Request
curl -X POST "https://ksty.ch/api/v1/html-to-pdf.browser" \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"html":"<h1>Invoice #1042</h1><p>Thanks for your business.</p>","page_size":"A4","orientation":"portrait","wait_ms":250,"max_pages":50,"output":"json"}'
<?php
$ch = curl_init('https://ksty.ch/api/v1/html-to-pdf.browser');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        'X-Api-Key: YOUR_API_KEY',
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => '{    "html": "<h1>Invoice #1042</h1><p>Thanks for your business.</p>",    "page_size": "A4",    "orientation": "portrait",    "wait_ms": 250,    "max_pages": 50,    "output": "json"}',
]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
const response = await fetch('https://ksty.ch/api/v1/html-to-pdf.browser', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_API_KEY',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    "html": "<h1>Invoice #1042</h1><p>Thanks for your business.</p>",
    "page_size": "A4",
    "orientation": "portrait",
    "wait_ms": 250,
    "max_pages": 50,
    "output": "json"
})
});
const data = await response.json();
import requests

response = requests.post(
    'https://ksty.ch/api/v1/html-to-pdf.browser',
    headers={'X-Api-Key': 'YOUR_API_KEY'},
    json={
    "html": "<h1>Invoice #1042</h1><p>Thanks for your business.</p>",
    "page_size": "A4",
    "orientation": "portrait",
    "wait_ms": 250,
    "max_pages": 50,
    "output": "json"
},
)
data = response.json()
Response 200 OK
{
    "filename": "document.pdf",
    "content_type": "application/pdf",
    "bytes": 124738,
    "pages": 1,
    "page_size": "A4",
    "orientation": "portrait",
    "engine": "chrome",
    "encrypted": false,
    "assets_blocked": [],
    "pdf_base64": "JVBERi0xLjQK…"
}
Response fields
FieldTypeDescription
filename string The name you asked for, or document.pdf.
content_type string Always application/pdf.
bytes integer Size of the finished document.
pages integer Pages in the finished document.
page_size string The paper size that was applied.
orientation string portrait or landscape, as applied.
engine string Always "chrome" here — the field that tells you which engine produced a document when you use both endpoints.
encrypted boolean Always false: password protection is an mPDF feature, so use the default endpoint when you need it.
assets_blocked array Assets that did not load. Expect every remote URL to appear here — this engine has no network access.
pdf_base64 string The document, base64-encoded. Absent when output=binary.

Errors

Errors are JSON with a stable error.code — branch on the code, never on the message. Everything below carries the X-Request-Id of the failed call. Requests rejected by this API are not charged, and a 502 after a charge is refunded automatically.

CodeHTTPCause & what to do
invalid_request this API 400 No document was supplied ("html", "url" and "documents" all missing), a parameter is out of range (unknown page_size, orientation other than portrait/landscape, a base_url that is not http(s)), or the "documents" array is malformed or longer than 10 entries. The message names the parameter at fault. Nothing is charged, so fixing and re-sending costs no credits.
payload_too_large this API 413 The HTML exceeds 2 MB, the extra CSS exceeds 200 KB, or several merged documents exceed the combined 2 MB budget. Inline less, or split the job into more than one call. Images referenced as data: URIs count towards the HTML limit — host them and enable remote_assets instead, or reduce their size.
render_failed this API 422 The renderer could not turn your markup into a page — unsupported CSS or markup it cannot lay out. The message carries the renderer's own reason. This is a 422 rather than a 502 on purpose: the input is the problem, so retrying it unchanged will fail the same way. Remember mPDF supports no flexbox, grid or JavaScript — use the .browser endpoint for a page that needs them.
too_many_pages this API 422 The finished document has more pages than "max_pages" allows (default 50). Raise max_pages (up to 200) when the length is expected. The error reports the real page count, so .info is a cheap way to measure a document before committing to it.
fetch_failed this API 502 A "url" (or a url inside "documents") could not be fetched: timeout, non-2xx, oversize body, or an address in a blocked range. Confirm the URL is publicly reachable. Un-billed, so retries are free. For pages behind a login, fetch them yourself and send the markup as "html".
engine_unavailable this API 503 Only on .browser — the headless Chrome engine is not installed or cannot start on this server. Fall back to the default endpoint, which uses the pure-PHP engine and is always available.
preview_unavailable this API 503 Only on .image — the rasteriser needed to turn a page into a PNG is missing on this server. Request the PDF itself from the default endpoint and rasterise it on your side.
invalid_key platform 401 The X-Api-Key header is missing, malformed, revoked or expired. Send an active key. Repeated bad keys from one address are throttled, so fix the header rather than retrying.
forbidden_scope platform 403 The key is valid but not scoped to this API, or it is a test key on an API without sandbox support. Add this API to the key's scopes on the Keys page, or use a key scoped to all APIs.
insufficient_credits platform 402 The balance cannot cover the call. Top up. Nothing is charged and the request never reaches the API, so no work is half-done.
rate_limited platform 429 The key exceeded its per-minute limit. Back off and retry. The limit is per key, so a second key does not share the budget.
upstream_error platform 502 The API failed unexpectedly after being charged. Safe to retry — credits are refunded automatically whenever this is returned.

Response headers

Every successful call reports its own cost, so you never have to guess what a request spent or reconcile it later.

HeaderMeaning
X-Credits-Charged Credits taken for this call. Zero on test-mode calls and on errors raised before the work started.
X-Credits-Remaining Balance left afterwards, across per-API grants, promotional and paid credits.
X-Request-Id Identifier for this call. It appears in your usage log and in every error body — quote it in support requests.