openapi: 3.0.3
info:
  title: Arianet Cloud API
  version: 1.1.0
  description: |
    Public web service for buying and managing Arianet cloud servers. It is the
    same service the `arianet` CLI, the Telegram bot and partner integrations use.

    ## Authentication

    Every endpoint needs a personal API token created in the panel
    (**Settings → API Tokens**), sent as a Bearer token:

    ```
    Authorization: Bearer <token>
    ```

    A token can be restricted to a set of **scopes** and to an **IP allowlist**.
    A token created without scopes is unrestricted. A request outside the token's
    scopes answers `403 INSUFFICIENT_SCOPE`; from a disallowed address,
    `403 IP_NOT_ALLOWED`.

    ## Response envelope

    ```json
    { "success": true, "data": { } }
    { "success": true, "data": [ ], "pagination": { "current_page": 1, "per_page": 15, "total": 40, "last_page": 3 } }
    { "success": false, "error": { "code": "INSUFFICIENT_BALANCE", "message": "Not enough balance. Required balance: 4.50" } }
    ```

    Branch on `error.code`; `error.message` is for humans and may change.

    ## Asynchronous operations

    Creating, deleting, reinstalling, restarting and powering a server on or off
    answer **202 Accepted**: the request was validated and started, the work is
    not finished. Poll `GET /servers/{id}/status` until `status` is `active` and
    `instance_status` is `RUNNING` (or `STOPPED` after a power-off), or read `GET /servers/{id}/actions` for the operation history.

    ## Idempotency

    `POST /servers` spends money, so it requires an `Idempotency-Key` header
    (8-128 characters of `A-Z a-z 0-9 _ - : .`, for example a UUID). Retrying with
    the same key and the same body never buys a second server: the original answer
    is replayed for 24 hours with the header `Idempotent-Replayed: true`. The same
    key with a different body answers `422`; a retry while the first request is
    still running answers `409 CONFLICT` - wait a few seconds and retry.
    Treat the key like a secret: the replayed answer contains the root password.

    ## Rate limits

    Limits are per user (creation: per token). When exceeded the API answers
    `429 RATE_LIMIT_EXCEEDED`; creation also sends `Retry-After`.

    | Group | Limit |
    |---|---|
    | Reads | 120 / minute, burst 20 |
    | Writes (actions, ssh keys, firewalls, tokens) | about 20 / minute, burst 5 |
    | `POST /servers` | 5 / hour sustained per token, plus 10 / hour per account |
  contact:
    name: Arianet Support
    email: support@arianet.io
    url: https://support.arianet.io

servers:
  - url: https://api.ariaservice.net/api/v1
    description: Production

security:
  - BearerAuth: []

tags:
  - name: Auth
    description: The calling user and their API tokens
  - name: Catalog
    description: Regions, operating systems and plans (public catalogue data)
  - name: Servers
    description: Server lifecycle and actions
  - name: SSH Keys
    description: Public keys that can be injected into new servers
  - name: Firewalls
    description: Provider firewalls and their rules
  - name: Billing
    description: Wallet balance, transactions and invoices

