Sites API
Sites are addressed by id or slug in every path ({site}).
The site resource
Section titled “The site resource”{"data": { "id": "01k…", "name": "Shop", "slug": "shop", "status": "ready", "framework": "laravel", "runtime": "frankenphp", "build_mode": "native", "php_version": "8.4", "node_version": null, "repository": "acme/shop", "branch": "main", "push_to_deploy": true, "domain": "shop.example.com", "url": "https://shop.example.com", "web_directory": "public", "root_path": "/srv/falak/sites/shop", "app_port": null, "test_domain": null, "server_ids": ["01k…"], "targets": [{"id": "…", "server_id": "…", "server_name": "web-1", "server_ip": "203.0.113.1", "role": "leader", "status": "ready", "status_message": null, "command_id": null}], "strategy": "zero-downtime", "current_release": {"id": "01k…", "commit": "a1b2…", "branch": "main", "deployment_id": "01k…", "active": true}, "created_at": "2026-09-26T10:00:00+00:00"}}| Field | Values |
|---|---|
framework |
laravel, symfony, statamic, wordpress, php, next, nuxt, node, static, docker |
runtime |
frankenphp, php-fpm, node, bun, deno, static, docker, compose |
build_mode |
native, docker (on-server is not supported yet) |
targets[].role |
leader or member |
strategy, current_release |
Added by the Deployments module |
GET /api/v1/sites/{site} also returns deploy_script, shared_paths and laravel. Compose sites carry compose (below).
GET /api/v1/sites — sites.view
Section titled “GET /api/v1/sites — sites.view”All sites of the organization.
GET /api/v1/sites/{site} — sites.view
Section titled “GET /api/v1/sites/{site} — sites.view”One site.
POST /api/v1/sites — sites.create
Section titled “POST /api/v1/sites — sites.create”Same body and validation as the create form. Returns 201 with the site plus warnings[] from the git provider (for example when a webhook could not be created). Creating a site does not deploy it.
Fields
Section titled “Fields”| Field | Rule |
|---|---|
name |
Required; up to 64 characters, ^[A-Za-z0-9][A-Za-z0-9 ._-]*$, unique in the organization |
slug |
Optional; ^[a-z0-9][a-z0-9-]{0,62}$, unique; derived from the name when omitted |
framework |
Required unless runtime is compose (then default docker) |
runtime |
Optional; the preset’s default otherwise |
build_mode |
Optional; the runtime’s default otherwise |
server_ids[] |
Required; 1–50 server ids |
leader_server_id |
Optional; default the first server |
source_connection_id |
A git connection id |
repository |
Required with a connection; e.g. acme/shop or a URL for custom git |
branch |
Required with a repository |
push_to_deploy |
Boolean, default false |
php_version |
7.4–8.5 |
node_version |
18, 20, 22, 24 |
web_directory |
Relative path, no .. |
app_port |
1024–65535 (allocated from 3000–3999 when omitted, for runtimes that need one) |
docker_image |
Image reference (Docker runtime) |
dockerfile |
Path in the repository (Docker builds) |
root_directory |
Git sites (also PATCH): the repository subfolder the app lives in, e.g. apps/api; relative, surrounding slashes trimmed, no ./.. segments. Returned as root_directory (null = the repository root). See Monorepos. |
health_check_path |
Starts with / |
test_domain_enabled |
Boolean |
isolated |
Boolean: own Linux user for the site |
variables |
Object {KEY: value}, up to 500; initial environment; ${{ service.KEY }} allowed |
domain |
A domain choice (not for Compose sites) |
project_id, environment_id |
Placement; default: the Default project’s production environment. An environment of another organization or project is a 422. |
template |
{slug, version, source: catalog|custom} |
Domain choices
Section titled “Domain choices”| Value | Result |
|---|---|
{"type": "generated"} |
<slug>.<leader-ip-with-dashes>.sslip.io (the organization’s generated-domain suffix) |
{"type": "custom", "name": "shop.example.com"} or "shop.example.com" |
Your domain, with automatic TLS once DNS points at the server |
{"type": "test"} |
Only the test domain (422 when none is configured) |
The chosen domain becomes the site’s primary domain and APP_URL in the initial environment. A name used by another site is a 422. Without domain, the site only gets its test domain.
Example
Section titled “Example”curl -X POST https://falak.example.com/api/v1/sites \ -H "Authorization: Bearer $FALAK_TOKEN" -H "Accept: application/json" -H "Content-Type: application/json" \ -d '{ "name": "Shop", "framework": "laravel", "server_ids": ["01k8aaaa…", "01k8bbbb…"], "leader_server_id": "01k8aaaa…", "source_connection_id": "01k8cccc…", "repository": "acme/shop", "branch": "main", "push_to_deploy": true, "php_version": "8.4", "domain": {"type": "custom", "name": "shop.example.com"}, "variables": {"DB_URL": "${{ shop-db.DATABASE_URL }}"} }'Docker Compose sites
Section titled “Docker Compose sites”Set runtime: "compose" and:
| Field | Rule |
|---|---|
compose_source |
repo or inline |
compose_file |
repo: path in the repository; default compose.yaml, then docker-compose.yml |
compose_content |
inline: the Compose file (up to 256 KiB, no build:; must pass the policy unless privileged compose is allowed) |
public_services[] |
Up to 20 {service, port, domain?, health_check_path?}; domain is a name or a domain choice; null / {"type": "test"} means the test domain. Generated names are <service>-<slug>.<ip-with-dashes>.<suffix>. health_check_path is what the deploy health check requests through the service’s domain (without it: the site’s check path for the first service, any answer below 500 for the others). |
compose_files |
repo: list of compose files in -f order (the first is the project file) |
compose_profiles |
repo: list of profiles to run |
compose_services |
repo and inline: {<service>: {mode: keep|database|site, engine?, database_id?, site?}} — run a service as a Falak database or its own site. engine: postgresql|mysql|mariadb|redis|valkey; redis/valkey for services on the official redis / valkey/valkey images (see A Falak Redis or Valkey) |
compose_adjustments |
repo: {keep_binds: ["service:./path"]} — missing bind sources kept as folders instead of named volumes |
With compose_files, creation reads the repository first: the files must load, public services must exist and required ${VAR}s need a value in variables (422 otherwise).
The response then carries compose {source, file, version, public_services[] (with host_port, test_domain, url, health_check_path), template}. After creation, public_services[].domain mirrors the service’s primary domain; manage a service’s domains in Settings → Networking. See Docker Compose and Compose apps from git.
GET /api/v1/sites/{site}/env — sites.env.view
Section titled “GET /api/v1/sites/{site}/env — sites.env.view”Returns the latest environment version as dotenv. Recorded in the audit log as a reveal. Rate limited to 60/minute.
{"data": {"content": "APP_ENV=production\nAPP_KEY=base64:…\n", "version": 3}}PUT /api/v1/sites/{site}/env — sites.env.manage
Section titled “PUT /api/v1/sites/{site}/env — sites.env.manage”Body {"content": "<dotenv>"} replaces all variables (which keys are exposed to the deploy script is kept). Takes effect on the next deployment.
{"data": {"version": 4, "changed": true, "keys": ["APP_ENV", "APP_KEY"]}}422 with errors.content when the dotenv cannot be parsed.
PUT /api/v1/sites/{site}/laravel — sites.manage
Section titled “PUT /api/v1/sites/{site}/laravel — sites.manage”Laravel toggles; each is optional (unchanged when omitted):
| Field | Meaning |
|---|---|
scheduler |
Run schedule:run every minute on the leader |
horizon |
Supervise php artisan horizon |
octane |
Run Laravel Octane |
octane_server |
frankenphp (FrankenPHP runtime only; its default), swoole (default on PHP-FPM) or roadrunner |
maintenance |
artisan down --retry=60 / artisan up on every server immediately |
{"data": {"scheduler": true, "horizon": false, "octane": true, "maintenance": false, "octane_server": "frankenphp", "octane_port": 8412}}The Octane port is allocated by Falak and cannot be set. 422 for a Laravel toggle on a non-Laravel site, an unavailable server, or no free port. See Laravel Octane.