API reference
The TurboStack API lets you read and manage Clients, Groups and Hosts programmatically - the same objects you manage in the TurboStack Platform. Use it to automate configuration changes and deployments from your own tooling.
- Base URL (Uniform Resource Locator):
https://my.turbostack.app - Version:
v1- every path is under/api/v1 - Format: JSON request and response bodies
- Auth: an HTTP Bearer token on every request
Interactive API explorer (Swagger)
The platform ships an interactive Swagger UI, a user interface (UI) where you can browse every
endpoint, see its parameters and response schema, and try calls live. Open it at
/swagger; the full machine-readable spec is at
/api-docs/api-docs.json.
Tip
The Swagger UI and the OpenAPI spec are the authoritative, always-current reference. This page covers every endpoint with ready-to-use examples; consult the spec for the exact, up-to-date schema of every field.
Expand an endpoint to see its parameters with their defaults and its response codes and example payloads; Try it out sends a live request from the browser.
Authentication
Every endpoint requires an Authorization: Bearer <token> header:
Authorization: Bearer <token>
Create a token under API tokens in your Profile Settings. In Swagger UI,
click Authorize and enter Bearer <token>.
Important
Treat your API token as a secret. Anyone with it can read and modify your clients, groups and hosts. Store it in an environment variable and never commit it to source control.
Rate limit
The API accepts 6000 requests per minute, counted per token owner (or per IP address for an unauthenticated request). That is generous enough that normal integrations never reach it.
Go over it and the API answers 429 Too Many Requests with a Retry-After header telling you how
many seconds to wait. The response also carries X-RateLimit-Limit and X-RateLimit-Remaining, so
you can back off before you get there.
The limit is about protecting the platform, not about pacing your work. If you are close to it, the
usual cause is polling in a tight loop. A deployment takes minutes, so check its status every few
seconds at most - see
Setup for the examples
The PHP and Python samples below assume this one-time setup. The cURL samples read the token from
the TURBOSTACK_API_TOKEN environment variable.
export TURBOSTACK_API_TOKEN="<your-token>"
<?php
// Minimal client (cURL, no dependencies). Reused by every PHP sample below.
function ts(string $method, string $path, ?array $body = null): array
{
$base = 'https://my.turbostack.app';
$token = getenv('TURBOSTACK_API_TOKEN'); // never hard-code your token
$ch = curl_init($base . $path);
$headers = ["Authorization: Bearer {$token}", "Accept: application/json"];
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
if ($body !== null) {
$headers[] = "Content-Type: application/json";
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
}
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
$res = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status >= 400) {
throw new RuntimeException("TurboStack API error: HTTP {$status} - {$res}");
}
return json_decode($res, true) ?? [];
}
import os
import requests
BASE = "https://my.turbostack.app"
session = requests.Session()
session.headers.update({
"Authorization": f"Bearer {os.environ['TURBOSTACK_API_TOKEN']}", # never hard-code your token
"Accept": "application/json",
})
The configuration model
Host and group configuration uses the same desired-state model as the TurboStack Platform. Where the
Platform shows it as YAML (the Source (YAML) view), the API represents it
as JSON. You read the current state with a GET and submit changes by POSTing an updated JSON
body. Every key of that model is listed in the
YAML configuration reference, including which level it belongs to and
what it changes on the server.
Note
Saving configuration with a POST describes the desired state; it does not deploy by itself. You
must trigger a deployment to apply it - see the
Pagination
List endpoints (clients, hosts and groups) return a paginated response. The items are under the
data array, alongside pagination metadata:
{
"data": [ ],
"current_page": 1,
"per_page": 15,
"total": 42,
"last_page": 3
}
Control paging with the page and size query parameters, and narrow the results with search.
To walk every page, request page=1 and keep incrementing page until current_page equals
last_page. A page beyond the last one is not an error: it returns 200 with an empty data.
Note
Deduplicate on id, and do not read total as a count of hosts or groups. The host and group
lists are joined to the accounts that can see them, so an entry comes back once per account that
shares its customer id. An account with sub-accounts therefore repeats the same host several
times, across page boundaries as well, and total counts those repeated rows. Collect the pages,
collapse them on id, and count what is left.
Clients
A client is an account that owns groups and hosts.
GET
/api/v1/clients
List all clients you can access. The result is a paginated list (see
Query parameters
curl "https://my.turbostack.app/api/v1/clients?page=1&size=50&search=acme" \
-H "Authorization: Bearer $TURBOSTACK_API_TOKEN" \
-H "Accept: application/json"
$clients = ts('GET', '/api/v1/clients?page=1&size=50&search=acme');
clients = session.get(
f"{BASE}/api/v1/clients",
params={"page": 1, "size": 50, "search": "acme"},
).json()
Response 200
{
"data": [
{ "id": 7, "firstname": "Ada", "lastname": "Lovelace", "email": "ada@acme.example" },
{ "id": 8, "firstname": "Alan", "lastname": "Turing", "email": "alan@example.test" }
],
"current_page": 1,
"per_page": 50,
"total": 2,
"last_page": 1
}
GET
/api/v1/clients/{id}
Fetch one client by its id.
Path parameters
curl "https://my.turbostack.app/api/v1/clients/7" \
-H "Authorization: Bearer $TURBOSTACK_API_TOKEN" \
-H "Accept: application/json"
$client = ts('GET', '/api/v1/clients/7');
client = session.get(f"{BASE}/api/v1/clients/7").json()
Response 200
{ "id": 7, "firstname": "Ada", "lastname": "Lovelace", "email": "ada@acme.example" }
GET
/api/v1/clients/{id}/hosts
List the hosts that belong to a client. The result is a paginated list (see
Path parameters
Query parameters
curl "https://my.turbostack.app/api/v1/clients/7/hosts?page=1&size=50&search=shop" \
-H "Authorization: Bearer $TURBOSTACK_API_TOKEN" \
-H "Accept: application/json"
$hosts = ts('GET', '/api/v1/clients/7/hosts?page=1&size=50&search=shop');
hosts = session.get(
f"{BASE}/api/v1/clients/7/hosts",
params={"page": 1, "size": 50, "search": "shop"},
).json()
Response 200
{
"data": [
{ "id": 123, "client_id": 7, "active": 1, "name": "shop-production" }
],
"current_page": 1,
"per_page": 50,
"total": 1,
"last_page": 1
}
GET
/api/v1/clients/{id}/groups
List the groups that belong to a client. The result is a paginated list (see
Path parameters
Query parameters
curl "https://my.turbostack.app/api/v1/clients/7/groups?page=1&size=50&search=security" \
-H "Authorization: Bearer $TURBOSTACK_API_TOKEN" \
-H "Accept: application/json"
$groups = ts('GET', '/api/v1/clients/7/groups?page=1&size=50&search=security');
groups = session.get(
f"{BASE}/api/v1/clients/7/groups",
params={"page": 1, "size": 50, "search": "security"},
).json()
Response 200
{
"data": [
{ "id": 42, "client_id": 7, "name": "shared-security" }
],
"current_page": 1,
"per_page": 50,
"total": 1,
"last_page": 1
}
Hosts
A host is a server you configure and deploy. Its configuration uses the
GET
/api/v1/hosts
List all hosts. The result is a paginated list (see search parameter.
Query parameters
curl "https://my.turbostack.app/api/v1/hosts?page=1&size=50&search=shop" \
-H "Authorization: Bearer $TURBOSTACK_API_TOKEN" \
-H "Accept: application/json"
$hosts = ts('GET', '/api/v1/hosts?page=1&size=50&search=shop');
hosts = session.get(
f"{BASE}/api/v1/hosts",
params={"page": 1, "size": 50, "search": "shop"},
).json()
Response 200
Each host carries its configuration under the json key and a live monitoring object.
{
"data": [
{
"id": 123,
"client_id": 7,
"active": 1,
"name": "shop-production",
"json": { "webserver": "nginx", "mysql_version": "8.4", "redis_enabled": true },
"monitoring": { "load": {}, "disk": {}, "memory": {} }
}
],
"current_page": 1,
"per_page": 50,
"total": 1,
"last_page": 1
}
GET
/api/v1/hosts/{id}
Fetch one host's full configuration (the desired-state model).
Path parameters
curl "https://my.turbostack.app/api/v1/hosts/123" \
-H "Authorization: Bearer $TURBOSTACK_API_TOKEN" \
-H "Accept: application/json"
$host = ts('GET', '/api/v1/hosts/123');
host = session.get(f"{BASE}/api/v1/hosts/123").json()
Response 200
The configuration model is returned under the json key.
{
"id": 123,
"client_id": 7,
"active": 1,
"name": "shop-production",
"json": {
"webserver": "nginx",
"mysql_version": "8.4",
"redis_enabled": true
},
"monitoring": { "load": {}, "disk": {}, "memory": {} }
}
POST
/api/v1/hosts/{id}
Update a host's configuration. The request body wraps the configuration model in a json key (the
same model as the Source view). This saves the desired state; it does not deploy on its own.
Path parameters
curl -X POST "https://my.turbostack.app/api/v1/hosts/123" \
-H "Authorization: Bearer $TURBOSTACK_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "json": { "webserver": "nginx", "mysql_version": "8.4", "redis_enabled": true } }'
$updated = ts('POST', '/api/v1/hosts/123', [
'json' => [
'webserver' => 'nginx',
'mysql_version' => '8.4',
'redis_enabled' => true,
],
]);
updated = session.post(
f"{BASE}/api/v1/hosts/123",
json={"json": {"webserver": "nginx", "mysql_version": "8.4", "redis_enabled": True}},
).json()
Response 200
{ "message": "Host saved successfully" }
Note
Saving does not apply the change. Trigger a deployment to apply it - see the next endpoint and Publishing changes.
POST
/api/v1/hosts/{id}/deploy
Start a deployment of the host, applying its saved configuration. The request body must specify the
deployment type.
Path parameters
Request body
deploy- a standard deploy of the current configuration.fullDeploy- a full deploy that re-applies the complete configuration.fullDeleteDeploy- a full deploy that also removes resources no longer in the configuration.
curl -X POST "https://my.turbostack.app/api/v1/hosts/123/deploy" \
-H "Authorization: Bearer $TURBOSTACK_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "type": "deploy" }'
$deploy = ts('POST', '/api/v1/hosts/123/deploy', ['type' => 'deploy']);
deploy = session.post(
f"{BASE}/api/v1/hosts/123/deploy",
json={"type": "deploy"},
).json()
Response 200
{ "message": "Deploy started" }
Note
If the host is already deploying, the endpoint returns 400 with
{ "error": "Host is currently deploying" }.
GET
/api/v1/hosts/{id}/deploy
Get the status of the host's most recent deployment.
Path parameters
curl "https://my.turbostack.app/api/v1/hosts/123/deploy" \
-H "Authorization: Bearer $TURBOSTACK_API_TOKEN" \
-H "Accept: application/json"
$status = ts('GET', '/api/v1/hosts/123/deploy');
status = session.get(f"{BASE}/api/v1/hosts/123/deploy").json()
Response 200
{
"publishing": true,
"publishing_status": "deploy",
"is_deploying": true,
"job_stdout": "",
"deploy_at": "2026-07-22T09:30:00Z",
"deploy_type": "deploy"
}
Response fields
Values of publishing_status
Tip
To wait for a deployment, poll this endpoint and stop when publishing_status is one of the
finished values, or when is_deploying is false. Do not poll faster than once every few
seconds; a deployment takes minutes, not milliseconds.
A deployment that stops reporting progress for more than five minutes is set to timeout
automatically, and publishing goes back to false so a new deployment can start. That is a
safeguard against a host being locked by a deployment that never reports back - it does not
necessarily mean the work on the server failed. Check the host's
Publishing history before you retry.
If the host has no deploy history, the endpoint returns 404 with
{ "error": "No deploy history found" }.
DELETE
/api/v1/hosts/{id}/deploy
Reset a stuck deploy state so a new deployment can be triggered.
Path parameters
curl -X DELETE "https://my.turbostack.app/api/v1/hosts/123/deploy" \
-H "Authorization: Bearer $TURBOSTACK_API_TOKEN" \
-H "Accept: application/json"
$reset = ts('DELETE', '/api/v1/hosts/123/deploy');
reset = session.delete(f"{BASE}/api/v1/hosts/123/deploy").json()
Response 200
{ "message": "Deploy reset" }
GET
/api/v1/hosts/{id}/credentials
Get the host's connection credentials. Sensitive - handle the response securely.
Path parameters
curl "https://my.turbostack.app/api/v1/hosts/123/credentials" \
-H "Authorization: Bearer $TURBOSTACK_API_TOKEN" \
-H "Accept: application/json"
$credentials = ts('GET', '/api/v1/hosts/123/credentials');
credentials = session.get(f"{BASE}/api/v1/hosts/123/credentials").json()
Response 200
The master_user and admin_user blocks are only returned for admin-access or cms-access tokens.
{
"hostname": "shop-production.example.com",
"master_user": { "user": "prod", "password": "..." },
"admin_user": { "user": "admin", "password": "..." },
"facts": {},
"network": {},
"platform": {},
"system_users": [],
"ftp_users": []
}
Groups
A group holds shared configuration that its member hosts inherit.
GET
/api/v1/groups
List all groups you can access. The result is a paginated list (see
Query parameters
curl "https://my.turbostack.app/api/v1/groups?page=1&size=50&search=security" \
-H "Authorization: Bearer $TURBOSTACK_API_TOKEN" \
-H "Accept: application/json"
$groups = ts('GET', '/api/v1/groups?page=1&size=50&search=security');
groups = session.get(
f"{BASE}/api/v1/groups",
params={"page": 1, "size": 50, "search": "security"},
).json()
Response 200
Each group carries its configuration under the json key.
{
"data": [
{
"id": 42,
"client_id": 7,
"name": "shared-security",
"json": { "firewall_whitelist": ["203.0.113.10"] }
}
],
"current_page": 1,
"per_page": 50,
"total": 1,
"last_page": 1
}
GET
/api/v1/groups/{id}
Fetch one group's configuration.
Path parameters
curl "https://my.turbostack.app/api/v1/groups/42" \
-H "Authorization: Bearer $TURBOSTACK_API_TOKEN" \
-H "Accept: application/json"
$group = ts('GET', '/api/v1/groups/42');
group = session.get(f"{BASE}/api/v1/groups/42").json()
Response 200
The configuration model is returned under the json key.
{
"id": 42,
"client_id": 7,
"name": "shared-security",
"json": { "firewall_whitelist": ["203.0.113.10"] }
}
POST
/api/v1/groups/{id}
Save or update a group's configuration. The request body wraps the configuration model in a json
key.
Path parameters
curl -X POST "https://my.turbostack.app/api/v1/groups/42" \
-H "Authorization: Bearer $TURBOSTACK_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "json": { "firewall_whitelist": ["203.0.113.10", "198.51.100.0/24"] } }'
$saved = ts('POST', '/api/v1/groups/42', [
'json' => [
'firewall_whitelist' => ['203.0.113.10', '198.51.100.0/24'],
],
]);
saved = session.post(
f"{BASE}/api/v1/groups/42",
json={"json": {"firewall_whitelist": ["203.0.113.10", "198.51.100.0/24"]}},
).json()
Response 200
{ "message": "Group saved successfully" }
Common workflow: update a host and deploy
Saving a change and applying it are two steps: POST the new configuration, then trigger a
deployment, then poll its status.
# 1. update the host's configuration (saves the desired state)
curl -X POST "https://my.turbostack.app/api/v1/hosts/123" \
-H "Authorization: Bearer $TURBOSTACK_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "json": { "webserver": "nginx", "mysql_version": "8.4" } }'
# 2. deploy the host to apply the change
curl -X POST "https://my.turbostack.app/api/v1/hosts/123/deploy" \
-H "Authorization: Bearer $TURBOSTACK_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "type": "deploy" }'
# 3. check the deployment status
curl "https://my.turbostack.app/api/v1/hosts/123/deploy" \
-H "Authorization: Bearer $TURBOSTACK_API_TOKEN"
ts('POST', '/api/v1/hosts/123', ['json' => ['webserver' => 'nginx', 'mysql_version' => '8.4']]);
ts('POST', '/api/v1/hosts/123/deploy', ['type' => 'deploy']);
$status = ts('GET', '/api/v1/hosts/123/deploy');
session.post(f"{BASE}/api/v1/hosts/123", json={"json": {"webserver": "nginx", "mysql_version": "8.4"}})
session.post(f"{BASE}/api/v1/hosts/123/deploy", json={"type": "deploy"})
status = session.get(f"{BASE}/api/v1/hosts/123/deploy").json()
Tip
Start read-only (GET) to explore your data before you make any POST change. When you do
write, try it on a host that is not business-critical first: POST /api/v1/hosts/{id} replaces
the whole configuration, so send back the complete document you read, not just the keys you
changed.
Related
- YAML configuration reference
- API tokens
- Deploy your first site
- Core concepts
- Publishing changes
- The Source (YAML) view
- Groups
- Glossary - what the terms and abbreviations mean