paths:
  /auth/me:
    get:
      tags: [Auth]
      summary: Current user
      operationId: getMe
      responses:
        '200':
          description: The user the token belongs to
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data: { $ref: '#/components/schemas/User' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /auth/tokens:
    get:
      tags: [Auth]
      summary: List API tokens
      description: Needs scope `tokens:read`.
      operationId: listTokens
      responses:
        '200':
          description: Tokens of the calling user. The secret value is never returned.
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      tokens:
                        type: array
                        items: { $ref: '#/components/schemas/Token' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /auth/tokens/{id}:
    delete:
      tags: [Auth]
      summary: Revoke a token
      description: Needs scope `tokens:write`.
      operationId: revokeToken
      parameters:
        - { $ref: '#/components/parameters/Id' }
      responses:
        '200': { $ref: '#/components/responses/Message' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /auth/logout:
    delete:
      tags: [Auth]
      summary: Revoke the token used for this request
      operationId: logout
      responses:
        '200': { $ref: '#/components/responses/Message' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /regions:
    get:
      tags: [Catalog]
      summary: Regions and datacenters
      description: Needs scope `regions:read`.
      operationId: listRegions
      parameters:
        - name: system_name
          in: query
          description: "`1` or `true` returns internal datacenter names instead of display names (not cached)."
          schema: { type: string }
      responses:
        '200':
          description: Regions with their datacenters
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      items:
                        type: array
                        items: { $ref: '#/components/schemas/Region' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /os/datacenter/{datacenter_id}:
    get:
      tags: [Catalog]
      summary: Operating systems available in a datacenter
      description: Needs scope `os:read`. The `id` of a template (or of one of its `children`) is the `os_id` for server creation.
      operationId: listOsByDatacenter
      parameters:
        - $ref: '#/components/parameters/DatacenterId'
      responses:
        '200':
          description: Operating systems grouped by family
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      groups:
                        type: array
                        items: { $ref: '#/components/schemas/OsGroup' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /plans/datacenter/{datacenter_id}:
    get:
      tags: [Catalog]
      summary: Plans orderable in a datacenter
      description: Needs scope `plans:read`.
      operationId: listPlansByDatacenter
      parameters:
        - $ref: '#/components/parameters/DatacenterId'
      responses:
        '200':
          description: Plans grouped by family, with prices
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      groups:
                        type: array
                        items: { $ref: '#/components/schemas/PlanGroup' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /plans/{id}:
    get:
      tags: [Catalog]
      summary: One plan
      description: Needs scope `plans:read`.
      operationId: getPlan
      parameters:
        - { $ref: '#/components/parameters/Id' }
      responses:
        '200':
          description: The plan
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data: { $ref: '#/components/schemas/Plan' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /servers:
    get:
      tags: [Servers]
      summary: List servers
      description: Needs scope `servers:read`.
      operationId: listServers
      parameters:
        - { $ref: '#/components/parameters/Page' }
        - { $ref: '#/components/parameters/Limit' }
        - name: status
          in: query
          description: Filter by server status.
          schema: { type: string, example: active }
      responses:
        '200':
          description: A page of servers
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Server' }
                  pagination: { $ref: '#/components/schemas/Pagination' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }
    post:
      tags: [Servers]
      summary: Create a server
      description: |
        Needs scope `servers:write`. The cost is reserved from the wallet at once.
        Answers **202**: the server is being provisioned, poll it until it is `active`.

        Authentication is either `password` (default; `auth_value` is the root
        password) or `ssh` (`ssh_key_id` of one of your SSH keys). The generated
        or chosen root password is returned in this response only.
      operationId: createServer
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreateServerRequest' }
            examples:
              password:
                summary: Root password
                value:
                  plan_id: 101
                  datacenter_id: 9
                  os_id: 55
                  hostname: web-1
                  auth_type: password
                  auth_value: "Str0ng!Passw0rd"
              ssh:
                summary: SSH key
                value:
                  plan_id: 101
                  datacenter_id: 9
                  os_id: 55
                  hostname: web-1
                  auth_type: ssh
                  ssh_key_id: 12
      responses:
        '202':
          description: Creation started
          headers:
            Idempotent-Replayed:
              description: "`true` when this is the stored answer of an earlier request with the same key."
              schema: { type: string, enum: ['true'] }
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data: { $ref: '#/components/schemas/CreatedServer' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '402': { $ref: '#/components/responses/InsufficientBalance' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409': { $ref: '#/components/responses/Conflict' }
        '422': { $ref: '#/components/responses/Unprocessable' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '502': { $ref: '#/components/responses/Upstream' }
        '503': { $ref: '#/components/responses/Unavailable' }

  /servers/{id}:
    get:
      tags: [Servers]
      summary: Get a server
      description: Needs scope `servers:read`.
      operationId: getServer
      parameters:
        - { $ref: '#/components/parameters/Id' }
      responses:
        '200':
          description: The server
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data: { $ref: '#/components/schemas/Server' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
    delete:
      tags: [Servers]
      summary: Delete a server
      description: |
        Needs scope `servers:write`. Irreversible; a protected server must have
        protection switched off first. Answers **202**.
      operationId: deleteServer
      parameters:
        - { $ref: '#/components/parameters/Id' }
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                reason:
                  type: string
                  default: other
                  description: Why the server is deleted (defaults to `other`).
                note:
                  type: string
                  maxLength: 1000
      responses:
        '202': { $ref: '#/components/responses/ServerActionAccepted' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/Unprocessable' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '502': { $ref: '#/components/responses/Upstream' }
        '503': { $ref: '#/components/responses/Unavailable' }

  /servers/{id}/status:
    get:
      tags: [Servers]
      summary: Lightweight status
      description: Needs scope `servers:read`. Cheap to poll.
      operationId: getServerStatus
      parameters:
        - { $ref: '#/components/parameters/Id' }
      responses:
        '200':
          description: Current status
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      id: { type: integer, format: int64 }
                      status: { type: string, example: active }
                      instance_status: { type: string, nullable: true, example: RUNNING }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /servers/{id}/actions:
    get:
      tags: [Servers]
      summary: Operation history of a server
      description: Needs scope `servers:read`. Newest first.
      operationId: listServerActions
      parameters:
        - { $ref: '#/components/parameters/Id' }
        - name: limit
          in: query
          description: Number of entries (1-100, default 15).
          schema: { type: integer, minimum: 1, maximum: 100, default: 15 }
      responses:
        '200':
          description: Recent operations
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      server_id: { type: integer, format: int64 }
                      actions:
                        type: array
                        items: { $ref: '#/components/schemas/ServerAction' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '502': { $ref: '#/components/responses/Upstream' }
        '503': { $ref: '#/components/responses/Unavailable' }

  /servers/{id}/restart:
    post:
      tags: [Servers]
      summary: Restart a server
      description: Needs scope `servers:write`.
      operationId: restartServer
      parameters:
        - { $ref: '#/components/parameters/Id' }
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                mode: { type: string, enum: [soft, hard] }
      responses:
        '202': { $ref: '#/components/responses/ServerActionAccepted' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/Unprocessable' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '502': { $ref: '#/components/responses/Upstream' }
        '503': { $ref: '#/components/responses/Unavailable' }

  /servers/{id}/power-on:
    post:
      tags: [Servers]
      summary: Power a server on
      description: Needs scope `servers:write`.
      operationId: powerOnServer
      parameters:
        - { $ref: '#/components/parameters/Id' }
      responses:
        '202': { $ref: '#/components/responses/ServerActionAccepted' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/Unprocessable' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '502': { $ref: '#/components/responses/Upstream' }
        '503': { $ref: '#/components/responses/Unavailable' }

  /servers/{id}/power-off:
    post:
      tags: [Servers]
      summary: Power a server off
      description: Needs scope `servers:write`. A powered-off server may still be billed.
      operationId: powerOffServer
      parameters:
        - { $ref: '#/components/parameters/Id' }
      responses:
        '202': { $ref: '#/components/responses/ServerActionAccepted' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/Unprocessable' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '502': { $ref: '#/components/responses/Upstream' }
        '503': { $ref: '#/components/responses/Unavailable' }

  /servers/{id}/rename:
    post:
      tags: [Servers]
      summary: Rename a server
      description: Needs scope `servers:write`. Applies immediately (200).
      operationId: renameServer
      parameters:
        - { $ref: '#/components/parameters/Id' }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: { type: string, minLength: 1, maxLength: 30 }
      responses:
        '200':
          description: Renamed
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      server_id: { type: integer, format: int64 }
                      name: { type: string }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/Unprocessable' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '502': { $ref: '#/components/responses/Upstream' }
        '503': { $ref: '#/components/responses/Unavailable' }

  /servers/{id}/reinstall:
    post:
      tags: [Servers]
      summary: Reinstall the operating system
      description: Needs scope `servers:write`. **Erases all data on the server.** Answers 202.
      operationId: reinstallServer
      parameters:
        - { $ref: '#/components/parameters/Id' }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [os_id]
              properties:
                os_id: { type: integer, format: int64 }
      responses:
        '202': { $ref: '#/components/responses/ServerActionAccepted' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/Unprocessable' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '502': { $ref: '#/components/responses/Upstream' }
        '503': { $ref: '#/components/responses/Unavailable' }

  /servers/{id}/toggle-protection:
    post:
      tags: [Servers]
      summary: Set or flip delete protection
      description: |
        Needs scope `servers:write`. With `{"enabled": true|false}` the state is set
        explicitly; with no body it flips the current state. Prefer the explicit form.
      operationId: toggleServerProtection
      parameters:
        - { $ref: '#/components/parameters/Id' }
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                enabled: { type: boolean }
      responses:
        '200':
          description: Protection updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      server_id: { type: integer, format: int64 }
                      protected: { type: boolean }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/Unprocessable' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '502': { $ref: '#/components/responses/Upstream' }
        '503': { $ref: '#/components/responses/Unavailable' }

  /ssh-keys:
    get:
      tags: [SSH Keys]
      summary: List SSH keys
      description: Needs scope `ssh-keys:read`.
      operationId: listSshKeys
      parameters:
        - { $ref: '#/components/parameters/Page' }
        - { $ref: '#/components/parameters/Limit' }
      responses:
        '200':
          description: A page of SSH keys
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/SshKey' }
                  pagination: { $ref: '#/components/schemas/Pagination' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }
    post:
      tags: [SSH Keys]
      summary: Add an SSH key
      description: Needs scope `ssh-keys:write`. A key belongs to one datacenter, because providers keep keys per location.
      operationId: createSshKey
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, public_key, datacenter_id]
              properties:
                name: { type: string, minLength: 1, maxLength: 255 }
                public_key: { type: string, maxLength: 4096, example: "ssh-ed25519 AAAAC3Nza... me@laptop" }
                datacenter_id: { type: integer, format: int64 }
                is_protected: { type: boolean }
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data: { $ref: '#/components/schemas/SshKey' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/Unprocessable' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '502': { $ref: '#/components/responses/Upstream' }
        '503': { $ref: '#/components/responses/Unavailable' }

  /ssh-keys/{id}:
    get:
      tags: [SSH Keys]
      summary: Get an SSH key
      description: Needs scope `ssh-keys:read`.
      operationId: getSshKey
      parameters:
        - { $ref: '#/components/parameters/Id' }
      responses:
        '200':
          description: The key
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data: { $ref: '#/components/schemas/SshKey' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
    put:
      tags: [SSH Keys]
      summary: Update an SSH key
      description: Needs scope `ssh-keys:write`. At least one field is required; the key material itself cannot be changed.
      operationId: updateSshKey
      parameters:
        - { $ref: '#/components/parameters/Id' }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string, minLength: 1, maxLength: 255 }
                is_protected: { type: boolean }
      responses:
        '200':
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data: { $ref: '#/components/schemas/SshKey' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/Unprocessable' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '502': { $ref: '#/components/responses/Upstream' }
        '503': { $ref: '#/components/responses/Unavailable' }
    delete:
      tags: [SSH Keys]
      summary: Delete an SSH key
      description: Needs scope `ssh-keys:write`. Servers already created with the key are not affected.
      operationId: deleteSshKey
      parameters:
        - { $ref: '#/components/parameters/Id' }
      responses:
        '200': { $ref: '#/components/responses/Message' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/Unprocessable' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '502': { $ref: '#/components/responses/Upstream' }
        '503': { $ref: '#/components/responses/Unavailable' }

  /firewalls:
    get:
      tags: [Firewalls]
      summary: List firewalls
      description: Needs scope `firewalls:read`.
      operationId: listFirewalls
      parameters:
        - { $ref: '#/components/parameters/Page' }
        - { $ref: '#/components/parameters/Limit' }
      responses:
        '200':
          description: A page of firewalls
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Firewall' }
                  pagination: { $ref: '#/components/schemas/Pagination' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }
    post:
      tags: [Firewalls]
      summary: Create a firewall
      description: |
        Needs scope `firewalls:write`. The firewall is created empty at the provider;
        add rules with `POST /firewalls/{id}/rules` and attach it to servers with
        `POST /firewalls/{id}/attach`.
      operationId: createFirewall
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, datacenter_id]
              properties:
                name: { type: string, minLength: 1, maxLength: 255 }
                datacenter_id: { type: integer, format: int64 }
                description: { type: string, maxLength: 500 }
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data: { $ref: '#/components/schemas/Firewall' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/Unprocessable' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '502': { $ref: '#/components/responses/Upstream' }
        '503': { $ref: '#/components/responses/Unavailable' }

  /firewalls/{id}:
    get:
      tags: [Firewalls]
      summary: Get a firewall with its rules
      description: Needs scope `firewalls:read`.
      operationId: getFirewall
      parameters:
        - { $ref: '#/components/parameters/Id' }
      responses:
        '200':
          description: The firewall
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data: { $ref: '#/components/schemas/Firewall' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }
    put:
      tags: [Firewalls]
      summary: Update a firewall
      description: |
        Needs scope `firewalls:write`. At least one field is required. The answer
        carries no rules; read the firewall again if you need them.
      operationId: updateFirewall
      parameters:
        - { $ref: '#/components/parameters/Id' }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name: { type: string, minLength: 1, maxLength: 255 }
                description: { type: string, maxLength: 500 }
      responses:
        '200':
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data: { $ref: '#/components/schemas/Firewall' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/Unprocessable' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '502': { $ref: '#/components/responses/Upstream' }
        '503': { $ref: '#/components/responses/Unavailable' }
    delete:
      tags: [Firewalls]
      summary: Delete a firewall
      description: Needs scope `firewalls:write`.
      operationId: deleteFirewall
      parameters:
        - { $ref: '#/components/parameters/Id' }
      responses:
        '200': { $ref: '#/components/responses/Message' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/Unprocessable' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '502': { $ref: '#/components/responses/Upstream' }
        '503': { $ref: '#/components/responses/Unavailable' }

  /firewalls/{id}/rules:
    post:
      tags: [Firewalls]
      summary: Add a rule
      description: Needs scope `firewalls:write`. The answer is the new rule, including the `id` to delete it with.
      operationId: addFirewallRule
      parameters:
        - { $ref: '#/components/parameters/Id' }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [direction, protocol]
              properties:
                direction: { type: string, enum: [ingress, egress] }
                protocol: { type: string, enum: [tcp, udp, icmp, esp, gre] }
                port_range: { type: string, maxLength: 50, example: "443" }
                remote_ip: { type: string, maxLength: 100, example: "0.0.0.0/0" }
                description: { type: string, maxLength: 255 }
      responses:
        '201':
          description: Rule added
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data: { $ref: '#/components/schemas/FirewallRule' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/Unprocessable' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '502': { $ref: '#/components/responses/Upstream' }
        '503': { $ref: '#/components/responses/Unavailable' }

  /firewalls/{id}/rules/{rule_id}:
    delete:
      tags: [Firewalls]
      summary: Delete a rule
      description: Needs scope `firewalls:write`. `rule_id` is the rule's `id` from the firewall's `rules` (a string).
      operationId: deleteFirewallRule
      parameters:
        - { $ref: '#/components/parameters/Id' }
        - name: rule_id
          in: path
          required: true
          schema: { type: string }
      responses:
        '200': { $ref: '#/components/responses/Message' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/Unprocessable' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '502': { $ref: '#/components/responses/Upstream' }
        '503': { $ref: '#/components/responses/Unavailable' }

  /firewalls/{id}/attach:
    post:
      tags: [Firewalls]
      summary: Attach a firewall to servers
      description: Needs scope `firewalls:write`. The servers must be in the firewall's datacenter.
      operationId: attachFirewall
      parameters:
        - { $ref: '#/components/parameters/Id' }
      requestBody:
        $ref: '#/components/requestBodies/ServerIds'
      responses:
        '200': { $ref: '#/components/responses/Message' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/Unprocessable' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '502': { $ref: '#/components/responses/Upstream' }
        '503': { $ref: '#/components/responses/Unavailable' }

  /firewalls/{id}/detach:
    post:
      tags: [Firewalls]
      summary: Detach a firewall from servers
      description: Needs scope `firewalls:write`.
      operationId: detachFirewall
      parameters:
        - { $ref: '#/components/parameters/Id' }
      requestBody:
        $ref: '#/components/requestBodies/ServerIds'
      responses:
        '200': { $ref: '#/components/responses/Message' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/Unprocessable' }
        '429': { $ref: '#/components/responses/RateLimited' }
        '502': { $ref: '#/components/responses/Upstream' }
        '503': { $ref: '#/components/responses/Unavailable' }

  /balance:
    get:
      tags: [Billing]
      summary: Wallet balances
      description: Needs scope `balance:read`. One wallet per currency; `primary` marks the default wallet used when `currency_id` is omitted at server creation.
      operationId: getBalance
      responses:
        '200':
          description: Wallets
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: object
                    properties:
                      wallets:
                        type: array
                        items: { $ref: '#/components/schemas/Wallet' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /balance/transactions:
    get:
      tags: [Billing]
      summary: Wallet transactions
      description: Needs scope `balance:read`.
      operationId: listTransactions
      parameters:
        - { $ref: '#/components/parameters/Page' }
        - { $ref: '#/components/parameters/Limit' }
      responses:
        '200':
          description: A page of transactions
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Transaction' }
                  pagination: { $ref: '#/components/schemas/Pagination' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /invoices:
    get:
      tags: [Billing]
      summary: List invoices
      description: Needs scope `invoices:read`. Zero-amount invoices are not listed.
      operationId: listInvoices
      parameters:
        - { $ref: '#/components/parameters/Page' }
        - { $ref: '#/components/parameters/Limit' }
        - name: status
          in: query
          schema: { type: string, enum: [draft, issued, paid, overdue, cancelled] }
      responses:
        '200':
          description: A page of invoices
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Invoice' }
                  pagination: { $ref: '#/components/schemas/Pagination' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '429': { $ref: '#/components/responses/RateLimited' }

  /invoices/{number}:
    get:
      tags: [Billing]
      summary: Get an invoice with its line items
      description: Needs scope `invoices:read`. The number is text, for example `2041` or `INV-2026-000123`.
      operationId: getInvoice
      parameters:
        - name: number
          in: path
          required: true
          schema: { type: string, pattern: '^[A-Za-z0-9-]{1,50}$' }
      responses:
        '200':
          description: The invoice
          content:
            application/json:
              schema:
                type: object
                properties:
                  success: { type: boolean, example: true }
                  data: { $ref: '#/components/schemas/Invoice' }
        '400': { $ref: '#/components/responses/BadRequest' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '429': { $ref: '#/components/responses/RateLimited' }

components:
  securitySchemes:
    BearerAuth:
      type: http
      scheme: bearer
      description: |
        Personal API token. Scopes a token may carry: `regions:read`, `os:read`,
        `plans:read`, `servers:read`, `servers:write`, `balance:read`, `invoices:read`,
        `ssh-keys:read`, `ssh-keys:write`, `firewalls:read`, `firewalls:write`,
        `tokens:read`, `tokens:write`. A token with no scopes may do everything.

  parameters:
    Id:
      name: id
      in: path
      required: true
      schema: { type: integer, format: int64 }
    DatacenterId:
      name: datacenter_id
      in: path
      required: true
      schema: { type: integer, format: int64 }
    Page:
      name: page
      in: query
      schema: { type: integer, minimum: 1, default: 1 }
    Limit:
      name: limit
      in: query
      description: Page size, 1-100 (an out-of-range value falls back to 15).
      schema: { type: integer, minimum: 1, maximum: 100, default: 15 }
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: true
      description: "8-128 characters of `A-Z a-z 0-9 _ - : .`. Reuse the same key to retry the same order safely."
      schema: { type: string, minLength: 8, maxLength: 128, pattern: '^[A-Za-z0-9_\-:.]{8,128}$', example: "order-7f3c9a52-0d1e-4a6b" }

  requestBodies:
    ServerIds:
      required: true
      content:
        application/json:
          schema:
            type: object
            required: [server_ids]
            properties:
              server_ids:
                type: array
                minItems: 1
                maxItems: 50
                items: { type: integer, format: int64, minimum: 1 }

  responses:
    Message:
      description: Done
      content:
        application/json:
          schema:
            type: object
            properties:
              success: { type: boolean, example: true }
              data:
                type: object
                properties:
                  message: { type: string }
    ServerActionAccepted:
      description: The operation was accepted and is running.
      content:
        application/json:
          schema:
            type: object
            properties:
              success: { type: boolean, example: true }
              data:
                type: object
                properties:
                  server_id: { type: integer, format: int64 }
                  message: { type: string, example: Restart started }
    BadRequest:
      description: Malformed request. Codes `VALIDATION_ERROR`, `INVALID_ID`, `BAD_REQUEST`, `IDEMPOTENCY_KEY_REQUIRED`, `INVALID_IDEMPOTENCY_KEY`, `NO_DEFAULT_WALLET`.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Unauthorized:
      description: Missing, invalid, expired or revoked token (`UNAUTHORIZED`).
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Forbidden:
      description: "`FORBIDDEN` (account not active, or not allowed to do this), `INSUFFICIENT_SCOPE` or `IP_NOT_ALLOWED`."
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    NotFound:
      description: No such resource in your account (`NOT_FOUND`).
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Conflict:
      description: An earlier request with the same Idempotency-Key is still running (`CONFLICT`). Retry shortly.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    InsufficientBalance:
      description: The wallet cannot cover the order (`INSUFFICIENT_BALANCE`).
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Unprocessable:
      description: The request is well formed but cannot be done now (`VALIDATION_ERROR`), for example the server is protected, the plan is sold out, or an Idempotency-Key was reused with a different body. `FEATURE_NOT_SUPPORTED` means the datacenter does not offer the requested resource (a firewall or SSH key); check `supports` in `GET /regions`.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    RateLimited:
      description: Too many requests (`RATE_LIMIT_EXCEEDED`). Back off; honour `Retry-After` when present.
      headers:
        Retry-After:
          description: Seconds to wait.
          schema: { type: integer }
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Upstream:
      description: The platform could not complete the request (`UPSTREAM_ERROR`). Nothing is exposed about why; retry, and contact support if it persists.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
    Unavailable:
      description: The platform is temporarily unreachable (`UPSTREAM_UNAVAILABLE`, `WRITES_UNAVAILABLE`). Retry with the same Idempotency-Key.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }

  schemas:
    Error:
      type: object
      properties:
        success: { type: boolean, example: false }
        error:
          type: object
          properties:
            code:
              type: string
              enum:
                - UNAUTHORIZED
                - FORBIDDEN
                - IP_NOT_ALLOWED
                - INSUFFICIENT_SCOPE
                - NOT_FOUND
                - VALIDATION_ERROR
                - RATE_LIMIT_EXCEEDED
                - INVALID_ID
                - IDEMPOTENCY_KEY_REQUIRED
                - INVALID_IDEMPOTENCY_KEY
                - NO_DEFAULT_WALLET
                - INSUFFICIENT_BALANCE
                - BAD_REQUEST
                - CONFLICT
                - REQUEST_FAILED
                - UPSTREAM_ERROR
                - UPSTREAM_UNAVAILABLE
                - WRITES_UNAVAILABLE
                - INTERNAL_ERROR
            message: { type: string }

    Pagination:
      type: object
      properties:
        current_page: { type: integer, example: 1 }
        per_page: { type: integer, example: 15 }
        total: { type: integer, format: int64, example: 40 }
        last_page: { type: integer, example: 3 }

    User:
      type: object
      properties:
        id: { type: integer, format: int64 }
        email: { type: string, nullable: true }
        mobile: { type: string, nullable: true }
        status: { type: string, example: active }
        verified: { type: boolean }

    Token:
      type: object
      properties:
        id: { type: integer, format: int64 }
        name: { type: string }
        prefix: { type: string, description: "First characters of the token, to recognise it." }
        last_used: { type: string, format: date-time, nullable: true }
        expires: { type: string, format: date-time, nullable: true }
        created_at: { type: string, format: date-time }

    Region:
      type: object
      properties:
        id: { type: integer, format: int64 }
        name: { type: string }
        is_region: { type: boolean }
        status: { type: string }
        is_default: { type: boolean }
        country_code: { type: string, nullable: true }
        flag: { type: string }
        datacenters:
          type: array
          items: { $ref: '#/components/schemas/Datacenter' }

    Datacenter:
      type: object
      description: "`id` is the `datacenter_id` used everywhere else."
      properties:
        id: { type: integer, format: int64 }
        name: { type: string }
        is_default: { type: boolean }
        status: { type: string }
        country_code: { type: string, nullable: true }
        flag: { type: string }
        supports:
          type: object
          description: Optional resources this datacenter offers. Creating one where it is `false` is refused with `422 FEATURE_NOT_SUPPORTED`.
          properties:
            ssh_keys: { type: boolean }
            firewalls: { type: boolean }
        features:
          type: array
          items:
            type: object
            properties:
              name: { type: string, nullable: true }
              details: { type: string, nullable: true }
              quantity: { type: integer }

    OsGroup:
      type: object
      properties:
        id: { type: integer, format: int64 }
        name: { type: string }
        type: { type: string }
        order: { type: integer }
        templates:
          type: object
          properties:
            data:
              type: array
              items: { $ref: '#/components/schemas/OsTemplate' }

    OsTemplate:
      type: object
      properties:
        id: { type: integer, format: int64, description: "Use as `os_id`." }
        name: { type: string }
        image_file: { type: string }
        status: { type: boolean }
        meta_data: { type: object, additionalProperties: true }
        datacenter:
          type: object
          properties:
            id: { type: integer, format: int64 }
            name: { type: string }
            display_name: { type: string, nullable: true }
        os_group:
          type: object
          properties:
            id: { type: integer, format: int64 }
            name: { type: string }
            type: { type: string }
        children:
          type: object
          description: Versions of this operating system.
          properties:
            data:
              type: array
              items:
                type: object
                properties:
                  id: { type: integer, format: int64, description: "Use as `os_id`." }
                  name: { type: string }
                  image_file: { type: string }
                  status: { type: boolean }
                  meta_data: { type: object, additionalProperties: true }

    PlanGroup:
      type: object
      properties:
        id: { type: integer, format: int64 }
        name: { type: string }
        plans:
          type: array
          items: { $ref: '#/components/schemas/Plan' }

    Plan:
      type: object
      properties:
        id: { type: integer, format: int64, description: "Use as `plan_id`." }
        name: { type: string }
        display_name: { type: string, nullable: true }
        recommended: { type: boolean }
        cycle: { type: string, nullable: true, example: hourly }
        cpu: { $ref: '#/components/schemas/ResourceValue' }
        ram: { $ref: '#/components/schemas/ResourceValue' }
        storage: { $ref: '#/components/schemas/ResourceValue' }
        prices:
          type: array
          items:
            type: object
            properties:
              code: { type: string, description: Currency code, example: USD }
              currency: { type: string, nullable: true }
              hourly: { type: string, example: "0.0070" }
              monthly: { type: string, example: "4.50" }
              yearly: { type: string }

    ResourceValue:
      type: object
      properties:
        size: { type: integer, example: 2 }
        unit: { type: string, example: GB }

    Server:
      type: object
      properties:
        id: { type: integer, format: int64 }
        status: { type: string, description: "Platform status: `creating`, `active`, `suspended`, `powering_on`, `powering_off`, `restarting`, `reinstalling_os`, `failed`, `terminated`, ..." }
        instance_status: { type: string, nullable: true, description: "Status reported by the provider: `RUNNING`, `STOPPED`, `REBOOTING`, `REBUILDING`, `BUILD`, `ERROR`, ..." }
        hostname: { type: string, nullable: true }
        ip_addresses:
          type: array
          items:
            type: object
            properties:
              ip: { type: string }
              type: { type: string, enum: [primary, secondary, floating] }
        protected: { type: boolean, nullable: true }
        cycle: { type: string, nullable: true, example: hourly }
        plan:
          type: object
          properties:
            id: { type: integer, format: int64 }
            name: { type: string }
        datacenter:
          type: object
          properties:
            id: { type: integer, format: int64 }
            name: { type: string }
            display_name: { type: string, nullable: true }
            country: { type: string, nullable: true }
        os:
          type: object
          properties:
            id: { type: integer, format: int64 }
            name: { type: string }
        created_at: { type: string, format: date-time }

    CreatedServer:
      type: object
      description: What is known the moment creation is accepted. IP addresses usually appear a little later; read the server again.
      properties:
        id: { type: integer, format: int64 }
        status: { type: string, example: creating }
        instance_status: { type: string, nullable: true }
        name: { type: string, nullable: true }
        ip_addresses:
          type: array
          items:
            type: object
            properties:
              ip: { type: string }
              type: { type: string }
        protected: { type: boolean }
        cycle: { type: string, nullable: true }
        plan:
          type: object
          properties:
            name: { type: string, nullable: true }
        datacenter:
          type: object
          properties:
            id: { type: integer, format: int64, nullable: true }
        os:
          type: object
          properties:
            name: { type: string, nullable: true }
        root_password: { type: string, description: "Returned only here (and on an idempotent replay). Absent for ssh-key servers." }
        created_at: { type: string, format: date-time }

    CreateServerRequest:
      type: object
      required: [plan_id, datacenter_id, os_id]
      properties:
        plan_id: { type: integer, format: int64 }
        datacenter_id: { type: integer, format: int64 }
        os_id: { type: integer, format: int64 }
        currency_id:
          type: integer
          format: int64
          description: Wallet currency to pay with. Defaults to the currency of your primary wallet (`NO_DEFAULT_WALLET` if there is none).
        project_id: { type: integer, format: int64 }
        service_count: { type: integer, minimum: 1, maximum: 10 }
        hostname: { type: string, maxLength: 255 }
        hostnames: { type: string, maxLength: 255, description: "Alias of `hostname`; wins when both are sent." }
        auth_type: { type: string, enum: [password, ssh], default: password }
        auth_value: { type: string, description: "Root password. Required when `auth_type` is `password`." }
        ssh_key_id: { type: integer, format: int64, description: "Required when `auth_type` is `ssh` (`auth_id` is accepted as an alias)." }

    ServerAction:
      type: object
      properties:
        id: { type: string }
        type: { type: string, example: restart }
        status: { type: string, enum: [pending, running, success, failed], example: success }
        started_at: { type: string, format: date-time, nullable: true }
        finished_at: { type: string, format: date-time, nullable: true }
        created_at: { type: string, format: date-time }

    SshKey:
      type: object
      properties:
        id: { type: integer, format: int64 }
        name: { type: string, nullable: true }
        key: { type: string, description: The public key. }
        protected: { type: boolean }
        datacenter_id: { type: integer, format: int64, nullable: true }
        created_at: { type: string, format: date-time }

    Firewall:
      type: object
      properties:
        id: { type: integer, format: int64 }
        name: { type: string, nullable: true }
        protected: { type: boolean }
        datacenter_id: { type: integer, format: int64, nullable: true }
        rules:
          type: array
          items: { $ref: '#/components/schemas/FirewallRule' }
        created_at: { type: string, format: date-time }

    FirewallRule:
      type: object
      properties:
        id: { type: string, description: Identifier to delete the rule with. }
        direction: { type: string, enum: [ingress, egress] }
        protocol: { type: string, enum: [tcp, udp, icmp, esp, gre] }
        port_range: { type: string, nullable: true }
        remote_ip: { type: string, nullable: true }
        description: { type: string, nullable: true }

    Currency:
      type: object
      properties:
        id: { type: integer, format: int64 }
        code: { type: string, example: USD }
        name: { type: string }
        symbol: { type: string, nullable: true }

    Wallet:
      type: object
      properties:
        id: { type: integer, format: int64 }
        balance: { type: number, format: double }
        primary: { type: boolean }
        status: { type: string }
        currency: { $ref: '#/components/schemas/Currency' }

    Transaction:
      type: object
      properties:
        id: { type: integer, format: int64 }
        mode: { type: string, description: "`credit` or `debit`." }
        amount: { type: number, format: double }
        status: { type: string, nullable: true }
        reference: { type: string }
        type: { type: string, nullable: true }
        currency: { $ref: '#/components/schemas/Currency' }
        created_at: { type: string, format: date-time }

    Invoice:
      type: object
      properties:
        number: { type: string, example: INV-2026-000123 }
        status: { type: string, enum: [draft, issued, paid, overdue, cancelled] }
        payment_status: { type: string }
        subtotal: { type: number, format: double }
        discount: { type: number, format: double }
        tax: { type: number, format: double }
        total: { type: number, format: double }
        issued_at: { type: string, format: date-time }
        due_at: { type: string, format: date-time, nullable: true }
        paid_at: { type: string, format: date-time, nullable: true }
        items:
          type: array
          description: Only on `GET /invoices/{number}`.
          items:
            type: object
            properties:
              amount: { type: number, format: double }
              currency: { $ref: '#/components/schemas/Currency' }
              description: { type: string, nullable: true }
