Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

FieldRequiredTypeNotes
templateyesstring (URL)HTTPS URL to the entry-point .typ source.
template_pathnostringPath the template is known by inside the compile bundle. Defaults to main.typ.
inputsnoany JSON valueExposed to the template as sys.inputs.data (stringified). Same contract as multipart.
assetsnoarrayEach entry has path (in-bundle filename) and url (HTTPS source).
outputyesstring (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_type you pass into generate_signed_url must match the Content-Type the uploader sends. flootypst always sends application/pdf on 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.