openapi: 3.0.3
info:
  title: IP NetworX API
  version: 1.0.0
  description: |
    The public, token-authenticated REST API for managing your own hosting
    sites: WordPress, PHP version, SSL, DNS records, and projects.

    **Base URL.** `https://api.ip-networx.com/v1` — every path in this reference
    is relative to it. The `servers` entry is rewritten to the host you fetched
    the spec from, so a downloaded copy always targets that deployment.

    **Authentication.** Send your token as a Bearer credential:
    `Authorization: Bearer ipxapi_…`. A token is bound to one team and only ever
    sees that team's resources. Create and revoke tokens in the customer panel
    under API tokens.

    **Scopes.** A token has full access by default. You may optionally restrict
    it to a subset of scopes (`sites:read`, `sites:write`, `dns:read`,
    `dns:write`, `wordpress:read`, `wordpress:write`, `projects:read`,
    `projects:write`) for least-privilege use; a request outside a token's
    scopes returns `403`.

    **Identifiers.** Every resource is addressed by an opaque UUID — internal
    numeric ids are never exposed.

    **Idempotency.** `POST /sites` and `POST /sites/{site}/wordpress` accept an
    optional `Idempotency-Key` header; a retry with the same key returns the
    original result instead of acting twice.

    **Rate limits.** Requests are throttled per token, and per IP before
    authentication, in separate budgets by cost. Default ceilings:

    | Bucket | Limit | Applies to |
    | --- | --- | --- |
    | Reads | 120 / min | `GET` requests |
    | Writes | 60 / min | `POST`, `PATCH`, `PUT`, `DELETE` |
    | Provisioning | 30 / min | site create, WordPress install |

    Every response carries `X-RateLimit-Limit` and `X-RateLimit-Remaining`; a
    `429 Too Many Requests` adds `Retry-After` and `X-RateLimit-Reset`. These
    figures are environment defaults — treat the `X-RateLimit-Limit` response
    header as the authoritative live ceiling rather than hard-coding them.
servers:
  - url: https://{host}/v1
    description: The API host for this environment.
    variables:
      host:
        # The served value is injected from config('domains.api') by
        # DocsController::spec(), so the spec always points at the host it was
        # fetched from. This on-disk default is the production host (what the
        # raw file and any non-injected render show).
        default: api.ip-networx.com
        description: The IPX API host serving this spec.
security:
  - bearerAuth: [ ]
tags:
  - name: Account
    description: Token identity.
  - name: Sites
    description: Your hosting sites — list, create, update, status.
  - name: Projects
    description: Organise sites into projects.
  - name: DNS
    description: Zone info and your DNS records.
  - name: PHP
    description: The site's PHP version.
  - name: WordPress
    description: WordPress install status, users, and one-time wp-admin login links.
  - name: Orders
    description: Your orders and the site capacity each has.
  - name: Connectors
    description: Your connected Cloud DNS accounts (for the cloud nameserver choice).

