openapi: 3.1.0 info: title: lionbackup Public API version: "1.3.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 payment_method: type: string enum: [transfer, sepa_debit] description: | How the organization pays its invoices. `sepa_debit` exactly while a SEPA core direct debit mandate is active, `transfer` otherwise — including after a revocation. Derived from the mandate state at the time of the request, as the portal's payment card shows it; the mandate itself is on `getOrganization`. The invoice remains the authority on how it is paid: each one fixes its payment method when it is generated and states it on the PDF. A later revocation does not change an issued invoice, and an invoice with nothing to collect (a month discounted to zero, for example) is issued for transfer even while the mandate is active. Billing data: absent where `role` is `member`. A pure member holds no billing role, and the portal does not show it the payment method either. examples: [transfer] OrganizationDetail: description: | The single read: the list item plus the SEPA mandate block. `sepa_mandate` is `null` while the organization never granted a mandate; otherwise it is the current mandate or, after a revocation, the one revoked last, with `status: revoked` until a new mandate is granted. Its reference is named on the direct-debit invoices issued while it was active and is never reused; earlier references are on those invoices and in the portal's audit log. Only roles that may see the payment data get this answer, so `payment_method` is always there. allOf: - $ref: "#/components/schemas/Organization" - type: object required: [payment_method, sepa_mandate] properties: sepa_mandate: oneOf: - type: "null" - $ref: "#/components/schemas/SepaMandate" SepaMandate: type: object description: | A SEPA core direct debit mandate as the API shows it: the IBAN masked to its first four and last two characters, the BIC never. The full IBAN appears only on the invoice itself (PDF and embedded ZUGFeRD XML). There is no write path: a mandate is a consent a person gives in the portal. required: [status, reference, iban_masked, account_holder, granted_at, revoked_at] properties: status: type: string enum: [active, revoked] reference: type: string maxLength: 35 description: the mandate reference as printed on the direct-debit invoices examples: [LBK-9B2E6C1A-20261022] iban_masked: type: string description: "country, check digits and the last two characters; everything else hidden" examples: ["DE89 **** **** **** **** 00"] account_holder: { type: string, maxLength: 70 } granted_at: type: [string, "null"] description: consent time, UTC, `YYYY-MM-DD HH:MM:SS` examples: ["2026-10-22 09:14:03"] revoked_at: type: [string, "null"] description: when the mandate was revoked in the portal, UTC, `YYYY-MM-DD HH:MM:SS`; `null` while it is active 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"] } anomaly_detection: type: boolean description: | The per-project switch of the backup anomaly detection. Off for a new project unless set at creation; whoever may write the project flips it (`updateProject`, or the project settings in the portal). Backup runs are recorded either way, so a project switched on later starts from its history and skips the learning phase when three or more runs exist. Switching it off resolves the project's open alerts and keeps the history. examples: [true] anomaly_phase: type: [string, "null"] # "off" quoted: YAML 1.1 parsers (PyYAML in api-contract-check) read a bare off as false. enum: ["off", learning, monitored, paused, ended, null] description: | Where the detector stands with this project, read-only: `learning` until three backups have been recorded (format and encryption are checked from the first one), `monitored` once the statistical checks run, `paused` while the project is closing or its organization is blocked for billing, `ended` once the project is closed, `off` while the switch is off. The detector catches up within about five minutes, so for that long after a flip the phase still shows the previous state; it is `null` until the detector has recorded the project at all. examples: [monitored] 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 Beta (noch weniger erprobt als Linux und Windows) } 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 description: | Every organization the owner has a role in, sorted by name like the portal's organization page. Pick an organization by `id` or `name`, not by its position: a new organization shifts the positions. An item carries `payment_method` unless the owner's role there is `member`. operationId: listOrganizations responses: "429": { $ref: "#/components/responses/RateLimited" } "200": description: organizations with the owner's role and their payment method 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}: parameters: - { name: orgId, in: path, required: true, schema: { type: string, format: uuid } } get: summary: Get an organization with its payment method and SEPA mandate state description: | Readable by the roles the portal shows the organization's payment card to: `owner`, `admin`, `writer`, `reader`, `billing` and `auditor`. A pure `member` holds no billing role and is refused with 403 `permission_denied` (it sees the organization in the list, so the refusal reveals nothing); an organization the owner has no role in answers 404 `not_found`. The mandate block carries the IBAN masked and no BIC, and the API has no write path for mandates — granting and revoking happens in the portal. operationId: getOrganization responses: "429": { $ref: "#/components/responses/RateLimited" } "200": description: the organization 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: organization: { $ref: "#/components/schemas/OrganizationDetail" } examples: sepaDebit: summary: an active mandate — invoices are collected by direct debit value: organization: id: 9b2e6c1a-4d3f-4e8b-a7c5-0f1d2e3a4b5c name: Beispiel GmbH status: approved role: owner payment_method: sepa_debit sepa_mandate: status: active reference: LBK-9B2E6C1A-20261022 iban_masked: "DE89 **** **** **** **** 00" account_holder: Beispiel GmbH granted_at: "2026-10-22 09:14:03" revoked_at: null revoked: summary: a revoked mandate — back to bank transfer, shown until a new mandate is granted value: organization: id: 9b2e6c1a-4d3f-4e8b-a7c5-0f1d2e3a4b5c name: Beispiel GmbH status: approved role: admin payment_method: transfer sepa_mandate: status: revoked reference: LBK-9B2E6C1A-20261022 iban_masked: "DE89 **** **** **** **** 00" account_holder: Beispiel GmbH granted_at: "2026-10-22 09:14:03" revoked_at: "2026-11-03 16:40:12" transfer: summary: bank transfer, no mandate ever granted value: organization: id: 0b6bb0a6-1f0a-4a5b-9a3f-6b1f7c2d4e88 name: Example Ltd status: approved role: billing payment_method: transfer sepa_mandate: null "401": { $ref: "#/components/responses/Unauthorized" } "403": { $ref: "#/components/responses/Forbidden" } "404": { $ref: "#/components/responses/NotFound" } "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 } anomaly_detection: type: boolean default: false description: switch the backup anomaly detection on from the start; a JSON boolean, a string is refused 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, `alert_email` malformed, or `anomaly_detection` not a JSON boolean. `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 / anomaly detection (owner/admin/writer) description: | Absent keys keep their value; `alert_email` and `billing_reference` clear with `null`. `anomaly_detection` is the per-project switch of the backup anomaly detection — a JSON boolean, anything else answers 400, because `"false"` must never switch it on. 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"] } anomaly_detection: { type: boolean } examples: rename: summary: Rename the project value: { name: web-servers } enableAnomalyDetection: summary: Switch the backup anomaly detection on value: { anomaly_detection: true } 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 (`name` empty or over 100 characters, `alert_email` malformed, `anomaly_detection` not a JSON boolean)" 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 Beta (noch weniger erprobt als Linux und Windows) } 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" }