An HTTP REST API built in Rust to compile Typst documents into PDFs. It supports multiple files, source code, caching fonts, typst packages, authentication, configurable limits and more stuff.
There are two main ways to run the API:
- As a standalone binary (the recommended way is using the install script provided below).
- As a docker container (the recommended way it to use Docker Compose).
You can also of course compile and run the source code with Cargo.
The intended use of this repository is to allow people self-host a Typst server without much complications.
For most documents you only need about 35MB of RAM, however for a very large textbook with images, custom fonts, etc. you might require about ~200MB.
The requirements for most cases are:
- 0.5 GB of RAM
- 1 GB of disk space
- 1 core of CPU/vCPU
For cases where you are exposing your API to the public you should try to have:
- 2 GB of RAM
- 3 GB of disk space
- 1 core of CPU/vCPU
Note: if you are exposing your API to the public, be sure to prevent API abuse by configuring maximum payload size and maximum concurrent compilations as explained below.
- In a directory of your choice on your server create 2 files, a
.envand adocker-compose.yml, and create afontsdirectory. - Add to the
.envthe contents of.env.example, configure the variables to your needs. - Put the contents of the
docker-compose.ymlinto your local docker compose file. It already has the proper configuration and pulls the Docker image from DockerHub. - From this same local directory, do
docker compose up -d. - Check
http://localhost:8080/health, it should return 200 (Ok). The root endpointhttp://localhost:8080should return a welcome json with the API version, the swagger-ui docs endpoint and other useful information. - Visit
http://localhost:8080/playgroundto interactively test the API in your browser. - Visit
http://localhost:8080/add-fontsto easily add new font to your font database.
You can also the logs of the API by running docker compose logs typst-api from that same directory.
To update simply do from the same directory:
docker compose pullOr update the image used in the docker-compose.yml if you want a specific version that it's not the latest.
Now restart the container:
docker compose up -d --force-recreate- Clone this repo
git clone https://github.com/Mapaor/typst-api(and docd typst-api). - Copy the
.env.exampleto.envand adjust the variables if needed. - No need to modify the
docker-compose.ymlbecause we have adocker-compose.override.ymlthat already does the job. - Run
docker compose up --build -d. This will trigger theDockerfileactions and start building the docker image and after that starting the container. - The API will be available at
http://localhost:8080 - Now you can modify the Rust source code as you please and every time you hit a
docker compose up --build -dthe container will restart with your new custom image. This way you can tweak the API even further to fit your exact needs.
You can pull and run the image directly from Docker Hub by using:
docker run -p 8080:8080 mapaor4/typst-api:latestThe API will be available at http://localhost:8080. You can of course set any another port you like. For example, you can map the container's default port (8080) to the host's 3001 port:
docker run -p 3001:8080 mapaor4/typst-api:latestThe API will now be available at http://localhost:3001.
You can download the binary from GitHub releases and then execute it, but for ease of use an installer script is provided that already downloads the correct binary, configures a local directory, and optionally (only for Linux) sets up a systemd service.
Run the installation script:
# Default installation (gets installed at `~/.local/typst-api`)
curl -sSL https://raw.githubusercontent.com/Mapaor/typst-api/main/scripts/install.sh | bash
# Install a specific version
curl -sSL https://raw.githubusercontent.com/Mapaor/typst-api/main/scripts/install.sh | bash -s -- --version v0.2.4
# Install with a systemd service (Recommended. Linux only)
curl -sSL https://raw.githubusercontent.com/Mapaor/typst-api/main/scripts/install.sh | sudo bash -s -- --with-serviceIf you are on a Linux server I recommend the --with-service version because it sets up a service to automatically run the API on boot.
Then you can check the logs with sudo journalctl -u typst-api -n 100 --no-pager.
If you are on macOS (or Linux but chose not to use the systemd service flag), you'll need to run the server manually:
cd ~/.local/typst-api
./typst-apiTo run it in the background (detached), you can use nohup:
cd ~/.local/typst-api
nohup ./typst-api > typst-api.log 2>&1 &Run the PowerShell installation script:
# Default installation (gets installed at `$env:USERPROFILE\.local\typst-api`)
Invoke-Expression (Invoke-RestMethod -Uri "https://raw.githubusercontent.com/Mapaor/typst-api/main/scripts/install.ps1")Run the server manually with:
cd $env:USERPROFILE\.local\typst-api
.\typst-api.exeTo run it in the background (detached), you can start it as a hidden process:
cd $env:USERPROFILE\.local\typst-api
Start-Process -FilePath ".\typst-api.exe" -WindowStyle HiddenThe installer creates a .env file in the installation directory. You can edit this file to configure your server. If you installed the systemd service, the .env configuration is copied to /etc/typst-api/typst-api.env.
To update to a newer or re-read an updated .env file, simply re-run the installer script. Also don't forget to restart the service if you had set it up (sudo systemctl restart typst-api).
You can also re-run the installer script as many times as you want, it will simply override the previous installation.
To uninstall, delete the installation directory (e.g., rm -rf ~/.local/typst-api) and, if using systemd, disable and remove the service (sudo systemctl disable typst-api && sudo rm /etc/systemd/system/typst-api.service).
- Download the artifact for your OS and architecture from the GH releases.
- Put in the same directory where you want to save your binary a
.envfile (contents copied from.env.example) and afonts/empty folder. - Execute the binary from that same directory.
The API is a Rust binary crate, so if you have Rust installed you can clone the repo and run it with:
cargo run --releaseYou can run it with env variables as we've seen before.
On bash:
PORT=3001 cargo run --releaseOn PowerShell:
$env:PORT = "3001"
cargo run --releaseOr you can simply create a .env file (or copy and rename the .env.example) in the root of the repository and set the variables there, it will be loaded automatically.
You can also use cargo build --release to build the binary without running it, and then run the binary located in target/release/typst-api like we explained before.
You could do cargo run or cargo build without the --release flag for local development but for production (the actual API usage) the flag is recommended (it does additional performance optimizations for the final binary).
typst-api is an application binary rather than a Rust library you can reuse. The Docker image lives on Docker Hub and the standalone binaries on GitHub. If you want you can still compile it from source using Cargo. The crate has not been published to crates.io because it wouldn't add much to the already existing standalone binaries and also to avoid any potential confusion with the official Typst crates such as typst-pdf, typst-cli, typst-kit, etc.
Send an application/json request with your typst source code like shown in examples/typst-source-code/README.md.
curl -X POST http://localhost:8080/compile/source -H "Content-Type: application/json" -d '{"source": "= Hello Typst!\nThis is a very simple document.", "filename": "main.typ"}' --output "examples/typst-source-code/example.pdf"You can also visit http://localhost:8080/playground to test the API in the browser. If you check the static/ folder of this repo you'll see how the playground is internally doing the API call in vanilla javascript.
const response = await fetch('/compile/source', {
method: 'POST',
headers: headers,
body: JSON.stringify({
source: sourceCode,
filename: "main.typ"
})
});Send a multipart/form-data request with your Typst files, images and other files. The main file should either be explicitly named in the form field as main or named main.typ in its filename.
curl -X POST http://localhost:8080/compile \
-F "main=@my_document.typ" \
--output result.pdfNote: If compilation fails, curl saves the JSON error response to the output file. To see the error in your console instead, remove the --output flag.
try {
Invoke-WebRequest -Uri http://localhost:8080/compile -Method Post -Form @{
main = Get-Item "my_document.typ"
} -OutFile "result.pdf" | Out-Null
Write-Host "Success! Saved to result.pdf"
} catch {
$_.ErrorDetails.Message
}If you have Postman desktop installed (and optionally the VSCode Extension as well), you can easily test the API by doing the following:
- Create a new
POSTrequest tohttp://localhost:8080/compile. - Go to the Body tab and select
form-data(instead ofraw). - Add a key named
main, change its type fromTexttoFile. - Select (upload) your
.typfile in the value column. - Click Send. Postman is great because it will show the visual PDF if the request is successful or it will show the JSON error if it fails.
Both /compile and compile/source default to PDF but they also support SVG output. Simply add a query parameter ?format=svg to the request to choose SVG output. If the output has multiple pages it will be returned as a ZIP (containing page-1.svg, page-2.svg, etc.).
curl -X POST "http://localhost:8080/compile/source?format=svg" -H "Content-Type: application/json" -d '{"source":"#set page(fill: none, width: auto, height: auto, margin: 0cm)\n#text(fill: red)[$ e^(i pi)+1=0 $]"}' --output result.svgNote: SVG backgrounds are white by default, use #set page(fill: none) to get a transparent background.
curl -X POST "http://localhost:8080/compile?format=svg" -F "main=@my_document.typ" --output result.zipYou can optionally add a pages query parameter to select which pages you want to export, for example: ?format=svg&pages=2,3,7-9,11-, like in the Typst CLI. You can also use pages for PDFs.
You can check a full set of examples in the examples/ directory, which contain both the Typst and related files, the PowerShell and Curl commands to run the examples and the expected PDF output.
The API includes admin endpoints which are prefixed with /admin. To set authentication for them add an ADMIN_TOKEN to the .env file. If the variable is not set, these endpoints fall back to AUTH_TOKEN authentication. If neither is set, all endpoints are publicly accessible.
GET /admin/inforeturns a json with useful info like status, config options and stats.POST /admin/fontsaccepts amultipart/form-datapayload containing font files and saves them to the server's fonts directory and updates the font cache.POST /admin/fonts/refreshreloads local fonts (from theTYPST_FONT_PATHSdirectory) without having to restart the container.GET /admin/packagesreturns a list of all cached Typst packages and their total size.POST /admin/packages/preloadreceives a JSON array of packages to be downloaded and added to the cache.POST /admin/packages/sync-alltriggers a background sync of all packages from the Typst registry. As of september 2026 all the versions of all the packages are about ~1.9 GB.DELETE /admin/packages/cacheclears the local package cache.POST /admin/fonts/synctriggers a background sync of the curated 133 fonts mirror repository (used in the Typst Web App).
By default, when using Docker Compose the package cache persists across container restarts because it uses a docker named volume, additionally you can configure in the .env file the following variables:
PRELOAD_PACKAGES: A list of comma-separated essential packages you want to have available on start up.CACHE_ALL_PACKAGES: If set to true downloads the entire Typst registry (~1.9GB) in the background when the server starts.CACHE_ALL_133_TYPST_FONTS: If set to true downloads and caches all 133 curated fonts from the typst web app mirror in the background when the server starts.
You can then use any reverse proxy, tunnel or VPN you might typically use to expose your containers or running services to your other devices or to the whole internet.
My recommendations:
-
If you are the only one who is gonna use the API from a specific set of owned devices, use Tailscale and make the requests to the tailnet IP of your server or home server.
-
If you have a home server (without a public IP) and you want the API to be accessible through the internet use Tailscale Funnel to expose your api.
⟶ Check out my guide to expose your API in less than 5 minutes:
FUNNEL_GUIDE.md. -
If you have a home server without a public IP and you want the API to be accessible through the internet and also at a custom domain, use Caddy as a reverse proxy on your server and then Cloudflare Tunnel (Cloudflared) for creating a tunnel between your host and the world.
⟶ Checkout my guide to set up
typst-apiwith Caddy and Cloudflare Tunnel on a Debian-based OS on a custom domain:CUSTOM_DOMAIN_GUIDE.md -
Use Pangolin (instead of Cloudflare Tunnel) running on a VPS if you plan to work with very big documents or concurrent requests and want to avoid the Cloudflare 100MB limitation, or if you want to serve your api at a subsubdomain (api.typst.mydomain.com) and don't want to pay for Cloudflare Advanced Certificate Manager.
Note: If you do expose the API running on your server to the general public (the internet) make sure to set the ADMIN_TOKEN to protect sensible routes. And also, either set authentication (so that only you and people who you trust can use the API) or enforce limits like maximum payload size and maximum concurrent compilation to prevent API abuse.
Here are some client-side example of websites that do requests to a server selfhosting this Typst:
- md.katex.cc — A markdown online live editor that supports rendering equations. It uses KaTeX for the HTML rendering and Typst for the download PDF option.
If you create a selfhosted instance of typst-api which is public, please share it! Simply fill an issue stating your example website or do a PR editing this REAMDE.
- Dynamic custom fonts, allow users to upload custom
.ttfor.otffiles as part of themultipart/form-datapayload (allowing ephemeral custom fonts per compile request). - Allow to refresh the font cache without needing a container restart.
- Add better diagnostics (return line and column) in the format_errors response.
- Allow to make a HTTP request with typst code (instead of a typst file). Using an
application/jsonnew endpoint/compile/source. - Handle CORS with
tower-httpand add configuration options in the env file. - Add concurrency limits (not only timeout of individual requests but also a maximum of active compilations)
- Test the API authentication all the possible cases manually.
- Publish the first image of the library to DockerHub
- Handle package cache properly, create config options in the env file as well as endpoints for handling them.
- Create an initial test suite
- Create an OpenAPI documentation
- Generate also a
linux/arm64docker image (besides the currentlinux/amd64). - Handle the creation and publishing of binaries for new releases with
cargo-dist
- Improve logs (with http-tower
tracefeature) and add the env variable of logging level to the env example - Create guides to expose the API using Tailscale Funnel and Cloudflare Tunnel
- Create a root endpoint
GET /whose json response contains useful information - Create a
GET /admin/infowith useful admin information like config options and stats - Create a very simple static client-side example
/playgroundto test the API in the browser . - Create a bash script to run the download the standalone binaries and set up a running service
- Create an
/add-fontsstatic page to easily upload fonts to the backend.
- Implement SVG output using
typst-svg - Add PNG support using default Typst PNG export or converted from SVG using
resvg - Investigate how the Typst web app handles the (still experimental) HTML export, probably with
typst-html. - Allow additional output configuration (eg. PDF or PDF-A, DPI, PDF metadata, etc.) Check the current output options of the typst compiler and the typst web app.
- Implement something similar like a 'watch' option (like the CLI) for compiling a file that is constantly changing without having to compile it all again. Maybe a websockets layer of the API. The good thing is axum already supports websockets. We could also use socketioxide (like socket.io for rust). Or maybe a 2-layer is not needed, a single WebSocket endpoint (ws://localhost:8080/watch) where the client connects, sends an initial "setup" payload with files, and then sends diffs or update messages. Or maybe none of this is needed and debounce compilation on the client-side is already enough.
- Create a way to generate versioned docker images corresponding to a few typst compiler versions. Find a way to name them properly, for example:
typst-api:0.2.2-v0.15.1ortypst-api:latest-v0.14.2. - Also create an endpoint to check for a particular package version which typst requirements it has. Maybe
typst-kitalready has some sort of package resolution/compatibility internal information(?). - Add other limits (maximum file count or maximum source size or maximum package fetching?) although maybe our current global payload limit already handles their combination correctly so that the API cannot be abused. Not very important for now as if someone imports lots of packages they'll hit the timeout limit.