openapi: 3.1.0
info:
  title: ACDN Management API
  version: 1.2.0
  summary: Manage ACDN CDN, storage, DNS, and VPS.
  description: |
    The ACDN Management API lets authenticated customers provision and manage
    Storage Zones and credentials, Pull Zones and CDN settings, DNS zones and records,
    VPS SSH public keys, and VPS instances. All requests and
    responses use JSON unless a successful operation returns no content.

    Create an API token in the ACDN dashboard and send it as a Bearer token.
    This specification covers the ACDN management endpoints only. Object
    operations use the separate S3-compatible API.

    Send Accept: application/json on every request. Mutations of DNS, S3 credentials,
    CDN settings and hostnames, and cache purge require a verified email address.
    Existing API tokens retain their full account access.

    Additional CDN endpoints share a 60-request/minute account limit. Purge and
    hostname checks each have a separate 5/minute limit.
    DNS endpoints share 60/minute, with delegation checks also
    limited to 5/minute. S3 credential mutations share 20/minute.
    On 429, wait for Retry-After seconds. Do not blindly retry resource
    creation after a timeout: list resources first to determine whether it succeeded.
    Only operations explicitly documenting an idempotency key support one.

    A successful CDN settings response means settings were saved, not that every
    delivery location has applied them. Purge returns 202 and completes asynchronously;
    there is no purge completion endpoint. Use hostname status to poll TLS issuance.
  contact:
    name: ACDN
    url: https://acdn.uz/
servers:
  - url: https://acdn.uz/api
    description: Production
security:
  - bearerAuth: []
tags:
  - name: Storage Zones
    description: Manage S3-compatible Storage Zones. A Storage Zone corresponds to an S3 bucket.
  - name: Pull Zones
    description: Manage CDN Pull Zones backed by an origin URL or an ACDN Storage Zone.
  - name: SSH Keys
    description: Manage SSH public keys available when provisioning VPS instances.
  - name: VPS Instances
    description: Provision and manage virtual private servers, billing, and power.
  - name: DNS Zones
    description: Manage DNS zones, records, and nameserver delegation.
