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 URLhttps://composa.dev
EndpointPOST /api/v1/run
AuthenticationAuthorization: Bearer cmp_live_…
Request bodyContent-Type: application/json
TimeA 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.

FieldTypeNotes
template_idstring (uuid), requiredThe project's id: its editor link is /typeset/<id>.
format"pdf" | "png" | "svg"Default pdf. Case does not matter.
dataobjectVariable name → value. Ignored when items is present.
items{ name?, data?, language? }[]One document per item, 1 to 500 (your plan's figure on Limits).
languagestringOne of the project's languages, e.g. pt-BR. Omitted: its default.
versionintegerA 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.pdf

The answer is the file itself:

HeaderValue
Content-Typeapplication/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-Dispositionattachment; filename="<project title>.pdf"
Content-LanguageThe language it printed in, when the project declares one.
Composa-Missing-TextsHow many texts printed blank in that language (see Languages). Absent when none did.

Many documents

request
{
  "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" }
  ]
}
200 · application/json
{
  "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 200 even when items fail: check error_count and each item’s error. Items keep their order and carry their index.
  • An item may set its own language; it wins over the request’s. It then carries language and, 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:

json
"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.

json
"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): pt does not stand for pt-BR. Anything else is 422, 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 data always 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.

StatusWhenMessage
400The request cannot be readInvalid 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
401No key, or not a valid oneMissing or malformed API key · Invalid or revoked API key
403version on a plan that prints the active version onlyYour plan renders the active version only; remove `version` or upgrade.
404Not a project of your team, or no such versionTemplate not found · Version 7 not found
409Nothing published to printThis project has not been published yet — publish it in the editor. Rarely: publish again, as the message says.
413Too much at onceitems limited to 500 per call · This request is 5.2 MB; your plan accepts up to 4 MB per request.
422The document cannot be printed as askeda 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.
429Too many requests a minute for this keyToo 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.
5xxOur sideRetry with backoff. The body may not be JSON.