openapi: 3.1.0 info: title: lionbackup Public API version: "1.1.0-alpha" description: | Automation API for lionbackup — the backend behind the Terraform provider. **Authentication**: OAuth2 client credentials against Authentik. Exchange a service-account API key (created in the portal under *Developer*) for a short-lived JWT (≈15 min) at the token endpoint, then send it as a Bearer token. The API validates the JWT purely against the provider's JWKS and checks on every request that the service account is still active — deleting a key in the portal takes effect immediately. **Permission parity**: a service account has exactly the permissions of the human user who owns it. Requests are authorized by the same role resolution the portal UI uses. **Rate limits**: 60 requests per minute per client IP (enforced at the edge, at the portal and in the application) and 60 requests per minute per service account. Every authenticated response carries `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset` (unix time at which the current one-minute window ends) — unless the counter store is unavailable, in which case the per-account limit fails open and the three headers are absent. Over budget the API answers `429 {"error":"rate_limited"}` with a `Retry-After` header (seconds); wait that long and retry. Request bodies are capped at 1 MB. **Errors**: every error body is `{"error":""}` with a stable snake_case code from the `Error` schema. Each operation lists the codes it can answer with. Two codes apply to every path and are therefore not repeated per operation: `method_not_allowed` (HTTP 405, a known path with a method it does not offer) and `internal_error` (HTTP 500). **This document**: `GET /openapi.yaml` (no authentication) returns the specification exactly as deployed. contact: email: support@lionbackup.cloud servers: - url: https://api.prod.lionbackup.cloud/api/v1 description: production - url: https://api.dev.lionbackup.cloud/api/v1 description: development security: - bearerJwt: [] components: securitySchemes: bearerJwt: type: http scheme: bearer bearerFormat: JWT description: | Obtain via client credentials at `https://authentik..lionbackup.cloud/application/o/token/` with `client_id=lionbackup-api`, `client_secret=`, `grant_type=client_credentials`, `scope=profile lionbackup_api`. headers: X-RateLimit-Limit: description: | Requests allowed per one-minute window for this service account. Absent when the counter store (memcached) is unavailable: the per-account limit then fails open rather than blocking, and no budget headers are sent (the per-IP limits still apply). schema: { type: integer } X-RateLimit-Remaining: description: | Requests left in the current window (0 when limited). Absent when the counter store is unavailable (fail-open, see `X-RateLimit-Limit`). schema: { type: integer } X-RateLimit-Reset: description: | Unix time at which the current window ends. Absent when the counter store is unavailable (fail-open, see `X-RateLimit-Limit`). schema: { type: integer } schemas: Error: type: object required: [error] properties: error: type: string description: | Machine-readable snake_case code; clients branch on it, so the set is part of the contract. Never a human sentence. | code | HTTP | meaning | | --- | --- | --- | | `invalid_token` | 401 | no/malformed Bearer, signature or claims fail, or the service account was revoked | | `owner_inactive` | 403 | the human owner of the service account is not active | | `permission_denied` | 403 | the owner's role does not allow this action | | `not_found` | 404 | unknown resource, or no role on it (no existence leak) | | `method_not_allowed` | 405 | known path, method not offered | | `invalid_request` | 400 | body is not a JSON object, or a field fails validation | | `invalid_zone` | 400 | `availability_zone_id` is not the id of an active zone | | `retention_out_of_range` | 400 | `retention_days` outside the allowed bounds | | `organization_closed` | 409 | the organization is `closing`/`closed` | | `project_closed` | 409 | the project is `closed`/`deleted`, or `closing` and a write token was asked for (read tokens stay available while files are in retention) | | `rate_limited` | 429 | over the per-service-account budget | | `internal_error` | 500 | unexpected failure; retrying does not help | | `service_unavailable` | 503 | database or identity provider unreachable; retry later | | `usage_limit_exceeded` | 403 | *backup-client token path only:* the project token used up its `usage_count_limit` | | `project_closed` | 403 | *backup-client token path only:* the project is `closed`/`deleted`, or `closing` and the token is a write token (read tokens keep resolving while files are in retention) | enum: - invalid_token - owner_inactive - permission_denied - not_found - method_not_allowed - invalid_request - invalid_zone - retention_out_of_range - organization_closed - project_closed - rate_limited - internal_error - service_unavailable - usage_limit_exceeded Health: type: object required: [status, checks] properties: status: type: string enum: [ok, degraded, down] description: | `ok` everything reachable, `degraded` the cache is missing but the API answers, `down` a required dependency is gone (HTTP 503). checks: type: object description: | One entry per dependency. `ok` or `down`; `unknown` means the check was skipped because the cache is down — without the cache the probe's cost per call is unbounded, so only free local checks run. `unknown` is not an outage: it yields `degraded`, never `down`. properties: database: type: string enum: [ok, down, unknown] description: the read path every request uses (hard dependency) database_primary: type: string enum: [ok, down, unknown] description: the write path of POST/PATCH/DELETE (hard dependency) cache: type: string enum: [ok, down, unknown] description: memcached — rate counter and JWKS cache (soft dependency) authentik: type: string enum: [ok, down, unknown] description: the identity provider's signing keys (hard dependency) Organization: type: object properties: id: { type: string, format: uuid } name: { type: string } status: type: string enum: [registered, pending, approved, rejected, suspended, closing, closed, deleted] description: | `approved` is the usable state — every organization created in the portal starts there, and it is the one to pick when creating projects. `closing` and `closed` are the owner-initiated soft-close states; `createProject` refuses them with 409 `organization_closed`. The remaining values are administrative states set by lionbackup staff. role: type: string enum: [owner, admin, writer, reader, billing, auditor, member] description: the owning user's effective role Project: type: object properties: id: { type: string, format: uuid } name: { type: string, maxLength: 100 } status: { type: string, enum: [active, closing, closed, deleted] } organization_id: { type: string, format: uuid } availability_zone_id: type: string format: uuid description: | UUID of the zone as returned by `GET /zones` (field `id`). The zone's name such as `de01-1` is found there under `name`. alert_email: { type: [string, "null"], format: email } billing_reference: { type: [string, "null"] } immutable_storage: { type: boolean } retention_days: { type: [integer, "null"] } created_at: { type: string } TokenMeta: type: object properties: id: { type: string, format: uuid } type: { type: string, enum: [read, write] } operating_system: { type: string, enum: [Linux, Windows, macOS], description: macOS ist Alpha (experimentell) } is_active: { type: boolean } usage_count: { type: integer } usage_count_limit: { type: [integer, "null"] } created_at: { type: string } expire: { type: [string, "null"] } responses: Unauthorized: description: "`invalid_token`: missing/invalid/expired JWT, or the service account was revoked" content: application/json: schema: allOf: - $ref: "#/components/schemas/Error" - type: object properties: error: { enum: [invalid_token] } OwnerInactive: description: "`owner_inactive`: the human owner of the service account is not active" content: application/json: schema: allOf: - $ref: "#/components/schemas/Error" - type: object properties: error: { enum: [owner_inactive] } Forbidden: description: | `permission_denied`: authenticated, but the owner's role does not allow this action. `owner_inactive`: the owner is not active. content: application/json: schema: allOf: - $ref: "#/components/schemas/Error" - type: object properties: error: { enum: [permission_denied, owner_inactive] } NotFound: description: "`not_found`: unknown resource OR no role on it (no existence leak)" content: application/json: schema: allOf: - $ref: "#/components/schemas/Error" - type: object properties: error: { enum: [not_found] } RateLimited: description: | `rate_limited`: over the per-IP (60/min) or per-service-account (60/min) budget. Wait `Retry-After` seconds and retry; the Terraform provider does this by itself. headers: Retry-After: description: seconds until the current window ends schema: { type: integer, minimum: 1 } X-RateLimit-Limit: { $ref: "#/components/headers/X-RateLimit-Limit" } X-RateLimit-Remaining: { $ref: "#/components/headers/X-RateLimit-Remaining" } X-RateLimit-Reset: { $ref: "#/components/headers/X-RateLimit-Reset" } content: application/json: schema: allOf: - $ref: "#/components/schemas/Error" - type: object properties: error: { enum: [rate_limited] } ServiceUnavailable: description: | `service_unavailable`: the database or the identity provider is unreachable, so permissions cannot be resolved. Transient — retry later rather than inspecting the token. content: application/json: schema: allOf: - $ref: "#/components/schemas/Error" - type: object properties: error: { enum: [service_unavailable] } paths: /openapi.yaml: get: summary: This specification, as deployed operationId: getOpenApi security: [] responses: "429": { $ref: "#/components/responses/RateLimited" } "200": description: the OpenAPI 3.1 document content: application/yaml: schema: { type: string } /health: get: summary: Service health and dependency state description: | Unauthenticated, for uptime checks. `status` is `ok` when everything the API needs is reachable, `degraded` when only the cache is missing (the API still answers, just without a per-account rate limit, and the checks that need the cache report `unknown`), and `down` when the database or the identity provider is unreachable. `down` answers with HTTP 503 so a probe fails on it; `degraded` stays 200. The result is cached for a few seconds, so hitting this often costs nothing extra. It carries no counts and no customer data. operationId: getHealth security: [] responses: "429": { $ref: "#/components/responses/RateLimited" } "200": description: the API is serving (status `ok` or `degraded`) content: application/json: schema: { $ref: "#/components/schemas/Health" } "503": description: a required dependency is unreachable (status `down`) content: application/json: schema: { $ref: "#/components/schemas/Health" } /zones: get: summary: List availability zones (for project creation) operationId: listZones responses: "429": { $ref: "#/components/responses/RateLimited" } "200": description: all zones with status headers: X-RateLimit-Limit: { $ref: "#/components/headers/X-RateLimit-Limit" } X-RateLimit-Remaining: { $ref: "#/components/headers/X-RateLimit-Remaining" } X-RateLimit-Reset: { $ref: "#/components/headers/X-RateLimit-Reset" } content: application/json: schema: type: object properties: zones: type: array items: type: object properties: id: { type: string, format: uuid } name: { type: string, examples: [de01-1] } status: { type: string } provider: { type: string } location_city: { type: string } storage_type: { type: string } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/OwnerInactive" } "503": { $ref: "#/components/responses/ServiceUnavailable" } /whoami: get: summary: Acting identity operationId: whoami responses: "429": { $ref: "#/components/responses/RateLimited" } "200": description: service account, owner, and effective org roles headers: X-RateLimit-Limit: { $ref: "#/components/headers/X-RateLimit-Limit" } X-RateLimit-Remaining: { $ref: "#/components/headers/X-RateLimit-Remaining" } X-RateLimit-Reset: { $ref: "#/components/headers/X-RateLimit-Reset" } content: application/json: schema: type: object properties: service_account: { type: string } owner_id: { type: string, format: uuid } owner_email: { type: string, format: email } organizations: type: object additionalProperties: { type: string } description: org id → effective role "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/OwnerInactive" } "503": { $ref: "#/components/responses/ServiceUnavailable" } /organizations: get: summary: List the owner's organizations operationId: listOrganizations responses: "429": { $ref: "#/components/responses/RateLimited" } "200": description: organizations with the owner's role headers: X-RateLimit-Limit: { $ref: "#/components/headers/X-RateLimit-Limit" } X-RateLimit-Remaining: { $ref: "#/components/headers/X-RateLimit-Remaining" } X-RateLimit-Reset: { $ref: "#/components/headers/X-RateLimit-Reset" } content: application/json: schema: type: object properties: organizations: type: array items: { $ref: "#/components/schemas/Organization" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/OwnerInactive" } "503": { $ref: "#/components/responses/ServiceUnavailable" } /organizations/{orgId}/projects: parameters: - { name: orgId, in: path, required: true, schema: { type: string, format: uuid } } get: summary: List projects of an organization operationId: listProjects responses: "429": { $ref: "#/components/responses/RateLimited" } "200": description: projects visible to the owner headers: X-RateLimit-Limit: { $ref: "#/components/headers/X-RateLimit-Limit" } X-RateLimit-Remaining: { $ref: "#/components/headers/X-RateLimit-Remaining" } X-RateLimit-Reset: { $ref: "#/components/headers/X-RateLimit-Reset" } content: application/json: schema: type: object properties: projects: type: array items: { $ref: "#/components/schemas/Project" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/OwnerInactive" } "404": { $ref: "#/components/responses/NotFound" } "503": { $ref: "#/components/responses/ServiceUnavailable" } post: summary: Create a project (org owner/admin) operationId: createProject requestBody: required: true content: application/json: schema: type: object required: [name, availability_zone_id] properties: name: { type: string, maxLength: 100 } availability_zone_id: type: string format: uuid description: | UUID of an active zone as returned by `GET /zones` (field `id`). The zone's name such as `de01-1` is found there under `name`; passing the name answers 400 `invalid_zone`. alert_email: { type: string, format: email } billing_reference: { type: string } immutable_storage: { type: boolean, default: false } retention_days: { type: integer } auto_delete_after_retention: { type: boolean, default: true } responses: "429": { $ref: "#/components/responses/RateLimited" } "201": description: created headers: X-RateLimit-Limit: { $ref: "#/components/headers/X-RateLimit-Limit" } X-RateLimit-Remaining: { $ref: "#/components/headers/X-RateLimit-Remaining" } X-RateLimit-Reset: { $ref: "#/components/headers/X-RateLimit-Reset" } content: application/json: schema: type: object properties: project: { $ref: "#/components/schemas/Project" } "400": description: | `invalid_request`: body not a JSON object, `name` empty or longer than 100 characters, or `alert_email` malformed. `invalid_zone`: `availability_zone_id` is not the id of an active zone. `retention_out_of_range`: `retention_days` outside the allowed bounds (only checked with `immutable_storage`). content: application/json: schema: allOf: - $ref: "#/components/schemas/Error" - type: object properties: error: { enum: [invalid_request, invalid_zone, retention_out_of_range] } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "409": description: "`organization_closed`: the organization is closing/closed" content: application/json: schema: allOf: - $ref: "#/components/schemas/Error" - type: object properties: error: { enum: [organization_closed] } "503": description: | `service_unavailable`: the database or the identity provider is unreachable — also when the project could not be created in the identity provider (owner group), in which case nothing was created and the request can be retried. content: application/json: schema: allOf: - $ref: "#/components/schemas/Error" - type: object properties: error: { enum: [service_unavailable] } /projects/{projectId}: parameters: - { name: projectId, in: path, required: true, schema: { type: string, format: uuid } } get: summary: Get a project operationId: getProject responses: "429": { $ref: "#/components/responses/RateLimited" } "200": description: the project headers: X-RateLimit-Limit: { $ref: "#/components/headers/X-RateLimit-Limit" } X-RateLimit-Remaining: { $ref: "#/components/headers/X-RateLimit-Remaining" } X-RateLimit-Reset: { $ref: "#/components/headers/X-RateLimit-Reset" } content: application/json: schema: type: object properties: project: { $ref: "#/components/schemas/Project" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/OwnerInactive" } "404": { $ref: "#/components/responses/NotFound" } "503": { $ref: "#/components/responses/ServiceUnavailable" } patch: summary: Update name / alert email / billing reference (owner/admin/writer) operationId: updateProject requestBody: required: true content: application/json: schema: type: object minProperties: 1 properties: name: { type: string, maxLength: 100 } alert_email: { type: [string, "null"], format: email } billing_reference: { type: [string, "null"] } responses: "429": { $ref: "#/components/responses/RateLimited" } "200": description: updated project headers: X-RateLimit-Limit: { $ref: "#/components/headers/X-RateLimit-Limit" } X-RateLimit-Remaining: { $ref: "#/components/headers/X-RateLimit-Remaining" } X-RateLimit-Reset: { $ref: "#/components/headers/X-RateLimit-Reset" } content: application/json: schema: type: object properties: project: { $ref: "#/components/schemas/Project" } "400": description: "`invalid_request`: body not a JSON object, no known field, or a field fails validation" content: application/json: schema: allOf: - $ref: "#/components/schemas/Error" - type: object properties: error: { enum: [invalid_request] } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "503": { $ref: "#/components/responses/ServiceUnavailable" } /projects/{projectId}/close: parameters: - { name: projectId, in: path, required: true, schema: { type: string, format: uuid } } post: summary: Close a project (org owner/admin; idempotent) description: | Soft close, portal parity: status → closing, every live WRITE token is revoked (read tokens survive while files remain in retention). operationId: closeProject responses: "429": { $ref: "#/components/responses/RateLimited" } "200": description: closing/closed project headers: X-RateLimit-Limit: { $ref: "#/components/headers/X-RateLimit-Limit" } X-RateLimit-Remaining: { $ref: "#/components/headers/X-RateLimit-Remaining" } X-RateLimit-Reset: { $ref: "#/components/headers/X-RateLimit-Reset" } content: application/json: schema: type: object properties: project: { $ref: "#/components/schemas/Project" } write_tokens_revoked: { type: integer } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "503": { $ref: "#/components/responses/ServiceUnavailable" } /projects/{projectId}/tokens: parameters: - { name: projectId, in: path, required: true, schema: { type: string, format: uuid } } get: summary: List backup tokens (metadata only) operationId: listTokens responses: "429": { $ref: "#/components/responses/RateLimited" } "200": description: token metadata — never a secret headers: X-RateLimit-Limit: { $ref: "#/components/headers/X-RateLimit-Limit" } X-RateLimit-Remaining: { $ref: "#/components/headers/X-RateLimit-Remaining" } X-RateLimit-Reset: { $ref: "#/components/headers/X-RateLimit-Reset" } content: application/json: schema: type: object properties: tokens: type: array items: { $ref: "#/components/schemas/TokenMeta" } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/OwnerInactive" } "404": { $ref: "#/components/responses/NotFound" } "503": { $ref: "#/components/responses/ServiceUnavailable" } post: summary: Create a backup token (owner/admin/writer) description: The raw token appears ONLY in this response (show-once). operationId: createToken requestBody: content: application/json: schema: type: object properties: type: { type: string, enum: [read, write], default: write } operating_system: { type: string, enum: [Linux, Windows, macOS], default: Linux, description: macOS ist Alpha (experimentell) } usage_count_limit: { type: integer } rate_limit_per_minute: { type: integer } rate_limit_per_hour: { type: integer } responses: "429": { $ref: "#/components/responses/RateLimited" } "201": description: created — store the secret now, it is not retrievable headers: X-RateLimit-Limit: { $ref: "#/components/headers/X-RateLimit-Limit" } X-RateLimit-Remaining: { $ref: "#/components/headers/X-RateLimit-Remaining" } X-RateLimit-Reset: { $ref: "#/components/headers/X-RateLimit-Reset" } content: application/json: schema: type: object properties: token: type: object properties: id: { type: string, format: uuid } type: { type: string, enum: [read, write] } secret: { type: string } "400": description: "`invalid_request`: body present but not a JSON object" content: application/json: schema: allOf: - $ref: "#/components/schemas/Error" - type: object properties: error: { enum: [invalid_request] } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "409": description: "`project_closed`: closed/deleted project, or closing and a write token was asked for — a closing project still issues read tokens while a stored file is in retention (deletion-close.md)" content: application/json: schema: allOf: - $ref: "#/components/schemas/Error" - type: object properties: error: { enum: [project_closed] } "503": { $ref: "#/components/responses/ServiceUnavailable" } /projects/{projectId}/tokens/{tokenId}: parameters: - { name: projectId, in: path, required: true, schema: { type: string, format: uuid } } - { name: tokenId, in: path, required: true, schema: { type: string, format: uuid } } delete: summary: Revoke a backup token (owner/admin) operationId: deleteToken responses: "429": { $ref: "#/components/responses/RateLimited" } "200": description: revoked (revocation is published to the storage zone) headers: X-RateLimit-Limit: { $ref: "#/components/headers/X-RateLimit-Limit" } X-RateLimit-Remaining: { $ref: "#/components/headers/X-RateLimit-Remaining" } X-RateLimit-Reset: { $ref: "#/components/headers/X-RateLimit-Reset" } content: application/json: schema: type: object properties: deleted: { type: boolean } "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "503": { $ref: "#/components/responses/ServiceUnavailable" }