openapi: "3.1.0"

info:
  title: "Forge — Tenant Control Plane API"
  version: "0.1.0"
  description: |
    Forge is the APEX Suite control plane. It is where tenants are provisioned, product
    configuration is managed, runtime API keys are issued, members are invited, and every unit of
    usage and cost is recorded and read back. Products consume configuration from Forge; your
    application presents Forge-issued API keys to the products at runtime.

    Two credentials appear in this specification:

    - **Portal session** — a browser session cookie obtained by signing in to the Forge portal.
      Most tenant-scoped reads and writes use it. It is intended for the portal experience, not
      for server-to-server integrations.
    - **Machine-to-machine access token** — an OAuth2 client-credentials token issued by the
      platform identity provider, used by provisioning integrations (tenant creation and
      provisioning status only).

    Every response to a mutating request carries an `X-Correlation-ID` header; send your own
    `X-Correlation-ID` request header to thread an operation through audit records, transaction
    records, and cost events. Errors use a single envelope:
    `{ "error": { "code": "...", "message": "..." } }`.

    All monetary values are South African Rand expressed in integer cents unless a field name
    says otherwise (`cost_zar`-style fields on other services are Rand decimals).

    Billing reads are portal-session authenticated in the current release; programmatic usage
    export for server-to-server reconciliation is on the platform roadmap — speak to your
    platform contact.

servers:
  - url: https://forge.dev.apex.reisiger.org
    description: Development
  - url: http://localhost:9140
    description: Local

tags:
  - name: Provisioning
    description: >-
      Tenant creation and runtime provisioning. Creation is atomic (tenant record, identity
      registration, product enablement, audit event) and replay-safe via the `Idempotency-Key`
      header. When runtime provisioning (sign-in client, first administrator membership) continues
      in the background, the create call answers `202` and you poll the provisioning status
      resource.
  - name: Tenants
    description: Read and update tenant records. Listing is filtered to tenants you are a member of.
  - name: API Keys
    description: >-
      Tenant API keys are the runtime credential your application presents to APEX products. Keys
      are prefixed `apex_live_` or `apex_test_`, scoped per product action, and shown in full
      exactly once at creation or rotation.
  - name: Product Configuration
    description: >-
      Per-tenant, per-product configuration. Saved configuration propagates to the products
      automatically — no redeployment is required.
  - name: Members
    description: Tenant membership — list members and invite new ones.
  - name: Communication Channels
    description: >-
      Tenant-owned messaging channels consumed by the communications product. Secret values are
      write-only; reads return masked references.
  - name: Billing & Usage
    description: >-
      The tenant cost plane: subscription, budgets, alerts, invoices, rate card, spend and cost
      summaries, and the per-call transaction ledger with a full per-model token and Rand
      breakdown. Billing reads are portal-session authenticated in the current release;
      programmatic usage export for server-to-server reconciliation is on the platform roadmap —
      speak to your platform contact.
  - name: Workflows
    description: >-
      Tenant-scoped automation definitions and their runs. Workflow mutations and run triggers
      require a tenant administrator role.
  - name: Platform
    description: Cross-tenant reads scoped by your session — team directory and the audit log.
  - name: Service
    description: Service liveness.

security:
  - sessionAuth: []

