Convert Markdown to PDF

The Markdown-to-PDF endpoint renders Markdown — CommonMark plus the GFM tables and strikethrough extensions — into a PDF document with a built-in print stylesheet. This is the shortest path from a README, a report, or generated notes to a clean PDF, without writing any HTML. Available on every plan.

Endpoint

POST https://api.pdfik.net/markdown-to-pdf

Request Body

ParameterTypeRequiredDescription
markdownstringYesThe Markdown source (CommonMark + GFM tables and strikethrough), up to 100,000 characters — longer input is rejected with 422. Raw HTML inside the Markdown is escaped, not rendered — see below.
webhook_urlstringNoOptional callback URL where the job outcome payload will be POSTed. Credentials must not be embedded in the URL (https://user:pass@host is rejected) — configure delivery headers in the dashboard instead.
optionsobjectNoThe same options object as /html-to-pdf: paper format, margin, header/footer templates, scale, watermark (Starter+), user_password (Pro+), compression (Pro+). See PDF Options.
renderobjectNoTimeout and wait selectors for browser load states (Pro+ plans only). See Browser Options.
deliveryobjectNoUpload the finished PDF straight to your own bucket via a presigned PUT URL (Pro+ plans only). See Delivery to Your Own Bucket.
testbooleanNoRun the job in test mode: full pipeline, no real render, a sample PDF is returned. Quotas are not debited. Defaults to false.

Markdown Support (markdown)

  • Dialect: CommonMark, plus the GFM tables and strikethrough extensions.
  • Raw HTML is escaped, not rendered. An <img> or <div> typed into the Markdown appears in the PDF as literal text. If your document needs HTML or custom CSS, use /html-to-pdf — this endpoint is for pure Markdown.
  • Styling is the built-in print stylesheet. Headings, tables, code blocks, lists and blockquotes come out with a consistent document theme; there is no custom CSS option. Page-level appearance (paper format, margins, header/footer, watermark) is controlled through options, exactly like on the other PDF endpoints.
  • The einvoice option is not available here — an e-invoice needs your own HTML (/html-to-pdf) or the dedicated /einvoice-to-pdf endpoint.

Size Limits

  • Input: markdown accepts at most 100,000 characters. Longer input is rejected with 422 validation-error before anything is charged.
  • Converted document: the HTML produced from the Markdown must fit the processing queue — the same ~240 KB bound as /html-to-pdf. Markdown whose converted form exceeds it — a very long document, or one that expands far beyond its own size — is rejected with 413 payload-too-large-for-queue, and the render is not charged. Split the document into smaller parts.

Delivery to Your Own Bucket (delivery)

Add a delivery object (mode, url) to have the finished PDF uploaded straight to your own storage via a presigned PUT URL — nothing is stored on PDFik's side: once the job is done, the job.finished webhook reports the outcome only (no file_url, no expires_at — we never record where the file went), and the download endpoint answers 404. Pro and Business plans. The URL must use https on the standard port 443 (any other port is rejected with 422). The full rules (URL requirements, expiry, retries, error behavior) are documented in Delivery to Your Own Bucket; it cannot be combined with test mode.

Request Headers

HeaderRequiredDescription
X-API-KeyYesYour API key (sk_live_...).
Idempotency-KeyNo1-255 characters from A-Z a-z 0-9 _ - . :. Retrying a request with the same key returns the original job instead of creating (and billing) a duplicate. Recommended for retry-after-timeout logic.

The Asynchronous Flow

  1. Submit Job: Post the Markdown to /markdown-to-pdf. You'll receive a job_id and a status of queued.
  2. Poll Status: Send GET requests to /jobs/{job_id} until status is done (or failed).
  3. Download File: Send a request to /jobs/{job_id}/download to stream the PDF binary directly. The file is available for 24 hours after generation (the exact moment is the expires_at field in the status response); at most 3 download attempts and 1 active stream are allowed per file. See File Retention & Downloads.

Integration Examples

1. Basic Request (curl)

Markdown travels inside a JSON string, so newlines are escaped as \n:

curl -X POST https://api.pdfik.net/markdown-to-pdf \
  -H "X-API-Key: sk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "markdown": "# Monthly Report\n\n| Metric | Value |\n| --- | --- |\n| Renders | 1,204 |\n\nGenerated ~~manually~~ automatically.",
    "options": { "format": "A4" }
  }'

