Documentation
Quickstart
Clone Katagami, publish a template, and render your first PDF on your own machine.
This walkthrough starts from an empty machine and ends with a PDF on disk. Every command below is copy-paste runnable and was run against the development stack it describes. Budget about ten minutes, most of which is the first container build.
Before you start
You need Docker Engine 24 or newer with Compose v2 (docker compose, not
docker-compose), plus git, curl, and jq. You do
not need a Rust toolchain, a Typst install, or any cloud account. The development
stack runs PostgreSQL and MinIO in containers alongside Katagami.
1. Clone and start the stack
git clone https://github.com/BRO3886/katagami.git
cd katagami
docker compose -f docker-compose.dev.yml up --build -d
The first run compiles Katagami from source, so expect a few minutes. It starts three
containers: the renderer on 127.0.0.1:8080, PostgreSQL, and MinIO for
S3-compatible object storage. The development stack ships throwaway credentials and
an admin token of smoke-admin-token.
Confirm the API is up:
curl http://127.0.0.1:8080/v1/templates
A fresh installation answers:
{"templates":[]}
Note
Katagami has no health endpoint. This read request is how you check that a renderer started and loaded its catalog. If it does not answer, read the logs withdocker compose -f docker-compose.dev.yml logs -f renderer.
2. Write a template
A template is ordinary Typst source. Katagami passes the request body’s data object
to your template as sys.inputs.at("data").
cat > invoice.typ <<'EOF'
#set page(width: 148mm, height: 105mm, margin: 12mm)
#set text(font: "Noto Sans", size: 11pt)
#text(size: 18pt, weight: "bold")[Invoice #sys.inputs.at("data").at("number")]
#v(6mm)
Billed to: #sys.inputs.at("data").at("customer")
Amount due: #sys.inputs.at("data").at("amount")
EOF
3. Build the publish payload
A published version carries its own schema, source, and fonts, so a render never
depends on files that happen to sit on the host. The payload sends each file
base64-encoded in a name-to-content map. source must contain main.typ, and
fonts must contain at least one decodable font. The repository ships Noto Sans
under assets/fonts/ for exactly this purpose.
jq -n \
--arg source "$(base64 < invoice.typ | tr -d '\n')" \
--arg font "$(base64 < assets/fonts/NotoSans-Regular.ttf | tr -d '\n')" \
'{
schema: {
type: "object",
additionalProperties: false,
required: ["number", "customer", "amount"],
properties: {
number: { type: "string" },
customer: { type: "string" },
amount: { type: "string" }
}
},
source: { "main.typ": $source },
fonts: { "NotoSans-Regular.ttf": $font }
}' > invoice-v1.json
The schema field is a JSON Schema. Katagami validates every render request against
it before Typst is invoked, so a caller that sends the wrong shape gets a clear 422
instead of a confusing compile error or a subtly wrong document.
Note
The map key is the font’s file name, but the name you use inside Typst is the font’s family name.NotoSans-Regular.ttf is written font: "Noto Sans" in the
template. Mismatching the two is the most common first-publish failure.
4. Create the template and publish version one
Template IDs and versions are created separately: first the ID, then an immutable version under it.
curl --fail-with-body -X POST http://127.0.0.1:8080/v1/admin/templates \
-H 'content-type: application/json' \
-H 'x-admin-token: smoke-admin-token' \
--data '{"id":"invoice"}'
curl --fail-with-body -X POST http://127.0.0.1:8080/v1/admin/templates/invoice/versions/v1 \
-H 'content-type: application/json' \
-H 'x-admin-token: smoke-admin-token' \
--data-binary @invoice-v1.json
Both return 201 Created with an empty body. Both require the x-admin-token
header; these two endpoints are the only way source ever enters the system.
5. Render a PDF
The render endpoint needs no admin token.
curl --fail-with-body -X POST http://127.0.0.1:8080/v1/templates/invoice/versions/v1/render \
-H 'content-type: application/json' \
--data '{"data":{"number":"INV-001","customer":"Ada Lovelace","amount":"1,240.00"}}' \
--output invoice.pdf
Open it with open invoice.pdf on macOS or xdg-open invoice.pdf on Linux. The
response is application/pdf bytes; nothing was written to object storage, because
persisted results are off unless you enable and request them.
Watch the guardrails work
Three requests that should fail, and the exact answers you get. Running these is the fastest way to understand what Katagami enforces.
Data that does not match the published schema is rejected before compilation:
curl -X POST http://127.0.0.1:8080/v1/templates/invoice/versions/v1/render \
-H 'content-type: application/json' \
--data '{"data":{"number":"INV-001"}}'
{"error":"schema validation failed"}
Republishing a version that already exists returns 409, because a published version
is immutable:
curl -o /dev/null -w '%{http_code}\n' \
-X POST http://127.0.0.1:8080/v1/admin/templates/invoice/versions/v1 \
-H 'content-type: application/json' \
-H 'x-admin-token: smoke-admin-token' \
--data-binary @invoice-v1.json
409
A publish without the right admin token returns 401:
{"error":"admin authentication required"}
Ship a change as a new version
Because versions never change in place, an update means publishing a new one. Callers
already pinned to v1 keep getting exactly the document they were getting before.
sed 's/Invoice #/Invoice · v2 #/' invoice.typ > invoice-v2.typ
jq --arg source "$(base64 < invoice-v2.typ | tr -d '\n')" \
'.source["main.typ"] = $source' invoice-v1.json > invoice-v2.json
curl --fail-with-body -X POST http://127.0.0.1:8080/v1/admin/templates/invoice/versions/v2 \
-H 'content-type: application/json' \
-H 'x-admin-token: smoke-admin-token' \
--data-binary @invoice-v2.json
curl http://127.0.0.1:8080/v1/templates
{"templates":[{"id":"invoice","versions":["v1","v2"]}]}
Render both versions and compare them. v1 still produces its original document.
Stop the stack
docker compose -f docker-compose.dev.yml down --volumes
Dropping --volumes keeps the PostgreSQL and MinIO data, so your published templates
survive a restart.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
curl: (7) Failed to connect on 8080 |
The renderer is still building or failed to start | docker compose -f docker-compose.dev.yml logs -f renderer |
401 admin authentication required |
Missing or wrong x-admin-token |
The development stack uses smoke-admin-token |
409 on publish |
That version is already published and cannot change | Publish under a new version name |
422 schema validation failed |
The request data does not match the published JSON Schema |
Compare against GET /v1/templates/{id} |
| PDF renders in the wrong font | Typst could not find that font family and used a fallback | Use the family name, not the file name |
413 |
The request or an object exceeded a limit | See Operations and limits |
502 control plane unavailable |
PostgreSQL or object storage is unreachable | Check both containers and their credentials |
Next steps
- API reference for the full publish payload, request assets, and every status code.
- Self-hosting to point Katagami at storage and a database you operate.
- System architecture for what happens between the request and the PDF.