API
Projects and backup tokens can be managed not only in the portal but through an
HTTP API — for your own automation or through the
Terraform provider. The API speaks JSON, lives at
https://api.lionbackup.cloud/api/v1 and applies exactly the same permission
checks as the portal.
Creating a key
In the portal under Developer you create a service account and an API key for it. The key is shown exactly once — keep it somewhere safe.
A service account has exactly the permissions of the human who owns it. Someone who may not create projects in the portal cannot create them through the API either; deactivating the account or deleting the key closes the door immediately.
Exchanging the key for an access token
The key is not a bearer token: you first exchange it for a short-lived access token (a JWT, valid for 15 minutes).
ACCESS_TOKEN=$(curl -s https://authentik.prod.lionbackup.cloud/application/o/token/ \
-d grant_type=client_credentials \
-d client_id=lionbackup-api \
-d client_secret="$LIONBACKUP_API_KEY" \
-d scope="profile lionbackup_api" | jq -r .access_token)
Always use the host authentik.prod.lionbackup.cloud for the exchange: the
short name authentik.lionbackup.cloud answers with a redirect, and a POST
does not follow redirects — the call would return no token.
Then send the token as the Authorization header:
API=https://api.lionbackup.cloud/api/v1
curl -s "$API/organizations" -H "Authorization: Bearer $ACCESS_TOKEN"
The development environment uses https://api.dev.lionbackup.cloud/api/v1
and https://authentik.dev.lionbackup.cloud/application/o/token/. A key is
valid only in the environment it was created in.
Step by step: create a project and backup tokens
The following calls build on each other and assume ACCESS_TOKEN and API
from the previous section. The identifiers in the responses are examples.
1. List the zones
A project needs the identifier (UUID) of the zone its data will live in. Only
zones with status = active can be booked.
curl -s "$API/zones" -H "Authorization: Bearer $ACCESS_TOKEN"
{
"zones": [
{
"id": "3f0c2a4e-8b1d-4c6a-9e2f-5d7b1a9c0e31",
"name": "de01-1",
"status": "active",
"provider": "Hetzner",
"location_city": "Nürnberg",
"storage_type": "managed object storage"
},
{
"id": "7a9d4b2c-1e5f-4a83-b6c0-2f8e9d1c4a57",
"name": "de01-2",
"status": "preparing",
"provider": "Hetzner",
"location_city": "Falkenstein",
"storage_type": "managed object storage"
}
]
}
The identifier of the zone you want goes straight into a variable:
ZONE_ID=$(curl -s "$API/zones" -H "Authorization: Bearer $ACCESS_TOKEN" \
| jq -r '.zones[] | select(.name == "de01-1") | .id')
2. Find your organization
Projects belong to an organization. The list shows every organization you are
a member of, together with your role — creating projects requires owner or
admin.
curl -s "$API/organizations" -H "Authorization: Bearer $ACCESS_TOKEN"
{
"organizations": [
{
"id": "9b2e6c1a-4d3f-4e8b-a7c5-0f1d2e3a4b5c",
"name": "Example Ltd",
"status": "approved",
"role": "owner"
}
]
}
ORG_ID=$(curl -s "$API/organizations" -H "Authorization: Bearer $ACCESS_TOKEN" \
| jq -r '.organizations[] | select(.name == "Example Ltd") | .id')
Selecting by name is deliberate: the service account sees every organization of its owner, and the order of the list is not guaranteed.
3. Create a project
Required fields are name (at most 100 characters) and availability_zone_id.
Optional are alert_email, billing_reference, immutable_storage (default
false) and — only with immutable storage — retention_days (1 to 365,
default 30).
curl -s -X POST "$API/organizations/$ORG_ID/projects" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"name\":\"web-servers\",\"availability_zone_id\":\"$ZONE_ID\",\"alert_email\":\"ops@example.com\"}"
Response with status 201:
{
"project": {
"id": "ea1296ad-5c7b-4f2e-8a1d-3b6c9e0f7d24",
"name": "web-servers",
"status": "active",
"organization_id": "9b2e6c1a-4d3f-4e8b-a7c5-0f1d2e3a4b5c",
"availability_zone_id": "3f0c2a4e-8b1d-4c6a-9e2f-5d7b1a9c0e31",
"alert_email": "ops@example.com",
"billing_reference": null,
"immutable_storage": false,
"retention_days": null,
"created_at": "2026-09-24 09:41:12.418273"
}
}
PROJECT_ID=ea1296ad-5c7b-4f2e-8a1d-3b6c9e0f7d24
4. Create backup tokens
A backup token belongs to exactly one project. With type = write the
client backs up, with type = read it restores. Without any body you get a
write token for Linux; operating_system accepts Linux, Windows and
macOS. Optionally usage_count_limit, rate_limit_per_minute and
rate_limit_per_hour cap its use.
curl -s -X POST "$API/projects/$PROJECT_ID/tokens" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"type":"write","operating_system":"Linux"}'
Response with status 201:
{
"token": {
"id": "c4d1e8f2-6a3b-4c9d-8e7f-1a2b3c4d5e6f",
"type": "write",
"secret": "9f3e7b1c…a41c"
}
}
secret is the actual backup token (128 hexadecimal characters). It is handed
out exactly once and cannot be retrieved later — the platform stores only a
hash. Put it into your secrets manager right away.
A read token for restores is created the same way:
curl -s -X POST "$API/projects/$PROJECT_ID/tokens" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{"type":"read"}'
{
"token": {
"id": "5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9",
"type": "read",
"secret": "d27c0a8e…5b93"
}
}
5. List tokens
The list carries metadata only — never a secret. Revoked tokens no longer appear.
curl -s "$API/projects/$PROJECT_ID/tokens" -H "Authorization: Bearer $ACCESS_TOKEN"
{
"tokens": [
{
"id": "5e6f7a8b-9c0d-4e1f-a2b3-c4d5e6f7a8b9",
"type": "read",
"operating_system": "Linux",
"is_active": true,
"usage_count": 0,
"usage_count_limit": null,
"created_at": "2026-09-24 09:43:05.102944",
"expire": null
},
{
"id": "c4d1e8f2-6a3b-4c9d-8e7f-1a2b3c4d5e6f",
"type": "write",
"operating_system": "Linux",
"is_active": true,
"usage_count": 0,
"usage_count_limit": null,
"created_at": "2026-09-24 09:42:37.556210",
"expire": null
}
]
}
6. Revoke a token
TOKEN_ID=c4d1e8f2-6a3b-4c9d-8e7f-1a2b3c4d5e6f
curl -s -X DELETE "$API/projects/$PROJECT_ID/tokens/$TOKEN_ID" \
-H "Authorization: Bearer $ACCESS_TOKEN"
{ "deleted": true }
7. Close a project
A project is not deleted but closed: its status changes to closing, every
write token is revoked immediately, and the response reports how many. The
call is idempotent — a project that is already closed answers 200 again.
curl -s -X POST "$API/projects/$PROJECT_ID/close" \
-H "Authorization: Bearer $ACCESS_TOKEN"
{
"project": {
"id": "ea1296ad-5c7b-4f2e-8a1d-3b6c9e0f7d24",
"name": "web-servers",
"status": "closing",
"organization_id": "9b2e6c1a-4d3f-4e8b-a7c5-0f1d2e3a4b5c",
"availability_zone_id": "3f0c2a4e-8b1d-4c6a-9e2f-5d7b1a9c0e31",
"alert_email": "ops@example.com",
"billing_reference": null,
"immutable_storage": false,
"retention_days": null,
"created_at": "2026-09-24 09:41:12.418273"
},
"write_tokens_revoked": 1
}
What you should know
Permissions per action
The API checks the same roles as the portal:
| Action | Role |
|---|---|
| read a project, list tokens | any role in the project |
| update a project, create tokens | owner, admin, writer |
| revoke a token | owner, admin |
| create or close a project | owner or admin of the organization |
404 instead of 403
A resource that does not exist and one you hold no role on answer alike: 404
with {"error":"not_found"}. That way the API does not reveal which
identifiers exist. You get a 403 with permission_denied only when you may
see the resource but not perform the action.
Zone publication is asynchronous
A new token is published to the storage zone after it has been created. Until
that has happened the zone rejects the token with 401 — usually a matter of
seconds, in case of a fault until the next reconciliation. Do not create
the token again; retry later instead. Conversely, a revoked token may remain
valid in the zone for a short while.
Closing a project
Closing revokes every write token immediately and sets the project to
closing. Read tokens stay valid for the retention period, and as long as a
backup is still within its retention you can also create new read tokens to
keep restoring stored backups. A closing project refuses new write tokens
(409 project_closed); once it is closed it accepts no tokens at all.
Likewise, a closed organization refuses new projects (409 organization_closed).
Rate limits
| Limit | Value | Applies to |
|---|---|---|
| Requests per IP address | 60/minute | every request to the API |
| Requests per service account | 60/minute | every authenticated request |
| Size of one request | 1 MB | the body you send |
Every authenticated response carries X-RateLimit-Limit,
X-RateLimit-Remaining and X-RateLimit-Reset (unix time at which the current
one-minute window ends). Over the limit the API answers 429 with
{"error":"rate_limited"} and a Retry-After header — wait that many seconds
and retry. The Terraform provider does this by itself.
The X-RateLimit-* headers can be absent: if the platform's counter cache is
down, the API temporarily does not meter per service account and then announces
no budget either. In your automation rely on the 429 status and
Retry-After, not on the headers.
Error format
Every error is a JSON object with exactly one field: {"error":"<code>"}. The
code is stable and meant for programs; branch on it rather than on the HTTP
status alone.
| Status | Code | Meaning |
|---|---|---|
400 | invalid_request | the body is not JSON, a required field is missing or a value is invalid |
400 | invalid_zone | availability_zone_id is unknown or the zone is not active |
400 | retention_out_of_range | retention_days is outside 1 to 365 |
401 | invalid_token | access token missing, invalid or expired, or service account deleted |
403 | owner_inactive | the human who owns the service account is deactivated |
403 | permission_denied | your role does not allow this action |
404 | not_found | resource unknown or not visible to you |
405 | method_not_allowed | the path exists, the HTTP method does not |
409 | organization_closed | the organization is closing or closed |
409 | project_closed | the project is closed, or closing and a write token was requested |
429 | rate_limited | rate limit — honour Retry-After |
500 | internal_error | unexpected error on the platform |
503 | service_unavailable | temporarily unavailable — retry later |
Terraform
For Terraform and OpenTofu there is a provider that uses the same API and manages projects and backup tokens as resources. Source, setup and a complete example are on the Terraform page.
Reference
The full specification is available as an OpenAPI 3.1 document:
openapi.yaml. The API serves the same file at
/api/v1/openapi.yaml — so it always describes exactly the version that is
running.
Loading the API reference…