For a Markdown file on disk, build the body from the file instead of quoting it inline:

jq -n --rawfile md report.md '{markdown: $md}' > request.json

curl -X POST https://api.pdfik.net/markdown-to-pdf \
  -H "X-API-Key: sk_live_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d @request.json

Response (202 Accepted):

{
  "job_id": "8bfa5cc3-09ef-436f-80d4-a0eb867123aa",
  "status": "queued",
  "detail": "Job successfully sent to processing queue."
}

2. JavaScript (ES6 Fetch)

const API_KEY = 'sk_live_YOUR_KEY';

async function markdownToPdf(markdown) {
  // 1. Submit the Markdown to start the conversion job
  const submitRes = await fetch('https://api.pdfik.net/markdown-to-pdf', {
    method: 'POST',
    headers: {
      'X-API-Key': API_KEY,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ markdown })
  });

  if (!submitRes.ok) throw new Error(`Failed to submit job: ${submitRes.statusText}`);
  const { job_id } = await submitRes.json();

  // 2. Poll the job status endpoint until it completes or fails
  let status = 'queued';
  while (status !== 'done' && status !== 'failed') {
    await new Promise(resolve => setTimeout(resolve, 2000));

    const statusRes = await fetch(`https://api.pdfik.net/jobs/${job_id}`, {
      headers: { 'X-API-Key': API_KEY }
    });

    if (statusRes.status === 429) continue; // rate limited: wait and poll again
    if (!statusRes.ok) throw new Error(`Status check failed: ${statusRes.status} ${await statusRes.text()}`);
    status = (await statusRes.json()).status;
  }

  if (status === 'failed') throw new Error('PDF generation job failed');

  // 3. Download the PDF file
  const downloadRes = await fetch(`https://api.pdfik.net/jobs/${job_id}/download`, {
    headers: { 'X-API-Key': API_KEY }
  });
  if (!downloadRes.ok) throw new Error(`Download failed: ${downloadRes.status} ${await downloadRes.text()}`);
  return await downloadRes.arrayBuffer(); // returns PDF binary buffer
}

3. Python (httpx)

import time
import httpx

API_KEY = "sk_live_YOUR_KEY"
BASE_URL = "https://api.pdfik.net"
HEADERS = {"X-API-Key": API_KEY}


def markdown_to_pdf(markdown: str, output_path: str = "output.pdf") -> None:
    with httpx.Client(base_url=BASE_URL, headers=HEADERS) as client:
        # 1. Submit
        submit = client.post("/markdown-to-pdf", json={"markdown": markdown})
        submit.raise_for_status()
        job_id = submit.json()["job_id"]

        # 2. Poll
        status = "queued"
        while status not in ("done", "failed"):
            time.sleep(2)
            r = client.get(f"/jobs/{job_id}")
            if r.status_code == 429:  # rate limited: wait and poll again
                continue
            r.raise_for_status()
            status = r.json()["status"]

        if status == "failed":
            raise RuntimeError("PDF generation job failed")

        # 3. Download
        with open(output_path, "wb") as f:
            with client.stream("GET", f"/jobs/{job_id}/download") as response:
                response.raise_for_status()
                for chunk in response.iter_bytes():
                    f.write(chunk)


markdown_to_pdf("# Hello\n\nRendered from Markdown.")

SDKs and CLI

The official clients cover this endpoint: markdownToPdf in the JavaScript and Java SDKs, markdown_to_pdf in the Python SDK, and pdfik markdown-to-pdf <file.md | -> in the CLI.