Foliant

API reference

Every endpoint, field, header and error. New here? Start with the getting started guide.

Base URL: https://api.foliant.dev. All requests and responses are JSON unless the response is the rendered file itself.

Regions

Two API endpoints, one account. Pick by where your data may be processed; keys, templates and balance work on both.

EndpointComputeWhat stays there
https://api.foliant.devEU, Frankfurt (eu-central-1)Everything. Documents are rendered here and all account data (email, keys, usage, balance, stored assets and templates) is stored here. This is the default and the only endpoint the dashboard and MCP config use.
https://us.api.foliant.devUS, N. Virginia (us-east-1)Document processing only: source, attached files and the rendered output stay in the US for the duration of the request and are not stored. Account lookups and metering still read and write the EU tables, so account metadata lives in the EU regardless of endpoint.

Every response carries x-foliant-region (eu or us) so you can verify where a render ran. GET /health on each endpoint reports region and home_region. The US endpoint adds roughly 100 ms per request for the transatlantic account lookup; use it when your users or their data are in the Americas, otherwise stay on the EU endpoint.

Data residency in one sentence for your Datenschutzbeauftragter: all data at rest is in Frankfurt; document content is processed in the region of the endpoint you call and never stored.

Authentication

Sign in with your email on the start page. After you open the link, your first key is created, shown once in the dashboard and sent to your inbox. Create more or revoke keys in the dashboard. Send it as a bearer token. Keys start with fl_live_. Only a hash is stored on our side, so a lost key has to be replaced, not recovered.

Authorization: Bearer fl_live_xxxxxxxxxxxxxxxx

The demo endpoint takes no key and is limited per IP.

Quickstart

curl -X POST https://api.foliant.dev/v1/render \
  -H "Authorization: Bearer $FOLIANT_API_KEY" \
  -H "Content-Type: application/json" \
  -d @- -o invoice.pdf <<'JSON'
{
  "source": "#set page(paper: \"a4\")\n= Invoice #sys.inputs.no\nTotal: #sys.inputs.total EUR",
  "inputs": { "no": "2026-0917", "total": "8496.60" }
}
JSON

Node, without dependencies:

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({ source, inputs: { customer: "Meridian Studio" } }),
});
if (!res.ok) throw new Error((await res.json()).error.message);
await fs.promises.writeFile("out.pdf", Buffer.from(await res.arrayBuffer()));
console.log("pages:", res.headers.get("x-foliant-pages"));

POST/v1/render

Compile and export a document. Charged per output page after a successful render. Compile errors and limit violations are not charged.

POST/v1/demo/render

Same body and response as /v1/render, no key. 3 renders per IP per UTC day, 5 pages per document, 64 KB source. The slot is consumed when the request is accepted, so a document that fails to compile still uses one.

GET/v1/me

Account state for the key: tier, pages used this month, balance, limits. Useful for showing quota inside your own tooling.

{
  "tier": "free",
  "pages_used": 41, "pages_limit": 300, "resets_at": 1761955200,
  "balance_cents": 0, "price_cents_per_page": 0.4,
  "limits": { "max_pages": 20, "max_source_bytes": 524288 }
}

GET PUT DELETE/v1/assets, /v1/assets/{name}

Stored binary files for your account: logos, fonts, CSV or JSON data. Reference them in any document as "/assets/{name}", or explicitly via files. Up to 50 per account, 8 MB each (2 MB on the free tier). Names are a single file name: letters, digits, ., -, _.

# upload raw bytes (content type from the header or the extension)
curl -X PUT https://api.foliant.dev/v1/assets/logo.png \
  -H "Authorization: Bearer $KEY" -H "Content-Type: image/png" \
  --data-binary @logo.png

# or as JSON: { "data": "<base64>" } or { "url": "https://…" }
curl -X PUT https://api.foliant.dev/v1/assets/Inter-Regular.ttf \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/fonts/Inter-Regular.ttf"}'

curl https://api.foliant.dev/v1/assets -H "Authorization: Bearer $KEY"
# { "assets": [ { "name": "logo.png", "size": 12034, "content_type": "image/png", "updated": 1759… } ] }

The dashboard uses the same endpoints with your session cookie, so uploads there and via API land in the same place.

GET PUT DELETE/v1/templates, /v1/templates/{name}