paths:
  /user/dns-zones:
    parameters:
      - $ref: '#/components/parameters/AcceptJson'
      - $ref: '#/components/parameters/AcceptLanguage'
    get:
      tags: [DNS Zones]
      operationId: listDnsZones
      summary: List DNS zones with records
      responses:
        '200':
          description: All owned zones, ordered by name.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: {type: array, items: {$ref: '#/components/schemas/DnsZone'}}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '429': {$ref: '#/components/responses/TooManyRequests'}
    post:
      tags: [DNS Zones]
      operationId: createDnsZone
      summary: Create a DNS zone
      description: Use a domain you own. Names are normalized to lowercase without a trailing dot. Creating a zone does not change its delegation at the registrar. Domain names must be unique and must not conflict with another customer's resources.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name: {type: string, maxLength: 255, description: 'Domain with at least two labels, labels of at most 63 characters, and a final label of at least two characters. Use ASCII or punycode.'}
            example: {name: example.com}
      responses:
        '201': {$ref: '#/components/responses/DnsZone'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/EmailNotVerified'}
        '422': {$ref: '#/components/responses/ValidationError'}
        '429': {$ref: '#/components/responses/TooManyRequests'}
  /user/dns-zones/{id}:
    parameters: &resourceParameters
      - $ref: '#/components/parameters/AcceptJson'
      - $ref: '#/components/parameters/AcceptLanguage'
      - $ref: '#/components/parameters/ResourceId'
    get:
      tags: [DNS Zones]
      operationId: getDnsZone
      summary: Get a DNS zone with records
      responses:
        '200': {$ref: '#/components/responses/DnsZone'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
        '429': {$ref: '#/components/responses/TooManyRequests'}
    delete:
      tags: [DNS Zones]
      operationId: deleteDnsZone
      summary: Delete a DNS zone and all its records
      description: Irreversibly deletes every record in the zone, including service-managed records. This can interrupt CDN delivery and other services. Move DNS and update registrar delegation before deleting an active zone.
      responses: &mutationResponses
        '204': {description: Operation completed. CDN configuration changes may take time to reach delivery locations.}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
        '404': {$ref: '#/components/responses/NotFound'}
        '422': {$ref: '#/components/responses/ValidationError'}
        '429': {$ref: '#/components/responses/TooManyRequests'}
  /user/dns-zones/nameservers/{id}:
    parameters: *resourceParameters
    get:
      tags: [DNS Zones]
      operationId: getDnsNameservers
      summary: Get required nameservers
      responses:
        '200':
          description: Configure these nameservers at your registrar.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    required: [nameservers]
                    properties:
                      nameservers: {type: array, items: {type: string}}
              example: {data: {nameservers: [ns1.acdn.uz, ns2.acdn.uz]}}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
        '429': {$ref: '#/components/responses/TooManyRequests'}
  /user/dns-zones/delegation/{id}:
    parameters: *resourceParameters
    get:
      tags: [DNS Zones]
      operationId: checkDnsDelegation
      summary: Check observed DNS delegation
      description: Compares publicly resolved NS records with the required nameservers. Cached DNS answers may delay visibility of registrar changes. If checked is false, retry later rather than treating it as proof of incorrect delegation.
      responses:
        '200':
          description: Delegation observation returned.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    required: [ok, checked, expected, current, missing, extra, error]
                    properties:
                      ok: {type: boolean}
                      checked: {type: boolean}
                      expected: {type: array, items: {type: string}}
                      current: {type: array, items: {type: string}}
                      missing: {type: array, items: {type: string}}
                      extra: {type: array, items: {type: string}}
                      error: {type: [string, 'null']}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
        '429': {$ref: '#/components/responses/TooManyRequests'}
  /user/dns-zones/entries/{domain_id}:
    parameters:
      - $ref: '#/components/parameters/AcceptJson'
      - $ref: '#/components/parameters/AcceptLanguage'
      - $ref: '#/components/parameters/DomainId'
    post:
      tags: [DNS Zones]
      operationId: createDnsRecord
      summary: Create a DNS record
      description: CNAME cannot coexist with other records at the same name. ALIAS cannot coexist with A or AAAA. Duplicate create requests are not idempotent; inspect the zone after a timeout before retrying.
      requestBody: &dnsRecordBody
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/DnsRecordRequest'}
            example: {name: www, type: 1, ttl: 3600, value: 192.0.2.10}
      responses:
        '201': {$ref: '#/components/responses/DnsRecord'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
        '404': {$ref: '#/components/responses/NotFound'}
        '422': {$ref: '#/components/responses/ValidationError'}
        '429': {$ref: '#/components/responses/TooManyRequests'}
  /user/dns-zones/entries/{domain_id}/{entry_id}:
    parameters:
      - $ref: '#/components/parameters/AcceptJson'
      - $ref: '#/components/parameters/AcceptLanguage'
      - $ref: '#/components/parameters/DomainId'
      - $ref: '#/components/parameters/EntryId'
    put:
      tags: [DNS Zones]
      operationId: updateDnsRecord
      summary: Update a DNS record
      description: Send all required record fields. Service-managed records marked read_only cannot be edited. CNAME and ALIAS coexistence restrictions also apply to updates.
      requestBody: *dnsRecordBody
      responses:
        '200': {$ref: '#/components/responses/DnsRecord'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/Forbidden'}
        '404': {$ref: '#/components/responses/NotFound'}
        '422': {$ref: '#/components/responses/ValidationError'}
        '429': {$ref: '#/components/responses/TooManyRequests'}
    delete:
      tags: [DNS Zones]
      operationId: deleteDnsRecord
      summary: Delete a DNS record
      description: Service-managed records marked read_only cannot be deleted individually. Deleting an active record may interrupt service until DNS is restored and caches expire.
      responses: *mutationResponses
  /user/storage-zones/s3-credentials/{id}:
    parameters: *resourceParameters
    post:
      tags: [Storage Zones]
      operationId: createS3Credential
      summary: Create an S3 credential
      description: Creates a separate key with the supplied permissions. All permissions may be false. Keep the returned secret private. Each successful call creates a new key; use the credentials endpoint to inspect keys after a timeout.
      requestBody: &s3PermissionsBody
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/S3Permissions'}
      responses:
        '201':
          description: Credential created. Response is private and must not be cached.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data: {$ref: '#/components/schemas/S3Credential'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/EmailNotVerified'}
        '404': {$ref: '#/components/responses/NotFound'}
        '422': {$ref: '#/components/responses/ValidationError'}
        '429': {$ref: '#/components/responses/TooManyRequests'}
  /user/storage-zones/s3-credentials/{id}/{credentialId}:
    parameters:
      - $ref: '#/components/parameters/AcceptJson'
      - $ref: '#/components/parameters/AcceptLanguage'
      - $ref: '#/components/parameters/ResourceId'
      - $ref: '#/components/parameters/CredentialId'
    put:
      tags: [Storage Zones]
      operationId: updateS3Credential
      summary: Replace S3 credential permissions
      description: Does not change the key ID or secret. Permissions apply to subsequent credential checks; in-flight requests may already be authorized.
      requestBody: *s3PermissionsBody
      responses: *mutationResponses
    delete:
      tags: [Storage Zones]
      operationId: deleteS3Credential
      summary: Revoke an S3 credential
      description: Irreversible. Applications using this credential will lose access. Create a replacement and update applications before revoking an active key. In-flight requests may already be authorized.
      responses: *mutationResponses
  /user/pull-zones/settings/origin/{id}:
    parameters: *resourceParameters
    put:
      tags: [Pull Zones]
      operationId: updatePullZoneOrigin
      summary: Set the content source
      description: Requires origin for source_type=origin, or an owned source_storage_zone_name for source_type=storage_zone. The origin URL is cleared for a storage source. An omitted origin_host_header is reset to empty. Other settings are unchanged.
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/OriginSettings'}
      responses: *mutationResponses
  /user/pull-zones/settings/pricing/{id}:
    parameters: *resourceParameters
    put:
      tags: [Pull Zones]
      operationId: updatePullZonePricing
      summary: Change the CDN pricing tier
      description: Changes the delivery tier and its pricing for future usage. Review current pricing before changing this setting.
      requestBody:
        required: true
        content:
          application/json:
            schema: {type: object, required: [tier], properties: {tier: {type: string, enum: [std, hv]}}}
      responses: *mutationResponses
  /user/pull-zones/settings/security/{id}:
    parameters: *resourceParameters
    put:
      tags: [Pull Zones]
      operationId: updatePullZoneSecurity
      summary: Replace CDN security settings
      description: Omitted booleans become false. When token_auth is true, token_key is required and must not be empty after trimming; invalid requests return 422 without changing settings. When token_auth is false, an omitted or null token_key becomes empty. Send the full security configuration. Changing token_key invalidates existing signed URLs. Other settings are unchanged.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/SecuritySettings'
                - if:
                    required: [token_auth]
                    properties: {token_auth: {const: true}}
                  then:
                    required: [token_key]
                    properties:
                      token_key: {type: string, minLength: 1, pattern: '\S'}
      responses: *mutationResponses
  /user/pull-zones/settings/headers/{id}:
    parameters: *resourceParameters
    put:
      tags: [Pull Zones]
      operationId: updatePullZoneHeaders
      summary: Replace CORS settings
      description: Omitted add_cors_headers becomes false. Extensions are normalized to lowercase alphanumeric names. Enabling CORS with an empty list uses the standard extension list returned in the Pull Zone response.
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/HeaderSettings'}
      responses: *mutationResponses
  /user/pull-zones/settings/network-limits/{id}:
    parameters: *resourceParameters
    patch:
      tags: [Pull Zones]
      operationId: updatePullZoneNetworkLimits
      summary: Update delivery limits
      description: Omitted limits are preserved; null resets a supplied limit to zero. Zero disables the corresponding limit. The monthly cutoff is in GiB and may stop delivery immediately if existing usage exceeds the new limit; it is not free traffic and does not change billing.
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/NetworkSettings'}
      responses: *mutationResponses
  /user/pull-zones/settings/caching/{id}:
    parameters: *resourceParameters
    put:
      tags: [Pull Zones]
      operationId: updatePullZoneCaching
      summary: Replace general caching settings
      description: Omitted booleans become false. Other settings are unchanged.
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/CachingSettings'}
      responses: *mutationResponses
  /user/pull-zones/settings/origin-shield/{id}:
    parameters: *resourceParameters
    put:
      tags: [Pull Zones]
      operationId: updatePullZoneOriginShield
      summary: Configure origin shield
      description: Omitted origin_shield becomes false.
      requestBody:
        required: true
        content:
          application/json:
            schema: {type: object, properties: {origin_shield: {type: boolean}}}
      responses: *mutationResponses
  /user/pull-zones/settings/coalescing/{id}:
    parameters: *resourceParameters
    put:
      tags: [Pull Zones]
      operationId: updatePullZoneCoalescing
      summary: Configure request coalescing
      description: Omitted coalescing becomes false. An omitted timeout is preserved.
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/CoalescingSettings'}
      responses: *mutationResponses
  /user/pull-zones/settings/edge-rules/{id}:
    parameters: *resourceParameters
    put:
      tags: [Pull Zones]
      operationId: replacePullZoneEdgeRules
      summary: Replace all edge rules
      description: Replaces the entire ordered list. Send an empty array to remove all rules. There is no merge with existing rules. Header names are used only for header conditions/actions; block_request ignores header and value. See the Edge Rules guide for matching and variable substitution.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [edge_rules]
              properties:
                edge_rules: {type: array, maxItems: 100, items: {$ref: '#/components/schemas/EdgeRule'}}
      responses: *mutationResponses
  /user/pull-zones/purge/{id}:
    parameters: *resourceParameters
    post:
      tags: [Pull Zones]
      operationId: purgePullZone
      summary: Purge the entire Pull Zone cache
      description: Invalidates the zone cache asynchronously. This does not delete origin or storage objects. No path-level purge or completion status endpoint is provided. Repeated calls request another full purge and can increase origin traffic.
      responses:
        '202': {$ref: '#/components/responses/Message'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/EmailNotVerified'}
        '404': {$ref: '#/components/responses/NotFound'}
        '429': {$ref: '#/components/responses/TooManyRequests'}
  /user/pull-zones/hostnames/{id}:
    parameters: *resourceParameters
    post:
      tags: [Pull Zones]
      operationId: addPullZoneHostname
      summary: Add a custom hostname
      description: Hostname must be available and must not conflict with another account or DNS record. Configure DNS before requesting certificate issuance. Re-adding an existing hostname returns 422.
      requestBody: &hostnameBody
        required: true
        content:
          application/json:
            schema: {$ref: '#/components/schemas/HostnameRequest'}
      responses:
        '201':
          description: Hostname added; TLS issuance has not yet been requested.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    required: [hostname, cname_target]
                    properties:
                      hostname: {type: string}
                      cname_target: {type: string}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/EmailNotVerified'}
        '404': {$ref: '#/components/responses/NotFound'}
        '422': {$ref: '#/components/responses/ValidationError'}
        '429': {$ref: '#/components/responses/TooManyRequests'}
    delete:
      tags: [Pull Zones]
      operationId: removePullZoneHostname
      summary: Remove a custom hostname
      description: Stops delivery under this hostname and removes its managed DNS record. The primary hostname cannot be removed. Update traffic routing first.
      requestBody: *hostnameBody
      responses:
        '200': {$ref: '#/components/responses/Message'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/EmailNotVerified'}
        '404': {$ref: '#/components/responses/NotFound'}
        '422': {$ref: '#/components/responses/ValidationError'}
        '429': {$ref: '#/components/responses/TooManyRequests'}
  /user/pull-zones/hostnames/force-ssl/{id}:
    parameters: *resourceParameters
    put:
      tags: [Pull Zones]
      operationId: setPullZoneHostnameForceSsl
      summary: Configure HTTP to HTTPS redirection
      description: Accepts the primary hostname or an additional hostname. Enabling the redirect for an additional hostname requires an active certificate; otherwise returns 422.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              allOf:
                - $ref: '#/components/schemas/HostnameRequest'
                - type: object
                  required: [force_ssl]
                  properties:
                    force_ssl: {type: boolean}
      responses:
        '200': {$ref: '#/components/responses/Message'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/EmailNotVerified'}
        '404': {$ref: '#/components/responses/NotFound'}
        '422': {$ref: '#/components/responses/ValidationError'}
        '429': {$ref: '#/components/responses/TooManyRequests'}
  /user/pull-zones/hostnames/check/{id}:
    parameters: *resourceParameters
    post:
      tags: [Pull Zones]
      operationId: checkPullZoneHostname
      summary: Verify DNS and request a TLS certificate
      description: Applies to an additional hostname. The zone must be enabled and DNS configured correctly. Returns 202 while issuance is pending, 200 if already active, or 422 if DNS is not ready. Poll GET hostname status rather than repeatedly requesting issuance.
      requestBody: *hostnameBody
      responses:
        '200': {$ref: '#/components/responses/CertificateStatus'}
        '202': {$ref: '#/components/responses/CertificateStatus'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '403': {$ref: '#/components/responses/EmailNotVerified'}
        '404': {$ref: '#/components/responses/NotFound'}
        '422':
          description: Invalid input or DNS not ready. DNS-not-ready responses contain status and message; validation errors contain message and errors.
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/CertificateStatus'
                  - $ref: '#/components/schemas/ValidationErrorResponse'
        '429': {$ref: '#/components/responses/TooManyRequests'}
  /user/pull-zones/hostnames/status/{id}:
    parameters:
      - $ref: '#/components/parameters/AcceptJson'
      - $ref: '#/components/parameters/AcceptLanguage'
      - $ref: '#/components/parameters/ResourceId'
      - name: hostname
        in: query
        required: true
        schema: {type: string, maxLength: 253}
    get:
      tags: [Pull Zones]
      operationId: getPullZoneHostnameStatus
      summary: Get additional-hostname certificate status
      responses:
        '200': {$ref: '#/components/responses/CertificateStatus'}
        '401': {$ref: '#/components/responses/Unauthorized'}
        '404': {$ref: '#/components/responses/NotFound'}
        '422': {$ref: '#/components/responses/ValidationError'}
        '429': {$ref: '#/components/responses/TooManyRequests'}
  /user/storage-zones:
    parameters:
      - $ref: '#/components/parameters/AcceptJson'
      - $ref: '#/components/parameters/AcceptLanguage'
    get:
      tags: [Storage Zones]
      operationId: listStorageZones
      summary: List Storage Zones
      description: Returns every Storage Zone owned by the authenticated user, ordered by name.
      responses:
        '200':
          description: Storage Zones returned successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StorageZoneListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags: [Storage Zones]
      operationId: createStorageZone
      summary: Create a Storage Zone
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StorageZoneCreateRequest'
            example:
              name: media
              storage_region: uz
      responses:
        '201':
          description: Storage Zone created successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StorageZoneResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          $ref: '#/components/responses/ValidationError'
  /user/storage-zones/credentials/{id}:
    parameters:
      - $ref: '#/components/parameters/AcceptJson'
      - $ref: '#/components/parameters/AcceptLanguage'
      - $ref: '#/components/parameters/ResourceId'
    get:
      tags: [Storage Zones]
      operationId: getStorageZoneCredentials
      summary: Get Storage Zone credentials
      description: |
        Returns the HTTP access key and all S3 credentials for a Storage Zone
        owned by the authenticated user. This is a read-only operation and does
        not create or rotate keys. Keep these secrets private; do not log them
        or expose them in client-side code.
      responses:
        '200':
          description: Storage Zone credentials returned successfully.
          headers:
            Cache-Control:
              schema:
                type: string
                const: private, no-store
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StorageZoneCredentialsResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /user/storage-zones/{id}:
    parameters:
      - $ref: '#/components/parameters/AcceptJson'
      - $ref: '#/components/parameters/AcceptLanguage'
      - $ref: '#/components/parameters/ResourceId'
    get:
      tags: [Storage Zones]
      operationId: getStorageZone
      summary: Get a Storage Zone
      description: Returns configuration and usage counters for one owned Storage Zone.
      responses:
        '200':
          description: Storage Zone returned successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StorageZoneResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    patch:
      tags: [Storage Zones]
      operationId: updateStorageZoneSettings
      summary: Update Storage Zone settings
      description: Updates the HLS segmenter setting. The request must include the setting value.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StorageZoneUpdateRequest'
      responses:
        '200':
          description: Storage Zone updated successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/StorageZoneResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationError'
    delete:
      tags: [Storage Zones]
      operationId: deleteStorageZone
      summary: Delete a Storage Zone
      description: |
        Deletes an empty Storage Zone. Delete all objects and any linked Pull
        Zones before deleting the Storage Zone.
      responses:
        '204':
          description: Storage Zone deleted successfully.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          $ref: '#/components/responses/Conflict'
  /user/pull-zones:
    parameters:
      - $ref: '#/components/parameters/AcceptJson'
      - $ref: '#/components/parameters/AcceptLanguage'
    get:
      tags: [Pull Zones]
      operationId: listPullZones
      summary: List Pull Zones
      description: Returns every Pull Zone owned by the authenticated user, ordered by name.
      responses:
        '200':
          description: Pull Zones returned successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PullZoneListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags: [Pull Zones]
      operationId: createPullZone
      summary: Create a Pull Zone
      description: Creates an enabled Pull Zone backed by an origin URL or an owned Storage Zone.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PullZoneCreateRequest'
            examples:
              origin:
                summary: Origin-backed Pull Zone
                value:
                  name: website
                  source_type: origin
                  origin: https://origin.example.com
                  tier: std
              storageZone:
                summary: Storage-backed Pull Zone
                value:
                  name: media
                  source_type: storage_zone
                  source_storage_zone_name: media
                  tier: hv
      responses:
        '201':
          description: Pull Zone created successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PullZoneResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '422':
          $ref: '#/components/responses/ValidationError'
  /user/pull-zones/{id}:
    parameters:
      - $ref: '#/components/parameters/AcceptJson'
      - $ref: '#/components/parameters/AcceptLanguage'
      - $ref: '#/components/parameters/ResourceId'
    get:
      tags: [Pull Zones]
      operationId: getPullZone
      summary: Get a Pull Zone
      responses:
        '200':
          description: Pull Zone returned successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PullZoneResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    patch:
      tags: [Pull Zones]
      operationId: updatePullZoneSettings
      summary: Enable or disable a Pull Zone
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PullZoneUpdateRequest'
      responses:
        '200':
          description: Pull Zone updated successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PullZoneResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationError'
    delete:
      tags: [Pull Zones]
      operationId: deletePullZone
      summary: Delete a Pull Zone
      responses:
        '204':
          description: Pull Zone deleted successfully.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
  /user/ssh-keys:
    parameters:
      - $ref: '#/components/parameters/AcceptJson'
      - $ref: '#/components/parameters/AcceptLanguage'
    get:
      tags: [SSH Keys]
      operationId: listSshKeys
      summary: List SSH keys
      description: Returns the authenticated user's SSH keys, with the default key first.
      responses:
        '200':
          description: SSH keys returned successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SshKeyListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags: [SSH Keys]
      operationId: createSshKey
      summary: Add an SSH key
      description: |
        Adds an SSH public key for VPS provisioning. A user may store up to 20
        keys. The first key is always made the default key.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SshKeyCreateRequest'
            example:
              name: Work laptop
              public_key: ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIC7examplekeymaterial
              is_default: true
      responses:
        '201':
          description: SSH key added successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SshKeyResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/EmailNotVerified'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '422':
          $ref: '#/components/responses/ValidationError'
  /user/ssh-keys/{id}:
    parameters:
      - $ref: '#/components/parameters/AcceptJson'
      - $ref: '#/components/parameters/AcceptLanguage'
      - $ref: '#/components/parameters/ResourceId'
    delete:
      tags: [SSH Keys]
      operationId: deleteSshKey
      summary: Delete an SSH key
      description: If the default key is deleted, the oldest remaining key becomes the default.
      responses:
        '204':
          description: SSH key deleted successfully.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/EmailNotVerified'
        '404':
          $ref: '#/components/responses/NotFound'
  /user/ssh-keys/default/{id}:
    parameters:
      - $ref: '#/components/parameters/AcceptJson'
      - $ref: '#/components/parameters/AcceptLanguage'
      - $ref: '#/components/parameters/ResourceId'
    post:
      tags: [SSH Keys]
      operationId: setDefaultSshKey
      summary: Set default SSH key
      description: Sets the specified SSH key as the default key for new VPS instances.
      responses:
        '200':
          description: Default SSH key updated successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SshKeyResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/EmailNotVerified'
        '404':
          $ref: '#/components/responses/NotFound'
  /user/vps/catalog:
    parameters:
      - $ref: '#/components/parameters/AcceptJson'
      - $ref: '#/components/parameters/AcceptLanguage'
    get:
      tags: [VPS Instances]
      operationId: getVpsCatalog
      summary: Get VPS catalog
      description: Returns purchasable plans, public image metadata, billing periods, promotion terms, and the account balance.
      responses:
        '200':
          description: VPS catalog returned successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VpsCatalogResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
  /user/vps:
    parameters:
      - $ref: '#/components/parameters/AcceptJson'
      - $ref: '#/components/parameters/AcceptLanguage'
    get:
      tags: [VPS Instances]
      operationId: listVpsInstances
      summary: List VPS instances
      responses:
        '200':
          description: VPS instances returned successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VpsInstanceListResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
    post:
      tags: [VPS Instances]
      operationId: createVpsInstance
      summary: Create a VPS instance
      description: Charges the account balance when sufficient; otherwise creates a pending card payment. Uses `order_token` as an idempotency key; reusing it with a different payload is rejected.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/VpsCreateRequest'
      responses:
        '200':
          description: The matching result of an earlier request with the same idempotency token.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VpsInstanceResponse'
        '201':
          description: VPS order created successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VpsInstanceResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/EmailNotVerified'
        '422':
          $ref: '#/components/responses/ValidationError'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /user/vps/password/{id}:
    parameters:
      - $ref: '#/components/parameters/AcceptJson'
      - $ref: '#/components/parameters/AcceptLanguage'
      - $ref: '#/components/parameters/ResourceId'
    get:
      tags: [VPS Instances]
      operationId: getVpsPassword
      summary: Retrieve the initial console password of an owned VPS
      description: |
        Requires a verified account. Returns the initial login password after the VPS is provisioned, when available.
        Treat the response as sensitive: do not cache or log it.
        Changing the password inside the VPS does not change the password returned by this endpoint.
      responses:
        '200':
          description: Initial console login password. Cache-Control is private, no-store.
          content:
            application/json:
              schema:
                type: object
                required: [data]
                properties:
                  data:
                    type: object
                    required: [password]
                    properties:
                      password:
                        type: string
                        format: password
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/EmailNotVerified'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /user/vps/status/{id}:
    parameters:
      - $ref: '#/components/parameters/AcceptJson'
      - $ref: '#/components/parameters/AcceptLanguage'
      - $ref: '#/components/parameters/ResourceId'
    get:
      tags: [VPS Instances]
      operationId: getVpsRuntimeStatus
      summary: Get VPS runtime status
      responses:
        '200':
          description: Lifecycle and runtime resource usage returned successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VpsRuntimeResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '429':
          $ref: '#/components/responses/TooManyRequests'
        '502':
          $ref: '#/components/responses/UpstreamError'
  /user/vps/reinstall/{id}:
    parameters:
      - $ref: '#/components/parameters/AcceptJson'
      - $ref: '#/components/parameters/AcceptLanguage'
      - $ref: '#/components/parameters/ResourceId'
    post:
      tags: [VPS Instances]
      operationId: reinstallVpsInstance
      summary: Reinstall the operating system on an existing VPS
      description: |
        Permanently deletes all VPS data and installs a clean operating system.
        Back up your data outside the VPS before requesting this operation.
        The VPS is unavailable during installation; the operation cannot be undone.
        The IP address, hostname, plan, resources and paid period are preserved.
        Reinstallation does not charge a new purchase or restart the paid period.

        Available only to the owner with a verified email, for a previously
        provisioned VPS in running or stopped state with an active, unexpired
        paid period and no pending power action. Suspended VPS instances cannot
        be reinstalled. Select an image from GET /user/vps/catalog and an owned
        SSH key from GET /user/ssh-keys. The selected key replaces the previous
        login key. A new console password is available from GET /user/vps/password/{id}
        after successful installation. SSH host keys will also change.

        Use a new request_token for each intentional reinstallation. Reusing any
        previously accepted token for this VPS returns 422 without starting
        another installation. If the response is lost, retry only with the same
        token or check GET /user/vps/{id}; do not generate a new token automatically.
        Poll GET /user/vps/status/{id} to follow reinstall_queued and reinstalling
        until running. If the status becomes reinstall_failed, contact support;
        the previous data may already be deleted. No automatic destructive retry
        is performed. The image and SSH key in VPS details reflect the requested
        configuration as soon as the request is accepted.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/VpsReinstallRequest'
      responses:
        '202':
          description: Reinstallation accepted. Follow the VPS lifecycle status for completion.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VpsInstanceResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/EmailNotVerified'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationError'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /user/vps/power/{action}/{id}:
    parameters:
      - $ref: '#/components/parameters/AcceptJson'
      - $ref: '#/components/parameters/AcceptLanguage'
      - $ref: '#/components/parameters/VpsPowerAction'
      - $ref: '#/components/parameters/ResourceId'
    post:
      tags: [VPS Instances]
      operationId: powerVpsInstance
      summary: Queue a VPS power action
      description: Repeating the same pending action is idempotent. A different action is rejected until the pending action completes.
      responses:
        '202':
          description: Power action queued or already pending.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VpsPowerResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/EmailNotVerified'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationError'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /user/vps/extension-options/{id}:
    parameters:
      - $ref: '#/components/parameters/AcceptJson'
      - $ref: '#/components/parameters/AcceptLanguage'
      - $ref: '#/components/parameters/ResourceId'
    get:
      tags: [VPS Instances]
      operationId: getVpsExtensionOptions
      summary: Get VPS extension quotes
      responses:
        '200':
          description: Extension options returned successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VpsExtensionOptionsResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationError'
  /user/vps/extend/{id}:
    parameters:
      - $ref: '#/components/parameters/AcceptJson'
      - $ref: '#/components/parameters/AcceptLanguage'
      - $ref: '#/components/parameters/ResourceId'
    post:
      tags: [VPS Instances]
      operationId: extendVpsInstance
      summary: Extend a VPS subscription
      description: |
        Pays for the extension from the account balance when sufficient; otherwise returns a pending card payment.
        Complete card payment through the checkout endpoint. An unpaid extension does not change the paid term or renewal schedule.
        Use a new `order_token` for each extension and reuse it only when retrying the same request with the same parameters.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/VpsExtendRequest'
      responses:
        '200':
          description: Extension completed successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VpsInstanceResponse'
        '202':
          description: Extension payment is required or still pending.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VpsInstanceResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/EmailNotVerified'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationError'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /user/vps/payment/providers/{id}:
    parameters:
      - $ref: '#/components/parameters/AcceptJson'
      - $ref: '#/components/parameters/AcceptLanguage'
      - $ref: '#/components/parameters/ResourceId'
    get:
      tags: [VPS Instances]
      operationId: getVpsPaymentProviders
      summary: Get payment providers for a pending VPS payment
      responses:
        '200':
          description: Available providers and pending payment returned successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VpsPaymentProvidersResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/EmailNotVerified'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationError'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /user/vps/payment/checkout/{id}:
    parameters:
      - $ref: '#/components/parameters/AcceptJson'
      - $ref: '#/components/parameters/AcceptLanguage'
      - $ref: '#/components/parameters/ResourceId'
    post:
      tags: [VPS Instances]
      operationId: checkoutVpsPayment
      summary: Start or continue VPS checkout
      description: >-
        Returns instructions to open or continue payment for a VPS purchase or extension.
        Supply `payment_provider` to choose a payment method, or omit it to continue with the current selection.
        Follow the returned redirect or submit the returned form fields to the specified URL.
        Pay only once per purchase or extension. Changing payment methods does not cancel previously opened invoices or issue refunds.
        Duplicate payments do not provide additional service or automatically credit your balance; contact support if you paid more than once.
        Retrieve the VPS resource to check payment and service status. Contact support if a completed payment is not reflected in the service.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/VpsCheckoutRequest'
      responses:
        '200':
          description: Browser checkout instructions returned successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VpsCheckoutResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/EmailNotVerified'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationError'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /user/vps/payment/{id}:
    parameters:
      - $ref: '#/components/parameters/AcceptJson'
      - $ref: '#/components/parameters/AcceptLanguage'
      - $ref: '#/components/parameters/ResourceId'
    delete:
      tags: [VPS Instances]
      operationId: cancelVpsPayment
      summary: Cancel a pending VPS order or extension payment
      description: |
        Cancels an unpaid VPS purchase or an eligible pending extension payment.
        Cancelling a purchase removes the unpaid VPS order. Cancelling an extension leaves the existing subscription unchanged.
        Extension cancellation may be unavailable once payment processing has started; a rejected cancellation returns 422.
        Do not pay a cancelled order. Cancellation is not a refund; contact support if you have already paid.
        A payment made after cancellation does not activate the VPS or automatically credit your balance.
      responses:
        '200':
          description: Pending payment cancelled successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VpsPaymentCancellationResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/EmailNotVerified'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationError'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /user/vps/subscription/{id}:
    parameters:
      - $ref: '#/components/parameters/AcceptJson'
      - $ref: '#/components/parameters/AcceptLanguage'
      - $ref: '#/components/parameters/ResourceId'
    delete:
      tags: [VPS Instances]
      operationId: cancelVpsSubscription
      summary: Cancel VPS renewal at the end of the paid period
      responses:
        '200':
          description: Subscription cancellation scheduled.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VpsInstanceResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/EmailNotVerified'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationError'
        '429':
          $ref: '#/components/responses/TooManyRequests'
    put:
      tags: [VPS Instances]
      operationId: resumeVpsSubscription
      summary: Resume a VPS subscription scheduled for cancellation
      responses:
        '200':
          description: Subscription resumed successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VpsInstanceResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/EmailNotVerified'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationError'
        '429':
          $ref: '#/components/responses/TooManyRequests'
  /user/vps/{id}:
    parameters:
      - $ref: '#/components/parameters/AcceptJson'
      - $ref: '#/components/parameters/AcceptLanguage'
      - $ref: '#/components/parameters/ResourceId'
    get:
      tags: [VPS Instances]
      operationId: getVpsInstance
      summary: Get a VPS instance
      responses:
        '200':
          description: VPS instance returned successfully.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/VpsInstanceResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '404':
          $ref: '#/components/responses/NotFound'
    delete:
      tags: [VPS Instances]
      operationId: deleteUnpaidVpsInstance
      summary: Delete an unpaid VPS draft or failed payment order
      responses:
        '204':
          description: Unpaid VPS order deleted successfully.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/EmailNotVerified'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationError'
        '429':
          $ref: '#/components/responses/TooManyRequests'
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: API token
      description: API token created in the authenticated user's ACDN dashboard.
  parameters:
    DomainId:
      name: domain_id
      in: path
      required: true
      schema: {type: string, pattern: '^[a-fA-F0-9]{24}$'}
    EntryId:
      name: entry_id
      in: path
      required: true
      schema: {type: string, pattern: '^[a-fA-F0-9]{24}$'}
    CredentialId:
      name: credentialId
      in: path
      required: true
      description: Credential id returned by the Storage Zone credentials endpoint, not access_key_id.
      schema: {type: string, pattern: '^[a-fA-F0-9]{24}$'}
    AcceptJson:
      name: Accept
      in: header
      required: true
      description: Response media type.
      schema:
        type: string
        enum: [application/json]
        default: application/json
      example: application/json
    AcceptLanguage:
      name: Accept-Language
      in: header
      required: false
      description: Preferred response language. When omitted or unsupported, the authenticated account language is used.
      schema:
        type: string
        enum: [en, ru]
    ResourceId:
      name: id
      in: path
      required: true
      description: ACDN resource identifier.
      schema:
        type: string
        pattern: '^[a-fA-F0-9]{24}$'
    VpsPowerAction:
      name: action
      in: path
      required: true
      description: Power action to queue.
      schema:
        type: string
        enum: [start, shutdown, stop, reboot]
  responses:
    Forbidden:
      description: Email verification is required, or the resource is service-managed and cannot be changed through this operation.
      content:
        application/json:
          schema: {$ref: '#/components/schemas/ErrorResponse'}
    Message:
      description: Operation result message.
      content:
        application/json:
          schema: {type: object, required: [message], properties: {message: {type: string}}}
    CertificateStatus:
      description: Current certificate status and a human-readable message.
      content:
        application/json:
          schema: {$ref: '#/components/schemas/CertificateStatus'}
    DnsZone:
      description: DNS zone with its records.
      content:
        application/json:
          schema:
            type: object
            required: [data]
            properties:
              data: {$ref: '#/components/schemas/DnsZone'}
    DnsRecord:
      description: DNS record saved.
      content:
        application/json:
          schema:
            type: object
            required: [data]
            properties:
              data: {$ref: '#/components/schemas/DnsRecord'}
    Unauthorized:
      description: The API token is missing, invalid, or expired.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            message: Unauthenticated.
    EmailNotVerified:
      description: The authenticated user's email address is not verified.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            message: Your email address is not verified.
    TooManyRequests:
      description: Too many requests were submitted in the current rate-limit window.
      headers:
        Retry-After:
          description: Seconds to wait before retrying.
          schema: {type: integer, minimum: 0}
        X-RateLimit-Limit:
          description: Request limit for the applicable rate-limit window.
          schema: {type: integer}
        X-RateLimit-Remaining:
          description: Requests remaining in the applicable window.
          schema: {type: integer}
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            message: Too Many Attempts.
    NotFound:
      description: The resource does not exist or is not owned by the authenticated user.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            message: Resource not found.
    Conflict:
      description: The requested operation conflicts with the current resource state.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          examples:
            notEmpty:
              value:
                message: Storage zone is not empty. Delete all files first.
            linked:
              value:
                message: Storage zone is linked to a pull zone. Delete the pull zone first.
    ValidationError:
      description: One or more request fields failed validation.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ValidationErrorResponse'
          example:
            message: The given data was invalid.
            errors:
              name:
                - The name has already been taken.
    UpstreamError:
      description: VPS runtime information is temporarily unavailable. Try again later.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
  schemas:
    DnsZone:
      type: object
      required: [id, name, entries]
      properties:
        id: {type: string}
        name: {type: string}
        entries: {type: array, items: {$ref: '#/components/schemas/DnsRecord'}}
    DnsRecordRequest:
      type: object
      required: [name, type, ttl, value]
      properties:
        name:
          type: string
          maxLength: 255
          description: 'Use @ for the apex, a relative name, or a name within the zone. Labels may contain letters, digits, underscores and hyphens; a leading wildcard label is allowed. Names are normalized to lowercase.'
        type:
          type: integer
          enum: [1, 2, 5, 6, 12, 15, 16, 28, 65401]
          description: 'DNS type code: A=1, NS=2, CNAME=5, SOA=6, PTR=12, MX=15, TXT=16, AAAA=28, ALIAS=65401.'
        ttl: {type: integer, minimum: 0, description: Cache lifetime in seconds.}
        priority: {type: [integer, 'null'], minimum: 0, description: Required for MX records.}
        value:
          type: string
          minLength: 1
          description: 'A requires IPv4; AAAA requires IPv6. CNAME, MX, NS, PTR and ALIAS require a hostname target. Use a trailing dot for an absolute target, a relative name within the zone, or @ for the apex. TXT and SOA use text values.'
    DnsRecord:
      allOf:
        - $ref: '#/components/schemas/DnsRecordRequest'
        - type: object
          required: [id, priority, read_only]
          properties:
            id: {type: string}
            read_only: {type: boolean, description: This record is managed by a linked service and cannot be edited or deleted individually.}
    S3Permissions:
      type: object
      required: [can_read, can_write, can_delete]
      properties:
        can_read: {type: boolean, description: List and download objects.}
        can_write: {type: boolean, description: Upload and copy objects.}
        can_delete: {type: boolean, description: Delete objects.}
    S3Credential:
      allOf:
        - $ref: '#/components/schemas/S3Permissions'
        - type: object
          required: [id, access_key_id, secret_access_key]
          properties:
            id: {type: string}
            access_key_id: {type: string}
            secret_access_key: {type: string}
    OriginSettings:
      type: object
      required: [source_type]
      properties:
        source_type: {type: string, enum: [origin, storage_zone]}
        origin: {type: [string, 'null'], format: uri}
        source_storage_zone_name: {type: [string, 'null']}
        origin_host_header: {type: [string, 'null'], maxLength: 255}
      allOf:
        - if: {properties: {source_type: {const: origin}}}
          then: {required: [origin], properties: {origin: {type: string, minLength: 1}}}
        - if: {properties: {source_type: {const: storage_zone}}}
          then: {required: [source_storage_zone_name], properties: {source_storage_zone_name: {type: string, minLength: 1}}}
    SecuritySettings:
      type: object
      properties:
        token_auth: {type: boolean, default: false}
        token_ip: {type: boolean, default: false}
        block_post_requests: {type: boolean, default: false}
        block_root_path_access: {type: boolean, default: false}
        token_key: {type: [string, 'null'], maxLength: 255}
    HeaderSettings:
      type: object
      properties:
        add_cors_headers: {type: boolean, default: false}
        cors_extensions: {type: [string, 'null'], maxLength: 1000}
    CachingSettings:
      type: object
      properties:
        cache_sort_query: {type: boolean, default: false}
        large_object_delivery: {type: boolean, default: false}
    CoalescingSettings:
      type: object
      properties:
        coalescing: {type: boolean, default: false}
        coalescing_timeout: {type: [integer, 'null'], minimum: 0, description: Timeout in seconds.}
    NetworkSettings:
      type: object
      properties:
        download_speed_limit_kbps: {type: [integer, 'null'], minimum: 0, maximum: 100000000, description: Maximum transfer speed per connection in KB/s.}
        download_speed_limit_after_kb: {type: [integer, 'null'], minimum: 0, maximum: 100000000, description: Transfer amount in KB before speed limiting starts.}
        request_limit_rps: {type: [integer, 'null'], minimum: 0, maximum: 1000000}
        request_limit_burst: {type: [integer, 'null'], minimum: 0, maximum: 1000000}
        connection_limit_per_ip: {type: [integer, 'null'], minimum: 0, maximum: 1000000}
        monthly_bandwidth_limit_gb: {type: [integer, 'null'], minimum: 0, maximum: 100000000, description: Monthly delivery cutoff in GiB; not a free allowance.}
    EdgeRule:
      type: object
      required: [enabled, conditions, actions]
      properties:
        description: {type: [string, 'null'], maxLength: 255, description: Empty descriptions are saved as Edge Rule.}
        enabled: {type: boolean}
        match: {type: [string, 'null'], enum: [any, all, none, null], default: any}
        conditions:
          type: array
          maxItems: 100
          items:
            type: object
            required: [field, values]
            properties:
              field: {type: string, enum: [request_url, remote_ip, request_header]}
              header: {type: [string, 'null'], maxLength: 255, description: Header name when field is request_header.}
              match: {type: [string, 'null'], enum: [any, all, none, null], description: Defaults to the enclosing rule match.}
              values: {type: array, maxItems: 100, items: {type: [string, 'null'], maxLength: 2000}, description: Empty values are discarded and other values trimmed.}
        actions:
          type: array
          minItems: 1
          maxItems: 100
          items:
            type: object
            required: [action]
            properties:
              action: {type: string, enum: [set_request_header, override_origin_url, override_cache_key, block_request]}
              header: {type: [string, 'null'], maxLength: 255}
              value: {type: [string, 'null'], maxLength: 2000}
    HostnameRequest:
      type: object
      required: [hostname]
      properties:
        hostname: {type: string, minLength: 1, maxLength: 253, description: 'ASCII or punycode hostname, normalized to lowercase without a trailing dot. Wildcards are not supported.'}
    CertificateStatus:
      type: object
      required: [status, message]
      properties:
        status: {type: string, enum: [disabled, pending, active, failed], description: 'disabled: certificate not enabled; pending: issuance in progress; active: certificate available; failed: issuance failed or timed out.'}
        message: {type: string}
    DeliveryHostname:
      type: object
      required: [hostname, force_ssl, certificate_status, certificate_expires_at, cname_target]
      properties:
        hostname: {type: string}
        force_ssl: {type: boolean}
        certificate_status: {type: string, enum: [disabled, pending, active, failed]}
        certificate_expires_at: {type: [string, 'null'], format: date-time}
        cname_target: {type: string}
    ErrorResponse:
      type: object
      required: [message]
      properties:
        message:
          type: string
    ValidationErrorResponse:
      allOf:
        - $ref: '#/components/schemas/ErrorResponse'
        - type: object
          required: [errors]
          properties:
            errors:
              type: object
              additionalProperties:
                type: array
                items:
                  type: string
    StorageZone:
      type: object
      required:
        - id
        - name
        - storage_region
        - files_count
        - size
        - current_month_usage_usd
        - hls_segmenter_enabled
        - created_at
      properties:
        id:
          type: string
          pattern: '^[a-fA-F0-9]{24}$'
        name:
          type: string
          description: Storage Zone name and S3 bucket name.
          pattern: '^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$'
          minLength: 1
          maxLength: 63
        storage_region:
          type: string
          enum: [uz]
        files_count:
          type: integer
          minimum: 0
          description: Number of logical objects in the Storage Zone.
        size:
          type: integer
          format: int64
          minimum: 0
          description: Total logical object size in bytes.
        current_month_usage_usd:
          type: number
          format: double
          minimum: 0
          description: Storage usage charge accumulated during the current month in USD.
        hls_segmenter_enabled:
          type: boolean
          description: Whether MP4 objects may be packaged as HLS through linked Pull Zones.
        created_at:
          type: [string, 'null']
          format: date-time
    StorageZoneCreateRequest:
      type: object
      required: [name, storage_region]
      properties:
        name:
          type: string
          pattern: '^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$'
          minLength: 1
          maxLength: 63
        storage_region:
          type: string
          enum: [uz]
    StorageZoneUpdateRequest:
      type: object
      required: [hls_segmenter_enabled]
      properties:
        hls_segmenter_enabled:
          type: boolean
    StorageZoneResponse:
      type: object
      required: [data]
      properties:
        data:
          $ref: '#/components/schemas/StorageZone'
    StorageZoneCredentialsResponse:
      type: object
      required: [data]
      properties:
        data:
          type: object
          required: [endpoint, bucket, region, access_key, s3_credentials]
          properties:
            endpoint:
              type: string
              format: uri
              description: Base URL for the HTTP object API and S3-compatible endpoint.
            bucket:
              type: string
              description: Storage Zone name. Use as the S3 bucket or the first HTTP object URL path segment.
            region:
              type: string
              enum: [uz]
              description: Region to configure in the S3 client.
            access_key:
              type: string
              description: Secret to send in the AccessKey header for HTTP object requests.
            s3_credentials:
              type: array
              description: Available S3 credentials, ordered by creation time. Empty when no S3 keys exist.
              items:
                type: object
                required: [id, access_key_id, secret_access_key, can_read, can_write, can_delete]
                properties:
                  id:
                    type: string
                    description: Credential identifier for permission updates and revocation.
                  access_key_id:
                    type: string
                  secret_access_key:
                    type: string
                    description: Secret access key for S3 request signing.
                  can_read:
                    type: boolean
                    description: Whether the key may list and download objects.
                  can_write:
                    type: boolean
                    description: Whether the key may upload and copy objects.
                  can_delete:
                    type: boolean
                    description: Whether the key may delete objects.
    StorageZoneListResponse:
      type: object
      required: [data]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/StorageZone'
    PullZone:
      type: object
      description: Includes saved CDN settings and current-month usage counters, which may lag live traffic. Settings are not a propagation status check. Contains the secret token-authentication key and potentially secrets in edge rules or origin URLs; do not log or cache this response. Use section-specific settings endpoints to change configuration.
      allOf:
        - $ref: '#/components/schemas/SecuritySettings'
        - $ref: '#/components/schemas/HeaderSettings'
        - $ref: '#/components/schemas/CachingSettings'
        - $ref: '#/components/schemas/CoalescingSettings'
        - $ref: '#/components/schemas/NetworkSettings'
        - type: object
          required: [origin_host_header, token_key, token_ip, block_post_requests, block_root_path_access, add_cors_headers, cors_extensions, cache_sort_query, large_object_delivery, origin_shield, coalescing, coalescing_timeout, download_speed_limit_kbps, download_speed_limit_after_kb, request_limit_rps, request_limit_burst, connection_limit_per_ip, monthly_bandwidth_limit_gb, edge_rules, additional_hostnames, sent_bytes, billable_bytes, charge_usd]
          properties:
            origin_host_header: {type: string}
            origin_shield: {type: boolean}
            edge_rules: {type: array, items: {$ref: '#/components/schemas/EdgeRule'}}
            additional_hostnames:
              type: array
              description: Point a custom subdomain CNAME to cname_target. For a root domain, use ACDN DNS and the managed ALIAS record instead. For hostnames in an owned ACDN DNS zone, records are managed automatically. Use the status endpoint for current certificate progress, including timed-out issuance.
              items: {$ref: '#/components/schemas/DeliveryHostname'}
            sent_bytes: {type: integer}
            billable_bytes: {type: integer}
            charge_usd: {type: number}
      required:
        - id
        - name
        - hostname
        - source_type
        - source_storage_zone_name
        - origin
        - tier
        - enabled
        - token_auth
        - force_ssl
        - config_changed_at
        - created_at
      properties:
        id:
          type: string
          pattern: '^[a-fA-F0-9]{24}$'
        name:
          type: string
          pattern: '^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$'
          minLength: 1
          maxLength: 63
        hostname:
          type: string
          description: Default public hostname assigned to the Pull Zone.
          example: media.vvvv.uz
        source_type:
          type: string
          enum: [origin, storage_zone]
        source_storage_zone_name:
          type: [string, 'null']
        origin:
          type: [string, 'null']
          format: uri
        tier:
          type: string
          enum: [std, hv]
        enabled:
          type: boolean
        token_auth:
          type: boolean
        force_ssl:
          type: boolean
        config_changed_at:
          type: [string, 'null']
          format: date-time
        created_at:
          type: [string, 'null']
          format: date-time
    PullZoneCreateBase:
      type: object
      required: [name, source_type, tier]
      properties:
        name:
          type: string
          pattern: '^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$'
          minLength: 1
          maxLength: 63
        source_type:
          type: string
          enum: [origin, storage_zone]
        tier:
          type: string
          enum: [std, hv]
    OriginPullZoneCreateRequest:
      allOf:
        - $ref: '#/components/schemas/PullZoneCreateBase'
        - type: object
          required: [source_type, origin]
          properties:
            name:
              type: string
              pattern: '^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$'
              minLength: 1
              maxLength: 63
            source_type:
              const: origin
            origin:
              type: string
              format: uri
            tier:
              type: string
              enum: [std, hv]
    StorageBackedPullZoneCreateRequest:
      allOf:
        - $ref: '#/components/schemas/PullZoneCreateBase'
        - type: object
          required: [source_type, source_storage_zone_name]
          properties:
            name:
              type: string
              pattern: '^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$'
              minLength: 1
              maxLength: 63
            source_type:
              const: storage_zone
            source_storage_zone_name:
              type: string
              minLength: 1
            tier:
              type: string
              enum: [std, hv]
    PullZoneCreateRequest:
      oneOf:
        - $ref: '#/components/schemas/OriginPullZoneCreateRequest'
        - $ref: '#/components/schemas/StorageBackedPullZoneCreateRequest'
      discriminator:
        propertyName: source_type
        mapping:
          origin: '#/components/schemas/OriginPullZoneCreateRequest'
          storage_zone: '#/components/schemas/StorageBackedPullZoneCreateRequest'
    PullZoneUpdateRequest:
      type: object
      required: [enabled]
      properties:
        enabled:
          type: boolean
    PullZoneResponse:
      type: object
      required: [data]
      properties:
        data:
          $ref: '#/components/schemas/PullZone'
    PullZoneListResponse:
      type: object
      required: [data]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/PullZone'
    SshKey:
      type: object
      required:
        - id
        - name
        - type
        - public_key
        - fingerprint
        - is_default
        - created_at
      properties:
        id:
          type: string
          pattern: '^[a-fA-F0-9]{24}$'
        name:
          type: string
          minLength: 1
          maxLength: 100
        type:
          type: string
          enum: [ssh-ed25519, ecdsa-sha2-nistp256, ssh-rsa]
        public_key:
          type: string
          description: Normalized OpenSSH public key without a trailing comment.
        fingerprint:
          type: string
          description: SHA-256 fingerprint in OpenSSH format.
          pattern: '^SHA256:'
        is_default:
          type: boolean
        created_at:
          type: [string, 'null']
          format: date-time
    SshKeyCreateRequest:
      type: object
      required: [name, public_key]
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 100
        public_key:
          type: string
          maxLength: 16384
          description: OpenSSH public key using a supported key type. RSA keys must be at least 1024 bits.
        is_default:
          type: boolean
          default: false
    SshKeyResponse:
      type: object
      required: [data]
      properties:
        data:
          $ref: '#/components/schemas/SshKey'
    SshKeyListResponse:
      type: object
      required: [data]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/SshKey'
    VpsCatalogPlan:
      type: object
      required: [key, name, cpu_cores, memory_mb, disk_gb, price_usd_month]
      properties:
        key:
          type: string
          enum: [nano, start, standard, pro, business]
        name:
          type: string
        cpu_cores:
          type: integer
          minimum: 1
        memory_mb:
          type: integer
          minimum: 1
        disk_gb:
          type: integer
          minimum: 1
        price_usd_month:
          type: number
          format: double
          minimum: 0
    VpsCatalogImage:
      type: object
      required: [key, name, default_user]
      properties:
        key:
          type: string
        name:
          type: string
        default_user:
          type: string
    VpsCatalog:
      type: object
      required: [plans, images, default_image, billing_periods, storage_label, included_ipv4, included_international_mbps, included_local_mbps, promotion, balance_usd, display_currency, usd_to_uzs_rate]
      properties:
        plans:
          type: array
          items:
            $ref: '#/components/schemas/VpsCatalogPlan'
        images:
          type: array
          items:
            $ref: '#/components/schemas/VpsCatalogImage'
        default_image:
          type: string
          description: Default image key from `images`.
        billing_periods:
          type: array
          items:
            type: integer
            enum: [1, 3, 6, 12]
        storage_label:
          type: string
        included_ipv4:
          type: integer
          minimum: 0
        included_international_mbps:
          type: integer
          minimum: 0
        included_local_mbps:
          type: integer
          minimum: 0
        promotion:
          type: object
          required: [discount_percent, billing_periods]
          properties:
            discount_percent:
              type: integer
              minimum: 0
              maximum: 100
            billing_periods:
              type: integer
              minimum: 0
        balance_usd:
          type: number
          format: double
          minimum: 0
        display_currency:
          type: string
          enum: [USD, UZS]
        usd_to_uzs_rate:
          type: number
          format: double
          minimum: 0
    VpsCatalogResponse:
      type: object
      required: [data]
      properties:
        data:
          $ref: '#/components/schemas/VpsCatalog'
    VpsSshKeyReference:
      type: object
      required: [id, name, fingerprint]
      properties:
        id:
          type: string
          pattern: '^[a-fA-F0-9]{24}$'
        name:
          type: string
        fingerprint:
          type: string
          pattern: '^SHA256:'
    VpsInstancePromotion:
      type: object
      required: [active, discount_percent, discounted_price_usd, current_cycle_discounted_periods, remaining_periods, ends_at]
      properties:
        active:
          type: boolean
        discount_percent:
          type: integer
          minimum: 0
          maximum: 100
        discounted_price_usd:
          type: number
          format: double
          minimum: 0
        current_cycle_discounted_periods:
          type: integer
          minimum: 0
        remaining_periods:
          type: integer
          minimum: 0
        ends_at:
          type: [string, 'null']
          format: date-time
    VpsInstance:
      type: object
      description: VPS configuration, subscription details, and current service and payment status.
      required:
        - id
        - name
        - hostname
        - ip
        - plan
        - image
        - cpu_cores
        - memory_mb
        - disk_gb
        - ssh_key
        - lifecycle_status
        - pending_power_action
        - billing_status
        - billing_cycle_months
        - billing_periods_completed
        - monthly_price_usd
        - order_total_usd
        - promotion
        - current_period_start
        - current_period_end
        - next_billing_at
        - grace_ends_at
        - delete_at
        - cancellation_requested_at
        - provisioned_at
        - pending_payment
        - created_at
        - updated_at
      properties:
        id:
          type: string
          pattern: '^[a-fA-F0-9]{24}$'
        name:
          type: string
          maxLength: 100
        hostname:
          type: [string, 'null']
        ip:
          type: [string, 'null']
          format: ipv4
        plan:
          type: [string, 'null']
        image:
          type: string
        cpu_cores:
          type: integer
          minimum: 1
        memory_mb:
          type: integer
          minimum: 1
        disk_gb:
          type: integer
          minimum: 1
        ssh_key:
          oneOf:
            - $ref: '#/components/schemas/VpsSshKeyReference'
            - type: 'null'
        lifecycle_status:
          type: string
          enum: [awaiting_payment, payment_failed, pending, queued, provisioning, reinstall_queued, reinstalling, reinstall_failed, migrating, running, stopped, failed, cleanup_failed, refund_queued, deleting]
        pending_power_action:
          type: [string, 'null']
          enum: [start, shutdown, stop, reboot, null]
        billing_status:
          type: [string, 'null']
          enum: [pending_payment, payment_failed, provisioning_failed, active, past_due, cancelled, null]
        billing_cycle_months:
          type: integer
          minimum: 1
        billing_periods_completed:
          type: integer
          minimum: 0
        monthly_price_usd:
          type: [number, 'null']
          format: double
          minimum: 0
        order_total_usd:
          type: [number, 'null']
          format: double
          minimum: 0
        promotion:
          $ref: '#/components/schemas/VpsInstancePromotion'
        current_period_start:
          type: [string, 'null']
          format: date-time
        current_period_end:
          type: [string, 'null']
          format: date-time
        next_billing_at:
          type: [string, 'null']
          format: date-time
        grace_ends_at:
          type: [string, 'null']
          format: date-time
        delete_at:
          type: [string, 'null']
          format: date-time
        cancellation_requested_at:
          type: [string, 'null']
          format: date-time
        provisioned_at:
          type: [string, 'null']
          format: date-time
        pending_payment:
          oneOf:
            - $ref: '#/components/schemas/VpsPendingPayment'
            - type: 'null'
        created_at:
          type: [string, 'null']
          format: date-time
        updated_at:
          type: [string, 'null']
          format: date-time
    VpsCreateRequest:
      type: object
      required: [order_token, name, plan, billing_periods, image, ssh_key_id]
      properties:
        order_token:
          type: string
          format: uuid
        name:
          type: string
          minLength: 1
          maxLength: 100
        plan:
          type: string
          enum: [nano, start, standard, pro, business]
        billing_periods:
          type: integer
          enum: [1, 3, 6, 12]
        image:
          type: string
          description: Image key from the VPS catalog.
        ssh_key_id:
          type: string
          pattern: '^[a-fA-F0-9]{24}$'
    VpsReinstallRequest:
      type: object
      required: [request_token, image, ssh_key_id, confirm_data_loss]
      properties:
        request_token:
          type: string
          format: uuid
          description: Unique token for this intentional reinstallation. Reuse it when retrying a request whose result is unknown.
        image:
          type: string
          description: Image key from GET /user/vps/catalog.
        ssh_key_id:
          type: string
          pattern: '^[a-fA-F0-9]{24}$'
          description: An SSH key owned by the authenticated user.
        confirm_data_loss:
          type: boolean
          const: true
          description: Explicit confirmation that all existing VPS data will be permanently deleted.
    VpsInstanceResponse:
      type: object
      required: [data]
      properties:
        data:
          $ref: '#/components/schemas/VpsInstance'
    VpsInstanceListResponse:
      type: object
      required: [data]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/VpsInstance'
    VpsRuntimeResource:
      type: object
      required: [used_bytes, total_bytes]
      properties:
        used_bytes:
          type: integer
          format: int64
          minimum: 0
        total_bytes:
          type: integer
          format: int64
          minimum: 0
    VpsRuntime:
      type: object
      required: [status, uptime_seconds, cpu_percent, memory, disk]
      properties:
        status:
          type: string
        uptime_seconds:
          type: integer
          format: int64
          minimum: 0
        cpu_percent:
          type: number
          format: double
          minimum: 0
        memory:
          $ref: '#/components/schemas/VpsRuntimeResource'
        disk:
          $ref: '#/components/schemas/VpsRuntimeResource'
    VpsRuntimeResponse:
      type: object
      required: [data]
      properties:
        data:
          type: object
          required: [lifecycle_status, runtime]
          properties:
            lifecycle_status:
              type: string
            runtime:
              oneOf:
                - $ref: '#/components/schemas/VpsRuntime'
                - type: 'null'
    VpsPowerResponse:
      type: object
      required: [data]
      properties:
        data:
          type: object
          required: [id, action, status]
          properties:
            id:
              type: string
              pattern: '^[a-fA-F0-9]{24}$'
            action:
              type: string
              enum: [start, shutdown, stop, reboot]
            status:
              const: queued
    VpsExtensionOption:
      type: object
      required: [months, months_label, discounted_months, regular_months, total_usd, period_end]
      properties:
        months:
          type: integer
          enum: [1, 3, 6, 12]
        months_label:
          type: string
        discounted_months:
          type: integer
          minimum: 0
        regular_months:
          type: integer
          minimum: 0
        total_usd:
          type: number
          format: double
          minimum: 0
        period_end:
          type: [string, 'null']
          format: date-time
    VpsExtensionOptionsResponse:
      type: object
      required: [data]
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/VpsExtensionOption'
    VpsExtendRequest:
      type: object
      required: [order_token, months]
      properties:
        order_token:
          type: string
          format: uuid
        months:
          type: integer
          enum: [1, 3, 6, 12]
    VpsPendingPayment:
      type: object
      required: [id, purpose, provider, status, amount_usd, amount_uzs, extension_months, period_start, period_end, expires_at]
      properties:
        id:
          type: string
          pattern: '^[a-fA-F0-9]{24}$'
        purpose:
          type: string
          enum: [vps_order, vps_extension]
        provider:
          type: [string, 'null']
          enum: [atmos, payme, freedompay, null]
        status:
          type: string
          enum: [pending, processing]
        amount_usd:
          type: number
          format: double
          minimum: 0
        amount_uzs:
          type: integer
          minimum: 0
        extension_months:
          type: [integer, 'null']
          minimum: 1
        period_start:
          type: [string, 'null']
          format: date-time
        period_end:
          type: [string, 'null']
          format: date-time
        expires_at:
          type: [string, 'null']
          format: date-time
        payment_url:
          type: string
          format: uri
          description: Present only for a processing hosted-checkout provider. Payme is continued through the checkout endpoint.
    VpsPaymentProvider:
      type: object
      required: [name, logo, enabled, test_mode]
      properties:
        name:
          type: string
        logo:
          type: string
          format: uri
        enabled:
          type: boolean
        test_mode:
          type: boolean
    VpsPaymentProvidersResponse:
      type: object
      required: [data]
      properties:
        data:
          type: object
          required: [providers, payment]
          properties:
            providers:
              type: object
              additionalProperties:
                $ref: '#/components/schemas/VpsPaymentProvider'
            payment:
              $ref: '#/components/schemas/VpsPendingPayment'
    VpsCheckoutRequest:
      type: object
      properties:
        payment_provider:
          type: string
          enum: [atmos, payme, freedompay]
        return_url:
          type: string
          format: uri
          pattern: '^https://[^#]+$'
          description: Optional HTTPS URL to return to after checkout, without a fragment. A payment_id query parameter identifies the payment; retrieve the VPS resource to confirm its status.
    VpsCheckoutResponse:
      type: object
      required: [data]
      properties:
        data:
          oneOf:
            - type: object
              required: [type, url]
              properties:
                type:
                  const: redirect
                url:
                  type: string
                  format: uri
            - type: object
              required: [type, url, fields]
              properties:
                type:
                  const: form_post
                url:
                  type: string
                  format: uri
                fields:
                  type: object
                  additionalProperties:
                    oneOf:
                      - type: string
                      - type: integer
    VpsPaymentCancellationResponse:
      type: object
      required: [data]
      properties:
        data:
          type: object
          required: [purpose, vps_instance]
          properties:
            purpose:
              type: string
              enum: [vps_order, vps_extension]
            vps_instance:
              oneOf:
                - $ref: '#/components/schemas/VpsInstance'
                - type: 'null'
