Skip to content
On this page:Overview

API reference

Base URL https://pagerush.dev. Authenticate with your project's API key as a bearer token. Machine-readable spec: /v1/openapi.json.

Client libraries

Official SDKs cover templates and versions, sync and async rendering, job status and downloads, and verifying webhook signatures. Render calls carry an idempotency key automatically, so a retry never renders or charges twice. They have no dependencies beyond the language's standard library and are open source on GitHub.

LanguageInstallRequires
Node.js / TypeScriptnpm install pagerushNode 20+
Pythonpip install pagerushPython 3.9+
PHPcomposer require pagerush/pagerush-phpPHP 8.1+
Gogo get github.com/arsi-consultancy/pagerush-sdks/go@latestGo 1.21+
import { PageRush } from "pagerush";

const pagerush = new PageRush(); // reads PAGERUSH_API_KEY
const result = await pagerush.templates.render("tpl_…", {
  data: { number: "INV-001", customer: "Example Ltd" },
});
await result.save("invoice.pdf");

How it works

You store a template once and render it with data for each document. A template is a Typst project: its main.typ (the source) plus any other files it imports or reads, such as more .typ modules, images or fonts. A template version renders the same document from the same data, unless it reads the current date or loads images from an address.

  • Every change is a new version, numbered 1, 2, 3… Versions are immutable, so each job records exactly what it rendered.
  • One version is live: renders use it unless they pass version. Publishing an older version is how you roll back.
  • The render's data is available as a virtual data.json at the project root. Read it with #let data = json("data.json"). main.typ and data.json are reserved file paths.
  • Long documents are handled by Typst itself: page numbers, headers and footers, and table headers that repeat on every page.
#let data = json("data.json")
#set page(paper: "a4", margin: 2cm)
#set text(size: 10pt)

= Invoice #data.number
Billed to: *#data.customer*

