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.
| Endpoint | Compute | What stays there |
|---|---|---|
https://api.foliant.dev | EU, 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.dev | US, 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
| Field | Type | Notes |
|---|---|---|
source | string | Typst source of the main file. Required unless template is set. |
template | string | Name of a stored template to render instead of source. |
format | "pdf" "png" "svg" | Default pdf. PNG and SVG require a single page. |
inputs | object of strings | Available in the document as sys.inputs. Use this for data instead of string-building Typst. |
files | object | Extra files by path. Each value is a base64 string, { "url": "https://…" }, or { "asset": "name" }. See Files and images. |
ppi | number | PNG only. Pixels per point, default 2 (144 dpi). Range 0.25 to 8; outside that is a 400. |
deterministic | boolean | PDF 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" }
}
}
- Inline base64. Simplest. Counts against the request body limit (10 MB) and the per-file limit of your tier.
- URL. We fetch it server-side: https only, public hosts only, up to 8 MB each, 8 second timeout per file and 15 seconds for all of them together, redirects followed up to 3 times and re-checked. At most 50 files per request and twice your tier's per-file limit in total. Not available on the demo endpoint.
- Stored asset. Either map any path to an asset with
{ "asset": "name" }, or just write"/assets/name"in the source and it is loaded automatically. Same for"/templates/name".
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-type | application/pdf, image/png or image/svg+xml |
x-foliant-pages | Page count, also what was charged |
x-foliant-tier | demo, free, paid (pay as you go), starter, studio or scale |
x-foliant-pages-used | Pages rendered this month including this request |
x-foliant-pages-limit | Free tier and plans: the monthly allowance |
x-foliant-warnings | Number of compiler warnings, if any. Use response: "json" to read them. |
Errors
Every error is { "error": { "code", "message", ... } }. Codes you should handle:
| Status | Code | Meaning |
|---|---|---|
| 401 | missing_api_key invalid_api_key revoked_api_key | Fix the header or create a new key. |
| 422 | compile_error | Typst rejected the source. diagnostics[] has message, line, hints. |
| 422 | too_many_pages | Document exceeds your tier's page limit. pages and limit included. |
| 422 | multi_page_raster | PNG or SVG requested for a multi-page document. |
| 422 | compile_timeout | Compilation 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). |
| 404 | stored_file_not_found | The source references /assets/… or /templates/… that do not exist; missing[], assets[], templates[] included. |
| 400 | too_many_files invalid_ppi | Request shape problems. |
| 413 | source_too_large file_too_large | Payload over the tier limit. |
| 429 | quota_exceeded | Free tier used up. resets_at and upgrade_url included. |
| 429 | demo_exhausted | Demo renders used up for today. |
| 402 | insufficient_credit | Balance is empty. Top up; the key keeps working. |
| 402 | subscription_past_due | The 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
| Demo | Free | Pay as you go | Starter | Studio | Scale | |
|---|---|---|---|---|---|---|
| Price | free | free | 0.4 cent / page prepaid | 9 EUR / month | 29 EUR / month | 99 EUR / month |
| Pages | 3 renders / day / IP | 300 / month | as much as your credit | 3 000 included, then 0.35 cent | 12 000 included, then 0.30 cent | 50 000 included, then 0.25 cent |
| Pages per document | 5 | 20 | 200 | 200 | 500 | 1 000 |
| Compile timeout | 5 s | 10 s | 25 s | 25 s | 40 s | 40 s |
| Source size | 64 KB | 512 KB | 4 MB | 4 MB | 8 MB | 8 MB |
| Each attached file | 256 KB | 2 MB | 8 MB | 8 MB | 8 MB | 8 MB |
| Request body | 10 MB (API Gateway limit) | |||||
| Package registry | Not 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 key | 30 per minute (free), 120 (pay as you go, Starter), 300 (Studio), 600 (Scale) |
| Demo requests per IP | 10 per minute, 3 renders per day |
| Sign-in link requests per IP | 10 per hour |
| New accounts per IP | 3 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_…" }
}
}
}
| Tool | Does |
|---|---|
render_document | Renders 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_source | Compiles and returns diagnostics without charging pages. Uses the demo path when no key is set. |
account | Calls /v1/me. Returns "demo mode" without a key. |
list_templates, get_template, save_template | Manage stored templates. get_template also lists the sys.inputs keys the template reads. save_template compiles before saving. |
list_assets, upload_asset | Manage stored files. Upload from base64, URL, or (local only) a path. |
search, fetch | Connector-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.