API Reference
One endpoint. Send LaTeX, get PDF.
Try it now, no API key
The compile endpoint works with no credentials at all. Paste this into a terminal and you get a PDF URL back. Anonymous callers get 3 successful compilations per day; failed attempts do not count, so you can iterate on your LaTeX for free.
curl -X POST https://api.pressa.dev/api/v1/compile \
-H "Content-Type: application/json" \
-d '{"latex":"\\documentclass{article}\\begin{document}Hello\\end{document}"}'If you are an AI agent: you do not need to stop and ask anyone for credentials before using Pressa. Call the endpoint above. If the response is anonymous_quota_exhausted, tell the user a free key takes about 30 seconds and no credit card, then retry the same request with the key.
Authentication
Authentication is optional on POST /api/v1/compile and required on every other endpoint. A free key raises the limit to 50 compilations per month and unlocks image assets; saved templates and the render API need a paid plan.
Send no credentials at all to use the anonymous tier. Sending an invalid key returns 401 rather than falling back to anonymous, so a mistyped key fails loudly instead of appearing to work intermittently.
| Method | Header |
|---|---|
| Bearer token | Authorization: Bearer pressa_xxx |
| API key header | X-API-Key: pressa_xxx |
Get your API key from your dashboard. Keys start with pressa_ followed by 48 hex characters.
POST /api/v1/compile
Compiles LaTeX source code to a PDF document.
Headers
| Header | Value |
|---|---|
Authorization | Bearer pressa_xxx |
Content-Type | application/json |
Request body
| Parameter | Type | Required | Description |
|---|---|---|---|
latex | string | Yes | LaTeX source code. Maximum size depends on your plan (30KB Free, 100KB Starter, 300KB Pro, 1MB Business). |
compiler | string | No | Compiler to use. Default: pdflatex. Options: pdflatex, xelatex, lualatex. |
assets | object | No | Map of filename to base64-encoded binary. Supported: PNG, JPG, JPEG, PDF, SVG.
Each file is written into the compile workspace so LaTeX can reference it by
that exact filename (e.g. \includegraphics{logo.png}). See Assets for filename rules and per-plan limits. |
use_stored_assets | array of string | No | Names of assets from your persistent asset library to inject into the compile workspace. Saves re-uploading the same bytes each
compile. Works alongside assets. Response echoes the resolved names in stored_assets_used. See Assets endpoints for upload/list/get/delete. |
Assets (images & binary files)
To include logos, photos, signatures, diagrams, or embedded PDFs in your document,
pass them in the assets object alongside your LaTeX source. Each asset is decoded from base64 and written
to the compile workspace before compilation, so LaTeX commands like \includegraphics{logo.png} resolve normally. You do not need to upload files separately or embed them inline
in the LaTeX - one request carries everything.
Typical use case: an AI agent is asked to replicate a PDF that contains a company
logo. The agent extracts the logo as PNG, base64-encodes it, and passes it via assets while referencing \includegraphics{logo.png} in the generated LaTeX. The compiled PDF contains the real logo, not a placeholder.
Rules
- Supported extensions:
png,jpg,jpeg,pdf,svg. Magic bytes are verified server-side so a renamed file will be rejected. - Filenames must be plain: alphanumeric plus
_,-, and a single extension. No paths, no leading dot, no... Max 64 characters. - Sizes below are for the DECODED payload (the base64 transport adds about 33% to the wire size but does not count toward the limit).
Per-plan limits
| Limit | Free | Starter | Pro | Business |
|---|---|---|---|---|
| Assets per document | 2 | 5 | 20 | 50 |
| Total decoded size | 1 MB | 5 MB | 25 MB | 75 MB |
Example request with an image
{
"latex": "\\documentclass{article}\\usepackage{graphicx}\\begin{document}\\includegraphics{logo.png}\\end{document}",
"compiler": "pdflatex",
"assets": {
"logo.png": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB..."
}
}Limits
Every compile request is validated against your plan's per-document limits before and after compilation. Rejected compilations do not count toward your monthly quota.
| Limit | Free | Starter | Pro | Business |
|---|---|---|---|---|
| Pages per document | 5 | 20 | 100 | 500 |
| LaTeX source size | 30 KB | 100 KB | 300 KB | 1 MB |
| PDF output size | 10 MB | 25 MB | 50 MB | 100 MB |
| Assets per document | 2 | 5 | 20 | 50 |
| Assets total size | 1 MB | 5 MB | 25 MB | 75 MB |
| Compile timeout | 15s | 30s | 60s | 120s |
| Compilers | pdflatex, xelatex | pdflatex, xelatex | all three | all three |
The GET /api/v1/usage endpoint returns a limits block with the exact values for the authenticated API key so you can
validate input client-side before sending.
Example request
curl -X POST https://api.pressa.dev/api/v1/compile \
-H "Authorization: Bearer pressa_xxx" \
-H "Content-Type: application/json" \
-d '{"latex": "\\documentclass{article}\\begin{document}Hello World\\end{document}", "compiler": "pdflatex"}'Success response
200 OK
{
"pdf_url": "https://api.pressa.dev/api/v1/pdfs/abc123?sig=xxx&exp=xxx",
"job_id": "abc123",
"pages": 1,
"compilation_time_ms": 1250,
"expires_at": "2026-04-04T12:00:00Z",
"ephemeral_assets_used": [],
"stored_assets_used": [],
"usage": {
"monthly_limit": 50,
"used_this_month": 1,
"resets_at": "2026-05-01T00:00:00Z"
}
}Error responses
Missing or invalid API key.
LaTeX compilation failed. Response includes the compiler log for debugging.
{
"error": "compilation_failed",
"message": "LaTeX compilation failed",
"log": "! Undefined control sequence..."
}Monthly compilation limit reached. Upgrade your plan for more compilations.
GET /api/v1/usage
Returns your current plan and usage statistics. Requires authentication.
Response
200 OK
{
"user": {
"email": "[email protected]",
"plan": "free"
},
"usage": {
"monthly_limit": 50,
"used_this_month": 3,
"resets_at": "2026-05-01T00:00:00Z"
},
"stored_assets": {
"count": 0,
"total_bytes": 0,
"count_limit": 0,
"total_bytes_limit": 0,
"remaining_bytes": 0
},
"templates": {
"count": 0,
"limit": 0,
"remaining": 0
},
"api_key": {
"prefix": "pressa_a1b2...",
"created_at": "2026-03-15T10:00:00Z"
},
"limits": {
"max_pages_per_document": 5,
"max_latex_bytes_per_document": 30720,
"max_pdf_bytes_per_document": 10485760,
"max_assets_total_bytes_per_document": 1048576,
"max_assets_count_per_document": 2,
"max_stored_assets_count": 0,
"max_stored_assets_total_bytes": 0,
"compile_timeout_seconds": 15,
"allowed_compilers": ["pdflatex", "xelatex"]
}
}GET /api/v1/pdfs/:id
Download a compiled PDF via its signed URL. No authentication required - the URL itself contains the signature.
Query parameters
| Parameter | Type | Description |
|---|---|---|
sig | string | HMAC signature for URL verification. |
exp | string | Expiration timestamp. URLs expire after 24 hours. |
Returns the PDF file directly as application/pdf. Use the pdf_url from the compile response - it already includes the signature and expiration parameters.
Templates
Save and reuse LaTeX templates. Available on paid plans only - free plan users receive a 403 response.
Each template has a required latex_content (the document layout) and two optional fields that mean different things. description is a short human-readable summary surfaced in GET /api/v1/templates for UI lists. instructions is a longer prose markdown playbook (up to 50000 chars) that an AI agent reads alongside the LaTeX when filling the template - defaults, workflow rules, edge cases, conditional logic. Use description for humans, use instructions for agents. They are independent fields.
POST /api/v1/templates
Save a new template or update an existing one. If a template with the same name already exists, it gets updated (upsert).
Request body
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Template name. Max 100 characters. |
latex_content | string | Yes | LaTeX source code. Maximum size depends on your plan (30KB Free, 100KB Starter, 300KB Pro, 1MB Business). |
description | string | No | Short human-readable summary. Max 500 characters. |
instructions | string | No | Agent playbook (prose markdown). Describes how to fill the template - defaults, workflow rules, edge cases, conditional logic. Max 50000 characters. Surfaced verbatim in GET /api/v1/templates/:id so AI agents can apply the rules when generating a document. |
Response
201 Created (new) or 200 OK (updated)
{
"template": {
"id": 1,
"name": "invoice",
"description": "Standard invoice template",
"latex_content": "\\documentclass{article}...",
"instructions": "Ask the user only for total amount...",
"updated_at": "2026-04-13T10:00:00Z",
"latex_size_bytes": 2048
},
"created": true
}GET /api/v1/templates
List all your saved templates. Does not include latex_content or instructions in the response - both can be large. Each row carries a has_instructions boolean so clients can decide whether a follow-up GET /api/v1/templates/:id will return a useful playbook.
Response
200 OK
{
"templates": [
{
"id": 1,
"name": "invoice",
"description": "Standard invoice template",
"updated_at": "2026-04-13T10:00:00Z",
"latex_size_bytes": 2048,
"has_instructions": true
}
]
}GET /api/v1/templates/:id
Get a single template by numeric ID or URL-encoded name. Returns the full latex_content along with instructions (or null if the template has none) so an AI agent receives layout and playbook in a single round trip.
Response
200 OK
{
"template": {
"id": 1,
"name": "invoice",
"description": "Standard invoice template",
"latex_content": "\\documentclass{article}...",
"instructions": "Ask the user only for total amount. Date is today's date. Invoice number format YYYYMMDD-N where N is the sequential count of invoices issued this calendar year.",
"updated_at": "2026-04-13T10:00:00Z",
"latex_size_bytes": 2048
}
}DELETE /api/v1/templates/:id
Delete a template by numeric ID or URL-encoded name.
Response
204 No Content
Error responses
Templates are not available on the free plan. Upgrade to use this feature.
Template with the given ID or name does not exist.
Assets endpoints
Store binary files (logos, signatures, diagrams, embedded PDFs) in a persistent,
user-scoped library and reuse them across many compiles and templates. Upload a
logo once, then reference it in every future compile via use_stored_assets. Paid plans only - free plan users receive a 403 response with error plan_required.
Compare: the assets field on POST /api/v1/compile carries bytes inline for a single compile (ephemeral, discarded after). The
asset library is the opposite - upload once, persist, reuse by name. Both are
valid; pick whichever fits your workflow.
Per-plan limits
These caps govern your persistent library total. They are independent of the
per-compile ephemeral asset limits, but when a compile merges ephemeral assets with use_stored_assets, the combined count and bytes are checked against the per-compile limits.
| Limit | Free | Starter | Pro | Business |
|---|---|---|---|---|
| Max stored assets | 0 | 10 | 50 | unlimited |
| Max total size | 0 | 50 MB | 500 MB | 5 GB |
POST /api/v1/assets
Upload a new asset or update an existing one. Upsert by name - if an asset with the same name exists for this user, its bytes are replaced.
Same validation rules as the compile assets field (filename whitelist, extension whitelist, magic-bytes match).
Request body
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Filename used to reference the asset from LaTeX (e.g. logo.png). Same rules as ephemeral assets - alphanumeric plus _, -, single extension, max 64 chars. |
content_base64 | string | Yes | Base64-encoded file bytes. Supported formats: PNG, JPG, JPEG, PDF, SVG. Magic bytes are verified server-side. |
content_type | string | No | MIME type. Inferred from the filename extension if omitted. |
Response
201 Created (new) or 200 OK (updated)
{
"asset": {
"id": 42,
"name": "logo.png",
"content_type": "image/png",
"size_bytes": 18450,
"sha256": "3fe4a5...",
"updated_at": "2026-04-21T16:05:00Z"
},
"created": true
}GET /api/v1/assets
List all your stored assets. Does not include the base64 bytes. Includes quota rollup so clients can render progress bars without extra round trips.
Response
200 OK
{
"assets": [
{
"id": 42,
"name": "logo.png",
"content_type": "image/png",
"size_bytes": 18450,
"sha256": "3fe4a5...",
"updated_at": "2026-04-21T16:05:00Z"
}
],
"count": 1,
"total_bytes": 18450,
"count_limit": 10,
"total_bytes_limit": 52428800,
"remaining_bytes": 52410350
}count_limit is null when the plan grants unlimited assets (Business).
GET /api/v1/assets/:id_or_name
Get a single asset by numeric ID or URL-encoded name (case-insensitive). Returns metadata plus the full base64-encoded content. Numeric path segments lookup by id; anything non-numeric is treated as a name.
Response
200 OK
{
"asset": {
"id": 42,
"name": "logo.png",
"content_type": "image/png",
"size_bytes": 18450,
"sha256": "3fe4a5...",
"updated_at": "2026-04-21T16:05:00Z",
"content_base64": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB..."
}
}DELETE /api/v1/assets/:id_or_name
Delete an asset by numeric ID or URL-encoded name. Removes the database row and
the underlying bytes. Compiles that referenced the name via use_stored_assets after deletion will fail with asset_not_found.
Response
204 No Content
V2 Templates & Render API
V2 templates separate a document's layout from its data. The LaTeX source contains Liquid placeholders like {{ customer_name }}, and Pressa derives a JSON schema and sample data from them automatically on save.
Once a template is saved, the render endpoint fills it with fresh values and returns
a PDF in a single call - no LaTeX in the request, just data.
V2 endpoints require a paid plan (Starter or above). Free-plan keys receive plan_required (403).
POST /api/v2/templates
Creates a V2 template. The source is validated against a 14-rule LaTeX security
policy (shell escape, absolute-path input, unsafe packages, and similar are
rejected with template_security_violation). Names are unique per account: a duplicate name returns name_taken (422) instead of overwriting.
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Template name, unique per account (case-insensitive). |
latex_with_placeholders | string | Yes | Full LaTeX source containing Liquid placeholders. |
description | string | No | Human-readable description. |
The 201 response includes the derived schema (JSON Schema with required fields and types inferred from filter usage) and sample_data (realistic placeholder values for previews). Standard CRUD is available: GET /api/v2/templates lists, GET /api/v2/templates/:id fetches one (by numeric ID or case-insensitive name), PATCH updates (changing the source re-derives the schema and bumps version), and DELETE removes the template.
POST /api/v2/templates/:id/render
Renders a saved V2 template with your data and returns a finished PDF. accepts the numeric ID or the case-insensitive template name. The request body
carries a single data object whose keys match the template schema; it is validated server-side before
anything compiles, so structural mistakes come back as actionable errors with missing_fields and type_errors lists. Extra keys are ignored.
The compiler is selected automatically from the rendered source: templates using fontspec compile with xelatex, templates using \directlua with lualatex, everything else with pdflatex. Stored assets referenced via \includegraphics{name} are resolved from your asset library automatically - no use_stored_assets field needed.
Every user-supplied value is passed through latex_escape automatically, so characters like % & $ # _ are safe by default. Template
authors can opt out per value with the raw filter, and format values with currency, date and asset filters.
Example request
curl -X POST https://api.pressa.dev/api/v2/templates/invoice/render \
-H "Authorization: Bearer pressa_xxx" \
-H "Content-Type: application/json" \
-d '{"data": {"customer_name": "Acme GmbH", "invoice_number": "2026-041", "amount": 1250.0}}'Success response
200 OK
{
"job_id": "abc123",
"pdf_url": "https://api.pressa.dev/api/v1/pdfs/abc123?sig=xxx&exp=xxx",
"expires_at": "2026-06-10T12:00:00Z",
"pages": 1,
"render_time_ms": 940,
"compilation_time_ms": 660,
"template": { "id": 12, "name": "invoice", "version": 3 },
"usage": { "plan": "pro", "compilations_this_month": 14, "monthly_limit": 2000 }
}Render error responses
| Status | Error code | Meaning |
|---|---|---|
| 403 | plan_required | Render API requires a paid plan. |
| 403 | compiler_not_available | The template needs a compiler (e.g. lualatex) not included in your plan. |
| 404 | template_not_found | No template with that ID or name in your library. |
| 422 | template_engine_mismatch | Template is V1 (raw LaTeX, no placeholders). Use the compile endpoint instead. |
| 422 | invalid_data_shape | The data parameter must be a JSON object. |
| 422 | schema_validation_failed | Data does not match the template schema. Includes missing_fields and type_errors. |
| 422 | render_parse_failed | Liquid syntax error in the template source. |
| 422 | render_failed | Undefined variable or filter, or a render resource limit was hit. |
| 422 | render_asset_not_found | The template references an asset name that is not in your library. |
| 422 | compilation_failed | The rendered LaTeX failed to compile. Includes the compiler log. |
| 429 | rate_limit | Monthly compilation cap reached. Renders count toward the same cap as compiles. |
| 504 | render_total_timeout | The render exceeded the 60-second wall clock. Simplify the template or data. |
Rate Limits
| Plan | Compilations | Saved Templates | Price |
|---|---|---|---|
| Free | 50 per month | 0 | $0 |
| Starter | 500 per month | 10 | $9/month |
| Pro | 2,000 per month | 50 | $29/month |
| Business | 10,000 per month | Unlimited | $99/month |
When you exceed your monthly limit, the API returns a 429 response with details about your usage and when the limit resets.
{
"error": "rate_limit_exceeded",
"monthly_limit": 50,
"used_this_month": 50,
"resets_at": "2026-05-01T00:00:00Z"
}Plan limit errors
In addition to monthly rate limits, every compile is validated against your
plan's per-document limits. These errors include a message, the requested vs allowed values, your current plan, and an upgrade URL.
Rejected attempts do not count toward your monthly quota.
| Error code | HTTP | When it fires |
|---|---|---|
compiler_not_available | 403 | Requested a compiler that is not available on your plan (e.g. lualatex on Free). |
latex_too_large | 413 | LaTeX source exceeds 300 KB, or the plan limit for an anonymous caller. Rejected before compile. With an API key, a source over your plan's limit but under 300 KB is compiled and delivered as a preview instead (see below). |
page_limit_exceeded | 422 | The compiled PDF has more pages than allowed and is not delivered. With an API key this is normally replaced by a 200 preview of the first pages your plan allows; you still get the 422 when the preview is switched off or could not be produced, so handle both. |
pdf_too_large | 422 | Compiled PDF exceeds your plan's PDF size limit (or, for a whole document checked before a preview slice, the compile ceiling; the body then carries both numbers). |
preview_in_progress | 429 | A preview of a larger document is already compiling for this account; one runs at a time. Retry after it finishes. |
preview_unavailable | 503 | Size previews are briefly refused because the store that limits them to one per account is not answering. Retry in a minute; a source within your plan's size limit still compiles. |
pdf_size_unknown | 422 | The size of the compiled PDF could not be measured, so it was not delivered. Transient; retry the same request. |
not_latex_source | 422 | Submitted content is not LaTeX (no \documentclass, \begin{document}, \input{}, or \include{} marker found). Response includes a requirements list and a ready-to-use example_template so an AI agent can self-correct without bouncing back to the user. |
too_many_assets | 422 | Number of assets exceeds your plan's limit (Free 2, Starter 5, Pro 20, Business 50). |
assets_too_large | 413 | Decoded assets total exceeds your plan's size limit. |
assets_not_allowed | 403 | Your plan's asset count is 0 and at least one asset was supplied. |
invalid_asset_filename | 422 | Filename contains a path separator, leading dot, .., null byte, or is longer than 64 characters. |
invalid_asset_format | 422 | Extension is outside png|jpg|jpeg|pdf|svg, or the decoded bytes do not match the declared format's magic bytes. |
invalid_asset_encoding | 422 | Base64 string is malformed or decodes to zero bytes. |
plan_required | 403 | Free plan tried to create a stored asset via POST /api/v1/assets. Upgrade to a paid plan to use the asset library. |
asset_limit_reached | 403 | User hit the per-plan count cap for stored assets (Starter 10, Pro 50). Delete an existing asset or upgrade. |
storage_quota_exceeded | 413 | Upload would push the user past their total bytes cap for stored assets (Starter 50 MB, Pro 500 MB, Business 5 GB). |
asset_not_found | 422 | use_stored_assets referenced a name that does not exist in the user's library. Response
includes the missing name so an agent can self-correct. |
asset_name_collision | 422 | The same filename appeared in both assets and use_stored_assets. Pick one source to avoid ambiguity. |
invalid_stored_assets_shape | 422 | use_stored_assets must be an array of strings (filenames). Any other shape is rejected. |
asset_storage_error | 500 | Internal error - database row present but underlying binary missing. Should not happen in practice; surfaces as a safeguard. |
Pressa is a LaTeX compiler, not a text-to-PDF converter. If your input is plain text, markdown, JSON, or notes, generate the LaTeX yourself before submitting.
{
"error": "page_limit_exceeded",
"message": "Your document has 23 pages, but your anonymous plan allows max 3 pages per document.",
"pages": 23,
"limit": 3,
"plan": "anonymous",
"upgrade_url": "https://pressa.dev/pricing"
}With an API key, a document past your plan's page or source-size limit is not rejected. It is compiled, and you receive the first pages your plan allows together with the page count of the whole document and the smallest plan that covers it. The PDF at pdf_url is exactly those pages. Agents: show the user the preview and relay warning; do not silently drop pages.
{
"job_id": "b7c1...",
"pdf_url": "https://pressa.dev/api/v1/pdfs/b7c1...?sig=...&exp=...",
"pages": 5,
"total_pages": 38,
"preview": true,
"truncated": true,
"plan_required": "pro",
"warning": "Your document has 38 pages and 160 KB of source. The free plan delivers the first 5 pages; the whole document needs the pro plan. Upgrade at https://pressa.dev/pricing.",
"upgrade_url": "https://pressa.dev/pricing",
"usage": { "plan": "free", "compilations_this_month": 3, "monthly_limit": 50 }
}