Automating / API
API
Reading your account without a browser, and proposing changes that wait for a person.
The API reads. It does not change anything – changes go through change requests, which wait for a person.
There are no sessions, cookies or CSRF here. A token says which account, so a client never has to know the account id, and the API sits outside the account prefix.
Tokens
Account → API tokens → New, as an admin. Choose whether it may propose changes; read-only is the default and the right answer most of the time.
The token is shown once. Only its digest is stored, so a stolen database yields nothing and nobody can be shown their token a second time. Tokens are prefixed, so one found in a log or a repository is recognisable for what it is.
Revoke one any time from the same page.
Authenticating
Authorization: Bearer kc_xxxxxxxxxxxx
The older Token token="..." scheme is understood too. A request without a valid token gets a
401 saying what to send.
Endpoints
Servers
GET /api/v1/servers
GET /api/v1/servers/:id
{
"id": 12,
"name": "web-1",
"provider": "hetzner",
"status": "running",
"api_managed": true,
"ip": "203.0.113.10",
"server_type": "cx42",
"location": "nbg1",
"cores": 8,
"memory": 16.0,
"disk": 160,
"os": "Ubuntu 24.04",
"hardening_status": "succeeded",
"created_at": "2026-01-14T09:02:11Z"
}
api_managed is there so a caller can tell why a self-hosted machine reports no type,
location or price, rather than treating the nulls as an error.
Apps
GET /api/v1/apps
GET /api/v1/apps/:id
Each app carries its repository and its environments, with branch, service and server.
Actions
GET /api/v1/actions
The catalogue of what may be proposed, with the parameters each action takes.
Change requests
GET /api/v1/change_requests[?status=pending|approved|applied|rejected|failed]
GET /api/v1/change_requests/:id
POST /api/v1/change_requests
POST needs a token with write access, and answers 202 Accepted:
curl -X POST https://kamalcloud.example.com/api/v1/change_requests \
-H "Authorization: Bearer $KAMAL_CLOUD_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"action_name": "environments.deploy",
"params": { "environment_id": 7 }
}'
{
"change_request": {
"id": 41,
"action": "environments.deploy",
"params": { "environment_id": 7 },
"status": "pending",
"description": "Deploy environment 7.",
"error": null,
"result": null,
"requested_at": "2026-09-14T11:20:04Z",
"decided_at": null,
"applied_at": null,
"decided_by": null
}
}
Nothing has been deployed. Somebody has to approve it. Once they have, decided_at and
decided_by say when and who, applied_at and result say what the work produced, and error
says why it failed if it did.
Responses
Lists return at most 50 entries. ?limit= asks for fewer; there is no page or offset. Errors are JSON with an error key: 401 for a bad token, 403 for a
read-only token proposing, 404 for something not in your account, 422 for a request the action
would not accept – which also returns the catalogue, so a client can correct itself.
Giving a token to an agent
This is what the design is for. An LLM agent with a KamalCloud token can answer questions about your fleet all day and cannot touch it. The only thing it can do is put a clearly described proposal in front of a person.