paths:
  # ── Provisioning ───────────────────────────────────────────────────────────

  /api/tenants:
    get:
      operationId: listTenants
      tags: [Tenants]
      summary: List tenants
      x-apex-availability: available
      description: >-
        Lists tenants visible to your session. Tenant-scoped users see their own tenant; platform
        operators see all tenants.
      security:
        - sessionAuth: []
      responses:
        "200":
          description: Tenants with their enabled products.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/TenantWithProducts"
        "401":
          $ref: "#/components/responses/Unauthorized"
    post:
      operationId: provisionTenant
      tags: [Provisioning]
      summary: Provision a new tenant
      x-apex-availability: available
      description: |
        Creates a tenant atomically: the tenant record, its identity registration, product
        enablement for the subscription tier, and an audit event. After the commit, tenant
        lifecycle events are published to the platform event bus and — when `adminEmail` or
        `callbackUrls` were supplied — runtime provisioning (per-tenant sign-in client, first
        administrator membership) starts in the background.

        Two credentials are accepted:
        - a machine-to-machine access token carrying the `forge/tenant.write` scope (provisioning
          integrations), or
        - a portal session held by a platform operator.

        Supply `adminEmail` when provisioning programmatically: it seeds the first membership,
        and without at least one membership nobody can sign in to the tenant. Presence of
        `callbackUrls` provisions a dedicated per-tenant sign-in client; omit it and the tenant
        shares the platform client.

        Send `Idempotency-Key: provision:<uuid>` to make the call replay-safe — a replayed key
        returns the originally created tenant with a success status instead of a duplicate-slug
        conflict.

        The status code tells you what remains:
        - **201** — the tenant is complete as returned.
        - **202** — runtime provisioning is in flight. Poll the `Location` header (also returned
          as `statusUrl` in the body) until `status` is `ready` or `failed`.
      security:
        - m2mAuth: []
        - sessionAuth: []
      parameters:
        - $ref: "#/components/parameters/IdempotencyKey"
        - $ref: "#/components/parameters/CorrelationIdHeader"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateTenantRequest"
            examples:
              programmaticOnboarding:
                summary: Provisioning integration with runtime inputs
                value:
                  slug: "your-tenant"
                  displayName: "Your Tenant"
                  orgName: "Your Tenant (Pty) Ltd"
                  tier: "growth-plan"
                  adminEmail: "admin@your-tenant.example"
                  callbackUrls: ["https://app.your-tenant.example/auth/callback"]
                  logoutUrls: ["https://app.your-tenant.example/"]
                  kycReference: "kyc-7f3a2b"
                  provenance: "kyc"
              operatorMinimal:
                summary: Operator-created tenant, members invited later
                value:
                  slug: "your-tenant"
                  displayName: "Your Tenant"
                  orgName: "Your Tenant (Pty) Ltd"
                  tier: "awareness"
      responses:
        "201":
          description: Tenant created and complete — no runtime provisioning outstanding.
          headers:
            X-Correlation-ID:
              $ref: "#/components/headers/XCorrelationId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProvisionedTenant"
        "202":
          description: >-
            Tenant created; runtime provisioning is in flight. Poll the `Location` header until
            provisioning `status` is `ready` or `failed`.
          headers:
            Location:
              description: Provisioning status resource for this tenant.
              schema:
                type: string
                examples: ["/api/tenants/cm0example0000tenant/provisioning"]
            X-Correlation-ID:
              $ref: "#/components/headers/XCorrelationId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ProvisionedTenant"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          description: A tenant with this slug already exists (and no matching idempotency key was presented).
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/tenants/{id}/provisioning:
    parameters:
      - $ref: "#/components/parameters/TenantIdOrSlug"
    get:
      operationId: getTenantProvisioning
      tags: [Provisioning]
      summary: Get provisioning status
      x-apex-availability: available
      description: >-
        The polling target of the `202` create flow. Reports the recorded status plus the
        individually verified artifacts, so a half-provisioned tenant tells you which piece is
        missing rather than just "failed". Accepts the tenant identifier or the slug. Authorized
        like tenant creation (machine-to-machine token with `forge/tenant.write`, or a platform
        operator session) because a tenant being provisioned typically has no members yet.
      security:
        - m2mAuth: []
        - sessionAuth: []
      responses:
        "200":
          description: Current provisioning state.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TenantProvisioning"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
    post:
      operationId: retryTenantProvisioning
      tags: [Provisioning]
      summary: Retry runtime provisioning
      x-apex-availability: available
      description: >-
        Re-runs runtime provisioning synchronously. Every step re-checks live state before acting,
        so calling this on a complete tenant is a no-op and calling it on a half-complete tenant
        finishes the rest — it will not create a duplicate sign-in client or membership. The body
        carries the same optional runtime inputs as creation; an empty body re-runs whatever is
        already recorded. Answers `502` when the run ends in `failed`.
      security:
        - m2mAuth: []
        - sessionAuth: []
      parameters:
        - $ref: "#/components/parameters/CorrelationIdHeader"
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ProvisionRetryRequest"
            examples:
              retryWithAdmin:
                value:
                  adminEmail: "admin@your-tenant.example"
      responses:
        "200":
          description: Provisioning completed (or was already complete).
          headers:
            X-Correlation-ID:
              $ref: "#/components/headers/XCorrelationId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TenantProvisioning"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "404":
          $ref: "#/components/responses/NotFound"
        "502":
          description: The provisioning run ended in `failed`; the body reports which step failed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TenantProvisioning"

  # ── Tenants ────────────────────────────────────────────────────────────────

  /api/tenants/{id}:
    parameters:
      - $ref: "#/components/parameters/TenantId"
    get:
      operationId: getTenant
      tags: [Tenants]
      summary: Get tenant detail
      x-apex-availability: available
      description: Returns the tenant with its enabled products and their configuration versions.
      responses:
        "200":
          description: Tenant detail.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TenantWithProducts"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
    put:
      operationId: updateTenant
      tags: [Tenants]
      summary: Update tenant
      x-apex-availability: available
      description: >-
        Updates tenant fields. Unlike creation — where `tier` is free-form — updates enforce the
        platform tier vocabulary: `awareness`, `intelligence`, or `enterprise`.
      parameters:
        - $ref: "#/components/parameters/CorrelationIdHeader"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateTenantRequest"
            examples:
              assignTier:
                value:
                  tier: "intelligence"
      responses:
        "200":
          description: Updated tenant.
          headers:
            X-Correlation-ID:
              $ref: "#/components/headers/XCorrelationId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TenantWithProducts"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  # ── API Keys ───────────────────────────────────────────────────────────────

  /api/tenants/{id}/api-keys:
    parameters:
      - $ref: "#/components/parameters/TenantId"
    get:
      operationId: listApiKeys
      tags: [API Keys]
      summary: List API keys
      x-apex-availability: available
      description: >-
        Lists the tenant's API keys. The full key and its hash are never returned — only the
        display prefix.
      parameters:
        - name: environment
          in: query
          required: false
          description: Filter by key environment.
          schema:
            type: string
            enum: [live, test]
      responses:
        "200":
          description: API keys (metadata only).
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/ApiKey"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
    post:
      operationId: createApiKey
      tags: [API Keys]
      summary: Create an API key
      x-apex-availability: available
      description: |
        Generates a new tenant API key and returns the full key **exactly once** — store it
        immediately; it cannot be retrieved again. Keys have the shape
        `apex_live_<64 hex>` or `apex_test_<64 hex>` and are the runtime credential your
        application presents to APEX products.

        Keys are scoped per product action (for example `zenith:read`, `echo:send`,
        `vector:verify`). The wildcard scope `*` is permitted for `test` keys only. LLM dispatch
        through the platform gateway requires the `llm:invoke` scope on the presented key.
      parameters:
        - $ref: "#/components/parameters/CorrelationIdHeader"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateApiKeyRequest"
            examples:
              productionKey:
                value:
                  name: "backend-integration"
                  description: "Server-side key for the production integration"
                  scopes: ["zenith:read", "vector:verify"]
                  environment: "live"
                  expiresInDays: 90
      responses:
        "201":
          description: The created key, including the full key value — shown only in this response.
          headers:
            X-Correlation-ID:
              $ref: "#/components/headers/XCorrelationId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiKeyCreated"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /api/tenants/{id}/api-keys/{keyId}:
    parameters:
      - $ref: "#/components/parameters/TenantId"
      - $ref: "#/components/parameters/KeyId"
    delete:
      operationId: revokeApiKey
      tags: [API Keys]
      summary: Revoke an API key
      x-apex-availability: available
      description: Revokes a key. Immediate, irreversible, and audit-logged.
      parameters:
        - $ref: "#/components/parameters/CorrelationIdHeader"
      responses:
        "200":
          description: The revoked key record.
          headers:
            X-Correlation-ID:
              $ref: "#/components/headers/XCorrelationId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiKey"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  /api/tenants/{id}/api-keys/{keyId}/rotate:
    parameters:
      - $ref: "#/components/parameters/TenantId"
      - $ref: "#/components/parameters/KeyId"
    post:
      operationId: rotateApiKey
      tags: [API Keys]
      summary: Rotate an API key
      x-apex-availability: available
      description: >-
        Creates a replacement key with the same scopes and environment, revokes the old key, and
        returns the new full key exactly once.
      parameters:
        - $ref: "#/components/parameters/CorrelationIdHeader"
      responses:
        "201":
          description: The replacement key, including the full key value — shown only in this response.
          headers:
            X-Correlation-ID:
              $ref: "#/components/headers/XCorrelationId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ApiKeyCreated"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  # ── Product configuration ──────────────────────────────────────────────────

  /api/tenants/{id}/products/{productId}/config:
    parameters:
      - $ref: "#/components/parameters/TenantId"
      - $ref: "#/components/parameters/ProductId"
    get:
      operationId: getProductConfig
      tags: [Product Configuration]
      summary: Get product configuration
      x-apex-availability: available
      description: Returns the tenant's configuration entry for one product.
      responses:
        "200":
          description: Product configuration entry.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TenantProduct"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: The product is not configured for this tenant.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    put:
      operationId: updateProductConfig
      tags: [Product Configuration]
      summary: Update product configuration
      x-apex-availability: available
      description: >-
        Replaces the tenant's configuration for one product. Values are validated against the
        product's configuration schema; invalid keys or values are rejected with `400`. Saved
        configuration propagates to the product automatically — no redeployment is required.
      parameters:
        - $ref: "#/components/parameters/CorrelationIdHeader"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateProductConfigRequest"
            examples:
              updateModel:
                value:
                  config:
                    generation_model: "claude-sonnet-4-6"
      responses:
        "200":
          description: The updated configuration entry (configuration version incremented).
          headers:
            X-Correlation-ID:
              $ref: "#/components/headers/XCorrelationId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TenantProduct"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  # ── Members ────────────────────────────────────────────────────────────────

  /api/tenants/{id}/members:
    parameters:
      - $ref: "#/components/parameters/TenantId"
    get:
      operationId: listMembers
      tags: [Members]
      summary: List tenant members
      x-apex-availability: available
      responses:
        "200":
          description: Members of the tenant.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/TenantMember"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
    post:
      operationId: inviteMember
      tags: [Members]
      summary: Invite a member
      x-apex-availability: available
      description: >-
        Invites a member to the tenant. Creates the identity user (with a temporary password when
        the user is new) and the membership record linking the user to the tenant with the given
        role.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/InviteMemberRequest"
            examples:
              inviteAdmin:
                value:
                  email: "colleague@your-tenant.example"
                  roles: "admin"
      responses:
        "201":
          description: The created membership.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/InviteMemberResult"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  # ── Communication channels ─────────────────────────────────────────────────

  /api/tenants/{id}/echo/channels:
    parameters:
      - $ref: "#/components/parameters/TenantId"
    get:
      operationId: listCommunicationChannels
      tags: [Communication Channels]
      summary: List channels
      x-apex-availability: available
      description: >-
        Lists the tenant's configured messaging channels. Secret values are never returned — each
        secret reports whether it is set plus a short reference suffix for visual confirmation.
        Requires a tenant administrator role.
      responses:
        "200":
          description: Configured channels with masked secrets.
          headers:
            X-Correlation-ID:
              $ref: "#/components/headers/XCorrelationId"
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/WhatsAppChannelMasked"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
    post:
      operationId: upsertCommunicationChannel
      tags: [Communication Channels]
      summary: Create or replace a channel
      x-apex-availability: available
      description: >-
        Creates or replaces a WhatsApp channel for the tenant. Idempotent on channel type:
        re-posting the same type replaces the existing entry. Secret fields are optional on
        re-post — omit a secret to keep its existing stored value (the rotation flow: submit only
        the field you are rotating). Requires a tenant administrator role.
      parameters:
        - $ref: "#/components/parameters/CorrelationIdHeader"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WhatsAppChannelInput"
            examples:
              newChannel:
                value:
                  phoneNumberId: "100000000000000"
                  businessAccountId: "200000000000000"
                  wabaModel: "managed"
                  apiVersion: "v21.0"
                  accessToken: "EAAG..."
                  appSecret: "0f0f0f..."
                  webhookVerifyToken: "choose-a-long-random-string"
      responses:
        "201":
          description: The stored channel with masked secrets.
          headers:
            X-Correlation-ID:
              $ref: "#/components/headers/XCorrelationId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WhatsAppChannelMasked"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /api/tenants/{id}/echo/channels/{type}:
    parameters:
      - $ref: "#/components/parameters/TenantId"
      - name: type
        in: path
        required: true
        description: Channel type. Currently only `whatsapp` is supported.
        schema:
          type: string
          enum: [whatsapp]
    delete:
      operationId: deleteCommunicationChannel
      tags: [Communication Channels]
      summary: Remove a channel
      x-apex-availability: available
      description: >-
        Removes a channel. The underlying secrets are scheduled for deletion with a recovery
        window, so an accidental removal is reversible through your platform contact. Requires a
        tenant administrator role.
      parameters:
        - $ref: "#/components/parameters/CorrelationIdHeader"
      responses:
        "204":
          description: Channel removed.
          headers:
            X-Correlation-ID:
              $ref: "#/components/headers/XCorrelationId"
        "400":
          description: Unsupported channel type.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  # ── Billing & usage ────────────────────────────────────────────────────────

  /api/tenants/{id}/billing:
    parameters:
      - $ref: "#/components/parameters/TenantId"
    get:
      operationId: getSubscription
      tags: [Billing & Usage]
      summary: Get subscription
      x-apex-availability: available
      responses:
        "200":
          description: The tenant's subscription.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Subscription"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: No subscription exists for this tenant.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/tenants/{id}/billing/alerts:
    parameters:
      - $ref: "#/components/parameters/TenantId"
    get:
      operationId: getBudgetAlerts
      tags: [Billing & Usage]
      summary: Get budget alerts
      x-apex-availability: available
      responses:
        "200":
          description: Configured budget alert thresholds.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/BudgetAlert"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
    put:
      operationId: updateBudgetAlerts
      tags: [Billing & Usage]
      summary: Replace budget alerts
      x-apex-availability: available
      description: Replaces the full set of alert thresholds for the tenant's subscription.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: array
              items:
                $ref: "#/components/schemas/BudgetAlertInput"
            examples:
              twoThresholds:
                value:
                  - thresholdPct: 80
                    enabled: true
                  - thresholdPct: 100
                    enabled: true
      responses:
        "200":
          description: The stored alert thresholds.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/BudgetAlert"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /api/tenants/{id}/billing/budget:
    parameters:
      - $ref: "#/components/parameters/TenantId"
    get:
      operationId: getBudget
      tags: [Billing & Usage]
      summary: Get budget
      x-apex-availability: available
      description: Returns the tenant's budget status (same shape as the budget-status resource).
      responses:
        "200":
          description: Budget cap and current-period spend.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BudgetStatus"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
    put:
      operationId: updateBudget
      tags: [Billing & Usage]
      summary: Set monthly budget cap
      x-apex-availability: available
      description: >-
        Sets the monthly spend budget in Rand cents; `null` removes the cap. Budgets are
        platform-plane: this operation requires a platform operator role — ask your platform
        contact to adjust the cap.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateBudgetRequest"
            examples:
              setCap:
                value:
                  budgetCents: 500000
      responses:
        "200":
          description: The updated budget.
          x-apex-note: "Schema partially documented — verify against the service."
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BudgetStatus"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /api/tenants/{id}/billing/budget-status:
    parameters:
      - $ref: "#/components/parameters/TenantId"
    get:
      operationId: getBudgetStatus
      tags: [Billing & Usage]
      summary: Get budget status
      x-apex-availability: available
      description: >-
        The tenant's monthly budget cap (Rand cents) alongside actual spend for the current
        billing period. The platform's spend guards read the same figures to enforce per-tenant
        limits.
      responses:
        "200":
          description: Budget cap and current-period spend.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/BudgetStatus"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /api/tenants/{id}/billing/costs:
    parameters:
      - $ref: "#/components/parameters/TenantId"
    get:
      operationId: getCostSummary
      tags: [Billing & Usage]
      summary: Get cost summary with margin analysis
      x-apex-availability: available
      description: >-
        Business costs (rate card multiplied by usage), actual costs (infrastructure, model usage
        and channel delivery), and the margin between them, for the given period.
      parameters:
        - $ref: "#/components/parameters/PeriodStartRequired"
        - $ref: "#/components/parameters/PeriodEndRequired"
      responses:
        "200":
          description: Cost summary.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CostSummary"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /api/tenants/{id}/billing/costs/series:
    parameters:
      - $ref: "#/components/parameters/TenantId"
    get:
      operationId: getCostTimeSeries
      tags: [Billing & Usage]
      summary: Get cost time series
      x-apex-availability: available
      description: >-
        Cost points over time, split by category, for charts and trend analysis.
      parameters:
        - $ref: "#/components/parameters/PeriodStartRequired"
        - $ref: "#/components/parameters/PeriodEndRequired"
        - name: interval
          in: query
          required: false
          description: Bucket size for the series.
          schema:
            type: string
            enum: [daily, weekly, monthly]
            default: daily
      responses:
        "200":
          description: Time-series cost breakdown.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CostTimeSeries"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /api/tenants/{id}/billing/invoices:
    parameters:
      - $ref: "#/components/parameters/TenantId"
    get:
      operationId: listInvoices
      tags: [Billing & Usage]
      summary: List invoices
      x-apex-availability: available
      parameters:
        - name: status
          in: query
          required: false
          schema:
            type: string
            enum: [draft, issued, paid, overdue, void]
        - name: limit
          in: query
          required: false
          schema:
            type: integer
        - name: cursor
          in: query
          required: false
          description: Pagination cursor from a previous page's `nextCursor`.
          schema:
            type: string
      responses:
        "200":
          description: Invoices, newest first.
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      $ref: "#/components/schemas/Invoice"
                  nextCursor:
                    type: [string, "null"]
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /api/tenants/{id}/billing/rate-card:
    parameters:
      - $ref: "#/components/parameters/TenantId"
    get:
      operationId: getRateCard
      tags: [Billing & Usage]
      summary: Get assigned rate card
      x-apex-availability: available
      responses:
        "200":
          description: The rate card assigned to the tenant's subscription.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RateCard"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: No rate card is assigned.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    put:
      operationId: assignRateCard
      tags: [Billing & Usage]
      summary: Assign a rate card
      x-apex-availability: available
      description: >-
        Assigns a published rate card to the tenant's subscription. Rate cards are platform-plane:
        this operation requires a platform operator role — pricing changes go through your
        platform contact.
      parameters:
        - $ref: "#/components/parameters/CorrelationIdHeader"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [rateCardId]
              properties:
                rateCardId:
                  type: string
      responses:
        "200":
          description: Assignment confirmation.
          headers:
            X-Correlation-ID:
              $ref: "#/components/headers/XCorrelationId"
          content:
            application/json:
              schema:
                type: object
                properties:
                  assigned:
                    type: boolean
              examples:
                - assigned: true
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /api/tenants/{id}/billing/rx-summary:
    parameters:
      - $ref: "#/components/parameters/TenantId"
    get:
      operationId: getRxSummary
      tags: [Billing & Usage]
      summary: Get Rx consumption summary
      x-apex-availability: available
      description: >-
        Aggregated Rx usage-unit totals for the period: total Rx units consumed, the
        infrastructure versus model-cost split, the per-product split, and the tenant's current
        Rx unit rate from the assigned rate card.
      parameters:
        - $ref: "#/components/parameters/PeriodStartRequired"
        - $ref: "#/components/parameters/PeriodEndRequired"
      responses:
        "200":
          description: Rx summary for the period.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/RxSummary"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /api/tenants/{id}/billing/spend:
    parameters:
      - $ref: "#/components/parameters/TenantId"
    get:
      operationId: getSpendSummary
      tags: [Billing & Usage]
      summary: Get current-period spend summary
      x-apex-availability: available
      description: Usage quantities per product and event type for the current billing period.
      responses:
        "200":
          description: Spend summary for the current period.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SpendSummary"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          description: No subscription exists for this tenant.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/tenants/{id}/billing/transactions:
    parameters:
      - $ref: "#/components/parameters/TenantId"
    get:
      operationId: listTransactions
      tags: [Billing & Usage]
      summary: List usage transaction records
      x-apex-availability: available
      description: >-
        The per-call usage ledger: one record per billable API call, each carrying the full cost
        breakdown — infrastructure share, model share with per-model token detail, Rx units, and
        the rate applied. Filter by period, product, and billable flag; paginate with `cursor`.
      parameters:
        - name: periodStart
          in: query
          required: false
          schema:
            type: string
            format: date-time
        - name: periodEnd
          in: query
          required: false
          schema:
            type: string
            format: date-time
        - name: productId
          in: query
          required: false
          schema:
            type: string
        - name: billable
          in: query
          required: false
          schema:
            type: boolean
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
        - name: cursor
          in: query
          required: false
          description: Pagination cursor — the `cursor` value from the previous page.
          schema:
            type: string
      responses:
        "200":
          description: Transaction records, newest first.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TransactionRecordList"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /api/tenants/{id}/billing/transactions/{correlationId}:
    parameters:
      - $ref: "#/components/parameters/TenantId"
      - name: correlationId
        in: path
        required: true
        description: >-
          The correlation identifier of the call — the `X-Correlation-ID` value your application
          sent (or the platform generated) for that request.
        schema:
          type: string
    get:
      operationId: getTransaction
      tags: [Billing & Usage]
      summary: Get one usage transaction record
      x-apex-availability: available
      description: >-
        Returns a single usage record by correlation identifier, with the full breakdown:
        infrastructure share, model share, total, Rx units, and per-model token and Rand detail.
      responses:
        "200":
          description: The transaction record.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TransactionRecord"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  # ── Workflows ──────────────────────────────────────────────────────────────

  /api/tenants/{id}/workflows:
    parameters:
      - $ref: "#/components/parameters/TenantId"
    get:
      operationId: listWorkflows
      tags: [Workflows]
      summary: List workflows
      x-apex-availability: available
      parameters:
        - name: status
          in: query
          required: false
          schema:
            type: string
            enum: [draft, active, paused, archived]
        - name: limit
          in: query
          required: false
          schema:
            type: integer
        - name: cursor
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Workflows.
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      $ref: "#/components/schemas/Workflow"
                  nextCursor:
                    type: [string, "null"]
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
    post:
      operationId: createWorkflow
      tags: [Workflows]
      summary: Create a workflow
      x-apex-availability: available
      description: Requires a tenant administrator role.
      parameters:
        - $ref: "#/components/parameters/CorrelationIdHeader"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateWorkflowRequest"
            examples:
              simpleWorkflow:
                value:
                  name: "nightly-verification"
                  definition:
                    trigger:
                      type: "manual"
                    steps:
                      - id: "verify"
                        type: "module_call"
                        productId: "vector"
                        endpoint: "/api/v1/your-tenant/verify"
                        method: "POST"
      responses:
        "201":
          description: The created workflow.
          headers:
            X-Correlation-ID:
              $ref: "#/components/headers/XCorrelationId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Workflow"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"

  /api/tenants/{id}/workflows/{workflowId}:
    parameters:
      - $ref: "#/components/parameters/TenantId"
      - $ref: "#/components/parameters/WorkflowId"
    get:
      operationId: getWorkflow
      tags: [Workflows]
      summary: Get a workflow
      x-apex-availability: available
      responses:
        "200":
          description: Workflow detail.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Workflow"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
    put:
      operationId: updateWorkflow
      tags: [Workflows]
      summary: Update a workflow
      x-apex-availability: available
      parameters:
        - $ref: "#/components/parameters/CorrelationIdHeader"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateWorkflowRequest"
      responses:
        "200":
          description: The updated workflow (version incremented on definition changes).
          headers:
            X-Correlation-ID:
              $ref: "#/components/headers/XCorrelationId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Workflow"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"
    delete:
      operationId: archiveWorkflow
      tags: [Workflows]
      summary: Archive a workflow
      x-apex-availability: available
      description: Archives (soft-deletes) the workflow. Requires a tenant administrator role.
      parameters:
        - $ref: "#/components/parameters/CorrelationIdHeader"
      responses:
        "200":
          description: Archive confirmation.
          headers:
            X-Correlation-ID:
              $ref: "#/components/headers/XCorrelationId"
          content:
            application/json:
              schema:
                type: object
                properties:
                  archived:
                    type: boolean
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  /api/tenants/{id}/workflows/{workflowId}/runs:
    parameters:
      - $ref: "#/components/parameters/TenantId"
      - $ref: "#/components/parameters/WorkflowId"
    get:
      operationId: listWorkflowRuns
      tags: [Workflows]
      summary: List runs
      x-apex-availability: available
      responses:
        "200":
          description: Runs of this workflow.
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      $ref: "#/components/schemas/WorkflowRun"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
    post:
      operationId: triggerWorkflowRun
      tags: [Workflows]
      summary: Trigger a run
      x-apex-availability: available
      description: Starts a run of the workflow. Requires a tenant administrator role.
      parameters:
        - $ref: "#/components/parameters/CorrelationIdHeader"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/TriggerRunRequest"
            examples:
              withTriggerData:
                value:
                  triggerData:
                    documentId: "doc-2481"
      responses:
        "201":
          description: The queued run.
          headers:
            X-Correlation-ID:
              $ref: "#/components/headers/XCorrelationId"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorkflowRun"
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  /api/tenants/{id}/workflows/{workflowId}/runs/{runId}:
    parameters:
      - $ref: "#/components/parameters/TenantId"
      - $ref: "#/components/parameters/WorkflowId"
      - name: runId
        in: path
        required: true
        schema:
          type: string
    get:
      operationId: getWorkflowRun
      tags: [Workflows]
      summary: Get run detail
      x-apex-availability: available
      description: Returns the run with its per-step execution records.
      responses:
        "200":
          description: Run detail with steps.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WorkflowRun"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          $ref: "#/components/responses/Forbidden"
        "404":
          $ref: "#/components/responses/NotFound"

  # ── Platform ───────────────────────────────────────────────────────────────

  /api/team:
    get:
      operationId: listTeam
      tags: [Platform]
      summary: List team members
      x-apex-availability: available
      description: Directory of tenant members visible to your session.
      responses:
        "200":
          description: Members.
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/TenantMember"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /api/audit:
    get:
      operationId: listAuditEvents
      tags: [Platform]
      summary: List audit events
      x-apex-availability: available
      description: >-
        The append-only audit log. Tenant-scoped users always see only their own tenant's events,
        regardless of filters; platform operators may filter across tenants. Every administrative
        action — sign-in, tenant lifecycle, member changes, configuration changes, key issuance —
        is recorded here.
      parameters:
        - name: tenantId
          in: query
          required: false
          schema:
            type: string
        - name: actorUserId
          in: query
          required: false
          schema:
            type: string
        - name: action
          in: query
          required: false
          description: Dotted action name, for example `tenant.updated` or `api_key.generated`.
          schema:
            type: string
        - name: productId
          in: query
          required: false
          schema:
            type: string
        - name: since
          in: query
          required: false
          schema:
            type: string
            format: date-time
        - name: until
          in: query
          required: false
          schema:
            type: string
            format: date-time
        - name: limit
          in: query
          required: false
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
        - name: cursor
          in: query
          required: false
          schema:
            type: string
      responses:
        "200":
          description: Audit events, newest first.
          content:
            application/json:
              schema:
                type: object
                properties:
                  items:
                    type: array
                    items:
                      $ref: "#/components/schemas/AuditEvent"
                  nextCursor:
                    type: [string, "null"]
        "400":
          $ref: "#/components/responses/ValidationError"
        "401":
          $ref: "#/components/responses/Unauthorized"

  /api/health:
    get:
      operationId: getHealth
      tags: [Service]
      summary: Service liveness
      x-apex-availability: available
      security: []
      responses:
        "200":
          description: Service status.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                  timestamp:
                    type: string
                    format: date-time
                  db:
                    type: string
                    description: Database connectivity — `connected` or `error`.
              examples:
                - status: "ok"
                  timestamp: "2026-08-06T12:00:00.000Z"
                  db: "connected"

