Reference
HTTP API
One endpoint prints a published template with your data: one document per call, or up to 500 at once. It is meant for servers and scripts; agents use the MCP server.
Basics
| Base URL | https://composa.dev |
| Endpoint | POST /api/v1/run |
| Authentication | Authorization: Bearer cmp_live_… |
| Request body | Content-Type: application/json |
| Time | A call answers within 60 seconds; set your client's timeout to match. |
API keys
Create keys in Press Room → API keys. A key belongs to your team: it prints every template the team owns, and nothing else. It is shown once — Composa keeps only a hash of it, so a lost key cannot be recovered, only replaced. Revoke a key on the same page; within a minute every call with it answers 401.
Keys are for this API only. Keep them on a server: never in a web page, a mobile app or a repository.
POST /api/v1/run
The mode follows the body: data prints one document and answers with its bytes; items prints one document per item and answers with JSON.
| Field | Type | Notes |
|---|---|---|
template_id | string (uuid), required | The project's id: its editor link is /typeset/<id>. |
format | "pdf" | "png" | "svg" | Default pdf. Case does not matter. |
data | object | Variable name → value. Ignored when items is present. |
items | { name?, data?, language? }[] | One document per item, 1 to 500 (your plan's figure on Limits). |
language | string | One of the project's languages, e.g. pt-BR. Omitted: its default. |
version | integer | A published version to print. Omitted: the active one. |
One document
curl -X POST https://composa.dev/api/v1/run \
-H "Authorization: Bearer $COMPOSA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"template_id": "YOUR_PROJECT_ID",
"data": { "customer_name": "Alice Moreau", "course": "Typography I" }
}' \
--fail-with-body --output certificate.pdfThe answer is the file itself:
| Header | Value |
|---|---|
Content-Type | application/pdf, image/png or image/svg+xml — or application/zip for a PNG or SVG of several pages (page-1.png, page-2.png, …). A PDF is always one file. |
Content-Disposition | attachment; filename="<project title>.pdf" |
Content-Language | The language it printed in, when the project declares one. |
Composa-Missing-Texts | How many texts printed blank in that language (see Languages). Absent when none did. |
Many documents
{
"template_id": "YOUR_PROJECT_ID",
"format": "pdf",
"items": [
{ "name": "alice", "data": { "customer_name": "Alice Moreau" } },
{ "name": "bruno", "data": { "customer_name": "Bruno Lima" }, "language": "pt-BR" }
]
}{
"template_id": "…",
"template_title": "Certificate",
"format": "pdf",
"item_count": 2,
"success_count": 1,
"error_count": 1,
"total_ms": 132,
"items": [
{ "index": 0, "name": "alice", "content_type": "application/pdf", "output_base64": "JVBERi0xLjcK…" },
{ "index": 1, "name": "bruno", "error": "This version has no language \"pt-BR\". It is in one language; leave out language." }
]
}- The call answers
200even when items fail: checkerror_countand each item’serror. Items keep their order and carry theirindex. - An item may set its own
language; it wins over the request’s. It then carrieslanguageand, when texts printed blank,missing_texts. - Ask for compression (
Accept-Encoding: gzip,curl --compressed): copies of one template compress very well.
import { writeFile } from "node:fs/promises";
// items: [{ name: "alice", data: { … } }, …]: up to 500
const res = await fetch("https://composa.dev/api/v1/run", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.COMPOSA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ template_id: "YOUR_PROJECT_ID", items }),
});
const out = await res.json();
for (const item of out.items) {
if (item.error) console.error(item.index, item.name, item.error);
else await writeFile(`${item.name ?? item.index}.pdf`, Buffer.from(item.output_base64, "base64"));
}Variables
A template’s variables are the parts that change from one copy to the next — {{customer_name}}in a text, a table’s rows, a chart’s series, a picture. data maps each name to its value. A name the template does not have is ignored; a name you leave out prints the sample it was designed with.
Text and numbers
Any value is printed as text: 42 prints 42, null prints nothing. Names are letters, digits and underscores, and do not start with a digit.
Lists: tables, repeated blocks, charts
A list takes an array of rows, or CSV:
"items": [
{ "product": "Paper", "qty": 3, "price": "12.50" },
{ "product": "Ink", "qty": 1, "price": "48.00" }
]
"items": { "csv": "product,qty,price\nPaper,3,12.50\nInk,1,48.00" }- A CSV can also be sent as a bare string, for a variable that is a list. Up to 5 MB.
- Column names are matched in letters, digits and underscores; every value is printed as text.
- CSV is read from the request only — a URL is refused. Fetch the file and send its text.
Pictures
A picture frame bound to a variable takes its picture in data, as a base64 data URL. The frame’s place, size and shape stay the template’s; the picture is fitted into it.
"data": { "customer_name": "Alice Moreau", "chart": "data:image/png;base64,iVBORw0KGgo…" }- PNG, JPEG, GIF or SVG. Not WebP.
- Up to 5 MB a picture, and 20 MB of pictures per request, counting every item.
- Only data URLs are read: an
https://value is an error, not skipped. - Send nothing and the frame prints the template’s own picture (or a grey placeholder if it has none).
- A bound frame prints opaque, whatever its opacity in the editor.
Versions
The API prints a published version, never the draft being edited: changes reach your integration when someone presses Publish in the editor, or an agent calls publish. A project never published answers 409.
Without version, the call prints the active version — the one the editor marks as served, which a person can roll back. Pinning a version keeps your integration on that one until you change it; it needs a plan that allows it, otherwise 403.
Languages
A project can hold its texts in several languages — each language’s texts are its own, not translations kept in step. language picks the one to print.
- The tag must match one of the project’s exactly (case aside):
ptdoes not stand forpt-BR. Anything else is422, and the message lists the languages there are. - A text with no version of its own in a language prints blank there, never in another language. A version can only be published that way after the publisher confirmed it; the response then says how many (
Composa-Missing-Texts). - What you send in
dataalways prints as sent.
PDF properties
The title, author, keywords, creator and producer a PDF reader shows belong to the template, set once on the project’s settings in the Press Room. Any of them may contain {{variable}} — a title of Invoice {{number}}gives each document its own. A blank title prints the project’s name; creator and producer default to Composa Design. PNG and SVG carry none of them.
Errors
An error answers with a status and a JSON body, { "error": "…" }, worded for a person. In a batch, what concerns one item — its data, its language, its printing — is that item’s error and the call still answers 200.
| Status | When | Message |
|---|---|---|
| 400 | The request cannot be read | Invalid JSON body · template_id is required · Unsupported format "x". Supported: pdf, svg, png · items must be a non-empty array · a CSV or a picture that cannot be read, naming the variable |
| 401 | No key, or not a valid one | Missing or malformed API key · Invalid or revoked API key |
| 403 | version on a plan that prints the active version only | Your plan renders the active version only; remove `version` or upgrade. |
| 404 | Not a project of your team, or no such version | Template not found · Version 7 not found |
| 409 | Nothing published to print | This project has not been published yet — publish it in the editor. Rarely: publish again, as the message says. |
| 413 | Too much at once | items limited to 500 per call · This request is 5.2 MB; your plan accepts up to 4 MB per request. |
| 422 | The document cannot be printed as asked | a language the project does not have · This document could not be printed. · This document took longer than 10 s to compile. · Your plan renders documents of up to 20 pages; this one has 31. |
| 429 | Too many requests a minute for this key | Too many requests: this API key may make 120 a minute on its plan. Try again in 16 s. Wait the Retry-After seconds, then send it again. |
| 5xx | Our side | Retry with backoff. The body may not be JSON. |