Skip to Content
Create PDF from Template

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/template

Request Body

ParameterTypeDescription
templateIdstringThe ID of the template to use. With pages, the default template, optional when every entry names its own or gives html
modifyobject(Optional) An object containing variables to modify in the template. With pages, shared by every template entry
pagesarray(Optional) The documents of one PDF, in order. See Documents from pages
strictboolean(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 --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 when modify is 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

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.

FieldTypeDescription
templateIdstring(Optional) The template for this entry. Defaults to the top-level templateId
htmlstring(Optional) HTML printed as is, with no variables. Give templateId or html, not both
modifyobject(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

LimitValueWhen exceeded
Entries in pages100422 before rendering, nothing charged
Pages in the PDF100422, nothing charged
Rendered HTML of all entries5 MB422 before rendering, nothing charged
Rendering time60 s422, nothing charged
Size of the PDF50 MB422, nothing charged
pages requests in flight1 per account429 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.