API Reference
Complete reference for the FormaTeX REST API. Compile LaTeX documents to PDF with a single HTTP call.
Quick Example
curl -X POST https://api.formatex.io/api/v1/compile \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{"latex": "\\documentclass{article}\\begin{document}Hello!\\end{document}"}' \
--output document.pdf
Authentication
Two auth methods: JWT tokens for account management, API keys for compilation.
| Method | Header | Used For |
|---|---|---|
| JWT Token | Authorization: Bearer <token> | User management, billing, admin |
| API Key | X-API-Key: <key> | Compilation endpoints |
Errors
All errors return a consistent JSON format with an error message and optional context.
{
"error": "human-readable error message",
"plan": "free",
"limit": 100,
"used": 100
}
Status Codes
| Code | Meaning |
|---|---|
400 | Invalid request body |
401 | Missing or invalid authentication |
403 | Plan limit violated or account deactivated |
404 | Resource not found |
409 | Conflict (e.g., email already registered) |
422 | Compilation failed |
429 | Rate limit exceeded |
503 | Billing not configured |
Rate Limits
| Endpoint Group | Limit |
|---|---|
| Auth | 20 req/min |
| User management | 60 req/min |
| Billing | 20 req/min |
| Admin | 60 req/min |
| Compilation | 100 req/min |
API Keys
Create and manage API keys for compilation. All endpoints require JWT.
Compilation
Compile LaTeX to PDF. All compilation endpoints require an API Key.
Rendering
Render equations, TikZ diagrams, and compile full documents to rasterized images — all without touching your monthly compilation quota. All endpoints require an API key.
Document Analysis
Static analysis endpoints for LaTeX and BibTeX. None of these endpoints compile a document or count against your monthly quota. All require an API key.
Batch Generation
Generate many PDFs from a single LaTeX template in one API call. Supply your template and data rows — FormaTeX compiles each row concurrently and returns a ZIP archive containing all PDFs plus a manifest.json summary.
Typical uses: personalised certificates, invoices, letters, report cards, conference badges, mail-merge documents.
- Up to 50 rows per request
- Up to 5 concurrent compilations (auto-throttled)
- Per-row timeout: 30 s
- Overall request timeout: 4 min
- Rate limit: 10 requests per minute
ZIP Archive Format
The returned ZIP always contains:
manifest.json— compilation summary (always the first entry)<filename>.pdf— one PDF per successful row, named by thefilenametemplate
{
"total": 3,
"success": 2,
"failed": 1,
"results": [
{ "index": 0, "filename": "report-Martin-1.pdf", "success": true },
{ "index": 1, "filename": "report-Chen-2.pdf", "success": true },
{ "index": 2, "filename": "report-López-3.pdf", "success": false, "error": "Undefined control sequence \\badmacro" }
]
}
Duplicate filenames after template rendering are automatically deduplicated by appending -2, -3, etc.
LaTeX Escaping
By default all substituted values are LaTeX-escaped, so special characters in your data are safe:
& → \& % → \%
$ → \$ # → \#
_ → \_ { → \{
} → \} ~ → \textasciitilde{}
^ → \textasciicircum{} \ → \textbackslash{}
Use {{field | raw}} to suppress escaping when the field already contains valid LaTeX markup.
Conversions & PDF Tools
Convert LaTeX documents to other formats and manipulate existing PDFs. All endpoints require an API key.
Developer Tooling
Static tooling endpoints for LaTeX. None of these count against your monthly compilation quota. All require an API key.
Templates
A template is a LaTeX document you save in your account with <<variable>> placeholders. To produce a PDF you send the values; FormaTeX substitutes them and compiles the result. The people and systems that call the API never need to write LaTeX.
Templates are available on Pro and above. A template compile is a normal compilation: it counts against your monthly API compilations and follows the same rate limits and engine rules as /compile.
| Plan | Saved templates |
|---|---|
| Free | None |
| Pro | 3 |
| Max | 25 |
| Entreprise | Unlimited |
How it works
- Create the template in the dashboard (Templates, New template), from your pipeline with
POST /api/v1/templates, or fork one from the template library. Forking copies the library template into your account and counts toward your limit. - Mark the dynamic values in the source with
<<key>>and describe each variable in the schema: type, required, default. - Compile with variables. Your backend sends
POST /api/v1/templates/user/:id/compilewith an API key and a JSON object of values, and receives the PDF.
Who can do what
Everything works with an API key. There is no separate credential for managing templates.
| Action | Endpoint | Credential |
|---|---|---|
| List your templates | GET /api/v1/templates | API key (X-API-Key) |
| Create a template | POST /api/v1/templates | API key |
| Read, update, delete a template | GET, PUT, DELETE /api/v1/templates/user/:id | API key |
| Compile a template | POST /api/v1/templates/user/:id/compile | API key |
| Upload, list, remove assets | /api/v1/templates/user/:id/assets... | API key |
The same operations also exist under /api/v1/users/me/templates... with the dashboard's session token (Authorization: Bearer <JWT> from POST /api/v1/auth/login). Both reach the same templates. A key is refused on the session routes and a session token is refused on the key routes, each with 401.
Management calls (everything except compile and list) are limited to 60 per minute per account. Over the limit you get 429 with a Retry-After header.
Placeholders
- A placeholder is
<<key>>. A key starts with a letter or underscore and continues with letters, digits and underscores. Keys are case sensitive. - Every occurrence in the source is replaced, including inside commands and environments.
- A placeholder with no value, no default and no schema entry is left in the document exactly as written.
- Variables you send that are not in the schema are still substituted wherever a matching placeholder exists.
\documentclass{article}
\begin{document}
Dear <<name>>,
Your order <<order_id>> totals <<amount>> and was placed on <<date>>.
\end{document}
Values are inserted as LaTeX. FormaTeX does not escape them. If a value can contain user text, escape these characters before you send it:
\ & % $ # _ { } ~ ^. The substituted document goes through the same validation as any other compile request.
| Character | Send as |
|---|---|
& | \& |
% | \% |
$ | \$ |
# | \# |
_ | \_ |
{ and } | \{ and \} |
~ | \textasciitilde{} |
^ | \textasciicircum{} |
\ | \textbackslash{} |
Variable schema
The schema is an array with one entry per variable.
| Field | Type | Description |
|---|---|---|
key | string | Name used in <<key>>. At most 50 characters. |
type | string | number | date | Validation applied to the value. Any other type is treated as string. |
required | boolean | The compile fails with 400 when the value is missing and there is no default. |
default | string | Used when the request does not send the variable. At most 10,000 characters. |
description | string | Human readable label, shown in the dashboard. |
numbermust parse as a number, for example42or129.90.datemust beYYYY-MM-DDor the wordtoday.todayis replaced by the current date when the template is compiled. A default oftodayworks the same way.- A required variable that has a default is satisfied by the default.
- A template can declare up to 50 variables.
Compile a template
# 1. Find the template id
curl https://api.formatex.io/api/v1/templates \
-H "X-API-Key: $FORMATEX_API_KEY"
# 2. Compile it with values
curl -X POST https://api.formatex.io/api/v1/templates/user/$TEMPLATE_ID/compile \
-H "X-API-Key: $FORMATEX_API_KEY" \
-H "Content-Type: application/json" \
-d '{"variables": {"name": "Alice Martin", "amount": "129.90"}}' \
--output invoice.pdf
The response body is the raw PDF. Send Accept: application/json to receive it base64 encoded in a JSON object instead.
Send every value as a JSON string, numbers included: "amount": "129.90". A JSON number or object is rejected with 400.
Errors
| Status | When | Body |
|---|---|---|
400 | A required variable is missing, or a value fails its type | { "error": "required variable \"name\" is missing" } or { "error": "variable \"amount\" must be a number, got \"ten\"" } |
400 | engine is not a supported engine | { "error": "unsupported engine: nope" } |
400 | The body is not valid JSON, or a value is not a string | { "error": "invalid request: ..." } |
401 | No API key, or a session token sent to a key route | { "error": "missing API key" } |
403 | The engine or a limit is not allowed on your plan | { "error": "..." } |
404 | The template does not exist or belongs to another account | { "error": "template not found" } |
422 | The LaTeX failed to compile | { "success": false, "error": "...", "log": "...", "duration": 410 } |
429 | More than 60 management calls in a minute | Retry-After header |
503 | The engine is temporarily disabled | { "error": "engine_unavailable", "message": "..." } |
Manage templates
Push a template from a pipeline with nothing but an API key:
curl -X POST https://api.formatex.io/api/v1/templates \
-H "X-API-Key: $FORMATEX_API_KEY" \
-H "Content-Type: application/json" \
-d @template.json
A template that belongs to another account is 404, never 403, so ids cannot be probed.
Assets
A template can carry files that every compile needs: a logo, a .bib, a .cls or a font. Stored assets are placed next to the source when the template compiles, so the source refers to them by path.
Allowed file types: images (.jpg, .jpeg, .png, .pdf, .eps, .svg, .bmp, .tiff, .tif), fonts (.ttf, .otf, .woff, .woff2, .pfb, .pfm, .tfm), LaTeX support files (.sty, .cls, .bst, .bib, .def, .cfg, .fd, .enc, .map) and data files (.csv, .json).
Limits per template, by plan:
| Plan | Files | Largest file | Total size |
|---|---|---|---|
| Pro | 30 | 15 MB | 100 MB |
| Max | 100 | 30 MB | 300 MB |
| Entreprise | 300 | 50 MB | 1 GB |
Uploading a path that already exists returns 409. A path that tries to leave the template folder, or a file type that is not allowed, returns 400. Going over a limit returns 403.
Use templates from an AI assistant
The MCP server gives an AI assistant five template tools: list_templates returns IDs, names and variables, get_template returns one template with its full source, create_template and update_template save a template or change one, and compile_template takes a template ID and a variables object. They use the same API key, the same Pro requirement and the same template limit as the endpoints above. The tools check the variable schema before saving, and update_template changes only the fields it is given. A saved version is replaced and cannot be recovered. Assets are not available through the MCP server: upload them in the dashboard or with the asset endpoints above.
Plans
View available plans and their limits. Public endpoint.
Plan Comparison
| Feature | Free | Pro | Max | Enterprise |
|---|---|---|---|---|
| Compilations/mo | 15 | 500 | 2,000 | 15,000 |
| Timeout | 30s | 120s | 300s | 300s |
| Max Size | 1 MB | 10 MB | 25 MB | 50 MB |
| Compilation Runs | 1 | 3 | 5 | 10 |
| API Keys | 2 | 5 | 15 | 50 |
| Engines | All 2 | All 4 | All 4 | All 4 |
| At Limit | Hard block | Soft limit | Soft limit | Soft limit |
Billing
Subscription management via Polar.sh. Requires JWT authentication.
Users
Manage your profile and view usage statistics. All endpoints require JWT.
Webhooks
Register HTTP callbacks to receive real-time notifications on compilation, quota, and plan events. Requires JWT. Failed deliveries are automatically retried up to 5 times with exponential backoff.
| Event | Trigger |
|---|---|
compilation.success | Compilation completed successfully |
compilation.failed | Compilation failed (LaTeX error or timeout) |
quota.warning | Usage reached 80% of monthly quota |
quota.exceeded | Usage reached 100% — compilations are blocked |
plan.changed | Plan upgraded, downgraded, or revoked |
Event Payload
{
"event": "compilation.success",
"data": {
"compilationId": "uuid",
"engine": "pdflatex",
"durationMs": 1240,
"inputSizeBytes": 2048,
"apiKeyPrefix": "a2e67d28...",
"timestamp": "2026-02-20T14:30:00Z"
}
}
If a secret is set, the payload is signed with HMAC-SHA256 delivered in the X-FormaTeX-Signature header.
MCP Server
Compile LaTeX directly from AI agents — Claude, Claude Code, Cursor, VS Code, Codex, n8n, and any other MCP client — with zero local TeX Live installation. The server authenticates with your FormaTeX API key and works on every plan, including Free. Compilations made through MCP count toward the same monthly API quota as REST calls.
There are two ways to connect:
- Hosted server at
https://mcp.formatex.io/mcp(Streamable HTTP). Nothing to install. - Local binary
formatex-mcp(stdio). Your client starts it and passes your key in theFORMATEX_API_KEYenvironment variable.
Available Tools
PDF output: Compile tools return text: job metadata and a Download PDF link. They do not inline the file, which keeps the conversation small. Open the link to download the PDF. When your agent needs the bytes, for example to save the file to disk, it calls fetch_pdf_bytes with the job ID. That is the only tool that returns the PDF as base64.
fix_latexPrompt
Analyzes compilation errors, finds root causes, and returns structured instructions for fixing the LaTeX source. Pass it your LaTeX source and the error log; the engine is optional.
“Use the fix_latex prompt to help me fix this error: [paste error log]”
The server also exposes two resources for clients that read MCP resources: formatex://engines (available engines) and formatex://usage (this month's usage and plan limits).
Setup
How it works
Your client connects to mcp.formatex.io over Streamable HTTP. Nothing to install.
Hosted server
https://mcp.formatex.io/mcpConnect your client to the hosted endpoint with your API key.
Claude Desktop and claude.ai custom connectors Claude Desktop and claude.ai add remote servers as custom connectors in the app, not in a configuration file.
Open Customize > Connectors in Claude Desktop or at claude.ai. On Team and Enterprise plans, an owner adds it under Organization settings > Connectors.
Choose Add custom connector and enter:
Name FormaTeX URL https://mcp.formatex.io/mcp Authenticate with FormaTeX. Either option works:
Sign in. Claude opens a FormaTeX page where you paste your API key once. There is nothing else to copy.
Request headers. Add this header instead of signing in:
Header Authorization: Bearer YOUR_FORMATEX_API_KEY
Turn FormaTeX on for your conversation, then run the prompt below.
Good to know
- Custom connectors are available on every plan. Free accounts can add one.
Health check
curl https://mcp.formatex.io/health
# {"status":"ok","service":"formatex-mcp","version":"1.1.0"}Keep the key out of git
Project files such as .mcp.json, .cursor/mcp.json and .vscode/mcp.json are usually committed. Use the environment variable form, a user-level file, or add the file to .gitignore. If a key ever reaches a repository, revoke it and create a new one.
Try it: In your client, run this prompt to verify the integration:
Use FormaTeX to compile this LaTeX to PDF and give me the download link: \documentclass{article}\begin{document}Hello, FormaTeX!\end{document}A working setup shows a tool call such as compile_smart and a Download PDF link in the answer.
Client configuration reference
| Client | Where to configure | Notes |
|---|---|---|
| Claude (claude.ai, Claude Desktop) | Customize, Connectors, Add custom connector | URL https://mcp.formatex.io/mcp. Sign in with OAuth; the authorization page asks for your API key. claude_desktop_config.json is only for local stdio servers. |
| Claude Code | .mcp.json at the project root, or claude mcp add | HTTP entries need "type": "http". ${VAR} expansion works in .mcp.json. |
| Cursor | .cursor/mcp.json or ~/.cursor/mcp.json | Remote entries take url and headers. Use ${env:FORMATEX_API_KEY} for the key. |
| VS Code | .vscode/mcp.json | The top-level key is servers, not mcpServers. Remote entries need "type": "http". |
| Codex | ~/.codex/config.toml | [mcp_servers.formatex] with url and bearer_token_env_var = "FORMATEX_API_KEY", or command = "formatex-mcp" for the local binary. |
For Claude Code, the CLI registration for the hosted server is:
claude mcp add --transport http formatex https://mcp.formatex.io/mcp \
--header "Authorization: Bearer $FORMATEX_API_KEY"
For the local binary:
go install github.com/formatexio/mcp/cmd/formatex-mcp@latest
claude mcp add --env FORMATEX_API_KEY=fex_your_key --transport stdio formatex -- /path/to/formatex-mcp
Prebuilt binaries are on the releases page.
Authentication
- Hosted server: send your API key (it starts with
fex_) asAuthorization: Bearer <key>orX-API-Key: <key>. A request without a key is rejected with401. - Claude custom connectors: the connector signs in with OAuth. The authorization page asks for your API key.
- Local binary: the key comes from the
FORMATEX_API_KEYenvironment variable.
The hosted server speaks Streamable HTTP at /mcp. There is no /sse endpoint, so a client that supports only the older SSE transport should use the local binary. GET /health reports the server status.
Plans
- Free:
pdflatexandxelatex. - Pro and above: adds
lualatexandlatexmk, and saved templates (list_templates,get_template,create_template,update_template,compile_template).
Use from the Anthropic Messages API
The Messages API MCP connector can call the hosted server directly. It is a beta feature: send the mcp-client-2025-11-20 beta header, list the server under mcp_servers, and enable its tools with an mcp_toolset entry. The connector supports tool calls only, so resources and the fix_latex prompt are available in MCP clients rather than through the API.
import os
import anthropic
client = anthropic.Anthropic()
response = client.beta.messages.create(
model="claude-sonnet-5-5",
max_tokens=2048,
betas=["mcp-client-2025-11-20"],
mcp_servers=[
{
"type": "url",
"url": "https://mcp.formatex.io/mcp",
"name": "formatex",
"authorization_token": os.environ["FORMATEX_API_KEY"],
}
],
tools=[{"type": "mcp_toolset", "mcp_server_name": "formatex"}],
messages=[
{
"role": "user",
"content": "Write a one-page LaTeX note on gradient descent, compile it, and give me the PDF link.",
}
],
)
Troubleshooting
The tools do not appear in my client
Quit and reopen the client completely after changing its configuration. Check the file for JSON errors, and for a local server use the absolute path to formatex-mcp. In Claude, make sure the FormaTeX connector is turned on for the conversation.
I get 401 or an authentication error
The server needs your API key (it starts with fex_) in an Authorization: Bearer header or an X-API-Key header. If your configuration reads the key from an environment variable, make sure the variable is set in the environment the client starts in, then restart the client.
How do I get the PDF?
Compile tools return the compilation details and an authenticated Download PDF link, not the file itself. Open the link, or ask your agent to call fetch_pdf_bytes with the job ID when it needs the raw bytes to save or inspect the file.
lualatex or latexmk fails on the Free plan
The Free plan includes pdflatex and xelatex. lualatex and latexmk need Pro or higher. Ask for pdflatex or xelatex, or upgrade your plan.
A template tool returns a plan error
Saved templates are available on Pro and higher, up to a template limit per plan. Upgrade your plan, delete a template you no longer need in the dashboard, or send the LaTeX source with compile_latex or compile_smart instead.
Claude Desktop ignores the URL in claude_desktop_config.json
That file only configures local servers. Add the hosted endpoint as a custom connector in Customize > Connectors instead, using https://mcp.formatex.io/mcp.
How does signing in to the Claude connector work?
When you choose Sign in, Claude opens a FormaTeX page that asks for your API key. Paste the full key once and approve. If it fails, remove the connector and add it again, or use the Request headers option with Authorization: Bearer and your key.
Claude Code reports an invalid MCP configuration
HTTP servers must set "type": "http" in .mcp.json, and an entry with only a url is rejected. The project file is .mcp.json at the project root, not .claude/mcp.json.
VS Code does not show the server
VS Code reads the top-level "servers" key in .vscode/mcp.json, not "mcpServers". Rename the key, then run MCP: List Servers to confirm it loads.
My client asks for an SSE URL
FormaTeX has no SSE endpoint. Use Streamable HTTP at https://mcp.formatex.io/mcp. In Gemini CLI the field is httpUrl, in Cline set type to streamableHttp, and in n8n choose HTTP Streamable.
My key ended up in a repository
Revoke it on the API keys page and create a new one. Then use an environment variable or a user-level file so the key stays out of git.
CLI
Compile LaTeX documents, watch for changes, format source files, and convert between formats — all from the terminal, with no local TeX Live installation.
formatex compile thesis.tex
Installation
macOS / Linux
curl -fsSL https://raw.githubusercontent.com/formatexio/cli/main/install.sh | sh
macOS (Homebrew)
brew install formatexio/tap/formatex
Windows (PowerShell)
iwr https://raw.githubusercontent.com/formatexio/cli/main/install.ps1 | iex
Go
go install github.com/formatexio/cli@latest
Or download a pre-built binary from the releases page for Linux, macOS, or Windows (amd64 and arm64).
Authentication
formatex login
Enter your API key when prompted — it will be verified and saved to ~/.config/formatex/config.json. You can also set FORMATEX_API_KEY as an environment variable, or pass --api-key to any command.
Commands
formatex compile
Compile a .tex file or .zip project to PDF.
formatex compile thesis.tex
formatex compile thesis.tex --engine xelatex --output dist/thesis.pdf
formatex compile project.zip --main chapters/main.tex
| Flag | Default | Description |
|---|---|---|
--engine, -e | pdflatex | pdflatex, xelatex, lualatex, latexmk |
--output, -o | <input>.pdf | Output PDF path |
--runs | auto | Compiler passes (1–5) |
--timeout | plan default | Max compile time in seconds |
--main | auto-detected | Entry-point .tex inside a ZIP |
--open | false | Open PDF after compilation |
On failure, structured errors are printed with file, line number, and AI explanation (if enabled on your plan):
Compilation failed.
Errors:
main.tex:42: Undefined control sequence \foo
AI: The command \foo is not defined. You may have meant \footnote,
or add \newcommand{\foo}{...} to your preamble.
formatex watch
Watch a .tex file and recompile on every save.
formatex watch thesis.tex
formatex watch thesis.tex --engine xelatex --open
| Flag | Default | Description |
|---|---|---|
--engine, -e | pdflatex | LaTeX engine |
--output, -o | <input>.pdf | Output PDF path |
--runs | auto | Number of compiler passes |
--timeout | plan default | Max compile time in seconds |
--open | false | Open PDF on first successful compile |
Uses filesystem events (not polling) for instant detection. Debounced at 300ms to handle editor swap files.
formatex format
Format a LaTeX source file using latexindent.
formatex format main.tex # print to stdout
formatex format main.tex --write # overwrite in-place
formatex format main.tex -o out.tex
| Flag | Default | Description |
|---|---|---|
--write, -w | false | Overwrite the source file in-place |
--output, -o | — | Write formatted output to a different file |
formatex convert
Convert a LaTeX document to another format.
formatex convert thesis.tex # → thesis.docx
formatex convert thesis.tex --format html # → thesis.html
formatex convert thesis.tex --format odt
| Flag | Default | Description |
|---|---|---|
--format, -f | docx | docx, html, odt, epub, markdown, txt |
--output, -o | <input>.<ext> | Output file path |
Global flags
| Flag | Description |
|---|---|
--api-key | API key (overrides config and env var) |
--base-url | Override the API base URL |
CI / CD
Set FORMATEX_API_KEY as a secret and call the CLI directly:
- name: Compile LaTeX
run: formatex compile thesis.tex --output dist/thesis.pdf
env:
FORMATEX_API_KEY: ${{ secrets.FORMATEX_API_KEY }}
For a dedicated GitHub Actions integration with artifact upload, see the FormaTeX GitHub Action.
Python SDK
The official Python client for the FormaTeX API. Supports compilation, rendering, conversion, linting, PDF tools, async jobs, and more.
Installation
pip install formatex
Requires Python 3.9+.
Quick start
from formatex import FormaTexClient
client = FormaTexClient(api_key="fex_your_key_here")
result = client.compile(r"""
\documentclass{article}
\begin{document}
Hello, FormaTeX!
\end{document}
""")
with open("output.pdf", "wb") as f:
f.write(result.pdf)
FormaTexClient
FormaTexClient(
api_key: str, # required — get yours at formatex.io/dashboard/api-keys
*,
timeout: float = 120.0,
)
Use as a context manager to close the underlying HTTP session automatically:
with FormaTexClient(api_key="fex_...") as client:
result = client.compile(latex)
Compilation
compile
result = client.compile(
latex: str,
*,
engine: str = "pdflatex", # pdflatex | xelatex | lualatex | latexmk
runs: int = 0, # 0 = auto (1–5, capped to plan)
timeout: int = 0, # 0 = plan default
files: list[dict] | None = None, # companion files (images, .bib, etc.)
)
# → CompileResult(pdf: bytes, engine: str, duration_ms: int, size_bytes: int, job_id: str, log: str)
Attach companion files using the file_entry helper:
from formatex import FormaTexClient, file_entry
from pathlib import Path
result = client.compile(latex, files=[
file_entry("logo.png", Path("assets/logo.png").read_bytes()),
file_entry("refs.bib", Path("refs.bib").read_bytes()),
])
compile_smart
Automatic engine detection with an 18-step auto-fix pipeline. Pass engine="auto" or omit it.
result = client.compile_smart(latex, engine="auto")
compile_zip
Compile a multi-file project from a ZIP archive.
zip_bytes = Path("project.zip").read_bytes()
result = client.compile_zip(
zip_bytes,
main="chapters/main.tex", # optional — auto-detected if omitted
engine="xelatex",
)
compile_ipynb
Compile a Jupyter notebook to PDF.
nb_bytes = Path("notebook.ipynb").read_bytes()
result = client.compile_ipynb(nb_bytes, engine="pdflatex")
Markup sources
# Compile from Markdown
result = client.compile_markdown(markdown_str)
# Compile from HTML
result = client.compile_html(html_str)
# Compile from reStructuredText
result = client.compile_rst(rst_str)
Async compilation
For long-running documents, submit a job and poll for completion.
# Submit
job = client.async_compile(latex, engine="lualatex")
print(job.job_id, job.status) # pending
# Poll manually
result_info = client.get_job(job.job_id)
# Or wait with exponential back-off (recommended)
result = client.wait_for_job(job.job_id, poll_interval=2, max_wait=300)
pdf_bytes = result.pdf
Rendering
# Render a LaTeX equation to PNG/SVG
img = client.render_equation(
r"E = mc^2",
format="png", # png | svg
dpi=150,
display=True, # display mode (vs inline)
)
with open("eq.png", "wb") as f:
f.write(img.image)
# Batch render multiple equations
results = client.render_equations([
{"latex": r"E = mc^2", "format": "png"},
{"latex": r"\int_0^\infty e^{-x}\,dx", "format": "svg"},
])
Conversion
result = client.convert(
latex,
format="docx", # docx | html | odt | epub | markdown | txt
)
with open("thesis.docx", "wb") as f:
f.write(result.file)
Linting
lint = client.lint(latex)
print(lint.error_count, lint.warning_count)
for d in lint.diagnostics:
print(f"{d.line}:{d.column} [{d.severity}] {d.message}")
PDF tools
# Extract text from page 0
extracted = client.pdf_extract(pdf_bytes, page=0)
print(extracted.text)
# Render pages as images
pages = client.pdf_pages(pdf_bytes, dpi=150)
# Compress
compressed = client.pdf_compress(pdf_bytes)
# Merge
merged = client.pdf_merge([pdf_a, pdf_b, pdf_c])
# Split into individual pages
split = client.pdf_split(pdf_bytes)
# Convert to PDF/A
pdfa = client.pdf_pdfa(pdf_bytes)
Document analysis
# Word count
wc = client.word_count(latex)
# Extract \usepackage dependencies
deps = client.extract_dependencies(latex)
# Check which packages are available on the server
status = client.check_packages(["tikz", "pgfplots", "fontawesome5"])
# Extract title, author, date, etc.
meta = client.extract_metadata(latex)
# Analyse a .bib file
bib = client.analyse_bibliography(bib_str)
Error handling
from formatex.exceptions import FormaTexError, CompilationError, RateLimitError
try:
result = client.compile(latex)
except CompilationError as e:
print("LaTeX errors:", e.log)
except RateLimitError:
print("Rate limit hit — back off and retry")
except FormaTexError as e:
print("API error:", e)
Node.js SDK
The official Node.js client for the FormaTeX API. Fully typed with TypeScript. Supports compilation, rendering, conversion, linting, PDF tools, async jobs, and more.
Installation
npm install formatex-sdk
# or
pnpm add formatex-sdk
Requires Node.js 18+.
Quick start
import { FormaTexClient } from "formatex-sdk";
const client = new FormaTexClient("fex_your_key_here");
const result = await client.compile(`
\\documentclass{article}
\\begin{document}
Hello, FormaTeX!
\\end{document}
`);
await fs.writeFile("output.pdf", result.pdf);
new FormaTexClient(apiKey, options?)
const client = new FormaTexClient(
apiKey: string, // required
options?: FormaTexClientOptions, // { baseUrl?, timeout? }
);
Compilation
compile
const result = await client.compile(latex: string, options?: CompileOptions);
// CompileOptions: { engine?, runs?, timeout?, files? }
// Returns: CompileResult { pdf: Buffer, engine, durationMs, sizeBytes, jobId, log }
Attach companion files using the fileEntry helper:
import { FormaTexClient, fileEntry } from "formatex-sdk";
import { readFileSync } from "fs";
const result = await client.compile(latex, {
engine: "xelatex",
files: [
fileEntry("logo.png", readFileSync("assets/logo.png")),
fileEntry("refs.bib", readFileSync("refs.bib")),
],
});
compileSmart
Automatic engine detection with an 18-step auto-fix pipeline.
const result = await client.compileSmart(latex, { engine: "auto" });
compileZip
Compile a multi-file project from a ZIP archive.
const zip = readFileSync("project.zip");
const result = await client.compileZip(zip, {
main: "chapters/main.tex", // optional
engine: "xelatex",
});
compileIpynb
Compile a Jupyter notebook to PDF.
const nb = readFileSync("notebook.ipynb");
const result = await client.compileIpynb(nb);
Markup sources
// Compile from Markdown
const result = await client.compileMarkdown(markdownStr);
// Compile from HTML
const result = await client.compileHtml(htmlStr);
// Compile from reStructuredText
const result = await client.compileRst(rstStr);
Async compilation
// Submit
const job = await client.asyncCompile(latex, { engine: "lualatex" });
console.log(job.jobId, job.status); // pending
// Wait with exponential back-off (recommended)
const result = await client.waitForJob(job.jobId, { maxWait: 300_000 });
await fs.writeFile("output.pdf", result.pdf);
// Or poll manually
const status = await client.getJob(job.jobId);
const pdf = await client.getJobPdf(job.jobId);
Rendering
// Render a LaTeX equation
const img = await client.renderEquation(r`E = mc^2`, {
format: "png", // "png" | "svg"
dpi: 150,
display: true,
});
await fs.writeFile("eq.png", img.image);
// Batch render
const results = await client.renderEquations([
{ latex: r`E = mc^2`, format: "png" },
{ latex: r`\int_0^\infty e^{-x}\,dx`, format: "svg" },
]);
Conversion
const result = await client.convert(latex, { format: "docx" });
// format: "docx" | "html" | "odt" | "epub" | "markdown" | "txt"
await fs.writeFile("thesis.docx", result.file);
Linting
const lint = await client.lint(latex);
console.log(lint.errorCount, lint.warningCount);
for (const d of lint.diagnostics) {
console.log(`${d.line}:${d.column} [${d.severity}] ${d.message}`);
}
PDF tools
// Extract text from a page
const extracted = await client.pdfExtract(pdfBuffer, 0);
console.log(extracted.text);
// Render pages as images
const pages = await client.pdfPages(pdfBuffer, { dpi: 150 });
// Compress
const compressed = await client.pdfCompress(pdfBuffer);
// Merge
const merged = await client.pdfMerge([pdfA, pdfB, pdfC]);
// Split into individual pages
const split = await client.pdfSplit(pdfBuffer);
// Convert to PDF/A
const pdfa = await client.pdfPDFA(pdfBuffer);
Document analysis
// Word count
const wc = await client.wordCount(latex);
// Extract \usepackage dependencies
const deps = await client.extractDependencies(latex);
// Check which packages are available
const status = await client.checkPackages(["tikz", "pgfplots", "fontawesome5"]);
// Extract title, author, date, etc.
const meta = await client.extractMetadata(latex);
// Analyse a .bib file
const bib = await client.analyzeBibliography(bibStr);
Error handling
import {
FormaTexError,
CompilationError,
RateLimitError,
AuthenticationError,
} from "formatex-sdk";
try {
const result = await client.compile(latex);
} catch (e) {
if (e instanceof CompilationError) {
console.error("LaTeX errors:", e.log);
} else if (e instanceof RateLimitError) {
console.error("Rate limited — back off and retry");
} else if (e instanceof AuthenticationError) {
console.error("Invalid API key");
} else if (e instanceof FormaTexError) {
console.error("API error:", e.message);
}
}
GitHub Actions
Two ways to compile LaTeX in your GitHub Actions workflows: the dedicated FormaTeX Action, or the CLI directly.
FormaTeX Action
The simplest integration. Compiles your document and uploads the PDF as a build artifact automatically.
name: Compile LaTeX
on:
push:
branches: [main]
pull_request:
jobs:
compile:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: formatexio/action@v1
with:
file: thesis.tex
engine: pdflatex # optional — default: pdflatex
output: dist/thesis.pdf # optional — default: <file>.pdf
env:
FORMATEX_API_KEY: ${{ secrets.FORMATEX_API_KEY }}
- uses: actions/upload-artifact@v4
with:
name: thesis-pdf
path: dist/thesis.pdf
Set FORMATEX_API_KEY as a repository secret under Settings → Secrets and variables → Actions.
CLI in CI
Install the CLI in your workflow and call it directly for more control.
name: Compile LaTeX
on:
push:
branches: [main]
jobs:
compile:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install FormaTeX CLI
run: curl -fsSL https://raw.githubusercontent.com/formatexio/cli/main/install.sh | sh
- name: Compile
run: formatex compile thesis.tex --output dist/thesis.pdf
env:
FORMATEX_API_KEY: ${{ secrets.FORMATEX_API_KEY }}
- uses: actions/upload-artifact@v4
with:
name: thesis-pdf
path: dist/thesis.pdf
Multi-file projects
Pass a ZIP archive when your document spans multiple files:
- name: Archive project
run: zip -r project.zip . -x "*.git*"
- name: Compile
run: formatex compile project.zip --main chapters/main.tex --output dist/thesis.pdf
env:
FORMATEX_API_KEY: ${{ secrets.FORMATEX_API_KEY }}
PR preview comments
Post a link to the compiled PDF as a pull request comment:
- name: Compile
id: compile
run: formatex compile thesis.tex --output dist/thesis.pdf
env:
FORMATEX_API_KEY: ${{ secrets.FORMATEX_API_KEY }}
- uses: actions/upload-artifact@v4
id: upload
with:
name: thesis-pdf
path: dist/thesis.pdf
- name: Comment on PR
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `PDF compiled successfully. [Download artifact](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})`,
})
Matrix builds (multiple engines)
strategy:
matrix:
engine: [pdflatex, xelatex, lualatex]
steps:
- uses: actions/checkout@v4
- name: Compile with ${{ matrix.engine }}
run: formatex compile thesis.tex --engine ${{ matrix.engine }} --output dist/thesis-${{ matrix.engine }}.pdf
env:
FORMATEX_API_KEY: ${{ secrets.FORMATEX_API_KEY }}