components:
  securitySchemes:
    sessionAuth:
      type: apiKey
      in: cookie
      name: better-auth.session_token
      description: >-
        Operator/portal session. Obtained by signing in to the Forge portal. Used from the portal
        UI, not from server integrations.
    m2mAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: >-
        Machine-to-machine access token (OAuth2 client-credentials) issued by the platform
        identity provider. Used for provisioning integrations. Missing scope → 401.

  parameters:
    TenantId:
      name: id
      in: path
      required: true
      description: Tenant identifier.
      schema:
        type: string
      example: "cm0example0000tenant"
    TenantIdOrSlug:
      name: id
      in: path
      required: true
      description: Tenant identifier or slug — an external caller may use the slug it asked for.
      schema:
        type: string
      example: "your-tenant"
    KeyId:
      name: keyId
      in: path
      required: true
      description: API key identifier.
      schema:
        type: string
    ProductId:
      name: productId
      in: path
      required: true
      description: Product identifier.
      schema:
        type: string
        enum: [zenith, pulse, vector, echo, prism, forge, quantum]
    WorkflowId:
      name: workflowId
      in: path
      required: true
      schema:
        type: string
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: >-
        Replay-safety key in the form `provision:<uuid>`. Retrying a request with the same key
        returns the originally created tenant with a success status instead of a duplicate-slug
        conflict. Recommended for all programmatic provisioning.
      schema:
        type: string
      example: "provision:9c1a2b3c-4d5e-6f70-8192-a3b4c5d6e7f8"
    CorrelationIdHeader:
      name: X-Correlation-ID
      in: header
      required: false
      description: >-
        Correlation identifier threaded through audit records, transaction records, and cost
        events. Generated when absent; supply your own to reconcile usage later.
      schema:
        type: string
    PeriodStartRequired:
      name: periodStart
      in: query
      required: true
      description: Period start (ISO 8601 date-time).
      schema:
        type: string
        format: date-time
    PeriodEndRequired:
      name: periodEnd
      in: query
      required: true
      description: Period end (ISO 8601 date-time).
      schema:
        type: string
        format: date-time

  headers:
    XCorrelationId:
      description: Correlation identifier for this request — quote it when raising a query.
      schema:
        type: string

  responses:
    Unauthorized:
      description: Missing or invalid credentials.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    Forbidden:
      description: Authenticated, but not permitted — wrong tenant or missing role.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    NotFound:
      description: The resource does not exist (or is not visible to you).
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    ValidationError:
      description: The request body or query parameters failed validation.
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"

  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              description: Stable machine-readable code, for example `NOT_FOUND` or `VALIDATION_ERROR`.
            message:
              type: string

    # ── Tenants ──────────────────────────────────────────

    Tenant:
      type: object
      properties:
        id:
          type: string
        slug:
          type: string
        displayName:
          type: string
        orgName:
          type: string
        tier:
          type: string
          enum: [awareness, intelligence, enterprise]
        domain:
          type: [string, "null"]
        timezone:
          type: string
        locale:
          type: string
        status:
          type: string
          enum: [active, suspended, deprovisioned]
        provisioningStatus:
          type: string
          enum: [provisioning, ready, failed]
        planLabel:
          type: [string, "null"]
          description: >-
            The external plan label supplied at creation when it was not platform tier
            vocabulary. Preserved verbatim; an engineer assigns the real tier later.
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    TenantProduct:
      type: object
      properties:
        id:
          type: string
        productId:
          type: string
        enabled:
          type: boolean
        tier:
          type: string
          enum: [starter, professional, enterprise]
        config:
          type: object
          additionalProperties: true
        configVersion:
          type: integer
          description: Monotonic version — increments on every configuration update.
        activatedAt:
          type: string
          format: date-time
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    TenantWithProducts:
      allOf:
        - $ref: "#/components/schemas/Tenant"
        - type: object
          properties:
            products:
              type: array
              items:
                $ref: "#/components/schemas/TenantProduct"

    CreateTenantRequest:
      type: object
      required: [slug, displayName, orgName, tier]
      properties:
        slug:
          type: string
          minLength: 2
          maxLength: 50
          pattern: "^[a-z0-9-]+$"
          description: Lowercase alphanumeric with dashes. Permanent tenant identifier.
        displayName:
          type: string
          minLength: 1
          maxLength: 200
        orgName:
          type: string
          minLength: 1
          maxLength: 200
        tier:
          type: string
          minLength: 1
          maxLength: 100
          description: >-
            Free-form at creation. A recognised platform tier (`awareness`, `intelligence`,
            `enterprise`) is honoured as-is; any other label creates the tenant at `awareness`
            (the smallest product set) with the label preserved on `planLabel` for later
            assignment.
        domain:
          type: [string, "null"]
          maxLength: 255
        timezone:
          type: string
          maxLength: 100
        locale:
          type: string
          maxLength: 10
        adminEmail:
          type: string
          format: email
          maxLength: 255
          description: >-
            Seeds the first tenant membership. Required in practice for programmatic
            onboarding — without at least one membership nobody can sign in to the tenant.
        callbackUrls:
          type: array
          minItems: 1
          maxItems: 10
          items:
            type: string
            format: uri
            maxLength: 1024
          description: >-
            Absolute sign-in redirect URLs. Presence provisions a dedicated per-tenant sign-in
            client; omit to share the platform client.
        logoutUrls:
          type: array
          minItems: 1
          maxItems: 10
          items:
            type: string
            format: uri
            maxLength: 1024
          description: Absolute sign-out redirect URLs.
        kycReference:
          type: string
          minLength: 1
          maxLength: 255
          description: Opaque onboarding reference from your provisioning system. Stored verbatim, never parsed.
        provenance:
          type: string
          enum: ["kyc", "admin"]
          description: Closed vocabulary. `kyc` = tenant created by a KYC-gated onboarding integration; `admin` = an operator created the tenant ahead of KYC (tenant-first onboarding, `kycReference` absent).

    ProvisionedTenant:
      allOf:
        - $ref: "#/components/schemas/TenantWithProducts"
        - type: object
          properties:
            statusUrl:
              type: string
              description: The provisioning status resource to poll after a `202`.

    UpdateTenantRequest:
      type: object
      properties:
        displayName:
          type: string
          minLength: 1
          maxLength: 200
        orgName:
          type: string
          minLength: 1
          maxLength: 200
        tier:
          type: string
          enum: [awareness, intelligence, enterprise]
        domain:
          type: [string, "null"]
          maxLength: 255
        timezone:
          type: string
          maxLength: 100
        locale:
          type: string
          maxLength: 10

    ProvisionRetryRequest:
      type: object
      description: >-
        Optional runtime inputs — the same as at creation. An empty body re-runs whatever is
        already recorded.
      properties:
        adminEmail:
          type: string
          format: email
          maxLength: 255
        callbackUrls:
          type: array
          minItems: 1
          maxItems: 10
          items:
            type: string
            format: uri
            maxLength: 1024
        logoutUrls:
          type: array
          minItems: 1
          maxItems: 10
          items:
            type: string
            format: uri
            maxLength: 1024

    TenantProvisioning:
      type: object
      properties:
        id:
          type: string
        slug:
          type: string
        status:
          type: string
          enum: [provisioning, ready, failed]
        error:
          type: [string, "null"]
        provisionedAt:
          type: [string, "null"]
          format: date-time
        steps:
          type: object
          description: Individually verified artifacts — tells you which piece is missing when provisioning half-succeeds.
          properties:
            identityRegistry:
              type: boolean
            authClient:
              type: boolean
            adminMembership:
              type: boolean
      examples:
        - id: "cm0example0000tenant"
          slug: "your-tenant"
          status: "ready"
          error: null
          provisionedAt: "2026-08-06T12:00:00.000Z"
          steps:
            identityRegistry: true
            authClient: true
            adminMembership: true

    # ── API keys ─────────────────────────────────────────

    ApiKey:
      type: object
      description: Key metadata. The full key and its hash are never returned after creation.
      properties:
        id:
          type: string
        tenantId:
          type: string
        name:
          type: string
        description:
          type: [string, "null"]
        prefix:
          type: string
          description: Display prefix of the key, for identification in lists.
        scopes:
          type: array
          items:
            type: string
          description: >-
            Granted scopes, `product:action` format — for example `zenith:read`, `zenith:write`,
            `echo:send`, `vector:verify`, `pulse:read`, `prism:export`, `quantum:predict`,
            `forge:billing`. The wildcard `*` is permitted for test keys only. LLM dispatch
            requires `llm:invoke`.
          x-apex-note: "Scope catalogue partially documented — verify the current list against the service."
        environment:
          type: string
          enum: [live, test]
        status:
          type: string
          enum: [active, revoked, expired]
        expiresAt:
          type: [string, "null"]
          format: date-time
        lastUsedAt:
          type: [string, "null"]
          format: date-time
        lastUsedIp:
          type: [string, "null"]
        createdBy:
          type: string
        createdByEmail:
          type: [string, "null"]
        revokedAt:
          type: [string, "null"]
          format: date-time
        revokedBy:
          type: [string, "null"]
        rotatedFromId:
          type: [string, "null"]
          description: When set, the key this one replaced via rotation.
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    ApiKeyCreated:
      allOf:
        - $ref: "#/components/schemas/ApiKey"
        - type: object
          required: [fullKey]
          properties:
            fullKey:
              type: string
              description: >-
                The complete key, `apex_live_<64 hex>` or `apex_test_<64 hex>`. Returned exactly
                once — store it now.
              examples: ["apex_test_0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f"]

    CreateApiKeyRequest:
      type: object
      required: [name, scopes, environment]
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 100
          pattern: "^[a-z0-9][a-z0-9-]*$"
          description: Lowercase slug-format name.
        description:
          type: string
          maxLength: 500
        scopes:
          type: array
          minItems: 1
          maxItems: 50
          items:
            type: string
          description: Scopes to grant. The wildcard `*` is rejected for `live` keys.
        environment:
          type: string
          enum: [live, test]
        expiresInDays:
          type: integer
          minimum: 1
          maximum: 365
          description: Key lifetime. Defaults to 90 days.

    UpdateProductConfigRequest:
      type: object
      required: [config]
      properties:
        config:
          type: object
          additionalProperties: true
          description: The product configuration document. Keys and values are validated against the product's configuration schema.

    # ── Members ──────────────────────────────────────────

    TenantMember:
      type: object
      properties:
        cognitoSub:
          type: string
          description: The member's identity-provider subject identifier.
        tenantId:
          type: string
        email:
          type: string
        roles:
          type: string
          description: Role string, for example `admin`, `manager`, `user`, or `viewer`.
        status:
          type: string
        isDefault:
          type: boolean
        createdAt:
          type: string
          format: date-time

    InviteMemberRequest:
      type: object
      required: [email, roles]
      properties:
        email:
          type: string
          format: email
        roles:
          type: string
          description: Role to grant — `admin`, `manager`, `user`, or `viewer`.
        temporaryPassword:
          type: string
          description: Optional temporary password for a newly created user.

    InviteMemberResult:
      type: object
      properties:
        cognitoSub:
          type: string
        email:
          type: string
        tenantId:
          type: string
        roles:
          type: string
        status:
          type: string

    # ── Communication channels ───────────────────────────

    WhatsAppChannelConfig:
      type: object
      properties:
        phone_number_id:
          type: string
        business_account_id:
          type: string
        waba_model:
          type: string
          enum: [managed, dedicated]
          description: >-
            `managed` — a platform-hosted business account hosts your tenant; `dedicated` — your
            own business account, which the platform authenticates against.
        api_version:
          type: string
          examples: ["v21.0"]

    SecretRefStatus:
      type: object
      properties:
        isSet:
          type: boolean
        arnSuffix:
          type: string
          description: Last characters of the stored secret reference, for visual confirmation. Empty when not set.

    WhatsAppChannelMasked:
      type: object
      properties:
        type:
          type: string
          const: "whatsapp"
        provider:
          type: string
          const: "meta_cloud_api"
        config:
          $ref: "#/components/schemas/WhatsAppChannelConfig"
        secrets:
          type: object
          description: Secret status only — raw values are never returned.
          properties:
            accessToken:
              $ref: "#/components/schemas/SecretRefStatus"
            appSecret:
              $ref: "#/components/schemas/SecretRefStatus"
            webhookVerifyToken:
              $ref: "#/components/schemas/SecretRefStatus"

    WhatsAppChannelInput:
      type: object
      required: [phoneNumberId, businessAccountId, wabaModel, apiVersion]
      properties:
        phoneNumberId:
          type: string
        businessAccountId:
          type: string
        wabaModel:
          type: string
          enum: [managed, dedicated]
        apiVersion:
          type: string
        accessToken:
          type: string
          description: Write-only. Omit on re-post to keep the existing stored value.
        appSecret:
          type: string
          description: Write-only. Omit on re-post to keep the existing stored value.
        webhookVerifyToken:
          type: string
          description: Write-only. Omit on re-post to keep the existing stored value.

    # ── Billing ──────────────────────────────────────────

    Subscription:
      type: object
      properties:
        id:
          type: string
        tenantId:
          type: string
        planId:
          type: string
        status:
          type: string
          enum: [trial, active, past_due, suspended, cancelled, archived]
        currentPeriodStart:
          type: string
          format: date-time
        currentPeriodEnd:
          type: string
          format: date-time
        cancelledAt:
          type: [string, "null"]
          format: date-time
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    BudgetAlert:
      type: object
      properties:
        id:
          type: string
        subscriptionId:
          type: string
        thresholdPct:
          type: number
          description: Percentage of the budget at which the alert fires.
        enabled:
          type: boolean
        triggeredAt:
          type: [string, "null"]
          format: date-time
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    BudgetAlertInput:
      type: object
      required: [thresholdPct, enabled]
      properties:
        thresholdPct:
          type: number
        enabled:
          type: boolean

    BudgetStatus:
      type: object
      properties:
        tenantId:
          type: string
        budgetCents:
          type: [integer, "null"]
          description: Monthly budget cap in Rand cents; `null` means no cap.
        spentCents:
          type: integer
        periodStart:
          type: string
          format: date-time
        periodEnd:
          type: string
          format: date-time

    UpdateBudgetRequest:
      type: object
      required: [budgetCents]
      properties:
        budgetCents:
          type: [integer, "null"]
          description: New monthly cap in Rand cents; `null` removes the cap.

    CostSummary:
      type: object
      properties:
        tenantId:
          type: string
        periodStart:
          type: string
          format: date-time
        periodEnd:
          type: string
          format: date-time
        businessCosts:
          type: object
          description: What usage costs at your rate card.
          properties:
            subtotalCents:
              type: integer
            breakdown:
              type: array
              items:
                type: object
                properties:
                  productId:
                    type: string
                  eventType:
                    type: string
                  quantity:
                    type: number
                  includedQuantity:
                    type: number
                  billableQuantity:
                    type: number
                  unitPriceCents:
                    type: integer
                  totalCents:
                    type: integer
        actualCosts:
          type: object
          description: What the usage actually cost to serve.
          properties:
            totalCents:
              type: integer
            byCategory:
              type: object
              additionalProperties:
                type: integer
            byProduct:
              type: object
              additionalProperties:
                type: integer
        marginCents:
          type: integer
        marginPct:
          type: number

    CostTimeSeries:
      type: object
      properties:
        tenantId:
          type: string
        periodStart:
          type: string
          format: date-time
        periodEnd:
          type: string
          format: date-time
        interval:
          type: string
          enum: [daily, weekly, monthly]
        series:
          type: array
          items:
            type: object
            properties:
              date:
                type: string
              llm_token:
                type: number
                description: Model-usage cost in the bucket (Rand cents).
              aws_infra:
                type: number
                description: Infrastructure cost in the bucket (Rand cents).
              channel_delivery:
                type: number
                description: Message-delivery cost in the bucket (Rand cents).
              total:
                type: number

    InvoiceLineItem:
      type: object
      properties:
        id:
          type: string
        invoiceId:
          type: string
        productId:
          type: string
        description:
          type: string
        quantity:
          type: number
        unitPriceCents:
          type: integer
        totalCents:
          type: integer

    Invoice:
      type: object
      properties:
        id:
          type: string
        tenantId:
          type: string
        periodStart:
          type: string
          format: date-time
        periodEnd:
          type: string
          format: date-time
        subtotalCents:
          type: integer
        taxCents:
          type: integer
        totalCents:
          type: integer
        status:
          type: string
          enum: [draft, issued, paid, overdue, void]
        issuedAt:
          type: [string, "null"]
          format: date-time
        paidAt:
          type: [string, "null"]
          format: date-time
        lineItems:
          type: array
          items:
            $ref: "#/components/schemas/InvoiceLineItem"
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    RateCardItem:
      type: object
      properties:
        id:
          type: string
        rateCardId:
          type: string
        productId:
          type: string
        eventType:
          type: string
        unitPriceCents:
          type: integer
        unitPriceRx:
          type: [number, "null"]
          description: Price of the event in Rx usage units, when the item is Rx-priced.
        includedQuantity:
          type: number
          description: Quantity included before per-unit charges apply.
        description:
          type: [string, "null"]

    RateCard:
      type: object
      properties:
        id:
          type: string
        version:
          type: string
        name:
          type: string
        description:
          type: [string, "null"]
        status:
          type: string
          enum: [draft, published, archived]
        publishedAt:
          type: [string, "null"]
          format: date-time
        items:
          type: array
          items:
            $ref: "#/components/schemas/RateCardItem"
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    SpendSummary:
      type: object
      properties:
        tenantId:
          type: string
        periodStart:
          type: string
          format: date-time
        periodEnd:
          type: string
          format: date-time
        products:
          type: array
          items:
            type: object
            properties:
              productId:
                type: string
              eventType:
                type: string
              quantity:
                type: number
        totalQuantity:
          type: number

    RxSummary:
      type: object
      description: Aggregated Rx usage-unit consumption for a period.
      properties:
        totalRxTokens:
          type: number
          description: Total Rx usage units consumed.
        totalAwsCents:
          type: integer
          description: Infrastructure share of cost, Rand cents.
        totalAiCents:
          type: integer
          description: Model-usage share of cost, Rand cents.
        totalCostCents:
          type: integer
        rxRateCents:
          type: [integer, "null"]
          description: The tenant's current Rx unit rate from the assigned rate card.
        transactionCount:
          type: integer
        byProduct:
          type: object
          additionalProperties:
            type: object
            properties:
              rxTokens:
                type: number
              awsCents:
                type: integer
              aiCents:
                type: integer
              count:
                type: integer

    AiModelCostDetail:
      type: object
      description: Per-model share of one call's model cost.
      properties:
        model:
          type: string
        provider:
          type: string
        inputTokens:
          type: integer
        outputTokens:
          type: integer
        costCents:
          type: integer

    TransactionRecord:
      type: object
      description: One row per billable API call — an immutable cost snapshot.
      properties:
        id:
          type: string
        correlationId:
          type: string
        tenantId:
          type: string
        productId:
          type: string
        endpoint:
          type: string
        method:
          type: string
        statusCode:
          type: integer
        durationMs:
          type: integer
        billable:
          type: boolean
        sourceProduct:
          type: [string, "null"]
          description: Set when another product made the call on your behalf.
        awsCents:
          type: integer
          description: Infrastructure share, Rand cents.
        aiCents:
          type: integer
          description: Model-usage share, Rand cents.
        totalCostCents:
          type: integer
        rxTokens:
          type: number
          description: Rx usage units charged for the call.
        rxRateCents:
          type: integer
          description: Rx unit rate applied at the time of the call.
        aiDetail:
          type: array
          description: Per-model token and Rand audit trail for the model share.
          items:
            $ref: "#/components/schemas/AiModelCostDetail"
        awsRateId:
          type: [string, "null"]
          description: Identifier of the infrastructure rate row applied.
        occurredAt:
          type: string
          format: date-time
        createdAt:
          type: string
          format: date-time
      examples:
        - id: "txn_01hexample"
          correlationId: "3f6f0d2a-8f9f-4a51-b0cc-6d1a2example"
          tenantId: "cm0example0000tenant"
          productId: "zenith"
          endpoint: "/api/v1/your-tenant/query"
          method: "POST"
          statusCode: 200
          durationMs: 2140
          billable: true
          sourceProduct: null
          awsCents: 3
          aiCents: 42
          totalCostCents: 45
          rxTokens: 45
          rxRateCents: 1
          aiDetail:
            - model: "claude-sonnet-4-6"
              provider: "anthropic"
              inputTokens: 5210
              outputTokens: 640
              costCents: 42
          awsRateId: "rate_01hexample"
          occurredAt: "2026-08-06T11:58:31.000Z"
          createdAt: "2026-08-06T11:58:32.000Z"

    TransactionRecordList:
      type: object
      properties:
        records:
          type: array
          items:
            $ref: "#/components/schemas/TransactionRecord"
        totalCount:
          type: integer
        cursor:
          type: [string, "null"]
          description: Pass as `cursor` on the next request to continue paging; `null` on the last page.

    # ── Workflows ────────────────────────────────────────

    WorkflowStep:
      type: object
      description: >-
        One step of a workflow definition. `type` selects the step kind; each kind carries its
        own fields — `module_call` (`productId`, `endpoint`, `method`, `body`), `condition`
        (`expression`, `thenSteps`, `elseSteps`), `parallel` (`branches`), `delay`
        (`durationMs`).
      required: [id, type]
      properties:
        id:
          type: string
        type:
          type: string
          enum: [module_call, condition, parallel, delay]
        productId:
          type: string
        endpoint:
          type: string
        method:
          type: string
        body:
          type: object
          additionalProperties: true
        expression:
          type: string
        thenSteps:
          type: array
          items:
            $ref: "#/components/schemas/WorkflowStep"
        elseSteps:
          type: array
          items:
            $ref: "#/components/schemas/WorkflowStep"
        branches:
          type: array
          items:
            type: array
            items:
              $ref: "#/components/schemas/WorkflowStep"
        durationMs:
          type: integer

    WorkflowDefinition:
      type: object
      required: [steps]
      properties:
        steps:
          type: array
          items:
            $ref: "#/components/schemas/WorkflowStep"
        trigger:
          type: object
          properties:
            type:
              type: string
              enum: [manual, webhook, schedule]
            config:
              type: object
              additionalProperties: true

    Workflow:
      type: object
      properties:
        id:
          type: string
        tenantId:
          type: string
        name:
          type: string
        definition:
          $ref: "#/components/schemas/WorkflowDefinition"
        status:
          type: string
          enum: [draft, active, paused, archived]
        version:
          type: integer
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    CreateWorkflowRequest:
      type: object
      required: [name, definition]
      properties:
        name:
          type: string
        definition:
          $ref: "#/components/schemas/WorkflowDefinition"

    UpdateWorkflowRequest:
      type: object
      properties:
        name:
          type: string
        definition:
          $ref: "#/components/schemas/WorkflowDefinition"
        status:
          type: string
          enum: [draft, active, paused, archived]

    TriggerRunRequest:
      type: object
      properties:
        triggerData:
          type: object
          additionalProperties: true
          description: Optional payload made available to the run's steps.

    StepExecution:
      type: object
      properties:
        id:
          type: string
        runId:
          type: string
        stepId:
          type: string
        stepType:
          type: string
          enum: [module_call, condition, parallel, delay]
        status:
          type: string
          enum: [pending, running, completed, failed, skipped]
        input:
          type: [object, "null"]
          additionalProperties: true
        output:
          type: [object, "null"]
          additionalProperties: true
        error:
          type: [string, "null"]
        startedAt:
          type: [string, "null"]
          format: date-time
        completedAt:
          type: [string, "null"]
          format: date-time
        createdAt:
          type: string
          format: date-time

    WorkflowRun:
      type: object
      properties:
        id:
          type: string
        workflowId:
          type: string
        tenantId:
          type: string
        status:
          type: string
          enum: [queued, running, waiting, completed, failed, cancelled]
        context:
          type: object
          additionalProperties: true
        correlationId:
          type: [string, "null"]
        startedAt:
          type: [string, "null"]
          format: date-time
        completedAt:
          type: [string, "null"]
          format: date-time
        failedAt:
          type: [string, "null"]
          format: date-time
        error:
          type: [string, "null"]
        steps:
          type: array
          items:
            $ref: "#/components/schemas/StepExecution"
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    # ── Audit ────────────────────────────────────────────

    AuditEvent:
      type: object
      properties:
        id:
          type: string
        tenantId:
          type: [string, "null"]
        tenantSlug:
          type: [string, "null"]
        productId:
          type: [string, "null"]
        action:
          type: string
          description: Dotted action name, for example `tenant.updated`, `member.invited`, `api_key.generated`.
        actorUserId:
          type: [string, "null"]
        actorEmail:
          type: [string, "null"]
          description: For machine actors this is `m2m:<client id>`.
        targetType:
          type: [string, "null"]
        targetId:
          type: [string, "null"]
        before:
          type: [object, "null"]
          additionalProperties: true
        after:
          type: [object, "null"]
          additionalProperties: true
        metadata:
          type: [object, "null"]
          additionalProperties: true
        correlationId:
          type: [string, "null"]
        createdAt:
          type: string
          format: date-time
