Two doors onto one press.

Packages and templates share assemble, scheduler, then press. Bearer POST /v1/render or OAuth on /mcp. The press only compiles typst-package.

Templates

kind typst-template is web-only. You send a ref plus JSON. web writes data.json and submits typst-package.

PieceRole
typst-packageYou send the directory. Typst sources plus optional package-root data.json / params.json. The press compiles that directory. This is the only press kind.
typst-templateYou send a published ref namespace/slug[@version] plus JSON data. web assembles the package and submits typst-package. There is no press kind named template. Drafts are session-only; there is no public create-template API.
paramsOptional JSON object written to params.json beside data.json. Typst reads it with json("params.json"). It is not the template. s_schema describes data; params_schema describes params.

POST /v1/render

Fill a published template

Name a published template as namespace/slug[@version] plus JSON data. web assembles the same file directory; the press still compiles typst-package only.

Auth
Authorization: Bearer rmpdf_…
Host
https://pdf.railman.io
FieldTypeNotes
kindrequired"typst-template"Assembled in web. Never a press kind.
templaterequiredstringnamespace/slug or namespace/slug@version.
versionintegerOptional sibling. Pinned onto the ref only when @version is omitted. @version in template wins.
datarequiredobjectJSON object. web writes it to data.json beside your Typst files, then submits typst-package.
paramsobjectOptional object written to params.json. Not the template; Typst reads json("params.json").
waitbooleanSame HTTP wait contract as the package path — not Typst syntax.

Request body

{
  "data": {
    "client": {
      "address": "88 Market Street, San Francisco, CA",
      "email": "ap@acme.example",
      "name": "Acme Studio"
    },
    "currency": "USD",
    "dueDate": "2026-09-16",
    "invoiceNumber": "INV-1042",
    "issueDate": "2026-08-17",
    "items": [
      {
        "description": "Typst press retainer",
        "quantity": 1,
        "unitPrice": 1200
      },
      {
        "description": "Extra compile hours",
        "quantity": 4,
        "unitPrice": 85
      }
    ],
    "notes": "Net 30. Wire details on file.",
    "seller": {
      "address": "Railman, SNRK",
      "email": "billing@railman.io",
      "name": "PDF Railman"
    },
    "shareUrl": "https://pdf.railman.io/library/railman/invoice",
    "taxRate": 0
  },
  "kind": "typst-template",
  "template": "railman/invoice@1",
  "wait": true
}

curl

curl -X POST https://pdf.railman.io/v1/render \
  -H "Authorization: Bearer rmpdf_…" \
  -H "Content-Type: application/json" \
  -d @template.json

200

application/pdf bytes

Same download contract as the package path.

202

{ "jobId": "…", "statusUrl": "/v1/jobs/{id}" }

Queued after assemble. Credits still come from the caller, not the template owner.

4xx

{ "type": "https://pdf.railman.io/problems/{code}", "detail": "…" }

Unknown ref, unpublished version, or schema mismatch.

How assemble works

  1. Resolve namespace/slug[@version] in web. Unpublished or unknown refs fail here.

  2. Template sources are one or more .typ files (local #import allowed). Do not put data.json in those sources — caller data is written at assemble. Typst reads it with json("data.json").

  3. Map caller data (U-JSON) through the template transformer to S-JSON. Identity means the payload is already template-ready. Dashboard fill skips the transformer and posts S-JSON directly.

  4. Write canonical data.json / params.json beside those Typst files and submit typst-package to the scheduler.

  5. Credits come from the caller, not the template owner. Public library is a catalog, not free renders.

Lifecycle

  1. Create a workspace draft in your user:<id> namespace. Session only.

  2. Edit Typst sources (one or more .typ files, one main), s_schema, and the transformer. Save draft is a normal session write — not publish. The editor schema form is the preview — there is no PDF iframe.

  3. Fill the saved draft from the dashboard to test (session job, skips transformer). Publish is not required.

  4. Publish is a separate manager step-up (Elevate). That integer version is the only thing API and MCP can resolve as a template ref. Drafts never appear on GET /v1/templates.

GET /v1/templates

List your published templates

Your published templates only. Drafts never appear. Use ref on POST /v1/render. GET /v1/templates/:id returns schema, not Typst sources.

Auth
Authorization: Bearer rmpdf_…
Host
https://pdf.railman.io

curl

curl https://pdf.railman.io/v1/templates \
  -H "Authorization: Bearer rmpdf_…"

200

{ "templates": [{ "id": "…", "namespace": "user:u_123", "ref": "user:u_123/invoice@1", "slug": "invoice", "title": "Invoice", "version": 1 }] }

Cover the slug on the key allowlist, or *.

GET /v1/library

List library templates

Public library catalog. Render still spends the caller’s credits.

Auth
Session cookie or Bearer rmpdf_…
Host
https://pdf.railman.io

curl

curl https://pdf.railman.io/v1/library \
  -H "Authorization: Bearer rmpdf_…"

200

{ "templates": [{ "namespace": "railman", "slug": "invoice", "version": 1 }, { "namespace": "railman", "slug": "receipt", "version": 1 }, { "namespace": "railman", "slug": "quote", "version": 1 }] }

Use namespace/slug as template on POST /v1/render.

GET /v1/usage

Read usage

Caller-scoped wallet remaining, UTC period, and debit totals. Same payload as MCP get_usage. Web never subtracts credits.

Auth
Authorization: Bearer rmpdf_…
Host
https://pdf.railman.io

curl

curl https://pdf.railman.io/v1/usage \
  -H "Authorization: Bearer rmpdf_…"

200

{ "plan": "free", "period": "2026-08", "remaining": 7960, "used_today": 4, "used_month": 40, "queued": 0, "monthly_credits": 8000, "credits_per_page": 8, "cache_hit_credits": 1 }

Identity is the API key owner. Failed jobs do not debit. MCP get_usage is the same JSON.

PDF Railman

Fill, then generate. Invoice, quote, and receipt need no account.