#table(
  columns: (1fr, auto, auto),
  table.header[*Item*][*Qty*][*Amount*],
  ..data.items.map(i => (i.name, str(i.qty), [₹#(i.qty * i.price)])).flatten(),
)
{
  "number": "INV-042",
  "customer": "Acme Pvt Ltd",
  "items": [{ "name": "Design", "qty": 2, "price": 4500 }]
}

Endpoints

GET /v1/templatesList the project's templates.
POST /v1/templatesCreate a template. Its version 1 goes live.
GET /v1/templates/:idA template with its version history.
PATCH /v1/templates/:idRename a template ({ name }).
DELETE /v1/templates/:idDelete a template and all its versions. Past jobs keep their history.
POST /v1/templates/:id/versionsAdd a version (live unless publish is false).
GET /v1/templates/:id/versions/:versionOne version's source, files and sample data.
POST /v1/templates/:id/publishMake a version live ({ version }). Also how you roll back.
POST /v1/templates/:id/renderRender the template with data.
GET /v1/jobs/:idA job's status, the template version it used, and a download link.

Create a template

POST /v1/templates with a name and source creates the template with version 1, live. Add versions with POST /v1/templates/:id/versions. Source plus files may be up to 1.5 MB per version.

FieldTypeDescription
namestring (required on create)Up to 120 characters.
sourcestringThe template's main.typ, in Typst. Send this or document.
documentobjectA template from the visual editor. Send this or source. Up to 20 images and fonts, embedded, at public https addresses, or added in the visual editor (fonts PageRush keeps). Images are PNG, JPEG, GIF, WebP or SVG, up to 1 MB each.
files{ path, content, encoding }[]Other files the template imports or reads, at paths relative to main.typ (no leading "/", no ".."). encoding is utf8 (default) for text or base64 for images and fonts. Up to 50 files.
sample_dataany JSONExample data for test renders in the dashboard. Not used by API renders.
publishbooleanNew versions only: make this version live. Default true; send false to stage a draft.
curl -X POST https://pagerush.dev/v1/templates \
  -H "Authorization: Bearer pk_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{"name": "Invoice", "source": "#let data = json(\"data.json\")\n= Invoice #data.number"}'

Starter templates

In the dashboard, New template can start from one of these, with sample data to test render it. Each is a single main.typ that reads everything from data.json: Indian number formatting (₹1,23,456.00), amounts in words in lakh and crore, and an "accent" colour you set in the data. Optional fields can be left out.

GST tax invoiceHSN/SAC lines, CGST + SGST or IGST, per-rate tax summary, amount in words, bank and UPI.
QuotationItemised estimate with GST, validity date and a customer acceptance block.
Payment receiptCompact half-page receipt: amount in words, payment mode and reference, invoices settled.
Account statementMulti-page transactions with running balance, repeating header and Page X of Y.
CertificateLandscape completion or achievement certificate in serif, with signatures and a seal.
Delivery challanPacking slip without prices: items, quantities, packages, consignee and transporter.

Render a PDF

POST /v1/templates/:id/render returns the PDF bytes directly, with the job id in X-Job-Id and the version used in X-Template-Version.

curl -X POST https://pagerush.dev/v1/templates/tpl_your_template/render \
  -H "Authorization: Bearer pk_live_your_key" \
  -H "Content-Type: application/json" \
  -d '{"data": {"number": "INV-042", "customer": "Acme Pvt Ltd", "items": [{"name": "Design", "qty": 2, "price": 4500}]}}' \
  -o invoice.pdf

Request body

FieldTypeDescription
dataany JSON (required)The data for this document, up to 2 MB as JSON (1 MB with callback_url). The template reads it with json("data.json").
versionintegerRender this version instead of the live one, e.g. to test a draft.
callback_urlhttps URLRespond 202 immediately and POST the PDF to this public https URL when it is ready, signed.
metadataobjectUp to 20 string pairs, echoed on the job and in callbacks (X-PageRush-Metadata). At most 4 KB as JSON, where each non-ASCII character counts as 6 bytes (it is sent as a \uXXXX escape).
delete_after_downloadbooleanDon't keep the PDF for 24 hours: sync responses aren't stored, async PDFs are deleted once delivered or downloaded. Defaults to the project's setting.
filenamestringFile name in Content-Disposition. Defaults to the template's name.

Async rendering & callbacks

Add callback_url and the API responds 202 with a job. When the PDF is ready we POST it to your URL as application/pdf (or a JSON pdf.failed event), with X-PageRush-Event, X-PageRush-Job-Id, X-PageRush-Timestamp and X-PageRush-Signature headers, plus X-PageRush-Metadata (your metadata as JSON) on a successful render. The signature covers the event, job id and metadata headers as well as the body, so none of them can be altered. Failed deliveries are retried for about 26 hours. A callback can occasionally arrive twice, so use X-PageRush-Job-Id to ignore repeats. When you rotate your webhook secret you can keep the old one working for 24 hours: callbacks then carry a signature for each secret, so accept a callback when any v2 matches. Poll GET /v1/jobs/:id any time for status and a signed download link.

import crypto from "node:crypto";

// X-PageRush-Signature: t=<unix seconds>,v2=<hex hmac>[,v2=<hex hmac>]
// Signed: "<t>\n<event>\n<job id>\n<X-PageRush-Metadata, or empty>\n" followed by the raw body.
// After a secret rotation there are two v2 values for 24 hours: accept the callback if any matches.
// rawBody must be the exact bytes received (e.g. express.raw()), not re-serialised JSON.
export function verify(rawBody, headers, secret) {
  const parts = (headers["x-pagerush-signature"] ?? "").split(",").map((p) => p.trim().split("="));
  const t = parts.find(([k]) => k === "t")?.[1];
  if (!/^\d{1,15}$/.test(t ?? "") || Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
  const signed = [t, headers["x-pagerush-event"], headers["x-pagerush-job-id"], headers["x-pagerush-metadata"] ?? ""];
  const expected = Buffer.from(
    crypto
      .createHmac("sha256", secret)
      .update(Buffer.concat([Buffer.from(signed.join("\n") + "\n"), rawBody]))
      .digest("hex"),
  );
  return parts.some(
    ([k, v]) => k === "v2" && v.length === expected.length && crypto.timingSafeEqual(expected, Buffer.from(v)),
  );
}

Downloading a PDF again

Every successful PDF, sync or async, is normally kept for 24 hours. Use the job id from the X-Job-Id response header (or the 202 body) with GET /v1/jobs/:id: its download_url is a signed link valid for up to an hour, and you can request a fresh one at any time within the 24 hours. Downloading again never renders or charges again. Past jobs can also be downloaded from your dashboard. After 24 hours the file is deleted, download_url is null and old links return 410 FILE_EXPIRED.

Deleting PDFs as soon as you have them

If you'd rather we didn't keep your documents, send "delete_after_download": true, or turn on Delete PDFs after download in your project's settings to make it the default (a request's own value always wins). A sync PDF is then returned in the response and never stored. An async PDF is stored only until your callback accepts it with a 2xx; if the callback fails for good, you can still fetch it once from download_url, which deletes it after a complete download. Afterwards the job reports download_url: null, and download links and Idempotency-Key replays return 410 FILE_EXPIRED.

Reliability features

  • Idempotency-Key header: retries return the original result without rendering or charging again.
  • Prefer: respond-async: if the compile service is temporarily overloaded, get a 202 job instead of a 503.
  • Failed renders are never charged; your quota is released automatically.
  • Your data is used only to render: it is deleted from our database once the job finishes, whether it succeeded or failed (database backups expire within 30 days).

Limits

  • Source plus files: 1.5 MB per version, up to 50 files.
  • Sample data: 400 KB per version.
  • Render data: 2 MB per request (1 MB with callback_url).
  • Output: 10,000 pages and 25 MB per PDF.
  • Render time: set by your plan.
  • API requests: 100 per 10 seconds per API key. Beyond that you get a 429 with Retry-After.
  • Concurrent renders: set by your plan. A sync render beyond it gets a 429 CONCURRENCY_LIMIT with Retry-After straight away and isn't charged; retry, or use callback_url so renders queue instead. In the rare case the render service is briefly unreachable, a sync render gets a 503 with Retry-After, and async renders retry on their own.
  • Billing: a PDF counts once against your quota or credits for every 50 pages started, worked out from the pages it produced. If a document needs more than your remaining quota (or credits) covers, it fails with QUOTA_EXCEEDED and nothing is charged.
  • Async renders run up to your plan's concurrent renders at a time, with up to 50 waiting per concurrent render. Beyond that you get a 429 with Retry-After.

Need more? Render time and concurrent renders can be raised for your account with a custom plan. The other limits apply to everyone, but if one gets in your way, email contact@pagerush.dev with what you're building.

Fonts

Renders use Typst 0.15.1 with these fonts. Set one with #set text(font: "Noto Sans"). Typst falls back to a font that has the character, so Hindi, Tamil or Bengali text renders correctly even when your template only names a Latin font. Every font is free to use: most are under the SIL Open Font License, New Computer Modern under the GUST Font License, and DejaVu Sans Mono under the Bitstream Vera license.

Latin text (built in)Libertinus Serif, New Computer Modern
Latin, Greek, Cyrillic and ₹Noto Sans, Noto Serif
Latin text in the visual editorInter
Code and tabular figuresNoto Sans Mono, JetBrains Mono, DejaVu Sans Mono
Hindi, Marathi, Nepali, SanskritNoto Sans Devanagari
Bengali, AssameseNoto Sans Bengali
TamilNoto Sans Tamil
TeluguNoto Sans Telugu
KannadaNoto Sans Kannada
MalayalamNoto Sans Malayalam
GujaratiNoto Sans Gujarati
Punjabi (Gurmukhi)Noto Sans Gurmukhi
OdiaNoto Sans Oriya
MathsNew Computer Modern Math

Packages

These Typst Universe packages are built in. Import them at exactly these versions, for example #import "@preview/tiaoma:0.3.0". Nothing is downloaded at render time, so renders stay fast and the output never changes under you. Other packages, or other versions, fail with an error that lists what's available. To use something else, copy its files into your template.

@preview/tiaoma:0.3.0QR codes (UPI payment links) and barcodes: Code 128, EAN, GS1 and more
@preview/rustycure:0.2.0Fast, simple QR codes
@preview/cetz:0.5.3Drawing: shapes, lines and custom graphics
@preview/cetz-plot:0.1.4Bar, pie and line charts on top of CeTZ
@preview/lilaq:0.6.0Data plots with axes, legends and ticks
@preview/fletcher:0.5.8Diagrams with nodes and arrows (flows, processes)
@preview/zero:0.7.1Number formatting: digit grouping, rounding, units
@preview/oxifmt:1.0.0String formatting, e.g. strfmt("{:.2}", amount)

Errors

Errors use application/problem+json with a stable code.

StatusCodeMeaning
400VALIDATION_FAILEDFix the request; errors[] has field-level details.
400INVALID_DATAThe render's data is missing, not JSON, or too large.
400URL_NOT_ALLOWEDcallback_url must be a public https URL.
401INVALID_API_KEYMissing, invalid or revoked key.
402QUOTA_EXCEEDEDMonthly quota and credits exhausted.
403PROJECT_DISABLED, ACCOUNT_SUSPENDEDThe project or account has been disabled.
404TEMPLATE_NOT_FOUNDNo such template (or version) in this key's project.
409TEMPLATE_NOT_PUBLISHEDThe template has no live version. Publish one or pass version.
409IDEMPOTENCY_CONFLICTThe Idempotency-Key was used for a different request.
409CONFLICTA request with this key is still being processed, or a limit was reached.
410FILE_EXPIREDThe stored PDF expired after 24 hours, or was deleted after download.
422TEMPLATE_COMPILE_FAILEDThe template doesn't compile with this data. errors[] has Typst's diagnostics, with paths like main.typ:12:5. Not charged.
422RENDER_TIMEOUTThe template ran longer than your plan allows. Not charged.
429RATE_LIMITED, CONCURRENCY_LIMITSlow down; honour Retry-After.
503RENDER_UNAVAILABLETemporary: the render service couldn't be reached or couldn't queue the job. Retry after Retry-After when present, or send Prefer: respond-async. Not charged.