Create PDF from Template
Send a POST request to this endpoint to render one of your templates into a PDF document, or to
build one PDF from many documents with pages.
You will need to authenticate to use this API. See authentication.
This endpoint consumes 1 credit per 20 pages, rounded up: 1 to 20 pages cost 1 credit, 21 to 40 cost 2, up to 5 credits for 100 pages. At the start of the render, the credits for its documents are spent, each document printing at least one page; the rest follows once the pages are counted. A render that fails costs nothing, a PDF longer than 100 pages included: its credits are refunded.
Request
POST https://api.ojo.so/v1/pdf/templateRequest Body
| Parameter | Type | Description |
|---|---|---|
| templateId | string | The ID of the template to use. With pages, the default template, optional when every entry names its own or gives html |
| modify | object | (Optional) An object containing variables to modify in the template. With pages, shared by every template entry |
| pages | array | (Optional) The documents of one PDF, in order. See Documents from pages |
| strict | boolean | (Optional) When true, answers 422 instead of rendering when there would be warnings. See Variables and warnings |
Page size comes from a CSS @page rule in the template first, as described in
Create PDF from HTML, then from the template’s
saved size, then A4. A template variable works in @page too, so one template can serve both
sizes:
@page {
size: {{pageSize}};
}cURL
curl --request POST \
--url https://api.ojo.so/v1/pdf/template \
--header 'Authorization: Bearer <your API key>' \
--header 'Content-Type: application/json' \
--data '{"templateId": "<Template ID>", "modify": {"customer": "Acme Inc.", "pageSize": "letter"}}'Variables and warnings
modify merges over the template’s saved variables. Objects merge key by key, so
{"issuer": {"signatory": "X"}} keeps the saved issuer.name. An array replaces the saved array
whole. A key you leave out prints its saved value, which is usually sample data.
When modify is not empty, the response lists warnings for:
- a key the template does not read:
'recipent_name' is not used by the template - an input you left out that printed its saved value:
'recipient_name' was not sent, so the stored sample value was used - an input you left out that has no saved value:
'x' was not sent and has no stored value, so it renders empty - a request field the API does not know, such as a misspelt option:
Unknown field `vieportWidth` was ignored. These are listed even whenmodifyis empty
At most 20 are listed, then and N more. Send "strict": true to get these as a 422 instead,
before anything is rendered or charged. The paths a template reads are listed as inputs by
Get Template. With pages, each warning starts with its entry, as in
pages[1]: .
Response
200
Everything went as planned and your PDF is generated.
{
"id": "pdf-id.pdf",
"pdfUrl": "https://example.com/path/to/document.pdf",
"pages": 1,
"credits": 1,
"createdAt": "2026-09-28T12:34:56Z"
}If pdfUrl has no scheme, prefix it with https:// to fetch the file.
Documents from pages
pages builds one PDF from up to 100 documents, merged in order. Each entry is a document that
starts on a new page and prints at least one: a template with its own values, or HTML.
| Field | Type | Description |
|---|---|---|
| templateId | string | (Optional) The template for this entry. Defaults to the top-level templateId |
| html | string | (Optional) HTML printed as is, with no variables. Give templateId or html, not both |
| modify | object | (Optional) Values for this entry |
A template entry takes the template’s saved variables, then the top-level modify, then its own
modify, each merged over the one before as Variables and warnings says. A document longer than a page prints more pages. Each document
has its own page size, by the rule above.
One certificate per attendee, in one file:
{
"templateId": "<Certificate template ID>",
"modify": { "course": "TypeScript 101" },
"pages": [
{ "modify": { "name": "Ada Lovelace" } },
{ "modify": { "name": "Grace Hopper" } }
]
}A report with a cover, a body and terms from different templates:
{
"modify": { "customer": "Acme Inc." },
"pages": [
{ "templateId": "<Cover template ID>" },
{ "templateId": "<Report template ID>", "modify": { "quarter": "Q3" } },
{ "html": "<h1>Terms</h1><p>...</p>" }
]
}The response adds the pages each entry printed:
{
"id": "pdf-id.pdf",
"pdfUrl": "https://example.com/path/to/document.pdf",
"pages": 3,
"credits": 1,
"entries": [
{ "index": 0, "pages": 1 },
{ "index": 1, "pages": 1 },
{ "index": 2, "pages": 1 }
],
"createdAt": "2026-09-28T12:34:56Z"
}pages makes one file. For one file per recipient, send one request each.
Limits
| Limit | Value | When exceeded |
|---|---|---|
Entries in pages | 100 | 422 before rendering, nothing charged |
| Pages in the PDF | 100 | 422, nothing charged |
| Rendered HTML of all entries | 5 MB | 422 before rendering, nothing charged |
| Rendering time | 60 s | 422, nothing charged |
| Size of the PDF | 50 MB | 422, nothing charged |
pages requests in flight | 1 per account | 429 with Retry-After |
The whole request is checked before anything renders, and a 422 lists every problem found.
A request with pages can take about a minute, plus any time spent waiting behind other renders.
If your client gives up before the answer arrives, the PDF may still be made and charged. Find it
with GET /v1/pdf or on the dashboard’s Storage page.