Blog/Guides

Markdown to PDF API

Guides··5 min read

A markdown to PDF API is a POST that accepts GitHub-flavoured Markdown and returns a Chromium PDF. RelayPDF does that on POST /v1/pdf: you send a markdown field, the worker renders GFM to HTML, then Chromium prints it. Same print options as html. Same response modes. You do not run a browser.

Use it when the source is already Markdown: release notes, weekly reports, agent-written docs. If you need pixel-perfect invoices, send HTML or a published Handlebars template instead. That is the product page’s own split, not a slogan.

What you actually send

The body is UTF-8 JSON. Provide exactly one of html, url, markdown, or templateId. Mixing sources is invalid_request. filename is optional and must end with .pdf. Authorization is a bearer key (pdf_live_…); query-string keys are rejected. Keys are created in the dashboard and shown once.

Documented body fields that apply to Markdown: markdown, filename, response, callbackUrl, options. templateId / templateData / strict are for published templates, not for this path. Full field table: https://relaypdf.com/docs/pdf

GFM tables and code fences

markdown is GFM. Tables, fenced code, task lists, strikethrough, and autolinks go through the same renderer as the rest of the document. There is no separate theme field, no Mermaid option, and no KaTeX flag on this API. If you need those, you are looking at a different product or you should emit HTML yourself and POST html.

A table that survives print is a GFM table plus page size and margins that leave room for the grid. Wide tables on letter with 10mm margins will wrap or clip the way HTML tables clip in Chrome. That is the printer, not a Markdown bug.

Fenced blocks become <pre><code>. Monospace wraps according to the printed page width. If a line is a 200-character stack dump, shrink format or raise scale only after you look at a real page. scale is 0.1–2; default is 1.

Page size

options.format defaults to letter. Documented named sizes: letter, A4, legal, tabloid, or another named Chromium format. landscape defaults to false. width and height override with a custom page. preferCSSPageSize defaults to false; set true only if the generated HTML includes @page. printBackground defaults to true.

margin is top / right / bottom / left as CSS lengths (10mm, 0.5in). headerTemplate and footerTemplate are HTML; they enable Chromium headers and footers. Placeholders: pageNumber, totalPages, date, title. Leave extra margin or they clip. pageRanges is a Chromium range string such as 1-3,5.

Markdown itself has no @page. If you want CSS page size, send html. For Markdown, set format (and margin) on options. Rendering options: https://relaypdf.com/docs/options

curl

Minimal conversion from the product page. Binary response is the default: 200 application/pdf with x-relaypdf-id, x-relaypdf-size, and content-disposition.

curl https://api.relaypdf.com/v1/pdf
  -H "Authorization: Bearer pdf_live_..."
  -H "Content-Type: application/json"
  -d '{"markdown":"# Weekly report\n\n- shipped\n- blocked","filename":"report.pdf"}'
  --output report.pdf

Tables, fences, and A4 in one body. Only documented options.

curl https://api.relaypdf.com/v1/pdf
  -H "Authorization: Bearer pdf_live_..."
  -H "Content-Type: application/json"
  -d '{
    "markdown": "# Weekly report\n\n| Item | Status |\n| --- | --- |\n| API | shipped |\n\n```bash\ncurl -I https://example.com\n```",
    "filename": "report.pdf",
    "options": {
      "format": "A4",
      "margin": { "top": "16mm", "right": "14mm", "bottom": "16mm", "left": "14mm" },
      "printBackground": true
    }
  }'
  --output report.pdf

Need a link instead of bytes? Set response to url. You get JSON with id, status, url, filename, sizeBytes, expiresAt. The file URL is public for 24 hours (GET /v1/files/:id, no key). response: async returns 202 with a pollUrl; poll GET /v1/jobs/:id with the same key, or set callbackUrl (https only). Failed jobs are not billed.

Python

pip install relaypdf. Methods are snake_case; JSON field names stay camelCase (printBackground, not print_background). from_markdown is POST /v1/pdf.
import os
from relaypdf import RelayPDF, RelayPDFError
client = RelayPDF(api_key=os.environ["RELAYPDF_API_KEY"])
md = """# Weekly report

| Item | Status |

| --- | --- |

| API | shipped |

```python

print('ok')

```

"""
try:
    pdf = client.pdf.from_markdown(

md,

        filename="report.pdf",
        options={
        "format": "A4",
        "printBackground": True,
        "margin": {
        "top": "16mm",
        "right": "14mm",
        "bottom": "16mm",
        "left": "14mm",
        },
        },
    )
    pdf.save("report.pdf")
    print(pdf.id, pdf.size_bytes)
except RelayPDFError as err:
    print(err.status, err.code, err.message, err.retry_after)

A 24-hour link is the same helper with response="url". You read url_result.url and url_result.expires_at. BinaryResult has save(); that is the default. SDK notes: https://relaypdf.com/docs/sdks/python

When Markdown is the wrong source

Same endpoint, different field. Public page → url (private, loopback, and metadata hosts are rejected). Layout you already control → html. Repeatable invoice with data → templateId plus templateData after you publish a Handlebars draft. Product surface for this article: https://relaypdf.com/markdown-to-pdf. Sibling when the source is a page you own: https://relaypdf.com/html-to-pdf.

waitUntil defaults to networkidle0. timeout is the Chromium render budget, max 60000 ms, not an SDK HTTP timeout. waitForSelector and waitForTimeout exist because this is the Chromium printer. Markdown documents rarely need them unless the rendered HTML pulls remote images that keep the network busy.

Errors are { error: { code, message } }. invalid_request is a bad body. payment_required is an empty wallet. rate_limited is 429 with Retry-After and is not billed. render_failed is not billed. Do not invent a Markdown-specific error; there isn’t one in the table.

CLI if you are not writing a client

npx @relaypdf/cli, then relaypdf login or relaypdf setup. relaypdf pdf --markdown prints through the same Chromium path. Credentials sit in ~/.config/relaypdf/credentials.json mode 0600 and are not printed. Agents should use device-flow login instead of asking for a pasted key.

Reports and agents

The product page lists release notes, reports, and agent-generated docs as the fit. Agents already emit GFM. You do not ask them for a full HTML document unless the layout is the product. POST markdown, set filename, pick format. If the run should not block, response: async and an https callbackUrl. Poll GET /v1/jobs/:id with the same key.

Do not send Markdown through POST /v1/convert. That path is LibreOffice or wkhtmltopdf for Office files. Chromium Markdown is only POST /v1/pdf. Mixing those endpoints is how you get convert_unavailable or a file that looks like a text dump.

That is the whole markdown to PDF API: one field, GFM in, Chromium PDF out, page size on options. Start on https://relaypdf.com/markdown-to-pdf, keep https://relaypdf.com/docs/pdf open for the body fields, and use HTML when Markdown’s HTML is not enough.

Ready to generate?

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