Blog/SDKs

HTML to PDF with curl

SDKs··5 min read

To turn HTML into a PDF with curl, POST JSON to https://api.relaypdf.com/v1/pdf. Send Authorization: Bearer pdf_live_... and Content-Type: application/json. Put the markup in html. Put a .pdf name in filename. Default response is binary: write the body with --output. That is the html to pdf api curl path. Canonical samples live at https://relaypdf.com/docs/quickstart/curl. The same JSON fields are used by the official SDKs.

Provide exactly one source: html, url, markdown, or templateId. Mixing two is invalid_request. Keys are created in the dashboard and shown once. Query-string keys are not accepted. Coding agents should run npx @relaypdf/cli setup instead of asking anyone to paste a key. The examples below use the public placeholder pdf_live_... Replace it, or export RELAYPDF_API_KEY and interpolate.

Canonical copy-paste

The live curl quickstart lists this as “Basic binary output” for POST /v1/pdf. It is the shortest request that returns a file. The docs hub repeats a slightly longer invoice body with the same headers and --output. Both are valid CreatePdfRequest JSON. Field names are camelCase.

curl https://api.relaypdf.com/v1/pdf
  -H "Authorization: Bearer pdf_live_..."
  -H "Content-Type: application/json"
  -d '{"html":"<h1>Hello</h1>","filename":"hello.pdf"}'
  --output hello.pdf

curl defaults to POST when you pass -d, so the quickstart omits -X POST. Either form works. --output writes the raw HTTP body. On success that body is application/pdf. On failure it is JSON { "error": { "code", "message" } }. Check the first bytes or the HTTP status before you treat the file as a PDF. Use -f or -w "%{http_code}" if you want the shell to fail on 4xx/5xx.

curl -X POST https://api.relaypdf.com/v1/pdf
  -H "Authorization: Bearer pdf_live_..."
  -H "Content-Type: application/json"
  -d '{
    "html": "<h1>Invoice #1042</h1><p>Total: $1,200.00</p>",
    "filename": "invoice.pdf"
  }'
  --output invoice.pdf

Binary vs response:url

Every generating endpoint accepts response: "binary" | "url" | "async". Default is binary. You do not need to send the field for the copy-paste above. Binary returns file bytes plus x-relaypdf-id, x-relaypdf-size, and content-disposition. That is the right mode when the next step is a local file, an email attachment, or a stream to the client.

Set "response": "url" when the next hop should fetch the PDF later. The HTTP body is JSON, not PDF bytes. Do not use --output hello.pdf for this mode unless you intend to save JSON. OpenAPI names the object FileUrlResponse. Required fields: id, status, url, filename, sizeBytes, expiresAt. status is completed. url is GET /v1/files/:id. That download is public for 24 hours and does not need a key. After expiry, GET returns not_found.

curl https://api.relaypdf.com/v1/pdf
  -H "Authorization: Bearer pdf_live_..."
  -H "Content-Type: application/json"
  -d '{
    "html": "<h1>Hello</h1>",
    "filename": "hello.pdf",
    "response": "url"
  }'

A completed url-mode body looks like this shape (values are illustrative):

{
  "id": "file_...",
  "status": "completed",
  "url": "https://api.relaypdf.com/v1/files/file_...",
  "filename": "hello.pdf",
  "sizeBytes": 12345,
  "expiresAt": "2026-08-24T16:00:00.000Z"
}

Then GET the url with a second curl --output hello.pdf. No Authorization header. Treat the 24-hour object as a transfer, not as permanent storage. If you need a webhook or a poll instead of a blocking POST, use "response": "async". That returns 202 with a pollUrl. Poll GET /v1/jobs/:id with the same bearer key until status is completed or failed. callbackUrl, if you set it, must be https. Async jobs are a separate guide; this page stays on binary and url.

responseHTTPBodyWhen
binary (default)200PDF bytes; x-relaypdf-id, x-relaypdf-size, content-dispositionSave or stream now. Use --output.
url200JSON FileUrlResponse (id, status, url, filename, sizeBytes, expiresAt)Hand a 24h GET /v1/files/:id to another system. No key on download.
async202Job accepted; pollUrl. GET /v1/jobs/:idLong render or webhook (callbackUrl https).

