# API reference

Base URL `https://html-pdf.com` · send your HTML, get a PDF back · 1 credit = 1 PDF page.

## Authentication

Send your key in the `Authorization` header (or `X-API-Key`):

```
Authorization: Bearer hp_live_...
```

Get a key by logging in at https://html-pdf.com/login, or with one call (ideal for agents):

```bash
curl -X POST https://html-pdf.com/v1/signup \
  -H "Content-Type: application/json" \
  -d '{"email": "you@example.com"}'
```

```json
{ "api_key": "hp_live_...", "credits": 0, "pending_credits": 100, "unit": "1 credit = 1 PDF page" }
```

The key is shown once. The email owner receives a link that confirms the email and adds the 100 free credits, then shows usage, keys and buying. Until the email is confirmed, conversions return `403 email_not_confirmed`. If the email already has an account you get `409 account_exists` and a login link is emailed instead.

## POST /v1/pdf

Render HTML to PDF.

```bash
curl https://html-pdf.com/v1/pdf \
  -H "Authorization: Bearer $HTML_PDF_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "html": "<h1>Invoice #042</h1><p>Total: $10</p>",
    "format": "A4",
    "margin": "15mm",
    "footer_html": "<div style=\"font-size:9px;width:100%;text-align:center\"><span class=\"pageNumber\"></span> / <span class=\"totalPages\"></span></div>"
  }' \
  -o invoice.pdf
```

### Options

| field | type | default | description |
|---|---|---|---|
| `html` | string | `html` or `url` | Full document or fragment. Fragments are wrapped in `<html><body>`. |
| `url` | string | `html` or `url` | Public `http(s)` page to render instead of `html`, e.g. `"https://example.com/invoice/42"`. Private/internal addresses are rejected. Can't be combined with `css`. |
| `css` | string | | Extra CSS injected into `<head>`. |
| `format` | string | `A4` | `A3`, `A4`, `A5`, `A6`, `Letter`, `Legal`, `Tabloid`. |
| `width`, `height` | string | | Custom size, e.g. `"210mm"`, `"8.5in"`, `"600px"`. Overrides `format`. |
| `landscape` | bool | `false` | |
| `margin` | string or object | ~1 cm | `"10mm"` or `{"top":"1in","right":"0.5in","bottom":"1in","left":"0.5in"}`. |
| `print_background` | bool | `true` | Print background colors and images. |
| `prefer_css_page_size` | bool | `false` | Use the size from CSS `@page { size: ... }`. |
| `scale` | number | `1` | 0.1 to 2. |
| `page_ranges` | string | all | e.g. `"1-3, 5"`. |
| `header_html`, `footer_html` | string | | Repeated on every page. Placeholders: `<span class="pageNumber">`, `totalPages`, `date`, `title`. Needs enough top/bottom `margin`. Default font size is tiny; set `font-size` inline. |
| `media` | string | `print` | `print` or `screen` CSS media. |
| `wait_delay` | number | `0` | Seconds (0 to 10) to wait before rendering, for JS-built pages. |
| `wait_for_expression` | string | | JS expression; render when it returns true, e.g. `"window.ready === true"`. |
| `max_pages` | int | | Fail (no charge) if the PDF would have more pages. |
| `filename` | string | `document.pdf` | Used in `Content-Disposition`. |
| `response` | string | `binary` | `binary` (PDF body), `base64` (JSON), or `url` (JSON with a link valid 24h). |

Unknown fields are rejected with `400 invalid_json` so typos don't fail silently.

### Response

`200 application/pdf` with headers:

| header | |
|---|---|
| `X-Pages` | pages in the PDF |
| `X-Credits-Used` | credits charged (= pages) |
| `X-Credits-Remaining` | balance after this request |
| `X-Conversion-Id` | e.g. `cv_...` |

With `"response": "base64"`:

```json
{ "id": "cv_...", "pages": 1, "credits_used": 1, "credits_remaining": 99, "filename": "document.pdf", "pdf_base64": "JVBERi0..." }
```

With `"response": "url"`:

```json
{ "id": "cv_...", "pages": 1, "credits_used": 1, "credits_remaining": 99, "url": "https://html-pdf.com/f/...", "expires_at": "2026-09-30T12:00:00Z" }
```

### URL to PDF

```bash
curl https://html-pdf.com/v1/pdf -H "Authorization: Bearer $HTML_PDF_KEY" \
  -d '{"url": "https://example.com", "media": "screen", "format": "Letter"}' -o page.pdf
```

The page is opened in headless Chrome and printed. Use `"media": "screen"` to keep the on-screen look, and `wait_delay` for pages that render with JavaScript. Pages behind a login won't work: fetch their HTML yourself and send it as `html`.

### Images, fonts and assets

Reference remote assets by absolute `https://` URL, or inline them as `data:` URIs. To upload local assets, send `multipart/form-data` with `index.html` plus the files, and an optional `options` field containing the JSON options:

```bash
curl https://html-pdf.com/v1/pdf -H "Authorization: Bearer $HTML_PDF_KEY" \
  -F "index.html=@index.html" -F "logo.png=@logo.png" -F "style.css=@style.css" \
  -F 'options={"format":"Letter"}' -o out.pdf
```

In `index.html`, reference them by name: `<img src="logo.png">`.

### Page breaks

Use CSS: `break-before: page`, `break-inside: avoid`, `@page { size: A4; margin: 20mm }` (with `prefer_css_page_size: true`).

## GET /v1/credits

```json
{ "balance": 99, "used_total": 1, "email": "you@example.com", "unit": "1 credit = 1 PDF page", "top_up_url": "https://html-pdf.com/dashboard",
  "auto_topup": { "enabled": false, "threshold": 100, "pack": { "usd": 10, "credits": 1000 }, "card": null, "settings_url": "https://html-pdf.com/dashboard#autotopup" } }
```

`auto_topup.card` is the saved card's label (e.g. `"visa •••• 4242"`), or `null` before the first purchase.

## GET /v1/conversions

Last 100 conversions: id, status, pages, bytes, duration_ms, source (`api`, `mcp`, `web`), created_at. Document content is never stored.

## POST /v1/checkout

Get a Stripe payment link for a credit pack, e.g. for an agent to send to its human. Credits are added as soon as the payment succeeds.

```bash
curl -X POST https://html-pdf.com/v1/checkout -H "Authorization: Bearer $HTML_PDF_KEY" -d '{"usd": 10}'
```

```json
{ "checkout_url": "https://checkout.stripe.com/...", "usd": 10, "credits": 1000 }
```

Packs (`usd` → credits): 10 → 1,000 · 25 → 3,000 · 50 → 7,000 · 100 → 15,000 · 250 → 50,000.

Running low? The account owner gets an email below 20 credits and at 0, and can enable **auto top-up** in the dashboard to refill automatically from a saved card.

## POST /v1/autotopup

Turn auto top-up on or off so an automation never runs out: when the balance drops below `threshold`, the card saved at the first purchase is charged for the `usd` pack (at most once every 10 minutes; it turns itself off if a payment fails). Send any of the three fields; the others keep their value. `threshold` can be at most the pack size, so one top-up always lifts the balance above it.

```bash
curl -X POST https://html-pdf.com/v1/autotopup -H "Authorization: Bearer $HTML_PDF_KEY" \
  -d '{"enabled": true, "threshold": 200, "usd": 25}'
```

Returns `{"auto_topup": {...}}` as in `GET /v1/credits`. The account owner is emailed about every change. Without a saved card you get `409 no_saved_card` with a `checkout_url`: buy once, then turn it on. Agents should ask the user before turning it on.

## POST /v1/keys

Create an additional API key for the account of the key you authenticate with. Revoke keys in the dashboard.

## Errors

All errors are JSON:

```json
{ "error": { "code": "insufficient_credits", "message": "You have no credits left.", "hint": "Buy credits at ...", "top_up_url": "https://html-pdf.com/dashboard", "docs_url": "..." } }
```

| status | code | meaning |
|---|---|---|
| 400 | `missing_html`, `invalid_url`, `html_and_url`, `invalid_json`, `invalid_*` | Fix the request. |
| 400 | `disposable_email` | `POST /v1/signup` with a temporary (temp-mail) address. Use a permanent email. |
| 401 | `missing_api_key`, `invalid_api_key` | |
| 402 | `insufficient_credits` | No credits left, or the PDF has more pages than your balance (then the error has `pages` and `balance`). Not charged. `checkout_url` is a ready Stripe payment link to give the user, for the pack that fits your recent usage (from $25 for 3,000 pages, see `checkout_pack`); also `top_up_url`, or `POST /v1/checkout` for other packs. |
| 403 | `email_not_confirmed` | Free credits are added when the account's email is confirmed. Open the emailed link, then retry. |
| 409 | `account_exists` | Signup with an existing email; login link emailed. |
| 409 | `no_saved_card` | `POST /v1/autotopup` before any purchase. Pay `checkout_url` once, then retry. |
| 413 | `too_large` | Body over 20 MB. |
| 422 | `render_failed`, `max_pages_exceeded` | Not charged. |
| 429 | `rate_limited` | 120 req/min per account. Retry after `Retry-After`. |
| 503 | `busy`, `engine_unavailable` | Not charged. Retry. |
| 504 | `render_timeout` | Over 30 s. Not charged. |

Failed requests never cost credits.

## Limits

- Request body: 20 MB
- Render time: 30 s
- Rate: 120 requests/minute per account
- Download links (`response: "url"`, MCP): 24 hours

## Security note

The renderer cannot reach private networks. Treat the HTML you send like code running in a sandboxed browser: it may load public URLs and run JavaScript.