Stored Typst sources. Render one by name with template instead of source; pass the variable parts in inputs. Templates can #include "/templates/other.typ" and use "/assets/…". Up to 50 per account, 512 KB each.

curl -X PUT https://api.foliant.dev/v1/templates/invoice.typ \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"source": "#image(\"/assets/logo.png\", width: 40mm)\n= Invoice #sys.inputs.no"}'

curl -X POST https://api.foliant.dev/v1/render \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"template": "invoice.typ", "inputs": {"no": "2026-0917"}}' -o invoice.pdf

Uploading a template also accepts raw text/plain bodies, so --data-binary @invoice.typ works.

Request body

FieldTypeNotes
sourcestringTypst source of the main file. Required unless template is set.
templatestringName of a stored template to render instead of source.
format"pdf" "png" "svg"Default pdf. PNG and SVG require a single page.
inputsobject of stringsAvailable in the document as sys.inputs. Use this for data instead of string-building Typst.
filesobjectExtra files by path. Each value is a base64 string, { "url": "https://…" }, or { "asset": "name" }. See Files and images.
ppinumberPNG only. Pixels per point, default 2 (144 dpi). Range 0.25 to 8; outside that is a 400.
deterministicbooleanPDF only. Fixed creation date and a source-derived document id, so identical input gives identical bytes. Default false.
response"inline" "json" "url"Default inline: body is the file. json: { data, content_type, pages, warnings } with base64 data. url: the file is stored for 24 hours and you get { url, expires_in, content_type, size, pages, warnings } with a presigned download link valid for one hour; account required. For agents that cannot receive binary data, or to hand a document to a browser or curl.

Files and images

Typst sees a small virtual filesystem: the main file plus whatever you attach. #image("logo.png"), #include "header.typ", #read("notes.txt"), #json("data.json"), #csv(…) and font files all resolve against it. Three ways to put a file there:

{
  "source": "#image(\"photo.jpg\", width: 60mm)\n#image(\"/assets/logo.png\")\n#set text(font: \"Inter\")",
  "files": {
    "photo.jpg":         { "url": "https://cdn.example.com/p/8812.jpg" },
    "Inter-Regular.ttf": "AAEAAAAQAQAABAAAR0RFRgJK…",
    "chart.svg":         { "asset": "q3-chart.svg" }
  }
}

Supported image formats: PNG, JPEG, GIF, WebP, SVG. Fonts: TTF, OTF, and collections. A missing file is a normal compile error with the line number.

Response

Inline mode returns the file with these headers:

content-typeapplication/pdf, image/png or image/svg+xml
x-foliant-pagesPage count, also what was charged
x-foliant-tierdemo, free, paid (pay as you go), starter, studio or scale
x-foliant-pages-usedPages rendered this month including this request
x-foliant-pages-limitFree tier and plans: the monthly allowance
x-foliant-warningsNumber of compiler warnings, if any. Use response: "json" to read them.

Errors

Every error is { "error": { "code", "message", ... } }. Codes you should handle:

StatusCodeMeaning
401missing_api_key invalid_api_key revoked_api_keyFix the header or create a new key.
422compile_errorTypst rejected the source. diagnostics[] has message, line, hints.
422too_many_pagesDocument exceeds your tier's page limit. pages and limit included.
422multi_page_rasterPNG or SVG requested for a multi-page document.
422compile_timeoutCompilation exceeded the tier's time budget (5 s demo, 10 s free, 25 s pay as you go and Starter, 40 s Studio and Scale).
404stored_file_not_foundThe source references /assets/… or /templates/… that do not exist; missing[], assets[], templates[] included.
400too_many_files invalid_ppiRequest shape problems.
413source_too_large file_too_largePayload over the tier limit.
429quota_exceededFree tier used up. resets_at and upgrade_url included.
429demo_exhaustedDemo renders used up for today.
402insufficient_creditBalance is empty. Top up; the key keeps working.
402subscription_past_dueThe plan's invoice is unpaid past the grace period and there is no prepaid credit. Fix the card in the portal.
{
  "error": {
    "code": "compile_error",
    "message": "Typst could not compile the document",
    "diagnostics": [
      { "severity": "error", "message": "unknown variable: totl", "line": 12, "hints": [] }
    ]
  }
}

Limits