paths:
  /whoami:
    get:
      tags: [ Account ]
      summary: Identify the calling token
      operationId: whoami
      responses:
        '200':
          description: The bound team and the token's granted scopes.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      team:
                        $ref: '#/components/schemas/TeamRef'
                      token:
                        type: object
                        properties:
                          name: { type: string }
                          prefix: { type: string }
                      scopes:
                        description: The granted scopes, or `all` for an unrestricted token.
                        oneOf:
                          - type: string
                          - type: array
                            items: { type: string }
        '401': { $ref: '#/components/responses/Unauthorized' }

  /sites:
    get:
      tags: [ Sites ]
      summary: List your sites
      operationId: listSites
      description: Requires the `sites:read` scope. Paginated; default page size is 200.
      parameters:
        - { in: query, name: search, required: false, schema: { type: string }, description: 'Filter by domain (IDN or ASCII), case-insensitive substring match.' }
        - { in: query, name: per_page, schema: { type: integer, default: 200, maximum: 500 }, description: Sites per page (capped). }
        - { in: query, name: page, schema: { type: integer, default: 1 } }
      responses:
        '200':
          description: A page of sites.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Site' }
                  meta: { $ref: '#/components/schemas/Pagination' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
    post:
      tags: [ Sites ]
      summary: Create a site
      operationId: createSite
      description: >-
        Requires the `sites:write` scope. The nameserver defaults to `auto` and
        an IP is auto-assigned during provisioning; choose `cloud`/`registrar`
        to host DNS on a connected provider. Accepts an optional
        `Idempotency-Key` header.
      parameters:
        - { in: header, name: Idempotency-Key, required: false, schema: { type: string }, description: Replay-guard for safe retries. }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [ domain, project, order ]
              properties:
                domain: { type: string, example: example.com, description: The apex domain. Must be registered and not already in the system. }
                project: { type: string, format: uuid, description: The project (uuid) to place the site in. }
                order: { type: string, format: uuid, description: The active order (uuid) the site belongs to. }
                ssl_enabled: { type: boolean, default: true }
                ssl_force_https: { type: boolean, default: true }
                nameserver:
                  type: string
                  default: auto
                  enum: [ auto, traditional, cloud, registrar ]
                  description: >-
                    Where the site's DNS is hosted. `auto` lets us decide;
                    `traditional` is our own nameservers; `cloud` is one of your
                    connected Cloud DNS accounts (see `connector`); `registrar`
                    hosts DNS at the registrar that holds the domain (resolved
                    automatically — 422 if none of yours does).
                connector:
                  type: string
                  format: uuid
                  description: >-
                    Only with `nameserver: cloud`. The uuid of one of your Cloud
                    DNS connectors (GET /connectors). Omit to auto-pick from your
                    pool. Platform accounts are never accepted.
      responses:
        '201':
          description: Site created; provisioning queued. Confirms the new id + domain, not the full detail.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      id: { type: string, format: uuid }
                      domain: { type: string }
                  message: { type: string }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '409': { $ref: '#/components/responses/IdempotencyConflict' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /sites/{site}:
    parameters:
      - { $ref: '#/components/parameters/SiteUuid' }
    get:
      tags: [ Sites ]
      summary: Get a site
      operationId: showSite
      description: Requires the `sites:read` scope.
      responses:
        '200':
          description: The site, with its encoded provisioning status.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SiteDetailEnvelope' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
    patch:
      tags: [ Sites ]
      summary: Update a site
      operationId: updateSite
      description: >-
        Requires the `sites:write` scope. Move the site between projects and/or
        toggle SSL. At least one field must be present.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              minProperties: 1
              properties:
                project: { type: string, format: uuid, description: Move the site to this project (uuid). }
                ssl_enabled: { type: boolean }
                ssl_force_https: { type: boolean }
                nameserver:
                  type: string
                  enum: [ auto, traditional, cloud, registrar ]
                  description: Switch where the site's DNS is hosted (runs the provider hand-off).
                connector: { type: string, format: uuid, description: 'Only with `nameserver: cloud` — the Cloud DNS connector uuid to pin.' }
      responses:
        '200':
          description: Confirms only the fields that changed — not the full site.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    description: Echoes only the updated fields, with their stored values.
                    properties:
                      project: { type: string, format: uuid }
                      ssl_enabled: { type: boolean }
                      ssl_force_https: { type: boolean }
                      nameserver: { type: string, enum: [ traditional, cloud, registrar ], description: The site's resolved DNS hosting after the switch. }
                  message: { type: string }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /sites/{site}/php:
    parameters:
      - { $ref: '#/components/parameters/SiteUuid' }
    get:
      tags: [ PHP ]
      summary: Get the PHP version
      operationId: showPhp
      description: Requires the `sites:read` scope.
      responses:
        '200':
          description: Current version and the versions available on the server.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      version: { type: string, nullable: true, example: '8.3' }
                      available:
                        type: array
                        items: { type: string }
                        example: [ native, '8.2', '8.3' ]
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/Error' }
    put:
      tags: [ PHP ]
      summary: Set the PHP version
      operationId: setPhp
      description: Requires the `sites:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [ version ]
              properties:
                version: { type: string, example: '8.2', description: '`native` or a `8.2`-style version.' }
      responses:
        '200':
          description: PHP version updated.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      version: { type: string }
                  message: { type: string }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /sites/{site}/wordpress:
    parameters:
      - { $ref: '#/components/parameters/SiteUuid' }
    get:
      tags: [ WordPress ]
      summary: WordPress install status
      operationId: showWordpress
      description: Requires the `wordpress:read` scope.
      responses:
        '200':
          description: Whether WordPress is installed, its status, and version.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: '#/components/schemas/WordpressStatus' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
    post:
      tags: [ WordPress ]
      summary: Install WordPress
      operationId: installWordpress
      description: >-
        Requires the `wordpress:write` scope. Basic fields only — the API never
        overwrites an existing install. Accepts an optional `Idempotency-Key`.
      parameters:
        - { in: header, name: Idempotency-Key, required: false, schema: { type: string } }
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [ username, password, email, title, language ]
              properties:
                username: { type: string, maxLength: 32 }
                password: { type: string, minLength: 6, maxLength: 64 }
                email: { type: string, format: email }
                title: { type: string, maxLength: 128 }
                language: { type: string, example: en_US }
      responses:
        '202':
          description: Install queued.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: '#/components/schemas/WordpressStatus' }
                  message: { type: string }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: 'Order not active, hosting not ready, an install is in flight, or WordPress is already installed.', content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
        '422': { $ref: '#/components/responses/ValidationError' }

  /sites/{site}/wordpress/users:
    parameters:
      - { $ref: '#/components/parameters/SiteUuid' }
    get:
      tags: [ WordPress ]
      summary: List WordPress users
      operationId: listWordpressUsers
      description: >-
        Requires the `wordpress:read` scope. The site's WordPress accounts, each
        flagged with `is_admin`. Use a user `id` with the per-user login endpoint
        below, or skip this and mint the primary admin link directly.
      responses:
        '200':
          description: The site's WordPress users.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/WordpressUser' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: 'Hosting is not ready, or WordPress is not installed on this site.', content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }

  /sites/{site}/wordpress/sso:
    parameters:
      - { $ref: '#/components/parameters/SiteUuid' }
    post:
      tags: [ WordPress ]
      summary: Log in as the primary admin
      operationId: wordpressAdminSso
      description: >-
        Requires the `wordpress:write` scope. Mints a one-time magic-link URL
        that logs straight into wp-admin as the site's primary administrator
        (the recorded admin user, else the lowest WordPress user id). The link
        is single-use and short-lived: treat it as a credential, redirect to it
        immediately, and never log or cache it.
      responses:
        '200':
          description: A one-time login URL and the administrator it signs in as.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: '#/components/schemas/WordpressLoginLink' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: 'Hosting is not ready, WordPress is not installed, or the site has no administrator account.', content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }

  /sites/{site}/wordpress/users/{wpUser}/sso:
    parameters:
      - { $ref: '#/components/parameters/SiteUuid' }
      - { in: path, name: wpUser, required: true, schema: { type: integer }, description: 'The WordPress user id (from the users listing).' }
    post:
      tags: [ WordPress ]
      summary: Log in as a specific user
      operationId: wordpressUserSso
      description: >-
        Requires the `wordpress:write` scope. Mints a one-time magic-link URL
        that logs into wp-admin as the given WordPress user id. Single-use and
        short-lived: treat it as a credential.
      responses:
        '200':
          description: A one-time login URL.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      url: { type: string, format: uri }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '409': { description: 'Hosting is not ready, WordPress is not installed, or the user id does not exist.', content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }

  /sites/{site}/dns:
    parameters:
      - { $ref: '#/components/parameters/SiteUuid' }
    get:
      tags: [ DNS ]
      summary: Zone info
      operationId: showZone
      description: >-
        Requires the `dns:read` scope. The constraints for building a valid
        record: supported types, the allowed TTL values, the per-zone cap, how
        many records are used, and the last sync result.
      responses:
        '200':
          description: Zone info.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: '#/components/schemas/ZoneInfo' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }

  /sites/{site}/dns/records:
    parameters:
      - { $ref: '#/components/parameters/SiteUuid' }
    get:
      tags: [ DNS ]
      summary: List DNS records
      operationId: listDnsRecords
      description: >-
        Requires the `dns:read` scope. Returns every record in the zone. Managed
        (system) records carry `managed: true` and are read-only.
      responses:
        '200':
          description: The zone's records.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/DnsRecord' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
    post:
      tags: [ DNS ]
      summary: Add a DNS record
      operationId: createDnsRecord
      description: >-
        Requires the `dns:write` scope. An out-of-range TTL is snapped to the
        nearest value the provider allows. A sync to the provider is queued.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/DnsRecordInput' }
      responses:
        '201':
          description: Record added; sync queued.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: '#/components/schemas/DnsRecord' }
                  message: { type: string }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /sites/{site}/dns/records/{record}:
    parameters:
      - { $ref: '#/components/parameters/SiteUuid' }
      - { in: path, name: record, required: true, schema: { type: string, format: uuid } }
    patch:
      tags: [ DNS ]
      summary: Update a DNS record
      operationId: updateDnsRecord
      description: Requires the `dns:write` scope. Managed records are read-only (`403`).
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/DnsRecordInput' }
      responses:
        '200':
          description: Record updated; sync queued.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: '#/components/schemas/DnsRecord' }
                  message: { type: string }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { description: 'Managed record — read-only.', content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
    delete:
      tags: [ DNS ]
      summary: Delete a DNS record
      operationId: deleteDnsRecord
      description: Requires the `dns:write` scope. Managed records are read-only (`403`).
      responses:
        '200':
          description: Record deleted; sync queued.
          content:
            application/json:
              schema:
                type: object
                properties:
                  message: { type: string }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { description: 'Managed record — read-only.', content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } } }
        '404': { $ref: '#/components/responses/NotFound' }

  /projects:
    get:
      tags: [ Projects ]
      summary: List projects
      operationId: listProjects
      description: Requires the `projects:read` scope.
      responses:
        '200':
          description: Your projects.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Project' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
    post:
      tags: [ Projects ]
      summary: Create a project
      operationId: createProject
      description: Requires the `projects:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [ name ]
              properties:
                name: { type: string, maxLength: 128 }
      responses:
        '201':
          description: Project created.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: '#/components/schemas/Project' }
                  message: { type: string }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /projects/{project}:
    parameters:
      - { in: path, name: project, required: true, schema: { type: string, format: uuid } }
    patch:
      tags: [ Projects ]
      summary: Rename a project
      operationId: updateProject
      description: Requires the `projects:write` scope.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [ name ]
              properties:
                name: { type: string, maxLength: 128 }
      responses:
        '200':
          description: Project renamed.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data: { $ref: '#/components/schemas/Project' }
                  message: { type: string }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }
    delete:
      tags: [ Projects ]
      summary: Delete a project
      operationId: deleteProject
      description: >-
        Requires the `projects:write` scope. A project must be empty, and a team
        must keep at least one project.
      responses:
        '200':
          description: Project deleted.
          content:
            application/json:
              schema:
                type: object
                properties:
                  message: { type: string }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }
        '404': { $ref: '#/components/responses/NotFound' }
        '422': { $ref: '#/components/responses/ValidationError' }

  /orders:
    get:
      tags: [ Orders ]
      summary: List your orders
      operationId: listOrders
      description: >-
        Requires the `sites:read` scope. Read-only — orders are provisioned via
        billing, not the API. Use this to discover order uuids and which order
        will accept a new site before POST /sites.
      parameters:
        - { in: query, name: accepts_new_site, required: false, schema: { type: boolean }, description: 'Filter to orders that can take a new site.' }
      responses:
        '200':
          description: Your orders.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Order' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

  /connectors:
    get:
      tags: [ Connectors ]
      summary: List your Cloud DNS connectors
      operationId: listConnectors
      description: >-
        Requires the `sites:read` scope. Your own connected Cloud DNS accounts —
        the uuids you may pass as `connector` on a `cloud` nameserver choice.
        Platform-shared accounts are never listed.
      responses:
        '200':
          description: Your Cloud DNS connectors.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Connector' }
        '401': { $ref: '#/components/responses/Unauthorized' }
        '403': { $ref: '#/components/responses/Forbidden' }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: 'Your API token: `Authorization: Bearer ipxapi_…`.'

  parameters:
    SiteUuid:
      in: path
      name: site
      required: true
      schema: { type: string }
      description: The site's uuid, or its domain (IDN/Unicode or ASCII/Punycode).

  responses:
    Unauthorized:
      description: Missing, invalid, expired, or revoked token.
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    Forbidden:
      description: The token lacks the required scope.
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    NotFound:
      description: No such resource in your team.
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    ValidationError:
      description: The request failed validation.
      content: { application/json: { schema: { $ref: '#/components/schemas/ValidationError' } } }
    IdempotencyConflict:
      description: A request with the same Idempotency-Key is already in progress.
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }
    Error:
      description: Error.
      content: { application/json: { schema: { $ref: '#/components/schemas/Error' } } }

  schemas:
    TeamRef:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
    Pagination:
      type: object
      properties:
        current_page: { type: integer }
        per_page: { type: integer }
        total: { type: integer }
        last_page: { type: integer }
    Error:
      type: object
      properties:
        message: { type: string }
    ValidationError:
      type: object
      properties:
        message: { type: string }
        errors:
          type: object
          additionalProperties:
            type: array
            items: { type: string }
    Components:
      type: object
      description: Encoded provisioning status, per dimension.
      properties:
        hosting: { type: string, enum: [ ready, provisioning, error ] }
        public_ip: { type: string, enum: [ ready, provisioning, error ] }
        nameserver: { type: string, enum: [ ready, provisioning, error ] }
        dns: { type: string, enum: [ connected, validating, external, disconnected ] }
        ssl: { type: string, enum: [ off, provisioning, active, error, proxied ] }
    Site:
      type: object
      properties:
        id: { type: string, format: uuid }
        domain: { type: string }
        idn: { type: string, description: The ASCII (punycode) form of the domain. }
        nameserver:
          type: string
          nullable: true
          enum: [ traditional, cloud, registrar, null ]
          description: Which kind of DNS hosting the site resolved to, or null while still auto-selecting. Distinct from `nameservers` (the NS hostnames to delegate to).
        nameservers:
          type: array
          description: The zone's delegated nameserver hostnames (empty until the zone is provisioned).
          items: { type: string }
        project: { $ref: '#/components/schemas/ProjectRef' }
        status: { type: string, enum: [ live, provisioning, error ] }
        components: { $ref: '#/components/schemas/Components' }
        ssl:
          type: object
          properties:
            enabled: { type: boolean }
            force_https: { type: boolean }
        php:
          type: object
          properties:
            version: { type: string, nullable: true }
        wordpress:
          type: object
          description: The embedded install summary on a site. Only `installed` and `version`; the full lifecycle `status` is on the dedicated WordPress endpoints (see `WordpressStatus`).
          properties:
            installed: { type: boolean }
            version: { type: string, nullable: true }
        usage:
          nullable: true
          type: object
          properties:
            disk_bytes: { type: integer }
            disk_quota_bytes: { type: integer, nullable: true }
            bandwidth_bytes: { type: integer }
            bandwidth_quota_bytes: { type: integer, nullable: true }
        created_at: { type: string, format: date-time, nullable: true }
    SiteDetail:
      allOf:
        - { $ref: '#/components/schemas/Site' }
        - type: object
          properties:
            order:
              nullable: true
              type: object
              properties:
                label: { type: string }
            ipaddress: { type: string, nullable: true, description: The site's public IP address. }
            subdomains:
              type: array
              items: { type: string }
    SiteEnvelope:
      type: object
      properties:
        data: { $ref: '#/components/schemas/SiteDetail' }
        message: { type: string }
    SiteDetailEnvelope:
      type: object
      properties:
        data: { $ref: '#/components/schemas/SiteDetail' }
    ProjectRef:
      nullable: true
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
    Project:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        sites_count: { type: integer, nullable: true }
        created_at: { type: string, format: date-time, nullable: true }
    Order:
      type: object
      properties:
        id: { type: string, format: uuid }
        label: { type: string }
        status: { type: string, enum: [ active, suspended, terminated ] }
        sites_used: { type: integer, nullable: true }
        sites_limit: { type: integer, nullable: true, description: 'Max sites, or null when unlimited.' }
        accepts_new_site: { type: boolean, description: Whether POST /sites will accept a new site against this order. }
        expires_at: { type: string, format: date-time, nullable: true }
        created_at: { type: string, format: date-time, nullable: true }
    Connector:
      type: object
      description: One of your connected Cloud DNS accounts.
      properties:
        id: { type: string, format: uuid }
        label: { type: string }
        provider: { type: string, nullable: true, description: The provider key (e.g. cloudflare). }
        provider_label: { type: string, nullable: true }
        enabled: { type: boolean }
        sites_used: { type: integer, nullable: true }
        sites_limit: { type: integer, nullable: true, description: 'Max sites, or null when unlimited.' }
    WordpressStatus:
      type: object
      properties:
        installed: { type: boolean }
        status: { type: string, nullable: true, enum: [ pending, installing, active, failed, null ] }
        version: { type: string, nullable: true }
    WordpressUser:
      type: object
      properties:
        id: { type: integer, description: The WordPress user id. }
        login: { type: string }
        display_name: { type: string, nullable: true }
        email: { type: string, nullable: true }
        roles:
          type: array
          items: { type: string }
          example: [ administrator ]
        is_admin: { type: boolean, description: Whether the user has the WordPress administrator role. }
        registered_at: { type: string, format: date-time, nullable: true }
    WordpressLoginLink:
      type: object
      properties:
        url: { type: string, format: uri, description: 'One-time, short-lived magic-link URL into wp-admin. Treat it as a credential.' }
        user: { $ref: '#/components/schemas/WordpressUser' }
    ZoneInfo:
      type: object
      properties:
        id: { type: string, format: uuid, nullable: true, description: 'The zone uuid, or null before the zone is provisioned.' }
        domain: { type: string }
        provisioned: { type: boolean }
        supported_types:
          type: array
          items: { type: string }
          example: [ A, AAAA, CNAME, MX, TXT, SRV, CAA ]
        ttl_options:
          type: array
          items: { type: integer }
        max_records: { type: integer, nullable: true }
        records_used: { type: integer }
        proxy_enabled: { type: boolean }
        sync:
          type: object
          properties:
            status: { type: string, nullable: true }
            last_synced_at: { type: string, format: date-time, nullable: true }
            error: { type: string, nullable: true }
    DnsRecord:
      type: object
      properties:
        id: { type: string, format: uuid }
        type: { type: string, example: A }
        name: { type: string, example: www }
        content: { type: string, example: 203.0.113.10 }
        ttl: { type: integer, example: 3600 }
        priority: { type: integer, nullable: true }
        weight: { type: integer, nullable: true }
        port: { type: integer, nullable: true }
        data: { type: object, nullable: true, additionalProperties: true }
        proxied: { type: boolean, nullable: true }
        managed: { type: boolean, description: System-managed records are read-only. }
        sync_status: { type: string, enum: [ pending, synced, dirty, error, unsupported ] }
        synced_at: { type: string, format: date-time, nullable: true }
    DnsRecordInput:
      type: object
      required: [ type, name, content, ttl ]
      properties:
        type: { type: string, enum: [ A, AAAA, CAA, CNAME, MX, NS, PTR, SRV, TLSA, TXT ] }
        name: { type: string, example: www }
        content: { type: string, example: 203.0.113.10 }
        ttl: { type: integer, example: 3600, description: Snapped to the provider's nearest allowed value. }
        priority: { type: integer, description: 'MX / SRV.' }
        weight: { type: integer, description: SRV. }
        port: { type: integer, description: SRV. }
        data:
          type: object
          additionalProperties: true
          description: 'Type-specific extras — CAA (flags, tag), TLSA (usage, selector, matching_type).'
        proxied: { type: boolean, description: 'A / AAAA / CNAME — route through the provider proxy/CDN.' }
