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.

Custom Typst

kind typst-package. You send a directory of text files. This is the only press kind.

POST /v1/render

Compile a custom Typst package

The press compiles a directory of text files. HTTP carries that directory as UTF-8 strings — not a nested document format. Typst still reads data with json("data.json").

Auth
Authorization: Bearer rmpdf_…
Host
https://pdf.railman.io
FieldTypeNotes
kindrequired"typst-package"Native compile path.
package.mainrequiredstringEntry file. Must be one of the .typ / .typst paths.
package.files[]required{ path, content }Directory listing. content is UTF-8 text, never nested JSON.
data.jsonfileOptional package-root JSON file. Typst reads it with json("data.json").
params.jsonfileOptional package-root JSON file. Same rule as data.json.
fonts"core" | "cjk" | "cjkRound"[]Allowlist profiles. You never upload font files.
waitbooleanHTTP wait, not Typst. Default true: this request waits up to 25s for a PDF. false returns 202 immediately when the job is queued. Cache hits are still 200.

Package

2 files

Typst

= Hello
#let data = json("data.json")
#data.title

POST envelope

{
  "fonts": [
    "core"
  ],
  "kind": "typst-package",
  "package": {
    "files": [
      {
        "content": "= Hello\n#let data = json(\"data.json\")\n#data.title\n",
        "path": "main.typ"
      },
      {
        "content": "{\"title\":\"Q3\"}",
        "path": "data.json"
      }
    ],
    "main": "main.typ"
  },
  "wait": true
}

curl

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

200

application/pdf bytes

sync or cache-hit. Header x-pdf-job-id is the job UUID.

202

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

Queued. Poll GET /v1/jobs/{id}.

4xx

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

RFC 9457. Images, remote imports, and unsafe paths are 422 before queue.

How the directory maps

Think of package.files as a folder, not a document. Each entry is a path plus UTF-8 text. data.json is a JSON file whose contents happen to be JSON — the HTTP field is still a string, the same way a .typ file is a string.

Templates skip this step: you send a JSON data object, and web writes data.json for you. Optional params becomes params.json. Those JSON files are not Typst sources.

Why wait is on the HTTP envelope

wait is not Typst. Default true means this HTTP request waits up to 25s for a terminal job and returns 200 application/pdf when ready; otherwise 202 with a job id. wait: false returns 202 immediately on a miss. Cache hits are still 200.

What the policy rejects

  • Images, binary assets, and user-supplied fonts.

  • Remote http(s) imports and undeclared packages.

  • Any path other than .typ / .typst plus package-root data.json / params.json.

  • json() of any path other than those two well-known files.

Fonts are allowlisted profiles: core, cjk, cjkRound. You never upload a font file.

GET /v1/jobs

List jobs

Caller-scoped history. cache_key is diagnostic, never a download id.

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

curl

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

200

{ "jobs": [{ "id": "…", "status": "done", "cache_hit": true, "expires_at": "…" }], "next_cursor": null }

Optional ?status=queued|rendering|done|failed|cached. GET /v1/jobs/{id} 302s to a short-lived origin download.

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.