URL mode (GCS-friendly)
POST /render-uri with Content-Type: application/json.
Designed for the common pattern: template + assets live in Google Cloud Storage, the rendered PDF should land back in GCS, and you don’t want to stream megabytes through the API tier. The caller signs short-lived URLs against its own bucket; flootypst never holds GCS credentials.
The same shape works against any storage with HTTPS signed-URL semantics (S3, Azure Blob, R2, etc.).
Request
{
"template": "https://storage.googleapis.com/your-bucket/main.typ?X-Goog-Signature=...",
"template_path": "main.typ",
"inputs": { "customer": "Acme", "amount": 1200.50 },
"assets": [
{ "path": "logo.png", "url": "https://storage.googleapis.com/your-bucket/logo.png?X-Goog-Signature=..." },
{ "path": "data.csv", "url": "https://storage.googleapis.com/your-bucket/data.csv?X-Goog-Signature=..." }
],
"output": "https://storage.googleapis.com/your-bucket/out/report.pdf?X-Goog-Signature=..."
}
Fields
| Field | Required | Type | Notes |
|---|---|---|---|
template | yes | string (URL) | HTTPS URL to the entry-point .typ source. |
template_path | no | string | Path the template is known by inside the compile bundle. Defaults to main.typ. |
inputs | no | any JSON value | Exposed to the template as sys.inputs.data (stringified). Same contract as multipart. |
assets | no | array | Each entry has path (in-bundle filename) and url (HTTPS source). |
output | yes | string (URL) | HTTPS URL to PUT the resulting PDF. The signed URL must permit PUT with application/pdf. |
Response
{
"url": "https://storage.googleapis.com/.../out.pdf?...",
"bytes": 13942,
"compile_ms": 87,
"upload_ms": 134
}
- 200 — PDF was compiled and successfully uploaded. Body is the JSON above.
- 400 — bad JSON, invalid URL, scheme other than HTTPS, host blocked by allowlist, asset exceeded fetch size cap.
- 408 — fetch or compile timed out.
- 422 — compile error.
- 500 — upstream returned non-2xx on GET or PUT, network error, DNS resolved only to disallowed addresses.
Generating signed URLs (Python / GCS)
from datetime import timedelta
from google.cloud import storage
client = storage.Client()
bucket = client.bucket("your-bucket")
def signed_get(name: str) -> str:
return bucket.blob(name).generate_signed_url(
version="v4",
expiration=timedelta(minutes=15),
method="GET",
)
def signed_put(name: str) -> str:
return bucket.blob(name).generate_signed_url(
version="v4",
expiration=timedelta(minutes=15),
method="PUT",
content_type="application/pdf",
)
payload = {
"template": signed_get("templates/invoice.typ"),
"template_path": "invoice.typ",
"inputs": {"customer": "Acme", "amount": 1200.50},
"assets": [
{"path": "logo.png", "url": signed_get("brand/logo.png")},
],
"output": signed_put("renders/invoice-2026-05.pdf"),
}
A few details to get right with GCS PUT signing:
- The
content_typeyou pass intogenerate_signed_urlmust match theContent-Typethe uploader sends. flootypst always sendsapplication/pdfon the upload. - The default GCS lifetime (15 minutes here) just needs to cover the request — flootypst attempts the upload immediately after the compile finishes.
End-to-end with curl
curl -sS -X POST https://typst.floomatik.com/render-uri \
-H 'Content-Type: application/json' \
--data @payload.json
Where payload.json is the request shown above.
Why a separate endpoint instead of content-type dispatch?
To keep each handler’s contract explicit. POST /render is the “give me a PDF” endpoint; POST /render-uri is the “fetch, render, store” endpoint. Different success responses (PDF bytes vs. JSON ack), different error sources (multipart parse vs. URL fetch), and different size profiles for the request body.