DemoFreePay as you goStarterStudioScale
Pricefreefree0.4 cent / page prepaid9 EUR / month29 EUR / month99 EUR / month
Pages3 renders / day / IP300 / monthas much as your credit3 000 included, then 0.35 cent12 000 included, then 0.30 cent50 000 included, then 0.25 cent
Pages per document5202002005001 000
Compile timeout5 s10 s25 s25 s40 s40 s
Source size64 KB512 KB4 MB4 MB8 MB8 MB
Each attached file256 KB2 MB8 MB8 MB8 MB8 MB
Request body10 MB (API Gateway limit)
Package registryNot available yet

Plans renew monthly and bill overage with the next invoice; unused included pages do not carry over. Prepaid credit on a plan account is used before overage. A plan whose invoice stays unpaid keeps rendering for 7 days, then falls back to prepaid credit and finally answers 402 subscription_past_due. Subscribe, change or cancel in the dashboard; cancellation takes effect at the end of the period.

Rate limits

Independent of page quotas. Every authenticated response carries x-ratelimit-limit, x-ratelimit-remaining and x-ratelimit-reset (unix seconds). Over the limit you get 429 rate_limited with a retry-after header.

Requests per API key30 per minute (free), 120 (pay as you go, Starter), 300 (Studio), 600 (Scale)
Demo requests per IP10 per minute, 3 renders per day
Sign-in link requests per IP10 per hour
New accounts per IP3 per day

Need more? Write to hello@foliant.dev with your use case.

Fonts

Bundled and always available: Libertinus Serif, New Computer Modern (text and math), DejaVu Sans Mono, Noto Sans, Noto Sans Symbols. For anything else attach the font file:

{
  "source": "#set text(font: \"Inter\")\nHello",
  "files": { "Inter-Regular.ttf": "<base64>" }
}

Typst discovers fonts among attached files automatically. Font files count toward the per-file size limit.

GET/health

Renders a one-line document and touches the database. Returns 200 { "ok": true, "render": true, "db": true, "ms": 12 } or 503. We probe it from three regions every 30 seconds; point your own monitoring at it if you like.

MCP server

Two ways to connect. Both expose the same tools and use the demo quota when no key is given.

Remote (nothing to install)

Streamable HTTP at https://api.foliant.dev/mcp. Without a bearer token the endpoint answers 401 with WWW-Authenticate: Bearer resource_metadata=…, which starts the OAuth 2.1 flow in clients that support it; the user signs in and the client receives an API key as its access token. Clients without OAuth send Authorization: Bearer fl_live_…. /mcp/demo works without any token on the demo quota.

{ "mcpServers": { "foliant": { "type": "http", "url": "https://api.foliant.dev/mcp" } } }

OAuth endpoints: GET /.well-known/oauth-authorization-server, GET /.well-known/oauth-protected-resource, POST /oauth/register (RFC 7591, public clients only), GET|POST /oauth/authorize (PKCE S256 required), POST /oauth/token (authorization_code only; the access token is an API key, no expiry, no refresh token). Since the server cannot reach your disk, render_document returns the file as base64 and files must be base64, URL or asset.

Local (npx)

@foliant/mcp runs on your machine over stdio. It can read local files for files and writes rendered output to output_path.

{
  "mcpServers": {
    "foliant": {
      "command": "npx",
      "args": ["-y", "@foliant/mcp"],
      "env": { "FOLIANT_API_KEY": "fl_live_…" }
    }
  }
}
ToolDoes
render_documentRenders source or a stored template to format. delivery: url (remote default, a download link valid one hour), inline (base64 resource), or file (local server writes to output_path). files values may be base64, {url}, {asset}, or (local only) a file path.
check_sourceCompiles and returns diagnostics without charging pages. Uses the demo path when no key is set.
accountCalls /v1/me. Returns "demo mode" without a key.
list_templates, get_template, save_templateManage stored templates. get_template also lists the sys.inputs keys the template reads. save_template compiles before saving.
list_assets, upload_assetManage stored files. Upload from base64, URL, or (local only) a path.
search, fetchConnector-standard knowledge tools over templates, assets and the documentation, so hosts like ChatGPT can use Foliant as a source.

Full guide with a worked session: Agents and MCP.

Local server environment: FOLIANT_API_KEY, optional FOLIANT_API_URL to point at another deployment.