Auth, host, and JSON rules

Host is https://api.relaypdf.com. Local override in the SDKs is http://localhost:8787; production curl should not use that. Bearer scheme only. The docs show Authorization: Bearer pdf_live_… Keys are hashed at rest (SHA-256). A missing or unknown key is unauthorized (401). An empty wallet is payment_required (402). Rate limits return rate_limited (429) with Retry-After. Failed operations, 429s, and 402s are never billed.

The body is one JSON object. Do not send multipart. Do not send form fields. html is a string of markup, not a file path. curl -d @payload.json is fine if the file is that object. filename should end in .pdf when you set it. options is an object of Chromium print fields, not a query string.

Options you can add on the same POST

PDF options sit under options on POST /v1/pdf. Names match https://relaypdf.com/docs/options. Defaults that matter for a first curl: format is letter; printBackground is true; waitUntil is networkidle0; landscape is false; preferCSSPageSize is false; scale is 1. timeout is the Chromium budget in milliseconds, max 60000. waitForTimeout is an extra wait, max 30000. headerTemplate and footerTemplate are HTML; they enable Chromium headers and need extra margin. Placeholders documented on the options page: pageNumber, totalPages, date, title.

curl https://api.relaypdf.com/v1/pdf
  -H "Authorization: Bearer pdf_live_..."
  -H "Content-Type: application/json"
  -d '{
    "html": "<h1>Invoice #1042</h1><p>Total: $1,200.00</p>",
    "filename": "invoice.pdf",
    "options": {
      "format": "letter",
      "printBackground": true,
      "margin": {
        "top": "16mm",
        "right": "12mm",
        "bottom": "16mm",
        "left": "12mm"
      }
    }
  }'
  --output invoice.pdf

A public page uses url instead of html. Markdown uses markdown. A published Handlebars layout uses templateId plus templateData. Those are still POST /v1/pdf. Screenshots are a different route: POST /v1/images. The curl quickstart shows a URL-to-PNG sample there. Office files go to POST /v1/convert, not /v1/pdf.

curl https://api.relaypdf.com/v1/pdf
  -H "Authorization: Bearer pdf_live_..."
  -H "Content-Type: application/json"
  -d '{"markdown":"# Hello from Markdown"}'
  --output hello.pdf

Errors and price

Failures are structured: { "error": { "code", "message" } }. Branch on code. Common codes on this endpoint: invalid_request (400) for field names, types, or more than one source; unauthorized (401); url_not_allowed (400) for private, loopback, or metadata hosts, or a non-https callbackUrl; payload_too_large (413); render_failed (502) when Chromium cannot print; payment_required (402); rate_limited (429). There is no published megabyte figure. Do not invent one.

A successful HTML, URL, or Markdown PDF is $0.015 on the live rate card (https://relaypdf.com/pricing). TemplateId jobs are the same $0.015. New accounts still get $5 trial credit with no card. Credit does not expire. Only a successful generation debits. Trial rate limit is 20/min; funded or auto-reload is 60/min; burst is 5 / 10s (SDK READMEs).

What this page is not

This is not the Node, Python, or Next.js how-to. Those call the same REST fields through an SDK. This is not merge, protect, stamp, or convert. Those are other POSTs. This is not OCR, e-sign, PDF/A, or HIPAA — none of those are product claims. Use this page when you want raw HTTP and a file on disk.

Ship it

Copy the five-line request from https://relaypdf.com/docs/quickstart/curl. Swap pdf_live_... for your key. Keep --output for binary. Switch to "response": "url" when you want JSON and a 24-hour GET /v1/files/:id. Add options only when letter and printBackground are not enough. Full field list: https://relaypdf.com/openapi.json. Product page: https://relaypdf.com/html-to-pdf. Docs hub: https://relaypdf.com/docs.

Ready to generate?

One API for HTML, Markdown, URLs, and Office. REST, SDK, CLI, or MCP.