Foliant

Getting started

Sign in, copy a key, post some Typst, get a PDF. Five minutes, no SDK.

  1. Get a key

    Enter your email on the start page and open the link we send. Your first API key is created on the spot: it appears once in the dashboard and arrives by email. Free accounts get 300 pages a month.

    Keys look like fl_live_…. Export it for the examples below:

    export FOLIANT_API_KEY=fl_live_…

    No account yet and just want to see it work? Skip this step and use /v1/demo/render instead of /v1/render with no header. Three renders a day, five pages each.

  2. First render

    curl -X POST https://api.foliant.dev/v1/render \
      -H "Authorization: Bearer $FOLIANT_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"source": "#set page(paper: \"a4\")\n= Hello\nRendered on #datetime.today().display()."}' \
      -o hello.pdf -D -

    The body is the PDF. The -D - prints the headers, which include the page count and your usage:

    content-type: application/pdf
    x-foliant-pages: 1
    x-foliant-tier: free
    x-foliant-pages-used: 1
    x-foliant-pages-limit: 300
    x-ratelimit-remaining: 29
  3. Pass data

    Do not build Typst by string concatenation. Put values in inputs and read them as sys.inputs:

    curl -X POST https://api.foliant.dev/v1/render \
      -H "Authorization: Bearer $FOLIANT_API_KEY" -H "Content-Type: application/json" -o out.pdf \
      -d @- <<'JSON'
    {
      "source": "#let input(k) = sys.inputs.at(k, default: \"\")\n= Order #input(\"no\")\nFor #input(\"customer\"), total #input(\"total\") EUR.",
      "inputs": { "no": "4711", "customer": "Meridian Studio", "total": "8496.60" }
    }
    JSON

    Inputs are strings; convert in Typst with int() or float() when you need arithmetic. Once a document is used more than once, store it as a template and send only the inputs.

  4. Add an image

    Attach files by path. Base64 inline, or a URL we fetch, or a stored asset:

    {
      "source": "#image(\"logo.png\", width: 40mm)",
      "files": { "logo.png": { "url": "https://example.com/logo.png" } }
    }

    For a logo you use everywhere, upload it once (PUT /v1/assets/logo.png or the dashboard) and write #image("/assets/logo.png"); no files entry needed.

  5. Handle errors

    Every error is JSON with a stable code. The one you will see most is compile_error, which includes the line:

    HTTP 422
    { "error": { "code": "compile_error", "message": "Typst could not compile the document",
      "diagnostics": [ { "severity": "error", "line": 2, "message": "unknown variable: totl", "hints": [] } ] } }

    Failed renders are not charged. To validate without producing output at all, add "check": true; that returns { ok, pages, warnings } and is always free.

    When the free tier is used up you get 429 quota_exceeded with resets_at and an upgrade_url. Add credit and the same key keeps working.

Node

import { writeFile } from "node:fs/promises";

const res = await fetch("https://api.foliant.dev/v1/render", {
  method: "POST",
  headers: { authorization: `Bearer ${process.env.FOLIANT_API_KEY}`, "content-type": "application/json" },
  body: JSON.stringify({ template: "invoice.typ", inputs: { no: "2026-0917", total: "8496.60" } }),
});
if (!res.ok) {
  const { error } = await res.json();
  throw new Error(`${error.code}: ${error.message}` + (error.diagnostics ? "\n" + error.diagnostics.map(d => `line ${d.line}: ${d.message}`).join("\n") : ""));
}
await writeFile("invoice.pdf", Buffer.from(await res.arrayBuffer()));
console.log("pages:", res.headers.get("x-foliant-pages"));

Python

import os, requests

r = requests.post(
    "https://api.foliant.dev/v1/render",
    headers={"Authorization": f"Bearer {os.environ['FOLIANT_API_KEY']}"},
    json={"source": "= Hello from Python", "format": "pdf"},
    timeout=60,
)
if r.status_code != 200:
    err = r.json()["error"]
    raise SystemExit(f"{err['code']}: {err['message']}\n" + "\n".join(f"line {d.get('line')}: {d['message']}" for d in err.get("diagnostics", [])))
open("hello.pdf", "wb").write(r.content)
print("pages", r.headers["x-foliant-pages"])

Where next