openapi: 3.0.3
info:
  title: Riptide Admin API
  version: 1.0.0
  description: >
    Typed control-plane surface for the console, external integrators, and the agent plane.
    Source-of-truth contract alongside /proto and /migrations (see docs/spec/admin-api.md).


    Conventions (SR-503, enforced by openapi/*_test.go): every `list*` operation takes `limit`
    (default 100, max 1000) and an opaque `cursor`, and answers `next_cursor` when more rows
    exist (keyset pagination; a cursor is only valid for the operation that issued it). Every
    mutating operation carries `Idempotency-Key`: a replay with the same key and an identical
    payload returns the original result, a replay with a different payload is answered 409
    CONFLICT. `PUT /{id}` is partial-update-by-presence (an omitted property leaves the stored
    value unchanged; arrays present replace the whole set) — there is no separate PATCH verb.
    Bulk operations are named `bulk<Verb><Entity>` (`bulkImportPublishers`,
    `bulkUpdateLineItemStatus`). Enum casing: lifecycle / state / kind enums are
    `UPPER_SNAKE`; vocabulary keys that are stored verbatim as identifiers (roles, event types,
    modes, dimensions) are `lower_snake`; no enum mixes the two. Credentials: `Authorization:
    Bearer` (session or JWT) or `X-Riptide-Api-Key` (durable API key) — `security` lists both.


    Evolution (docs/spec/admin-api.md "Deprecation and changelog"): every change under `/v1` is
    additive; a property or enum value that must be retired is marked `deprecated: true` with a
    description naming its replacement, keeps working for at least one release, and is listed
    in CHANGELOG-api.md at the repository root together with each release's additions. SDKs:
    Go (`riptide.dev/clients/go/admin`), TypeScript (`@riptide/admin-client`), Python
    (`riptide-admin`) and the `riptide` CLI are generated from this document and share one
    client behaviour — retries with backoff honouring `Retry-After`, a per-attempt timeout, a
    generated `Idempotency-Key` on every mutating call, typed `code` / `hint` errors and cursor
    pagination. services/docsportal hosts this document, every other OpenAPI document and the
    proto contracts.
servers:
  - url: /
security:
  - bearerAuth: []
  - apiKeyAuth: []
# Tags in the order their paths appear below; each tag is one MCP tool domain (tools/mcpgen) and
# one RBAC domain (x-riptide-rbac permissions are `<tag>.read` / `<tag>.write`).
tags:
  - name: auth
    description: Sessions, magic links, invites, single sign-on and the public branding lookup.
  - name: reference
    description: Tenant-agnostic reference dictionaries (geo, taxonomies) any authenticated actor may read.
  - name: operator
    description: Platform-operator surface (cross-tenant) - tenants, cells, rate plans, sandbox sample data.
  - name: operability
    description: Doctor, explain, diagnostics, audit log and usage read-outs.
  - name: iam
    description: Users, roles, API keys, sealing keys, agent identities and privacy requests.
  - name: supply
    description: Publishers, apps, placements, inventory domains, IP lists and marketplaces' supply side.
  - name: supply_transparency
    description: ads.txt / app-ads.txt / sellers.json verification and supply-chain policy.
  - name: campaign
    description: Advertisers, campaign orders, line items, creatives, templates and delivery controls.
  - name: demand
    description: Demand partners, routes, deals and deal synchronisation.
  - name: reporting
    description: Semantic-layer reports, report configs and schedules, exports, conversions, live counters.
  - name: marketplace
    description: Listings, proposals, activations, stack products and marketplace settlements.
  - name: billing
    description: Fee schedules, invoices, payouts, wallets, quotas and platform billing.
  - name: model
    description: Decisioner model registry - assign, evaluate, promote.
  - name: targeting
    description: Audiences, custom lists and bid modifiers.
  - name: forecast
    description: Inventory and delivery forecasts.
  - name: webhooks
    description: Webhook subscriptions, deliveries and secret rotation.

paths:
  /v1/auth/config:
    get:
      tags: [auth]
      operationId: getAuthConfig
      summary: >
        Read the public authentication settings, including whether self-service signup is enabled.
      security: []
      responses:
        '200':
          description: Auth config.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AuthConfig' }
        default: { $ref: '#/components/responses/Error' }

  /v1/auth/signup:
    post:
      tags: [auth]
      operationId: authSignup
      summary: >
        Start self-service signup by sending a magic-link challenge. Only a hash of the challenge is
        stored. The tenant and first admin are created after verification. Requires
        RIPTIDE_ALLOW_PUBLIC_SIGNUP.
      security: []
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/AuthSignupRequest' }
      responses:
        '200':
          description: Always ok when accepted (enumerate-safe).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AuthAccepted' }
        default: { $ref: '#/components/responses/Error' }

  /v1/auth/magic-link:
    post:
      tags: [auth]
      operationId: authMagicLink
      summary: >
        Request a magic-link login for a tenant user or an allowed operator. The response does not
        disclose whether the email address has an account.
      security: []
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/AuthMagicLinkRequest' }
      responses:
        '200':
          description: Always ok when accepted (enumerate-safe).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AuthAccepted' }
        default: { $ref: '#/components/responses/Error' }

  /v1/auth/verify:
    post:
      tags: [auth]
      operationId: authVerify
      summary: >
        Verify a magic-link token and issue an rt_sess_ session bearer. If the verified email belongs to
        multiple tenants, the session includes memberships[] and tenant_choice_required: true so the
        client can request a tenant selection. Membership information is returned only after
        verification.
      description: >
        Invite redemption atomically revalidates the pending, unrevoked, unexpired challenge,
        non-archived tenant and active user, grants the permitted role, and consumes the invite.
        Publisher invites require a non-archived publisher in the same tenant. An existing user must already
        have that same non-null publisher binding; a missing or different binding returns
        409 CONFLICT with a publisher_id field hint and never implicitly rebinds the user.
        An invalid, missing or archived invited publisher returns BAD_INPUT. An invalid,
        expired or revoked challenge, inactive user, or archived tenant returns 401.
        For a keyed verification, token consumption and its token-bound retry receipt commit
        together; invite user creation and the role grant share that commit. Receipt storage
        failures leave the challenge retryable. A retry after session creation or response
        failure revalidates current identity and permissions before issuing a fresh session.
        Signup provisioning uses its existing resumable workflow before this atomic
        verification step; workspace provisioning is not part of the verification transaction.
      security: []
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/AuthVerifyRequest' }
      responses:
        '200':
          description: Session issued.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AuthSession' }
        default: { $ref: '#/components/responses/Error' }

  /v1/branding:
    get:
      tags: [auth]
      operationId: getPublicBranding
      summary: >
        Read the product name, logo, favicon, and theme colors for a console host before login. The host
        is matched against the tenant's serving_domain or console host mapping. Hostnames and other
        tenant fields are excluded. Unknown hosts return the platform defaults with HTTP 200, so callers
        cannot use a 404 response to identify registered hosts.
      security: []
      parameters:
        - name: host
          in: query
          required: true
          description: The console hostname the browser loaded from (lower-case, no scheme or port).
          schema: { type: string, minLength: 1, maxLength: 253 }
      responses:
        '200':
          description: The public branding subset for the host (platform defaults for an unknown host).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PublicBranding' }
        default: { $ref: '#/components/responses/Error' }

  /v1/auth/logout:
    post:
      tags: [auth]
      operationId: authLogout
      summary: Revoke the current session bearer.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '204':
          description: Revoked (or already gone).
        default: { $ref: '#/components/responses/Error' }

  /v1/auth/me:
    get:
      tags: [auth]
      operationId: authMe
      summary: Current authenticated actor (user, tenant, roles, entitlements).
      responses:
        '200':
          description: Current session identity.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AuthMe' }
        default: { $ref: '#/components/responses/Error' }

  /v1/auth/sessions/revoke-all:
    post:
      tags: [auth]
      operationId: revokeAllSessions
      summary: >
        End all console sessions of the signed-in actor, including the current session. The actor is
        resolved from the session; callers cannot select an actor with a tenant_id parameter.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: Number of sessions revoked.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SessionsRevoked' }
        default: { $ref: '#/components/responses/Error' }

  /v1/reference/geo:
    get:
      tags: [reference]
      operationId: listGeoReference
      summary: >
        Read distinct countries, regions, metros, and cities from the loaded IP geolocation dataset. Any
        authenticated actor can read this shared dictionary. Returns 503 with a retryable hint until a
        dataset is loaded.
      parameters:
        - name: level
          in: query
          required: false
          schema: { $ref: '#/components/schemas/GeoReferenceLevel' }
          description: Hierarchy level to list (default city).
        - name: q
          in: query
          required: false
          schema: { type: string, maxLength: 64 }
          description: Case-insensitive substring match on the entry name (a metro code is matched by prefix).
        - name: country
          in: query
          required: false
          schema: { type: string, maxLength: 64 }
          description: Narrow to a country (ISO 3166-1 alpha-2, case-insensitive).
        - name: region
          in: query
          required: false
          schema: { type: string, maxLength: 64 }
          description: Narrow to a region code within country.
        - name: metro
          in: query
          required: false
          schema: { type: integer, format: int32, minimum: 1 }
          description: Narrow to a metro (DMA) code.
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: One page of dictionary entries plus the dataset they came from.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/GeoReferencePage' }
        default: { $ref: '#/components/responses/Error' }

  /v1/operator/cells:
    get:
      tags: [operator]
      operationId: listCells
      summary: >
        List cells with their region, tier, status, and endpoints. Operator access required.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: Cells sorted by id.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CellList' }
        default: { $ref: '#/components/responses/Error' }
  /v1/operator/tenants/{tenant_id}/rate-plan:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    post:
      tags: [operator]
      operationId: assignTenantRatePlan
      summary: >
        Assign a platform rate plan to a tenant and, by default, apply the plan's entitlement pack.
        Operator access required.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/TenantRatePlanAssign' }
      responses:
        '200':
          description: The tenant's rate plan after assignment.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PlatformRatePlan' }
        default: { $ref: '#/components/responses/Error' }
  /v1/operator/tenants:
    get:
      tags: [operator]
      operationId: listTenants
      summary: List tenants (operator only).
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of tenants.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TenantList' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [operator]
      operationId: createTenant
      summary: Create a tenant (operator only).
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/TenantCreate' }
      responses:
        '201':
          description: Created.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Tenant' }
        default: { $ref: '#/components/responses/Error' }

  /v1/operator/onboarding/tenants:
    post:
      tags: [operator]
      operationId: onboardTenant
      summary: >
        Create a tenant, apply entitlements, generate a sealing key, create the first admin user, and
        optionally provision a custom serving domain. The workflow is idempotent by Idempotency-Key and
        compensates for completed steps if a later step fails.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/OnboardTenantRequest' }
      responses:
        '201':
          description: Onboarded tenant resources.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TenantOnboardingResponse' }
        default: { $ref: '#/components/responses/Error' }

  /v1/operator/tenants/{tenant_id}:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [operator]
      operationId: getTenant
      summary: Get a tenant (operator only).
      responses:
        '200':
          description: The tenant.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Tenant' }
        default: { $ref: '#/components/responses/Error' }
    put:
      tags: [operator]
      operationId: updateTenant
      summary: >
        Update a tenant's branding or entitlements. Operator access required. A supplied entitlements
        array replaces the entire entitlement set, including the ssp, dsp, and ad_server module
        permissions. Supplied branding replaces the product name and serving, event, and sync hostnames
        used by the console and hostname-based tenant lookup.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/TenantUpdate' }
      responses:
        '200':
          description: Updated.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Tenant' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/health:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [operability]
      operationId: getTenantHealth
      summary: >
        Read tenant health, including the serving plan version when a plan store is connected,
        per-endpoint error rates, and governor shed counts from process counters. When ClickHouse is
        configured, the response also includes requests, fills, impressions, revenue_net, and fill_rate
        for the same window. Those metric fields are omitted when ClickHouse is not configured.
      responses:
        '200':
          description: The tenant's current health snapshot.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TenantHealth' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/agent-identities:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [iam]
      operationId: listAgentIdentities
      summary: >
        List the tenant's registered agent identities, newest first.
      x-riptide-rbac: [iam.read]
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - name: status
          in: query
          required: false
          description: Only identities in this lifecycle status.
          schema: { $ref: '#/components/schemas/LifecycleStatus' }
      responses:
        '200':
          description: A page of agent identities.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AgentIdentityList' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [iam]
      operationId: createAgentIdentity
      summary: >
        Register a named agent identity for an MCP or A2A client, bind its public key, and set its
        autonomy band. New actions use the tenant's configured write default. The response includes the
        active public key; the agent proves possession of its private key at the gateway.
      x-riptide-rbac: [iam.write]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/AgentIdentityCreate' }
      responses:
        '201':
          description: The registered identity.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AgentIdentity' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/agent-identities/{agent_identity_id}:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/AgentIdentityId'
    get:
      tags: [iam]
      operationId: getAgentIdentity
      summary: Get one agent identity with its keys, band and kill-switch state.
      x-riptide-rbac: [iam.read]
      responses:
        '200':
          description: The agent identity.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AgentIdentity' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/agent-identities/{agent_identity_id}/rotate-key:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/AgentIdentityId'
    post:
      tags: [iam]
      operationId: rotateAgentIdentityKey
      summary: >
        Register a new public key for an agent identity and revoke its previously active key. Calls
        signed with the revoked key are rejected after this operation returns. Other identity settings
        are unchanged.
      x-riptide-rbac: [iam.write]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/AgentIdentityKeyRegister' }
      responses:
        '200':
          description: The identity with the new active key.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AgentIdentity' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/agent-identities/{agent_identity_id}/band:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/AgentIdentityId'
    post:
      tags: [iam]
      operationId: setAgentIdentityBand
      summary: >
        Change an agent identity's autonomy band. Each tool call checks the current band, so a demotion
        applies to the agent's next call. The reason is recorded in the audit log.
      x-riptide-rbac: [iam.write]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/AgentIdentityBandSet' }
      responses:
        '200':
          description: The identity on its new band.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AgentIdentity' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/agent-identities/{agent_identity_id}/kill-switch/trip:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/AgentIdentityId'
    post:
      tags: [iam]
      operationId: tripAgentKillSwitch
      summary: >
        Trip an agent identity's kill switch. Subsequent mutating tool calls return a typed error
        until resetAgentKillSwitch is called. Read-only calls remain available. Anomaly alerts can
        also trip the switch automatically. A manual trip records the actor and reason.
      x-riptide-rbac: [iam.write]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/AgentKillSwitchRequest' }
      responses:
        '200':
          description: The identity with the switch tripped.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AgentIdentity' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/agent-identities/{agent_identity_id}/kill-switch/reset:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/AgentIdentityId'
    post:
      tags: [iam]
      operationId: resetAgentKillSwitch
      summary: Reset a tripped kill switch so the identity may call tools again; the reason is audited.
      x-riptide-rbac: [iam.write]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/AgentKillSwitchRequest' }
      responses:
        '200':
          description: The identity with the switch reset.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AgentIdentity' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/llm-usage:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [operability]
      operationId: listLlmUsage
      summary: >
        List language-model calls made on the tenant's behalf, including the prompt digest, token
        counts, and metered cost. Results are newest first and use cursor pagination.
      x-riptide-rbac: [operability.read]
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - name: surface
          in: query
          required: false
          description: Only calls made by this surface (e.g. insight_ask, playbook, creative_agent, diagnosis_agent).
          schema: { type: string, minLength: 1, maxLength: 64 }
        - name: since
          in: query
          required: false
          description: Only calls at or after this instant.
          schema: { type: string, format: date-time }
      responses:
        '200':
          description: A page of ledger rows.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/LlmUsageList' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/privacy-requests:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [iam]
      operationId: listPrivacyRequests
      summary: >
        List data-subject privacy requests, newest first, with cursor pagination.
      x-riptide-rbac: [iam.read]
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - name: status
          in: query
          required: false
          description: Only requests in this status.
          schema: { $ref: '#/components/schemas/PrivacyRequestStatus' }
        - name: kind
          in: query
          required: false
          description: Only requests of this kind.
          schema: { $ref: '#/components/schemas/PrivacyRequestKind' }
      responses:
        '200':
          description: A page of privacy requests.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PrivacyRequestList' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [iam]
      operationId: createPrivacyRequest
      summary: >
        Create an access, erasure, or opt-out request for an opaque user key. The key is hashed
        before storage in subject_key_hash. Processing is asynchronous and covers profiles, memberships, and event
        retention; the request records the processing evidence. New requests have status RECEIVED.
      x-riptide-rbac: [iam.write]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/PrivacyRequestCreate' }
      responses:
        '201':
          description: The filed request.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PrivacyRequest' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/privacy-requests/{privacy_request_id}:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/PrivacyRequestId'
    get:
      tags: [iam]
      operationId: getPrivacyRequest
      summary: Get one privacy request with its status and evidence.
      x-riptide-rbac: [iam.read]
      responses:
        '200':
          description: The privacy request.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PrivacyRequest' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/sample-data:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    post:
      tags: [operator]
      operationId: loadSampleData
      summary: >
        Load sample publishers, placements, advertisers, orders, line items, creatives, audiences, and a
        fee schedule into a sandbox tenant. Requires sandbox: true; other tenants receive 409 CONFLICT.
        Loading again replaces the previous sample set. Returns 202 while processing continues.
      x-riptide-rbac: [tenant.write]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '202':
          description: What was loaded.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SampleDataLoadResult' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/audit-log:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [operability]
      operationId: listAuditLog
      summary: >
        List recent immutable audit records for a tenant. Requires operability.read and applies tenant
        row-level security. The same records are available through the
        riptide.operability.query_audit_trail MCP tool.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of audit envelopes, most recent first.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AuditLogList' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/operability/doctor:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [operability]
      operationId: runDoctor
      summary: >
        Diagnose the console process configuration, including required environment variables and
        settings that must be supplied together, such as a sealing key and its ID. The CLI and
        riptide.operability.run_doctor MCP tool use the same diagnostic checks.
      parameters:
        - name: env_var
          in: query
          required: false
          description: >
            Optional override of which env vars to check; defaults to libs/doctor.EnvVars when
            omitted.
          schema:
            type: array
            items: { type: string }
      responses:
        '200':
          description: The doctor run's checks and any misconfigurations found.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DoctorResult' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/operability/explain:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    post:
      tags: [operability]
      operationId: explainRequest
      summary: >
        Explain a supplied decision trace with its stages and winner rationale. For example, use a trace
        captured from a debug=1 serving response. Complete decision traces cannot be retrieved by
        request ID. When decision_trace is omitted and request_id is supplied, a configured ClickHouse
        connection can return the request's ad-event timeline instead of a decision funnel.
      requestBody:
        required: false
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ExplainRequestInput' }
      responses:
        '200':
          description: The rendered explanation.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ExplainResult' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/diagnostics/serving:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [operability]
      operationId: diagnoseServing
      summary: >
        Read blocking issues, eligibility checks, and a serving diagnosis for one line item. The summary
        matches the text returned by riptide.operability.diagnose_delivery. This operation does not
        change the line item.
      x-riptide-rbac: [operability.read]
      parameters:
        - name: line_item_ref
          in: query
          required: true
          description: Line item reference code to diagnose (the same key diagnoseDelivery takes).
          schema: { type: string, minLength: 1 }
      responses:
        '200':
          description: The diagnosis.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ServingDiagnosis' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/operability/diagnose-delivery:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    post:
      tags: [operability]
      operationId: diagnoseDelivery
      summary: >
        Diagnose a line item's delivery using its current serving status, flight window, and audience
        configuration. The riptide.operability.diagnose_delivery MCP tool uses the same checks.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/DiagnoseDeliveryInput' }
      responses:
        '200':
          description: The blocking issues found (if any) and the checks that were run.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DiagnoseDeliveryResult' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/operability/preflight:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    post:
      tags: [operability]
      operationId: preflightChange
      summary: >
        Preview the structural difference between the current and proposed configuration, with warnings
        for unchanged values or unrecognized kinds. This check does not recompile the serving plan or
        evaluate the change's effect on delivery.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/PreflightChangeInput' }
      responses:
        '200':
          description: Whether the change would apply, warnings, and the before/after diff.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PreflightChangeResult' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/audio-to-video:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    put:
      tags: [supply]
      operationId: updateTenantAudioToVideo
      summary: >
        Update the tenant's audio-to-video settings or its advertiser-domain overrides. Configuration
        follows the tenant, publisher, brand, and platform-default hierarchy. Tenant access is
        sufficient; enabling the feature requires the audio_to_video entitlement.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/TenantAudioToVideoUpdate' }
      responses:
        '200':
          description: Updated.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Tenant' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/supply-transparency/preview/ads-txt:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [supply_transparency]
      operationId: previewAdsTxt
      x-riptide-rbac: [supply.read]
      summary: >
        Preview the generated ads.txt/app-ads.txt document for a publisher. Requires `supply.read`.
      parameters:
        - name: publisher_id
          in: query
          required: true
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: Generated ads.txt/app-ads.txt preview.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SupplyDocumentPreview' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/supply-transparency/preview/sellers-json:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [supply_transparency]
      operationId: previewSellersJson
      x-riptide-rbac: [supply.read]
      summary: >
        Preview the generated sellers.json document served canonically from /sellers.json.
        Requires `supply.read`.
      responses:
        '200':
          description: Generated sellers.json preview.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SupplyDocumentPreview' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/supply-transparency/verify:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    post:
      tags: [supply_transparency]
      operationId: verifySupplyDocument
      x-riptide-rbac: [supply.write]
      summary: >
        Crawl and verify one ads.txt, app-ads.txt, or sellers.json document, persisting the latest
        check fact for bid-time policy decisions. Requires `supply.write`.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/SupplyDocumentVerifyRequest' }
      responses:
        '200':
          description: Latest check fact.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SupplyDocumentCheck' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/supply-transparency/checks:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [supply_transparency]
      operationId: listSupplyDocumentChecks
      x-riptide-rbac: [supply.read]
      summary: List the tenant's latest supply transparency checks. Requires `supply.read`.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - name: kind
          in: query
          required: false
          schema: { $ref: '#/components/schemas/SupplyDocumentKind' }
        - name: subject_type
          in: query
          required: false
          schema: { $ref: '#/components/schemas/SupplyDocumentSubjectType' }
        - name: subject_id
          in: query
          required: false
          schema: { type: string }
      responses:
        '200':
          description: A page of supply document check facts.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SupplyDocumentCheckList' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/supply-transparency/checks/{check_id}:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/SupplyDocumentCheckId'
    get:
      tags: [supply_transparency]
      operationId: getSupplyDocumentCheck
      x-riptide-rbac: [supply.read]
      summary: Read one persisted supply transparency check fact. Requires `supply.read`.
      responses:
        '200':
          description: Supply document check fact.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SupplyDocumentCheck' }
        default: { $ref: '#/components/responses/Error' }
    delete:
      tags: [supply_transparency]
      operationId: deleteSupplyDocumentCheck
      x-riptide-rbac: [supply.write]
      summary: >
        Delete one supply transparency check, ending its scheduled re-crawl obligation. The
        subject's snapshot history is retained (drift audit trail). Requires `supply.write`.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '204':
          description: Check deleted; the subject will no longer be re-crawled.
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/supply-transparency/snapshots:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [supply_transparency]
      operationId: listSupplyDocumentSnapshots
      x-riptide-rbac: [supply.read]
      summary: >
        List crawled supply document snapshots (drift history) newest-first. Bodies are omitted;
        fetch one snapshot for its full document body. Requires `supply.read`.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - name: kind
          in: query
          required: false
          schema: { $ref: '#/components/schemas/SupplyDocumentKind' }
        - name: subject_type
          in: query
          required: false
          schema: { $ref: '#/components/schemas/SupplyDocumentSubjectType' }
        - name: subject_id
          in: query
          required: false
          schema: { type: string }
      responses:
        '200':
          description: A page of supply document snapshots, newest first, without bodies.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SupplyDocumentSnapshotList' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/supply-transparency/snapshots/{snapshot_id}:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/SupplyDocumentSnapshotId'
    get:
      tags: [supply_transparency]
      operationId: getSupplyDocumentSnapshot
      x-riptide-rbac: [supply.read]
      summary: >
        Read one supply document snapshot including its full body, drift diff against the previous
        snapshot, and structural analysis. Requires `supply.read`.
      responses:
        '200':
          description: Supply document snapshot with body, diff, and analysis.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SupplyDocumentSnapshot' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/supply-transparency/health:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [supply_transparency]
      operationId: getSupplyTransparencyHealth
      x-riptide-rbac: [supply.read]
      summary: >
        Aggregate supply-chain transparency health report - verification coverage, per-subject
        status, structural risk scores, and drift flags across the tenant's latest checks and
        snapshots. Requires `supply.read`.
      responses:
        '200':
          description: Aggregated supply transparency health report.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SupplyTransparencyHealthReport' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/publishers:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [supply]
      operationId: listPublishers
      summary: List a tenant's publishers.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of publishers.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PublisherList' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [supply]
      operationId: createPublisher
      summary: Create a publisher.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/PublisherCreate' }
      responses:
        '201':
          description: Created.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Publisher' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/publishers/bulk-import:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    post:
      tags: [supply]
      operationId: bulkImportPublishers
      summary: >
        Create publishers in a batch and return a result for each row. A failed row does not abort other
        rows. With dry_run: true, validate required fields and public_id conflicts both inside the batch
        and against stored publishers without saving changes.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/PublisherBulkImportRequest' }
      responses:
        '200':
          description: Per-row results (dry-run preview or applied).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PublisherBulkImportResponse' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/publishers/{publisher_id}:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/PublisherId'
    get:
      tags: [supply]
      operationId: getPublisher
      summary: Get a publisher.
      responses:
        '200':
          description: The publisher.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Publisher' }
        default: { $ref: '#/components/responses/Error' }
    put:
      tags: [supply]
      operationId: updatePublisher
      summary: Update a publisher.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/PublisherUpdate' }
      responses:
        '200':
          description: Updated.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Publisher' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/publishers/{publisher_id}/archive:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/PublisherId'
    post:
      tags: [supply]
      operationId: archivePublisher
      summary: >
        Archive a publisher.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: Archived.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Publisher' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/publishers/status-counts:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [supply]
      operationId: getPublisherStatusCounts
      summary: >
        Per-status row counts for the tenant's publishers.
      responses:
        '200':
          description: Status counts.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/StatusCounts' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/placements:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [supply]
      operationId: listPlacements
      summary: List a tenant's placements.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of placements.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PlacementList' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [supply]
      operationId: createPlacement
      summary: Create a placement (inactive by default until activated).
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/PlacementCreate' }
      responses:
        '201':
          description: Created.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Placement' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/placements/bulk-import:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    post:
      tags: [supply]
      operationId: bulkImportPlacements
      summary: >
        Create placements in a batch and return a result for each row. A failed row does not abort other
        rows. With dry_run: true, validate required fields and public_id conflicts both inside the batch
        and against stored placements without saving changes.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/PlacementBulkImportRequest' }
      responses:
        '200':
          description: Per-row results (dry-run preview or applied).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PlacementBulkImportResponse' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/placements/{placement_id}:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/PlacementId'
    get:
      tags: [supply]
      operationId: getPlacement
      summary: Get a placement.
      responses:
        '200':
          description: The placement.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Placement' }
        default: { $ref: '#/components/responses/Error' }
    put:
      tags: [supply]
      operationId: updatePlacement
      summary: Update a placement (including stitch and demand attachment config).
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/PlacementUpdate' }
      responses:
        '200':
          description: Updated.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Placement' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/placements/{placement_id}/archive:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/PlacementId'
    post:
      tags: [supply]
      operationId: archivePlacement
      summary: >
        Archive a placement.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: Archived.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Placement' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/placements/{placement_id}/decision-funnel:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/PlacementId'
    get:
      tags: [operability]
      operationId: getPlacementDecisionFunnel
      summary: >
        Read recent placement decision records from ClickHouse and aggregate the counts entering and
        leaving each stage, including removal reasons. The response includes the sampled records used.
        Without a configured ClickHouse source, the response contains an empty aggregate and
        source_configured=false.
      parameters:
        - name: limit
          in: query
          required: false
          schema: { type: integer, minimum: 1, maximum: 500, default: 100 }
      responses:
        '200':
          description: Aggregated placement funnel.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DecisionFunnelResponse' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/placements/status-counts:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [supply]
      operationId: getPlacementStatusCounts
      summary: >
        Per-status row counts for the tenant's placements.
      responses:
        '200':
          description: Status counts.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/StatusCounts' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/placements/{placement_id}/marketplaces:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/PlacementId'
    get:
      tags: [supply]
      operationId: listPlacementMarketplaces
      summary: >
        List the IDs of marketplaces that include a placement.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: The placement's marketplace membership.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PlacementMarketplaceMembership' }
        default: { $ref: '#/components/responses/Error' }
    put:
      tags: [supply]
      operationId: setPlacementMarketplaces
      summary: Replace a placement's full marketplace membership set.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/PlacementMarketplaceMembership' }
      responses:
        '200':
          description: Updated membership.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PlacementMarketplaceMembership' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/placements/{placement_id}/demand-routes:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/PlacementId'
    get:
      tags: [supply]
      operationId: listPlacementDemandRoutes
      summary: >
        List demand route IDs explicitly bound to a placement.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: The placement's explicit demand-route bindings.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PlacementDemandRouteMembership' }
        default: { $ref: '#/components/responses/Error' }
    put:
      tags: [supply]
      operationId: setPlacementDemandRoutes
      summary: Replace a placement's full explicit demand-route binding set.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/PlacementDemandRouteMembership' }
      responses:
        '200':
          description: Updated bindings.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PlacementDemandRouteMembership' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/placements/{placement_id}/marketplaces/resolve:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/PlacementId'
    get:
      tags: [supply]
      operationId: resolvePlacementMarketplaces
      summary: >
        Preview the ACTIVE demand routes included through a placement's marketplace memberships. The
        serving-plan compiler uses the same marketplace-to-route lookup.
      responses:
        '200':
          description: Resolved demand route ids.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ResolvedMarketplaceRoutes' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/apps:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [supply]
      operationId: listApps
      summary: List a tenant's apps (app inventory).
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of apps.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AppList' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [supply]
      operationId: createApp
      summary: >
        Create an app with optional publisher pricing overrides and demand-partner route approvals.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/AppCreate' }
      responses:
        '201':
          description: Created.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/App' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/apps/{app_id}:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/AppId'
    get:
      tags: [supply]
      operationId: getApp
      summary: Get an app.
      responses:
        '200':
          description: The app.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/App' }
        default: { $ref: '#/components/responses/Error' }
    put:
      tags: [supply]
      operationId: updateApp
      summary: Update an app.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/AppUpdate' }
      responses:
        '200':
          description: Updated.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/App' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/apps/{app_id}/archive:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/AppId'
    post:
      tags: [supply]
      operationId: archiveApp
      summary: >
        Archive an app.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: Archived.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/App' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/apps/status-counts:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [supply]
      operationId: getAppStatusCounts
      summary: >
        Per-status row counts for the tenant's apps.
      responses:
        '200':
          description: Status counts.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/StatusCounts' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/app-lookups:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [supply]
      operationId: listAppLookups
      summary: >
        List cached app lookups that map a publisher_public_id and app bundle to an app or a blocked
        result.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of app lookups.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AppLookupList' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [supply]
      operationId: createAppLookup
      summary: Create or upsert an app-lookup row (idempotent on tenant+cache_key).
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/AppLookupCreate' }
      responses:
        '201':
          description: Created.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AppLookup' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/app-lookups/{cache_key}:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/AppLookupCacheKey'
    get:
      tags: [supply]
      operationId: getAppLookup
      summary: Get one app-lookup row by cache_key.
      responses:
        '200':
          description: The app lookup.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AppLookup' }
        default: { $ref: '#/components/responses/Error' }
    put:
      tags: [supply]
      operationId: updateAppLookup
      summary: Update an app-lookup row (blocked flag and resolution targets).
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/AppLookupUpdate' }
      responses:
        '200':
          description: Updated.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AppLookup' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/verification-vendor-configs:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [supply]
      operationId: listVerificationVendorConfigs
      summary: >
        List the tenant's verification vendor slot templates, A through D, used for rendering and
        reporting.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of vendor configs.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/VerificationVendorConfigList' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [supply]
      operationId: createVerificationVendorConfig
      summary: Create a verification vendor config for one slot (unique per tenant+vendor).
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/VerificationVendorConfigCreate' }
      responses:
        '201':
          description: Created.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/VerificationVendorConfig' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/verification-vendor-configs/{config_id}:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/VerificationVendorConfigId'
    get:
      tags: [supply]
      operationId: getVerificationVendorConfig
      summary: Get a verification vendor config.
      responses:
        '200':
          description: The config.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/VerificationVendorConfig' }
        default: { $ref: '#/components/responses/Error' }
    put:
      tags: [supply]
      operationId: updateVerificationVendorConfig
      summary: Update a verification vendor config.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/VerificationVendorConfigUpdate' }
      responses:
        '200':
          description: Updated.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/VerificationVendorConfig' }
        default: { $ref: '#/components/responses/Error' }
    delete:
      tags: [supply]
      operationId: deleteVerificationVendorConfig
      summary: Delete a verification vendor config.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '204':
          description: Deleted.
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/creatives/{creative_id}/event-trackers:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/CreativeId'
    get:
      tags: [campaign]
      operationId: listCreativeEventTrackers
      summary: >
        List a creative's third-party event trackers.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: The creative's trackers.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/EventTrackerList' }
        default: { $ref: '#/components/responses/Error' }
    put:
      tags: [campaign]
      operationId: replaceCreativeEventTrackers
      summary: Replace the full set of a creative's event trackers.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/EventTrackerSet' }
      responses:
        '200':
          description: The stored trackers.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/EventTrackerList' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/line-items/{line_item_id}/event-trackers:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/LineItemId'
    get:
      tags: [campaign]
      operationId: listLineItemEventTrackers
      summary: List a line item's third-party event trackers (applied to all its creatives).
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: The line item's trackers.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/EventTrackerList' }
        default: { $ref: '#/components/responses/Error' }
    put:
      tags: [campaign]
      operationId: replaceLineItemEventTrackers
      summary: Replace the full set of a line item's event trackers.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/EventTrackerSet' }
      responses:
        '200':
          description: The stored trackers.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/EventTrackerList' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/placements/{placement_id}/event-trackers:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/PlacementId'
    get:
      tags: [supply]
      operationId: listPlacementEventTrackers
      summary: List a placement's third-party event trackers (supply-side measurement).
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: The placement's trackers.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/EventTrackerList' }
        default: { $ref: '#/components/responses/Error' }
    put:
      tags: [supply]
      operationId: replacePlacementEventTrackers
      summary: Replace the full set of a placement's event trackers.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/EventTrackerSet' }
      responses:
        '200':
          description: The stored trackers.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/EventTrackerList' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/publishers/{publisher_id}/event-trackers:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/PublisherId'
    get:
      tags: [supply]
      operationId: listPublisherEventTrackers
      summary: List a publisher's third-party event trackers (default for all its placements).
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: The publisher's trackers.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/EventTrackerList' }
        default: { $ref: '#/components/responses/Error' }
    put:
      tags: [supply]
      operationId: replacePublisherEventTrackers
      summary: Replace the full set of a publisher's event trackers.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/EventTrackerSet' }
      responses:
        '200':
          description: The stored trackers.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/EventTrackerList' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/deals/{deal_id}/event-trackers:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/DealId'
    get:
      tags: [demand]
      operationId: listDealEventTrackers
      summary: List a deal's agreed third-party event trackers.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: The deal's trackers.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/EventTrackerList' }
        default: { $ref: '#/components/responses/Error' }
    put:
      tags: [demand]
      operationId: replaceDealEventTrackers
      summary: Replace the full set of a deal's event trackers.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/EventTrackerSet' }
      responses:
        '200':
          description: The stored trackers.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/EventTrackerList' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/ivt-policy:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [supply]
      operationId: getIvtPolicy
      summary: >
        Read the tenant's invalid-traffic detection and enforcement policy. Returns the platform
        defaults when no policy is stored.
      responses:
        '200':
          description: The effective policy.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/IvtPolicy' }
        default: { $ref: '#/components/responses/Error' }
    put:
      tags: [supply]
      operationId: updateIvtPolicy
      summary: Create or replace the tenant's IVT policy (one per tenant; upsert).
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/IvtPolicyUpdate' }
      responses:
        '200':
          description: The stored policy.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/IvtPolicy' }
        default: { $ref: '#/components/responses/Error' }
    delete:
      tags: [supply]
      operationId: deleteIvtPolicy
      summary: >
        Delete the tenant's stored invalid-traffic policy and restore the platform defaults.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '204':
          description: Deleted.
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/supply-scorecards:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [supply]
      operationId: listSupplyScorecards
      summary: >
        List computed supply scorecards, including invalid-traffic rate and grade for each publisher and
        domain or app bundle.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of scorecards.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SupplyScorecardList' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/ip-lists:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [supply]
      operationId: listIpLists
      summary: >
        List reusable IP allow or block lists for line-item targeting. This operation returns list
        metadata; membership data is held in object storage.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of IP lists.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/IpListPage' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [supply]
      operationId: createIpList
      summary: >
        Create an IP allow/block list (metadata only; import entries then publish).
        scope=cell is operator-only (cell-wide edge shed). scope=publisher requires a
        publisher_id owned by the tenant.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/IpListCreate' }
      responses:
        '201':
          description: Created.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/IpList' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/ip-lists/{list_id}:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/IpListId'
    get:
      tags: [supply]
      operationId: getIpList
      summary: Get one IP list's metadata (uri/hash/version/entry_count/status).
      responses:
        '200':
          description: The IP list.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/IpList' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/ip-lists/{list_id}/archive:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/IpListId'
    post:
      tags: [supply]
      operationId: archiveIpList
      summary: >
        Archive an IP list. Archived lists drop out of plan compile and reject further import/publish.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: Archived.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/IpList' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/ip-lists/{list_id}/entries:bulkImport:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/IpListId'
    post:
      tags: [supply]
      operationId: bulkImportIpListEntries
      summary: >
        Import up to 10000 IP or CIDR lines into staging per call. A list can contain at most 2000000
        staged lines before publication. Publishing compiles the staged entries into a membership file
        and clears staging.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/IpListBulkImportRequest' }
      responses:
        '200':
          description: Import accepted (accepted/rejected counts).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/IpListBulkImportResponse' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/ip-lists/{list_id}/publish:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/IpListId'
    post:
      tags: [supply]
      operationId: publishIpList
      summary: >
        Compile staged IP/CIDR lines into a binary membership blob, write it to object storage,
        bump version/content_hash, and clear staging so the next plan compile projects IpListRef.
        Rejects empty allow-mode lists (would deny all traffic) and archived lists.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: Published list metadata.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/IpList' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/bid-modifiers:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [campaign]
      operationId: listBidModifiers
      summary: >
        List sets of bid multipliers that line items can reference through bid_modifier_id. The
        multipliers are included in the serving plan.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of bid modifiers.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BidModifierPage' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [campaign]
      operationId: createBidModifier
      summary: Create a bid modifier (metadata + optional initial terms; max 1000 terms).
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/BidModifierCreate' }
      responses:
        '201':
          description: Created.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BidModifier' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/bid-modifiers/{modifier_id}:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/BidModifierId'
    get:
      tags: [campaign]
      operationId: getBidModifier
      summary: Get one bid modifier including its terms.
      responses:
        '200':
          description: The bid modifier.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BidModifier' }
        default: { $ref: '#/components/responses/Error' }
    put:
      tags: [campaign]
      operationId: updateBidModifier
      summary: Update bid modifier metadata (name/description/active). Terms via replaceBidModifierTerms.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/BidModifierUpdate' }
      responses:
        '200':
          description: Updated.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BidModifier' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/bid-modifiers/{modifier_id}/archive:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/BidModifierId'
    post:
      tags: [campaign]
      operationId: archiveBidModifier
      summary: >
        Archive a bid modifier. Archived modifiers drop out of plan compile and reject further
        term edits; line items referencing them keep the FK until cleared.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: Archived.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BidModifier' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/bid-modifiers/{modifier_id}/terms:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/BidModifierId'
    put:
      tags: [campaign]
      operationId: replaceBidModifierTerms
      summary: >
        Replace-all bid modifier terms (max 1000). Matching multipliers multiply at apply time;
        clamp [0,10]; 0 ⇒ no-bid.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/BidModifierTermsReplace' }
      responses:
        '200':
          description: Updated modifier with the new term set.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/BidModifier' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/custom-lists:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [campaign]
      operationId: listCustomLists
      summary: >
        List custom targeting lists whose membership is included in the serving plan. Each list supports
        up to 10000 items.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of custom lists.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CustomListPage' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [campaign]
      operationId: createCustomList
      summary: Create a custom list (metadata + optional initial items; max 10000 items).
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CustomListCreate' }
      responses:
        '201':
          description: Created custom list.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CustomList' }
        default: { $ref: '#/components/responses/Error' }
  /v1/tenants/{tenant_id}/custom-lists/{list_id}:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/CustomListId'
    get:
      tags: [campaign]
      operationId: getCustomList
      summary: Get one custom list including its items.
      responses:
        '200':
          description: The custom list.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CustomList' }
        default: { $ref: '#/components/responses/Error' }
    put:
      tags: [campaign]
      operationId: updateCustomList
      summary: Update custom list metadata (name). Items via replaceCustomListItems.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CustomListUpdate' }
      responses:
        '200':
          description: Updated custom list.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CustomList' }
        default: { $ref: '#/components/responses/Error' }
  /v1/tenants/{tenant_id}/custom-lists/{list_id}/archive:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/CustomListId'
    post:
      tags: [campaign]
      operationId: archiveCustomList
      summary: Archive a custom list. Archived lists drop out of plan compile.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: Archived custom list.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CustomList' }
        default: { $ref: '#/components/responses/Error' }
  /v1/tenants/{tenant_id}/custom-lists/{list_id}/items:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/CustomListId'
    put:
      tags: [campaign]
      operationId: replaceCustomListItems
      summary: >
        Replace-all custom list items (max 10000). Values are normalized (trim + lower for
        domain/site/app_bundle/zip).
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CustomListItemsReplace' }
      responses:
        '200':
          description: Updated list with the new item set.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CustomList' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/creative-templates:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [campaign]
      operationId: listCreativeTemplates
      summary: >
        List creative templates. The markup_template uses {{MACRO}} substitution; creatives reference a
        template through template_id and template_fields.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of creative templates.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CreativeTemplatePage' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [campaign]
      operationId: createCreativeTemplate
      summary: Create a creative template.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreativeTemplateCreate' }
      responses:
        '201':
          description: Created creative template.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CreativeTemplate' }
        default: { $ref: '#/components/responses/Error' }
  /v1/tenants/{tenant_id}/creative-templates/{template_id}:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/CreativeTemplateId'
    get:
      tags: [campaign]
      operationId: getCreativeTemplate
      summary: Get one creative template.
      responses:
        '200':
          description: The creative template.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CreativeTemplate' }
        default: { $ref: '#/components/responses/Error' }
    put:
      tags: [campaign]
      operationId: updateCreativeTemplate
      summary: Update a creative template.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreativeTemplateUpdate' }
      responses:
        '200':
          description: Updated creative template.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CreativeTemplate' }
        default: { $ref: '#/components/responses/Error' }
  /v1/tenants/{tenant_id}/creative-templates/{template_id}/archive:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/CreativeTemplateId'
    post:
      tags: [campaign]
      operationId: archiveCreativeTemplate
      summary: Archive a creative template. Archived templates drop out of plan compile.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: Archived creative template.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CreativeTemplate' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/creative-templates/{template_id}/refresh:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/CreativeTemplateId'
    post:
      tags: [campaign]
      operationId: refreshCreativeTemplate
      x-riptide-rbac: [campaign.write]
      summary: >
        Start refreshing a template's dynamic creatives from its refresh_source_url. The platform
        fetches the feed and regenerates variants for creatives using the template. Returns 202 with a
        RUNNING progress record; refresh_status and last_refreshed_at track the result. Returns 409
        CONFLICT if a run is already active, or BAD_INPUT if refresh_source_url is missing. Requires
        campaign.write.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '202':
          description: The refresh run that was started.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CreativeTemplateRefreshRun' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/delivery-experiments:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [campaign]
      operationId: listDeliveryExperiments
      summary: >
        List delivery experiments with stable user holdout assignments. The control group suppresses
        subjects assigned to TREATMENT.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of delivery experiments.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DeliveryExperimentPage' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [campaign]
      operationId: createDeliveryExperiment
      summary: Create a delivery experiment (metadata + optional subjects).
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/DeliveryExperimentCreate' }
      responses:
        '201':
          description: Created delivery experiment.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DeliveryExperiment' }
        default: { $ref: '#/components/responses/Error' }
  /v1/tenants/{tenant_id}/delivery-experiments/{experiment_id}:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/DeliveryExperimentId'
    get:
      tags: [campaign]
      operationId: getDeliveryExperiment
      summary: Get one delivery experiment including subjects.
      responses:
        '200':
          description: The delivery experiment.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DeliveryExperiment' }
        default: { $ref: '#/components/responses/Error' }
    put:
      tags: [campaign]
      operationId: updateDeliveryExperiment
      summary: Update delivery experiment metadata (name, holdout_pct, description).
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/DeliveryExperimentUpdate' }
      responses:
        '200':
          description: Updated delivery experiment.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DeliveryExperiment' }
        default: { $ref: '#/components/responses/Error' }
  /v1/tenants/{tenant_id}/delivery-experiments/{experiment_id}/archive:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/DeliveryExperimentId'
    post:
      tags: [campaign]
      operationId: archiveDeliveryExperiment
      summary: Archive a delivery experiment. Archived experiments drop out of plan compile.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: Archived delivery experiment.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DeliveryExperiment' }
        default: { $ref: '#/components/responses/Error' }
  /v1/tenants/{tenant_id}/delivery-experiments/{experiment_id}/subjects:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/DeliveryExperimentId'
    put:
      tags: [campaign]
      operationId: replaceDeliveryExperimentSubjects
      summary: Replace-all experiment subjects (campaign orders and/or line items + arm).
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/DeliveryExperimentSubjectsReplace' }
      responses:
        '200':
          description: Updated experiment with the new subject set.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DeliveryExperiment' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/delivery-alert-rules:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [campaign]
      operationId: listDeliveryAlertRules
      summary: >
        List delivery and spending alert rules. The evaluator fires an alert when a threshold is crossed
        and sends a webhook when one is configured.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of delivery alert rules.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DeliveryAlertRulePage' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [campaign]
      operationId: createDeliveryAlertRule
      summary: Create a delivery alert rule.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/DeliveryAlertRuleCreate' }
      responses:
        '201':
          description: Created delivery alert rule (webhook_secret_ref echoed when set; secret never returned).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DeliveryAlertRuleCreated' }
        default: { $ref: '#/components/responses/Error' }
  /v1/tenants/{tenant_id}/delivery-alert-rules/{rule_id}:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/DeliveryAlertRuleId'
    get:
      tags: [campaign]
      operationId: getDeliveryAlertRule
      summary: >
        Read a delivery alert rule. The webhook secret is excluded from the response.
      responses:
        '200':
          description: The delivery alert rule.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DeliveryAlertRule' }
        default: { $ref: '#/components/responses/Error' }
    put:
      tags: [campaign]
      operationId: updateDeliveryAlertRule
      summary: Update a delivery alert rule. Omit webhook_secret_ref to leave unchanged.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/DeliveryAlertRuleUpdate' }
      responses:
        '200':
          description: Updated delivery alert rule.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DeliveryAlertRule' }
        default: { $ref: '#/components/responses/Error' }
  /v1/tenants/{tenant_id}/delivery-alert-rules/{rule_id}/archive:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/DeliveryAlertRuleId'
    post:
      tags: [campaign]
      operationId: archiveDeliveryAlertRule
      summary: Archive a delivery alert rule (disabled for evaluation).
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: Archived delivery alert rule.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DeliveryAlertRule' }
        default: { $ref: '#/components/responses/Error' }
  /v1/tenants/{tenant_id}/delivery-alert-rules/{rule_id}/mute:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/DeliveryAlertRuleId'
    post:
      tags: [campaign]
      operationId: muteDeliveryAlertRule
      summary: >
        Mute or unmute a delivery alert rule until a specified time. Muted rules are evaluated but do
        not fire alerts.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/DeliveryAlertMuteRequest' }
      responses:
        '200':
          description: The rule with its new muted_until.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DeliveryAlertRule' }
        default: { $ref: '#/components/responses/Error' }
  /v1/tenants/{tenant_id}/delivery-alert-events/{event_id}/acknowledge:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/DeliveryAlertEventId'
    post:
      tags: [campaign]
      operationId: acknowledgeDeliveryAlertEvent
      summary: Acknowledge an alert firing (records who and when; idempotent on an already-acknowledged event).
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: The acknowledged event.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DeliveryAlertEvent' }
        default: { $ref: '#/components/responses/Error' }
  /v1/tenants/{tenant_id}/delivery-alert-events:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [campaign]
      operationId: listDeliveryAlertEvents
      summary: List recent delivery alert firings (newest first).
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - name: rule_id
          in: query
          schema: { type: string, format: uuid }
          description: Optional filter to one rule.
      responses:
        '200':
          description: A page of alert events.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DeliveryAlertEventPage' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/item-catalogs:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [campaign]
      operationId: listItemCatalogs
      summary: >
        List product and SKU catalogs used for onsite listing auctions and product attribution. These
        are separate from marketplace listings.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of item catalogs.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ItemCatalogPage' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [campaign]
      operationId: createItemCatalog
      summary: Create an item catalog container.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ItemCatalogCreate' }
      responses:
        '201':
          description: Created item catalog.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ItemCatalog' }
        default: { $ref: '#/components/responses/Error' }
  /v1/tenants/{tenant_id}/item-catalogs/{catalog_id}:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/ItemCatalogId'
    get:
      tags: [campaign]
      operationId: getItemCatalog
      summary: Get one item catalog.
      responses:
        '200':
          description: The item catalog.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ItemCatalog' }
        default: { $ref: '#/components/responses/Error' }
    put:
      tags: [campaign]
      operationId: updateItemCatalog
      summary: Update item catalog metadata.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ItemCatalogUpdate' }
      responses:
        '200':
          description: Updated item catalog.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ItemCatalog' }
        default: { $ref: '#/components/responses/Error' }
  /v1/tenants/{tenant_id}/item-catalogs/{catalog_id}/archive:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/ItemCatalogId'
    post:
      tags: [campaign]
      operationId: archiveItemCatalog
      summary: Archive an item catalog (soft; items remain for attribution history).
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: Archived item catalog.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ItemCatalog' }
        default: { $ref: '#/components/responses/Error' }
  /v1/tenants/{tenant_id}/item-catalogs/{catalog_id}/items:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/ItemCatalogId'
    get:
      tags: [campaign]
      operationId: listCatalogItems
      summary: List catalog items (SKU/product rows) in an item catalog.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of catalog items.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CatalogItemPage' }
        default: { $ref: '#/components/responses/Error' }
    put:
      tags: [campaign]
      operationId: upsertCatalogItems
      summary: >
        Bulk upsert catalog items by external_item_id (idempotent on
        tenant+catalog+external_item_id). Max 1000 items per call.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CatalogItemUpsert' }
      responses:
        '200':
          description: Upserted items (full rows after write).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CatalogItemPage' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/export-destinations:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [reporting]
      operationId: listExportDestinations
      summary: >
        List the tenant's S3 and GCS event-export destinations. Credentials are stored in the secret
        store and referenced by secret_ref.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of export destinations.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ExportDestinationPage' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [reporting]
      operationId: createExportDestination
      summary: Create an export destination (S3 or GCS).
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ExportDestinationCreate' }
      responses:
        '201':
          description: Created export destination.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ExportDestination' }
        default: { $ref: '#/components/responses/Error' }
  /v1/tenants/{tenant_id}/export-destinations/{destination_id}:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/ExportDestinationId'
    get:
      tags: [reporting]
      operationId: getExportDestination
      summary: Get one export destination.
      responses:
        '200':
          description: The export destination.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ExportDestination' }
        default: { $ref: '#/components/responses/Error' }
    put:
      tags: [reporting]
      operationId: updateExportDestination
      summary: Update an export destination.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ExportDestinationUpdate' }
      responses:
        '200':
          description: Updated export destination.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ExportDestination' }
        default: { $ref: '#/components/responses/Error' }
  /v1/tenants/{tenant_id}/export-destinations/{destination_id}/archive:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/ExportDestinationId'
    post:
      tags: [reporting]
      operationId: archiveExportDestination
      summary: Archive an export destination (stops future export ticks).
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: Archived export destination.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ExportDestination' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/export-jobs:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [reporting]
      operationId: listExportJobs
      summary: >
        List on-demand customer log export jobs. For continuous exports, use event-export destinations.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of export jobs.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ExportJobPage' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [reporting]
      operationId: createExportJob
      summary: >
        Request a customer log export for one event kind and time range. A background job writes JSONL
        or CSV to object storage and sets blob_uri when the export is available.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ExportJobCreate' }
      responses:
        '201':
          description: Created export job (PENDING until the tick completes).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ExportJob' }
        default: { $ref: '#/components/responses/Error' }
  /v1/tenants/{tenant_id}/export-jobs/{job_id}:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/ExportJobId'
    get:
      tags: [reporting]
      operationId: getExportJob
      summary: Get one export job (status, blob_uri, row_count).
      responses:
        '200':
          description: The export job.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ExportJob' }
        default: { $ref: '#/components/responses/Error' }
  /v1/tenants/{tenant_id}/export-jobs/{job_id}/archive:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/ExportJobId'
    post:
      tags: [reporting]
      operationId: archiveExportJob
      summary: Archive an export job (soft; blob may be retained by retention policy).
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: Archived export job.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ExportJob' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/oidc-config:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [iam]
      operationId: getTenantOIDCConfig
      summary: >
        Read the tenant's OIDC single sign-on configuration. Secrets are referenced through secret_ref.
      responses:
        '200':
          description: OIDC config (defaults when unset).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TenantOIDCConfig' }
        default: { $ref: '#/components/responses/Error' }
    put:
      tags: [iam]
      operationId: updateTenantOIDCConfig
      summary: Upsert tenant OIDC SSO config (one row per tenant in v1).
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/TenantOIDCConfigUpdate' }
      responses:
        '200':
          description: Updated OIDC config.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TenantOIDCConfig' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/saml-config:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [iam]
      operationId: getTenantSAMLConfig
      summary: >
        Read the tenant's SAML single sign-on configuration. The identity-provider certificate is
        referenced through cert_ref in the secret store.
      responses:
        '200':
          description: SAML config (defaults when unset).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TenantSAMLConfig' }
        default: { $ref: '#/components/responses/Error' }
    put:
      tags: [iam]
      operationId: updateTenantSAMLConfig
      summary: Upsert tenant SAML SSO config (one row per tenant in v1).
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/TenantSAMLConfigUpdate' }
      responses:
        '200':
          description: Updated SAML config.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TenantSAMLConfig' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/sealing-keys:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [supply]
      operationId: listSealingKeys
      summary: >
        List metadata for a tenant's sealing keys. Key material is excluded.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of sealing keys.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SealingKeyList' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [supply]
      operationId: createSealingKey
      summary: >
        Create and activate a sealing key, retiring the tenant's previous ACTIVE key. The response
        contains metadata only; ciphertext and raw key material are excluded.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/SealingKeyCreate' }
      responses:
        '201':
          description: Created and activated.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SealingKey' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/sealing-keys/{key_id}/retire:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/SealingKeyId'
    post:
      tags: [supply]
      operationId: retireSealingKey
      summary: Retire a sealing key by its opaque key id (metadata response only).
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: Retired.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SealingKey' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/demand-partners:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [demand]
      operationId: listDemandPartners
      summary: List a tenant's demand partners.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of demand partners.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DemandPartnerList' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [demand]
      operationId: createDemandPartner
      summary: Create a demand partner.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/DemandPartnerCreate' }
      responses:
        '201':
          description: Created.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DemandPartner' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/demand-partners/bulk-import:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    post:
      tags: [demand]
      operationId: bulkImportDemandPartners
      summary: >
        Create demand partners in a batch and return a result for each row. A failed row does not abort
        other rows. With dry_run: true, validate required fields and name conflicts both inside the
        batch and against stored partners without saving changes.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/DemandPartnerBulkImportRequest' }
      responses:
        '200':
          description: Per-row results (dry-run preview or applied).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DemandPartnerBulkImportResponse' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/demand-partners/{demand_partner_id}:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/DemandPartnerId'
    get:
      tags: [demand]
      operationId: getDemandPartner
      summary: Get a demand partner.
      responses:
        '200':
          description: The demand partner.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DemandPartner' }
        default: { $ref: '#/components/responses/Error' }
    put:
      tags: [demand]
      operationId: updateDemandPartner
      summary: Update a demand partner's name and/or default buyer notice TTL.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/DemandPartnerUpdate' }
      responses:
        '200':
          description: Updated.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DemandPartner' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/demand-partners/{demand_partner_id}/archive:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/DemandPartnerId'
    post:
      tags: [demand]
      operationId: archiveDemandPartner
      summary: >
        Archive a demand partner.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: Archived.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DemandPartner' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/demand-partners/status-counts:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [demand]
      operationId: getDemandPartnerStatusCounts
      summary: >
        Per-status row counts for the tenant's demand partners.
      responses:
        '200':
          description: Status counts.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/StatusCounts' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/demand-routes:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [demand]
      operationId: listDemandRoutes
      summary: List a tenant's demand routes.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - name: demand_partner_id
          in: query
          required: false
          description: Restrict the list to routes owned by this partner.
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: A page of demand routes.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DemandRouteList' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [demand]
      operationId: createDemandRoute
      summary: >
        Create a demand route with endpoints, seats, deals, parameters, fanout, and audience settings.
        Configure request rewrites through the separate rewrite endpoints.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/DemandRouteCreate' }
      responses:
        '201':
          description: Created.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DemandRoute' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/demand-routes/{demand_route_id}:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/DemandRouteId'
    get:
      tags: [demand]
      operationId: getDemandRoute
      summary: Get a demand route.
      responses:
        '200':
          description: The demand route.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DemandRoute' }
        default: { $ref: '#/components/responses/Error' }
    put:
      tags: [demand]
      operationId: updateDemandRoute
      summary: >
        Update a demand route. demand_partner_id and integration are immutable after creation
        (create a new route to change either).
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/DemandRouteUpdate' }
      responses:
        '200':
          description: Updated.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DemandRoute' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/demand-routes/{demand_route_id}/archive:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/DemandRouteId'
    post:
      tags: [demand]
      operationId: archiveDemandRoute
      summary: >
        Archive a demand route.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: Archived.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DemandRoute' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/demand-routes/{demand_route_id}/rewrites:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/DemandRouteId'
    get:
      tags: [demand]
      operationId: listDemandRouteRewrites
      summary: >
        List a demand route's request-rewrite rules.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: The route's rewrite rules.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/RouteRewriteList' }
        default: { $ref: '#/components/responses/Error' }
    put:
      tags: [demand]
      operationId: setDemandRouteRewrites
      summary: >
        Replace a demand route's full rewrite rule set (validated: path is required; a clearing rule
        must not also carry a value; a rule is not both overwrite and clear).
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/RouteRewriteSet' }
      responses:
        '200':
          description: The route's rewrite rules after the replace.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/RouteRewriteList' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/demand-routes/{demand_route_id}/rewrites/preview:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/DemandRouteId'
    post:
      tags: [demand]
      operationId: previewDemandRouteRewrites
      summary: >
        Preview a route's request rewrites against a supplied OpenRTB bid request using the same rules
        as outbound demand requests. Returns the original request, rewritten request, and applied rules
        without saving changes.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/DemandRouteRewritePreviewRequest' }
      responses:
        '200':
          description: Before/after bid request preview.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DemandRouteRewritePreviewResponse' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/demand-routes/{demand_route_id}/clone:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/DemandRouteId'
    post:
      tags: [demand]
      operationId: cloneDemandRoute
      summary: >
        Create a new ACTIVE demand route with all configuration fields and rewrite rules copied from the
        source route.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '201':
          description: The cloned route.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DemandRoute' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/demand-routes/status-counts:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [demand]
      operationId: getDemandRouteStatusCounts
      summary: >
        Per-status row counts for the tenant's demand routes.
      responses:
        '200':
          description: Status counts.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/StatusCounts' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/deals:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [demand]
      operationId: listDeals
      summary: List a tenant's deals.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - name: advertiser_id
          in: query
          required: false
          schema: { type: string, format: uuid }
          description: Only deals associated with this advertiser (Deal.advertiser_id).
      responses:
        '200':
          description: A page of deals.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DealList' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [demand]
      operationId: createDeal
      summary: >
        Create a deal with seats, advertiser domains, verification, audience settings, and per-deal
        frequency caps.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/DealCreate' }
      responses:
        '201':
          description: Created.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Deal' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/deals/{deal_id}:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/DealId'
    get:
      tags: [demand]
      operationId: getDeal
      summary: Get a deal.
      responses:
        '200':
          description: The deal.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Deal' }
        default: { $ref: '#/components/responses/Error' }
    put:
      tags: [demand]
      operationId: updateDeal
      summary: Update a deal. external_id is immutable after creation.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/DealUpdate' }
      responses:
        '200':
          description: Updated.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Deal' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/deals/{deal_id}/push:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/DealId'
    post:
      tags: [demand]
      operationId: pushDeal
      summary: >
        Send a deal's terms to a demand partner's deal endpoint. The response tracks delivery through
        PENDING, SENT, and ACKNOWLEDGED, or FAILED with the partner's error. Returns 202 while delivery
        continues. Repeated pushes of unchanged terms are deduplicated by their payload hash.
      x-riptide-rbac: [demand.write]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/DealPushRequest' }
      responses:
        '202':
          description: The deal_sync row tracking the push.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DealSync' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/deals/{deal_id}/archive:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/DealId'
    post:
      tags: [demand]
      operationId: archiveDeal
      summary: >
        Archive a deal.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: Archived.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Deal' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/deals/{deal_id}/makegood:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/DealId'
    get:
      tags: [demand]
      operationId: getDealMakegood
      summary: >
        Recommend a makegood for a guaranteed deal with a delivery commitment. The calculation compares
        the deal's delivery counters, commitment, and flight, then reports on_track, behind, delivered,
        or shortfall and the amount needed to cover a shortfall. This recommendation changes neither
        money nor counters. Returns 404 for an unknown deal or 409 CONFLICT when the deal lacks
        guaranteed=true and either committed_impressions or committed_spend.
      responses:
        '200':
          description: The computed recommendation.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DealMakegoodRecommendation' }
        default: { $ref: '#/components/responses/Error' }

  /v1/deals/inbound:
    post:
      tags: [demand]
      operationId: receiveDeal
      summary: >
        Receive a partner's deal using a partner-scoped API key created by the receiving tenant. The key
        determines the tenant. The deal is created under the partner's external_ref; subsequent pushes
        with the same reference update it. A sync record stores the payload hash. Returns 202 while
        processing continues.
      x-riptide-rbac: [demand.write]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/DealInboundRequest' }
      responses:
        '202':
          description: The deal_sync row recording the inbound deal.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DealSync' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/deals/status-counts:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [demand]
      operationId: getDealStatusCounts
      summary: >
        Per-status row counts for the tenant's deals.
      responses:
        '200':
          description: Status counts.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/StatusCounts' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/marketplaces:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [demand]
      operationId: listMarketplaces
      summary: List a tenant's marketplaces (packaged demand-route bundles).
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of marketplaces.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/MarketplaceList' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [demand]
      operationId: createMarketplace
      summary: Create a marketplace.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/MarketplaceCreate' }
      responses:
        '201':
          description: Created.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Marketplace' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/marketplaces/{marketplace_id}:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/MarketplaceId'
    get:
      tags: [demand]
      operationId: getMarketplace
      summary: Get a marketplace.
      responses:
        '200':
          description: The marketplace.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Marketplace' }
        default: { $ref: '#/components/responses/Error' }
    put:
      tags: [demand]
      operationId: updateMarketplace
      summary: Update a marketplace's name and/or rev share. code is immutable after creation.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/MarketplaceUpdate' }
      responses:
        '200':
          description: Updated.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Marketplace' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/marketplaces/{marketplace_id}/deals:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/MarketplaceId'
    get:
      tags: [demand]
      operationId: listMarketplaceDeals
      summary: >
        List the deal IDs packaged by a marketplace.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: The marketplace's packaged deal membership.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/MarketplaceDealMembership' }
        default: { $ref: '#/components/responses/Error' }
    put:
      tags: [demand]
      operationId: setMarketplaceDeals
      summary: Replace a marketplace's full packaged deal set.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/MarketplaceDealMembership' }
      responses:
        '200':
          description: Updated packaged deal membership.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/MarketplaceDealMembership' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/marketplaces/{marketplace_id}/archive:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/MarketplaceId'
    post:
      tags: [demand]
      operationId: archiveMarketplace
      summary: >
        Archive a marketplace.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: Archived.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Marketplace' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/marketplaces/status-counts:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [demand]
      operationId: getMarketplaceStatusCounts
      summary: >
        Per-status row counts for the tenant's marketplaces.
      responses:
        '200':
          description: Status counts.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/StatusCounts' }
        default: { $ref: '#/components/responses/Error' }


  /v1/tenants/{tenant_id}/marketplace-listings:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [demand]
      operationId: listMarketplaceListings
      summary: List seller-owned marketplace listings (discovery packages).
      x-riptide-rbac: [demand.read]
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of marketplace listings.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/MarketplaceListingList' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [demand]
      operationId: createMarketplaceListing
      summary: >
        Create a DRAFT marketplace listing.
      x-riptide-rbac: [demand.write]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/MarketplaceListingCreate' }
      responses:
        '201':
          description: Created.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/MarketplaceListing' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/marketplace-listings/{listing_id}:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/MarketplaceListingId'
    get:
      tags: [demand]
      operationId: getMarketplaceListing
      summary: Get a seller-owned marketplace listing.
      x-riptide-rbac: [demand.read]
      responses:
        '200':
          description: The listing.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/MarketplaceListing' }
        default: { $ref: '#/components/responses/Error' }
    put:
      tags: [demand]
      operationId: updateMarketplaceListing
      summary: >
        Update a marketplace listing. Code is immutable. Updating a PUBLISHED listing
        reverts it to DRAFT and clears terms_summary (must re-publish).
      x-riptide-rbac: [demand.write]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/MarketplaceListingUpdate' }
      responses:
        '200':
          description: Updated.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/MarketplaceListing' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/marketplace-listings/{listing_id}/publish:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/MarketplaceListingId'
    post:
      tags: [demand]
      operationId: publishMarketplaceListing
      summary: >
        Publish a listing (DRAFT/PAUSED → PUBLISHED), compute and persist terms_summary.
        FEDERATED visibility requires the open_marketplace entitlement.
      x-riptide-rbac: [demand.write]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: Published.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/MarketplaceListing' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/marketplace-listings/{listing_id}/stack-product/publish:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/MarketplaceListingId'
    post:
      tags: [demand]
      operationId: publishStackProduct
      summary: Publish a STACK_PRODUCT listing (vendor entitlement + verified conformance required).
      x-riptide-rbac: [demand.write]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: Published stack product.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/MarketplaceListing' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/stack-products/install:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    post:
      tags: [demand]
      operationId: installStackProduct
      summary: Install a published stack product (INSTALL_STACK_PRODUCT activation).
      x-riptide-rbac: [demand.write]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/StackProductInstallRequest' }
      responses:
        '201':
          description: Installed.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ListingActivation' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/stack-products/activations/{activation_id}/uninstall:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - name: activation_id
        in: path
        required: true
        schema: { type: string, format: uuid }
    post:
      tags: [demand]
      operationId: uninstallStackProduct
      summary: Revoke a stack-product install (uninstall).
      x-riptide-rbac: [demand.write]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '204':
          description: Uninstalled.
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/tenant-extensions:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [demand]
      operationId: listTenantExtensions
      summary: >
        List the tenant's installed extensions with their kind, listing reference, and execution
        budgets. This read-only response excludes capability tokens and connection secrets.
      x-riptide-rbac: [demand.read]
      parameters:
        - name: extension_kind
          in: query
          required: false
          description: >
            Optional filter (e.g. BID_SOURCE). Open enum — unknown values return an empty page.
            REPORT_FIELD and DECISIONER are retired kinds (ADR D57): no listing carries them and
            filtering on either returns an empty page.
          schema:
            type: string
            enum: [BID_SOURCE, DEMAND_ADAPTER, ENRICHER, AUDIENCE_EVALUATOR, PRICING_MODULE, CREATIVE_RENDERER, VERIFICATION_VENDOR, REPORT_FIELD, DECISIONER, DEMAND_ATTACHMENT]
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of installed tenant extensions.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TenantExtensionList' }
        default: { $ref: '#/components/responses/Error' }

  /v1/operator/stack-products/conformance-runs:
    post:
      tags: [operator]
      operationId: runExtensionConformance
      summary: >
        Record an operator-verified passing result for extension conformance. This operation records
        evidence; it does not execute the conformance tests. Operator access required.
      x-riptide-rbac: [operator]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/RunExtensionConformanceRequest' }
      responses:
        '200':
          description: Recorded conformance run.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ExtensionConformanceRunResult' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/marketplace-listings/{listing_id}/pause:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/MarketplaceListingId'
    post:
      tags: [demand]
      operationId: pauseMarketplaceListing
      summary: Pause a PUBLISHED marketplace listing.
      x-riptide-rbac: [demand.write]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: Paused.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/MarketplaceListing' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/marketplace-discovery/listings:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [demand]
      operationId: listDiscoverableMarketplaceListings
      summary: >
        Discover available marketplace listings in the same tenant.
      x-riptide-rbac: [marketplace.discover]
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - name: buyer_seat
          in: query
          schema: { type: string }
          description: Seat used for PRIVATE allowlist matching.
        - name: media
          in: query
          schema: { type: string }
        - name: kind
          in: query
          schema: { $ref: '#/components/schemas/ListingKind' }
        - name: q
          in: query
          schema: { type: string }
        - name: min_floor
          in: query
          schema: { type: string, description: Decimal string informational floor filter. }
      responses:
        '200':
          description: Discoverable listings page.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/MarketplaceListingList' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/marketplace-discovery/listings/{listing_id}:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/MarketplaceListingId'
    get:
      tags: [demand]
      operationId: getDiscoverableMarketplaceListing
      summary: >
        Read a discoverable listing with its terms summary and deal summaries.
      x-riptide-rbac: [marketplace.discover]
      parameters:
        - name: buyer_seat
          in: query
          schema: { type: string }
      responses:
        '200':
          description: The listing when visible to the seat.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/MarketplaceListing' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/paste-site:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    post:
      tags: [supply]
      operationId: createPasteSiteDraft
      summary: >
        Create a default WEB placement and a PRIVATE SUPPLY_PACK draft for a publisher's site.
      description: |
        Does not grant entitlements. Idempotent via Idempotency-Key.
      x-riptide-rbac: [supply.write]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/PasteSiteCreate' }
      responses:
        '201':
          description: Placement + draft listing created.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PasteSiteResult' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/path-receipts/{fill_id}:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - name: fill_id
        in: path
        required: true
        schema: { type: string }
        description: Fill / bid_uid identifying the receipt (design 25 §7).
    get:
      tags: [marketplace]
      operationId: getPathReceipt
      summary: >
        Read the path receipt for a fill.
      description: >
        Returns the participants, fee lines, and deal context recorded for one fill. The same report is
        available through riptide.marketplace.get_path_receipt. A receipt is a reporting record, not a
        signed path attestation.
      x-riptide-rbac: [demand.read]
      responses:
        '200':
          description: Path receipt.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PathReceipt' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/listing-activations:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [demand]
      operationId: listListingActivations
      summary: List activations owned by the tenant (buyer binds).
      x-riptide-rbac: [marketplace.activate]
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - name: listing_id
          in: query
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: Activation page.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ListingActivationList' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [demand]
      operationId: createListingActivation
      summary: >
        Activate a listing by merging its deals onto a demand route. Repeating the activation is
        idempotent.
      x-riptide-rbac: [marketplace.activate]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ListingActivationCreate' }
      responses:
        '201':
          description: Created (or existing idempotency match).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ListingActivation' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/listing-activations/{activation_id}/revoke:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - name: activation_id
        in: path
        required: true
        schema: { type: string, format: uuid }
    post:
      tags: [demand]
      operationId: revokeListingActivation
      summary: Revoke an activation; deals drop on next plan compile.
      x-riptide-rbac: [marketplace.activate]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '204':
          description: Revoked (idempotent if already revoked).
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/listing-proposals:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [marketplace]
      operationId: listListingProposals
      summary: >
        List proposals for which the tenant is the proposer or listing owner, newest first. Results use
        cursor pagination; status and role filters are applied before pagination.
      x-riptide-rbac: [marketplace.propose]
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - name: status
          in: query
          required: false
          description: Only threads whose current state equals this value.
          schema: { $ref: '#/components/schemas/ProposalState' }
        - name: role
          in: query
          required: false
          description: Only threads where the tenant holds this party role (PROPOSER = threads it opened; OWNER = threads opened against its listings).
          schema: { $ref: '#/components/schemas/ProposalPartyRole' }
      responses:
        '200':
          description: A page of proposal threads (the caller's party rows).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ListingProposalList' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [marketplace]
      operationId: proposeListingTerms
      summary: >
        Create a listing proposal for negotiation between the proposer and listing owner.
      x-riptide-rbac: [marketplace.propose]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ListingProposalCreate' }
      responses:
        '201':
          description: Created proposal thread (caller party row).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ListingProposal' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/listing-proposals/{thread_id}:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - name: thread_id
        in: path
        required: true
        schema: { type: string, format: uuid }
    get:
      tags: [marketplace]
      operationId: getListingProposal
      summary: Get this tenant's row for a proposal thread.
      x-riptide-rbac: [marketplace.propose]
      responses:
        '200':
          description: Proposal row visible to the tenant.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ListingProposal' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/listing-proposals/{thread_id}/counter:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - name: thread_id
        in: path
        required: true
        schema: { type: string, format: uuid }
    post:
      tags: [marketplace]
      operationId: counterListingProposal
      summary: Counter proposal terms (revision CAS).
      x-riptide-rbac: [marketplace.propose]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ListingProposalCounter' }
      responses:
        '200':
          description: Updated proposal row.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ListingProposal' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/listing-proposals/{thread_id}/approve:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - name: thread_id
        in: path
        required: true
        schema: { type: string, format: uuid }
    post:
      tags: [marketplace]
      operationId: approveListingProposal
      summary: Approve proposal and create listing activation atomically.
      x-riptide-rbac: [marketplace.propose]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ListingProposalApprove' }
      responses:
        '200':
          description: Approved proposal plus activation id when created.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ListingProposalApproveResult' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/listing-proposals/{thread_id}/reject:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - name: thread_id
        in: path
        required: true
        schema: { type: string, format: uuid }
    post:
      tags: [marketplace]
      operationId: rejectListingProposal
      summary: Reject the latest proposal revision (revision CAS).
      x-riptide-rbac: [marketplace.propose]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ListingProposalMutate' }
      responses:
        '200':
          description: Rejected proposal row.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ListingProposal' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/listing-proposals/{thread_id}/withdraw:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - name: thread_id
        in: path
        required: true
        schema: { type: string, format: uuid }
    post:
      tags: [marketplace]
      operationId: withdrawListingProposal
      summary: Withdraw proposal (proposer only; revision CAS).
      x-riptide-rbac: [marketplace.propose]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ListingProposalMutate' }
      responses:
        '200':
          description: Withdrawn proposal row.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ListingProposal' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/campaign-orders:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [campaign]
      operationId: listCampaignOrders
      summary: List a tenant's campaign orders (booked advertiser orders).
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of campaign orders.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CampaignOrderList' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [campaign]
      operationId: createCampaignOrder
      summary: Create a campaign order.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CampaignOrderCreate' }
      responses:
        '201':
          description: Created.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CampaignOrder' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/campaign-orders/bulk-import:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    post:
      tags: [campaign]
      operationId: bulkImportCampaignOrders
      summary: >
        Create or update campaign orders by ref from a JSON array. Return a result for each row. With
        dry_run: true, validate without saving changes. Existing references are updated; new references
        create orders.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CampaignOrderBulkImportRequest' }
      responses:
        '200':
          description: Per-row results (dry-run preview or applied upsert).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CampaignOrderBulkImportResponse' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/campaign-orders/{campaign_order_id}:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/CampaignOrderId'
    get:
      tags: [campaign]
      operationId: getCampaignOrder
      summary: Get a campaign order.
      responses:
        '200':
          description: The campaign order.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CampaignOrder' }
        default: { $ref: '#/components/responses/Error' }
    put:
      tags: [campaign]
      operationId: updateCampaignOrder
      summary: >
        Update a campaign order's name, delivery_status, flight, timezone, budgets, and frequency caps.
        The ref, advertiser_id, and currency are immutable. Pausing or ending an order prevents its line
        items from serving; archiving an order also archives its line items.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CampaignOrderUpdate' }
      responses:
        '200':
          description: Updated.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CampaignOrder' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/campaign-orders/{campaign_order_id}/archive:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/CampaignOrderId'
    post:
      tags: [campaign]
      operationId: archiveCampaignOrder
      summary: >
        Archive a campaign order.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: Archived.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CampaignOrder' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/campaign-orders/{campaign_order_id}/duplicate:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/CampaignOrderId'
    post:
      tags: [campaign]
      operationId: duplicateCampaignOrder
      summary: >
        Copy a campaign order and, by default, its line items and weighted creative associations. Copies
        receive new references, DRAFT delivery statuses, and zero delivery counters. Existing creative
        records are reused. The source order is unchanged.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CampaignOrderDuplicateRequest' }
      responses:
        '201':
          description: The new DRAFT campaign order (deep copy).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CampaignOrder' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/campaign-orders/status-counts:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [campaign]
      operationId: getCampaignOrderStatusCounts
      summary: >
        Per-status row counts for the tenant's campaign orders.
      responses:
        '200':
          description: Status counts.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/StatusCounts' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/advertisers:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [campaign]
      operationId: listAdvertisers
      summary: List a tenant's advertisers.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of advertisers.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AdvertiserList' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [campaign]
      operationId: createAdvertiser
      summary: Create an advertiser.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/AdvertiserCreate' }
      responses:
        '201':
          description: Created.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Advertiser' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/advertisers/{advertiser_id}:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/AdvertiserId'
    get:
      tags: [campaign]
      operationId: getAdvertiser
      summary: Get an advertiser.
      responses:
        '200':
          description: The advertiser.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Advertiser' }
        default: { $ref: '#/components/responses/Error' }
    put:
      tags: [campaign]
      operationId: updateAdvertiser
      summary: Update an advertiser's name. ref is immutable after creation.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/AdvertiserUpdate' }
      responses:
        '200':
          description: Updated.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Advertiser' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/advertisers/{advertiser_id}/report-summary:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/AdvertiserId'
    get:
      tags: [reporting]
      operationId: getAdvertiserReportSummary
      summary: >
        Read a delivery summary for one advertiser_id. A caller with the advertiser role can only read
        the advertiser bound to its identity.
      responses:
        '200':
          description: Advertiser-scoped report summary.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AdvertiserReportSummary' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/advertisers/{advertiser_id}/campaign-orders:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/AdvertiserId'
    get:
      tags: [campaign]
      operationId: listAdvertiserCampaignOrders
      summary: >
        List this advertiser's campaign orders. A caller with the advertiser role can only read orders
        for the advertiser bound to its identity.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: Advertiser-scoped campaign orders.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CampaignOrderList' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/advertisers/{advertiser_id}/line-items:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/AdvertiserId'
    get:
      tags: [campaign]
      operationId: listAdvertiserLineItems
      summary: >
        List line items under this advertiser's campaign orders. A caller with the advertiser role can
        only read the advertiser bound to its identity.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: Advertiser-scoped line items.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/LineItemList' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [campaign]
      operationId: createAdvertiserLineItem
      summary: >
        Create a line item under one of this advertiser's campaign orders. Accepted fields are name,
        campaign_order_id, flight, goal, and rate. The advertiser role requires campaign.write and an
        identity bound to this advertiser.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/AdvertiserLineItemWrite' }
      responses:
        '201':
          description: Created line item.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/LineItem' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/advertisers/{advertiser_id}/line-items/{line_item_id}:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/AdvertiserId'
      - $ref: '#/components/parameters/LineItemId'
    put:
      tags: [campaign]
      operationId: updateAdvertiserLineItem
      summary: >
        Update name, flight, goal, or rate on a line item owned by this advertiser. Updates to another
        advertiser's line items are forbidden.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/AdvertiserLineItemWrite' }
      responses:
        '200':
          description: Updated line item.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/LineItem' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/advertisers/{advertiser_id}/archive:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/AdvertiserId'
    post:
      tags: [campaign]
      operationId: archiveAdvertiser
      summary: >
        Archive an advertiser.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: Archived.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Advertiser' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/advertisers/status-counts:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [campaign]
      operationId: getAdvertiserStatusCounts
      summary: >
        Per-status row counts for the tenant's advertisers.
      responses:
        '200':
          description: Status counts.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/StatusCounts' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/line-items:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [campaign]
      operationId: listLineItems
      summary: List a tenant's line items.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - name: creative_id
          in: query
          required: false
          schema: { type: string, format: uuid }
          description: Only line items with this creative attached (line_item_creative).
      responses:
        '200':
          description: A page of line items.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/LineItemList' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [campaign]
      operationId: createLineItem
      summary: Create a line item (deliverable within an optional campaign order).
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/LineItemCreate' }
      responses:
        '201':
          description: Created.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/LineItem' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/line-items/{line_item_id}:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/LineItemId'
    get:
      tags: [campaign]
      operationId: getLineItem
      summary: Get a line item.
      responses:
        '200':
          description: The line item.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/LineItem' }
        default: { $ref: '#/components/responses/Error' }
    put:
      tags: [campaign]
      operationId: updateLineItem
      summary: >
        Update a line item's name, cpm, priority, and delivery settings, including flight, pacing,
        audience, weighted creatives, and frequency caps. The kind is immutable.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/LineItemUpdate' }
      responses:
        '200':
          description: Updated.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/LineItem' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/line-items/{line_item_id}/archive:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/LineItemId'
    post:
      tags: [campaign]
      operationId: archiveLineItem
      summary: >
        Archive a line item.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: Archived.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/LineItem' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/line-items/{line_item_id}/extend:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/LineItemId'
    post:
      tags: [campaign]
      operationId: extendLineItem
      summary: >
        Archive the current line item and create a new one for the specified period. The new item has a
        fresh reference and keeps the source item's name, kind, cpm, priority, audience, and campaign
        order.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/LineItemPeriod' }
      responses:
        '201':
          description: The new line item covering the extended period.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/LineItem' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/line-items/{line_item_id}/duplicate:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/LineItemId'
    post:
      tags: [campaign]
      operationId: duplicateLineItem
      summary: >
        Create an INACTIVE copy of a line item with a new reference and the same configuration. The
        source line item is unchanged.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '201':
          description: The inactive copy.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/LineItem' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/line-items/{line_item_id}/eligibility:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/LineItemId'
    get:
      tags: [campaign]
      operationId: getLineItemEligibility
      summary: >
        Check whether a line item can serve using the serving engine's eligibility rules for order
        status, flight, dayparts, creatives, budgets, pacing, and caps. When the counter store is
        unavailable, counter-dependent checks return COUNTERS_UNAVAILABLE with severity INFO. The
        serving field is true only when no BLOCKING condition is present.
      parameters:
        - name: placement_id
          in: query
          required: false
          schema: { type: string, format: uuid }
          description: >
            Optional placement to evaluate placement-scoped conditions against (GC-03). When
            set, the report additionally checks the placement's blocked_exclusion_labels
            against the line item's effective exclusion labels and emits
            PLACEMENT_EXCLUSION_LABEL_BLOCKED naming the blocking label.
      responses:
        '200':
          description: The eligibility condition report.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/EligibilityReport' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/line-items/{line_item_id}/delivery-state:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/LineItemId'
    get:
      tags: [campaign]
      operationId: getLineItemDeliveryState
      summary: >
        Read a line item's delivery state: PENDING, SERVING, GOAL_REACHED, FLIGHT_ENDED, or INACTIVE.
        The response uses live pacing counters and the serving engine's calculations for buffered goals,
        delivered and remaining counts, booked spend, hourly goals and weights, bid-rate caps, and
        dynamic eCPM for budget-and-goal line items.
      responses:
        '200':
          description: The computed delivery state.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/LineItemDeliveryState' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/line-items/{line_item_id}/reset-delivery:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/LineItemId'
    post:
      tags: [campaign]
      operationId: resetLineItemDelivery
      summary: >
        Reset a line item's lifetime and current-day impression, spend, click, conversion, and hourly
        counters, then return its recomputed PENDING delivery state. Frequency-cap records are retained.
        The change is audited. Returns UNAVAILABLE when the console has no hot-store connection,
        configured by RIPTIDE_REDIS_ADDR.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: The delivery state after the reset.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/LineItemDeliveryState' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/line-items/bulk-status:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    post:
      tags: [campaign]
      operationId: bulkUpdateLineItemStatus
      summary: >
        Activate, pause, or archive up to 100 line items. Returns a separate success or machine-readable
        error for each ID, including activation errors for items without a servable creative. A failed
        item does not roll back successful changes to other items.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/LineItemBulkStatusRequest' }
      responses:
        '200':
          description: Per-item results, in request order.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/LineItemBulkStatusResult' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/line-items/status-counts:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [campaign]
      operationId: getLineItemStatusCounts
      summary: >
        Per-status row counts for the tenant's line items.
      responses:
        '200':
          description: Status counts.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/StatusCounts' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/creatives:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [campaign]
      operationId: listCreatives
      summary: List a tenant's creatives.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of creatives.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CreativeList' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [campaign]
      operationId: createCreative
      summary: Create a creative (VAST tag / hosted media / display).
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreativeCreate' }
      responses:
        '201':
          description: Created.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Creative' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/creatives/bulk-import:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    post:
      tags: [campaign]
      operationId: bulkImportCreatives
      summary: >
        Create or update creatives by ref from a JSON array. Return a result for each row. With dry_run:
        true, validate without saving changes. Existing references are updated; new references create
        creatives.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreativeBulkImportRequest' }
      responses:
        '200':
          description: Per-row results (dry-run preview or applied upsert).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CreativeBulkImportResponse' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/creatives/{creative_id}:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/CreativeId'
    get:
      tags: [campaign]
      operationId: getCreative
      summary: Get a creative.
      responses:
        '200':
          description: The creative.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Creative' }
        default: { $ref: '#/components/responses/Error' }
    put:
      tags: [campaign]
      operationId: updateCreative
      summary: >
        Update a creative's VAST URL or XML, duration, click URL, verification, and proximity fields.
        The kind is immutable. Use the pipeline action to advance processing status.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreativeUpdate' }
      responses:
        '200':
          description: Updated.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Creative' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/creatives/{creative_id}/archive:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/CreativeId'
    post:
      tags: [campaign]
      operationId: archiveCreative
      summary: >
        Archive a creative.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: Archived.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Creative' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/creatives/{creative_id}/pipeline/advance:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/CreativeId'
    post:
      tags: [campaign]
      operationId: advanceCreativePipeline
      summary: >
        Advance a creative through UPLOADED, STORED, WATERMARKED, TRANSCODED, and READY one step at a
        time. Reaching READY approves a PENDING creative only when the tenant's creative_auto_approve
        policy is enabled. Otherwise, approval stays PENDING until reviewCreative is called.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: The creative after advancing one pipeline step.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Creative' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/creatives/{creative_id}/review:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/CreativeId'
    post:
      tags: [campaign]
      operationId: reviewCreative
      summary: >
        Approve or reject a creative with a reason_code and feedback. The creative records the decision,
        actor, and time in approval_reason, approval_feedback, reviewed_at, and reviewed_by. Rejection
        requires reason_code. When creative_auto_approve is disabled, a material change to markup, click
        settings, or media resets APPROVED to PENDING for another review.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CreativeReviewRequest' }
      responses:
        '200':
          description: The creative after the review decision.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Creative' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/creatives/{creative_id}/media:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/CreativeId'
    post:
      tags: [campaign]
      operationId: createCreativeMediaUpload
      x-riptide-rbac: [campaign.write]
      summary: >
        Upload media for a hosted creative as multipart/form-data under file. An optional content_type
        overrides the file part's Content-Type. Returns 202; poll getCreativeMediaUpload for RECEIVED,
        VALIDATED, TRANSCODING, READY, or FAILED with an error. READY adds the media_variant_id and
        preview_url to the upload record and the creative's media variants. Requires campaign.write.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema: { $ref: '#/components/schemas/CreativeMediaUploadForm' }
      responses:
        '202':
          description: The accepted upload record (status RECEIVED).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CreativeMediaUpload' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/creatives/{creative_id}/media/{upload_id}:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/CreativeId'
      - $ref: '#/components/parameters/CreativeMediaUploadId'
    get:
      tags: [campaign]
      operationId: getCreativeMediaUpload
      x-riptide-rbac: [campaign.read]
      summary: >
        Read a media upload's status, validated content type, size, and SHA-256 hash. Once READY, the
        record also includes media_variant_id and preview_url. Requires campaign.read.
      responses:
        '200':
          description: The upload record.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CreativeMediaUpload' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/creatives/status-counts:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [campaign]
      operationId: getCreativeStatusCounts
      summary: >
        Per-status row counts for the tenant's creatives.
      responses:
        '200':
          description: Status counts.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/StatusCounts' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/fee-schedules:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [billing]
      operationId: listFeeSchedules
      summary: List a tenant's fee schedules.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of fee schedules.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/FeeScheduleList' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [billing]
      operationId: createFeeSchedule
      summary: >
        Create a fee schedule for a scope, with platform, demand, publisher, or serving fee lines using
        REVSHARE, CPM, or FLAT pricing. Only one schedule is allowed per tenant, scope, and scope_id; a
        duplicate returns CONFLICT. The change is audited and access is restricted to the tenant.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/FeeScheduleCreate' }
      responses:
        '201':
          description: Created.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/FeeSchedule' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/fee-schedules/{fee_schedule_id}:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/FeeScheduleId'
    get:
      tags: [billing]
      operationId: getFeeSchedule
      summary: Get a fee schedule.
      responses:
        '200':
          description: The fee schedule.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/FeeSchedule' }
        default: { $ref: '#/components/responses/Error' }
    put:
      tags: [billing]
      operationId: updateFeeSchedule
      summary: >
        Replace a fee schedule's name and fee lines. The scope and scope_id are immutable; create a new
        schedule to change them. The change is audited and access is restricted to the tenant.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/FeeScheduleUpdate' }
      responses:
        '200':
          description: Updated.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/FeeSchedule' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/fee-schedules/{fee_schedule_id}/archive:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/FeeScheduleId'
    post:
      tags: [billing]
      operationId: archiveFeeSchedule
      summary: >
        Archive a fee schedule.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: Archived.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/FeeSchedule' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/fee-schedules/status-counts:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [billing]
      operationId: getFeeScheduleStatusCounts
      summary: >
        Per-status row counts for the tenant's fee schedules.
      responses:
        '200':
          description: Status counts.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/StatusCounts' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/models:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [model]
      operationId: listModels
      summary: >
        List models for a decision_point, including the tenant's models and platform-shared models. The
        decision_point is required and can be bid, floor, traffic_shape, pace, creative, audience,
        insight, or anomaly.
      parameters:
        - name: decision_point
          in: query
          required: true
          schema: { type: string }
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of models.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ModelRegistryEntryList' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [model]
      operationId: createModel
      summary: >
        Register a model owned by the tenant for a decision point. Only operators can add
        platform-shared models.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ModelCreateRequest' }
      responses:
        '201':
          description: Registered model.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ModelRegistryEntry' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/models/{model_id}:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - name: model_id
        in: path
        required: true
        schema: { type: string, format: uuid }
    get:
      tags: [model]
      operationId: getModel
      summary: >
        Read a tenant-owned or platform-shared model by ID.
      responses:
        '200':
          description: The model.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ModelRegistryEntry' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/models/{model_id}/assignments:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - name: model_id
        in: path
        required: true
        schema: { type: string, format: uuid }
    post:
      tags: [model]
      operationId: assignModel
      summary: >
        Assign a model available to the tenant to a decision point in SHADOW, BOUNDED_AB, or LIVE mode.
        Repeating the same tenant, decision point, mode, and model assignment is idempotent.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ModelAssignmentRequest' }
      responses:
        '201':
          description: Assignment recorded.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ModelAssignment' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/models/{model_id}/evaluation-runs:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - name: model_id
        in: path
        required: true
        schema: { type: string, format: uuid }
    post:
      tags: [model]
      operationId: recordEvaluationRun
      summary: >
        Record candidate and baseline metrics for a model available to the tenant, for one decision
        point and objective. This operation saves the evaluation only. Use promoteModel to change the
        model's operating mode.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ModelEvaluationRunRequest' }
      responses:
        '201':
          description: Evaluation run recorded.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ModelEvaluationRun' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/models/{model_id}/promote:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - name: model_id
        in: path
        required: true
        schema: { type: string, format: uuid }
    post:
      tags: [model]
      operationId: promoteModel
      summary: >
        Change a model's mode to SHADOW, BOUNDED_AB, or LIVE for a decision point, including rollback to
        an earlier mode. Requires platform operator access. The operation records a promotion event and
        an audit record.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ModelPromoteRequest' }
      responses:
        '200':
          description: Promoted.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ModelPromotionEvent' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/reconciliation/statements:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [billing]
      operationId: listReconciliationStatements
      summary: >
        List imported partner statements, newest first.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - name: partner_id
          in: query
          required: false
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: A page of statements.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ReconciliationStatementList' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [billing]
      operationId: importReconciliationStatement
      summary: >
        Import a partner statement (rows already parsed from the partner's CSV / API export) for
        one partner and period. Rows are stored verbatim; comparison runs separately.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ReconciliationStatementImport' }
      responses:
        '201':
          description: The stored statement.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ReconciliationStatement' }
        default: { $ref: '#/components/responses/Error' }
  /v1/tenants/{tenant_id}/reconciliation/statements/{statement_id}:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/ReconciliationStatementId'
    get:
      tags: [billing]
      operationId: getReconciliationStatement
      summary: Get one imported statement with its rows.
      responses:
        '200':
          description: The statement.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ReconciliationStatement' }
        default: { $ref: '#/components/responses/Error' }
  /v1/tenants/{tenant_id}/reconciliation/compare:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    post:
      tags: [billing]
      operationId: runReconciliationCompare
      summary: >
        Run a compare job: the statement's per-day impressions and amounts against the platform's
        own receipts / usage for the same partner and period, with tolerance thresholds. Produces
        a discrepancy report (persisted; listReconciliationReports).
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ReconciliationCompareRequest' }
      responses:
        '201':
          description: The discrepancy report.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ReconciliationReport' }
        default: { $ref: '#/components/responses/Error' }
  /v1/tenants/{tenant_id}/reconciliation/reports:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [billing]
      operationId: listReconciliationReports
      summary: List discrepancy reports, newest first.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - name: statement_id
          in: query
          required: false
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: A page of reports.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ReconciliationReportList' }
        default: { $ref: '#/components/responses/Error' }
  /v1/tenants/{tenant_id}/reconciliation/reports/{report_id}:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/ReconciliationReportId'
    get:
      tags: [billing]
      operationId: getReconciliationReport
      summary: Get one discrepancy report with its per-day lines.
      responses:
        '200':
          description: The report.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ReconciliationReport' }
        default: { $ref: '#/components/responses/Error' }
  /v1/tenants/{tenant_id}/reconciliation/reports/{report_id}/adjustment-memo:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/ReconciliationReportId'
    post:
      tags: [billing]
      operationId: setReconciliationAdjustmentMemo
      x-riptide-rbac: [billing.write]
      summary: >
        Record how a DISCREPANT reconciliation report was resolved with a partner, such as a credit,
        accepted tolerance, or agreed rerun. Replaces the previous memo and records adjustment_memo_by
        and adjustment_memo_at. Requires billing.write.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ReconciliationAdjustmentMemoRequest' }
      responses:
        '200':
          description: The report with the memo recorded.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ReconciliationReport' }
        default: { $ref: '#/components/responses/Error' }
  /v1/tenants/{tenant_id}/rate-plan:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [billing]
      operationId: getTenantRatePlan
      summary: >
        Get the platform rate plan assigned to this tenant (tenant.plan_id → rate_plan).
        404 when no plan is assigned.
      responses:
        '200':
          description: The tenant's rate plan.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PlatformRatePlan' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/usage:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [billing]
      operationId: listUsageRecords
      summary: >
        List aggregated tenant usage for a period.
      parameters:
        - name: period_start
          in: query
          required: true
          schema: { type: string, format: date-time }
        - name: period_end
          in: query
          required: true
          schema: { type: string, format: date-time }
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: Usage records in the period.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/UsageRecordList' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/cost-telemetry:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [billing]
      operationId: listCostTelemetry
      summary: >
        List tenant cost estimates derived from recorded usage for a period. These are operational
        estimates; invoice and payment state are available through the billing endpoints.
      parameters:
        - name: period_start
          in: query
          required: true
          schema: { type: string, format: date-time }
        - name: period_end
          in: query
          required: true
          schema: { type: string, format: date-time }
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: Cost telemetry rows in the period.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CostTelemetryList' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/cost-telemetry/{cost_telemetry_id}:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - name: cost_telemetry_id
        in: path
        required: true
        schema: { type: string }
        description: The usage_record source_hash backing this derived cost telemetry row.
    get:
      tags: [billing]
      operationId: getCostTelemetry
      summary: Get one derived cost telemetry row by its usage_record source_hash.
      responses:
        '200':
          description: The derived cost telemetry row.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CostTelemetry' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/billing/prepaid/balance:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [billing]
      operationId: getPrepaidBalance
      summary: >
        Read the tenant's prepaid ledger balance for a currency.
      parameters:
        - name: currency
          in: query
          required: true
          schema: { type: string, minLength: 3, maxLength: 3 }
          description: ISO 4217 currency code (e.g. USD).
      responses:
        '200':
          description: Current prepaid balance.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PrepaidBalance' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/billing/prepaid/top-up:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    post:
      tags: [billing]
      operationId: topUpPrepaid
      summary: >
        Credit the tenant's prepaid ledger. Requires billing.write. The riptide.billing.top_up_prepaid
        MCP tool requires human approval for amounts above 10000 settlement units; this is an MCP
        control rather than an HTTP approval workflow.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/PrepaidTopUpCreate' }
      responses:
        '201':
          description: Ledger entry applied (or idempotent replay).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PrepaidTopUpResponse' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/billing/quota-state:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [billing]
      operationId: getTenantQuotaState
      summary: >
        Read whether the tenant can serve: ok, quota_exceeded when daily requests reach
        request_quota_per_day, or suspended when the prepaid balance is exhausted. The serving plan uses
        this verdict. A tenant without a recorded verdict returns ok.
      responses:
        '200':
          description: Current quota / prepaid state.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TenantQuotaState' }
        default: { $ref: '#/components/responses/Error' }

  /v1/operator/fx/rates:
    get:
      tags: [billing, operator]
      operationId: listFxRates
      summary: >
        List cached USD multipliers by currency, with the source snapshot date and cache age. Operator
        access required. Results use cursor pagination by currency code. The age_seconds and stale
        fields describe the entire cache.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: Cached FX rates.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/FxRateList' }
        default: { $ref: '#/components/responses/Error' }

  /v1/operator/fx/refresh:
    post:
      tags: [billing, operator]
      operationId: refreshFxRates
      summary: >
        Refresh the currency-rate cache from the configured source immediately. Operator access
        required. Scheduled meter refreshes continue separately. Returns 412 when RIPTIDE_FX_SOURCE_URL
        is unset.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: The refreshed snapshot.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/FxRefreshResult' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/advertiser-invoices:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [billing]
      operationId: listAdvertiserInvoices
      summary: >
        List the tenant's advertiser invoices, newest period first, with cursor pagination. Use
        advertiser_id to filter by customer. Billing access is sufficient; the ad_server module
        entitlement is not required.
      x-riptide-rbac: [billing.read]
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - name: advertiser_id
          in: query
          required: false
          description: Only invoices issued to this advertiser.
          schema: { type: string, format: uuid }
        - name: status
          in: query
          required: false
          description: Only invoices in this lifecycle status.
          schema: { $ref: '#/components/schemas/InvoiceStatus' }
      responses:
        '200':
          description: A page of advertiser invoices.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/InvoiceList' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/advertiser-invoices/{invoice_id}:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/InvoiceId'
    get:
      tags: [billing]
      operationId: getAdvertiserInvoice
      summary: Get one advertiser invoice with its totals and proof of performance.
      x-riptide-rbac: [billing.read]
      responses:
        '200':
          description: The advertiser invoice.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Invoice' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/invoices:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [billing]
      operationId: listInvoices
      summary: List platform invoices for a tenant (newest first).
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of invoices.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PlatformInvoiceList' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/invoices/{invoice_id}:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - name: invoice_id
        in: path
        required: true
        schema: { type: string, format: uuid }
    get:
      tags: [billing]
      operationId: getInvoice
      summary: Get one platform invoice by id (tenant-scoped).
      responses:
        '200':
          description: The invoice.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PlatformInvoice' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/invoices/{invoice_id}/transition:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/InvoiceId'
    post:
      tags: [billing]
      operationId: transitionInvoice
      summary: >
        Advance a platform invoice from DRAFT to REVIEW, ISSUED, and PAID, or mark it VOID before
        payment. Issuing assigns an invoice number, freezes the FX snapshot and tax lines, and starts
        payment collection. Payment-provider webhooks normally mark invoices PAID; manual payment
        marking is for off-platform payments. An invalid transition returns 409 CONFLICT with hint
        invoice_transition_illegal. The reason is audited.
      x-riptide-rbac: [billing.write]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/InvoiceTransitionRequest' }
      responses:
        '200':
          description: The invoice in its new status.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PlatformInvoice' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/invoices/{invoice_id}/document:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/InvoiceId'
    get:
      tags: [billing]
      operationId: getInvoiceDocument
      summary: >
        Render an invoice statement as HTML or PDF, using the same rendering as the emailed statement.
        Documents are available for ISSUED, PAID, and VOID invoices; VOID documents carry a watermark.
        DRAFT and REVIEW invoices return 409 CONFLICT.
      x-riptide-rbac: [billing.read]
      parameters:
        - name: format
          in: query
          required: false
          description: Document format; html when omitted.
          schema: { $ref: '#/components/schemas/InvoiceDocumentFormat' }
      responses:
        '200':
          description: The rendered statement.
          content:
            text/html:
              schema: { type: string }
            application/pdf:
              schema: { type: string, format: binary }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/invoices/{invoice_id}/credits:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/InvoiceId'
    post:
      tags: [billing]
      operationId: createInvoiceCredit
      summary: >
        Apply a credit that reduces an invoice's amount due to a minimum of zero. An ISSUED invoice
        becomes PAID when the remaining amount reaches zero. PAID or VOID invoices reject credits with
        409 CONFLICT. The reason is audited.
      x-riptide-rbac: [billing.write]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/InvoiceCreditCreate' }
      responses:
        '201':
          description: The recorded credit.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/InvoiceCredit' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/invoices/{invoice_id}/adjustments:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/InvoiceId'
    post:
      tags: [billing]
      operationId: createInvoiceAdjustment
      summary: >
        Add a positive or negative adjustment to a DRAFT or REVIEW invoice. After issuance, adjustments
        return 409 CONFLICT; use a credit instead. The reason is audited.
      x-riptide-rbac: [billing.write]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/InvoiceAdjustmentCreate' }
      responses:
        '201':
          description: The recorded adjustment.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/InvoiceAdjustment' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/invoices/{invoice_id}/dunning:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/InvoiceId'
    get:
      tags: [billing]
      operationId: getDunningState
      summary: >
        Read an issued invoice's payment collection status, including charge attempts, the next retry
        time, and whether serving was suspended for non-payment. Returns 404 for invoices that have not
        entered payment collection.
      x-riptide-rbac: [billing.read]
      responses:
        '200':
          description: The dunning state.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DunningState' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/wallet-bindings:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [billing]
      operationId: listWalletBindings
      summary: >
        List advertiser-to-prepaid-wallet bindings with cursor pagination.
      x-riptide-rbac: [billing.read]
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - name: advertiser_id
          in: query
          required: false
          description: Only the binding of this advertiser.
          schema: { type: string, format: uuid }
      responses:
        '200':
          description: A page of wallet bindings.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/WalletBindingList' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/wallet-bindings/{advertiser_id}:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/AdvertiserId'
    put:
      tags: [billing]
      operationId: setWalletBinding
      summary: >
        Create or replace an advertiser's prepaid wallet binding. Delivery reduces the wallet balance.
        With auto_pause enabled, the advertiser's line items pause when the balance reaches zero and
        resume after a top-up.
      x-riptide-rbac: [billing.write]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/WalletBindingSet' }
      responses:
        '200':
          description: The binding as stored.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/WalletBinding' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/vendor-settlements/run:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    post:
      tags: [billing]
      operationId: runVendorSettlement
      summary: >
        Start settlement for a period by combining publisher payout statements and partner costs, then
        submitting payable rows to the tenant's payout provider. Returns 202 while processing continues.
        A second run for the same period returns 409 CONFLICT while the previous run is active.
      x-riptide-rbac: [billing.write]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/VendorSettlementRunRequest' }
      responses:
        '202':
          description: The settlement run that was started.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/VendorSettlementRun' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/marketplace-settlements:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [billing, marketplace]
      operationId: listMarketplaceSettlements
      summary: >
        List amounts the tenant owes or is owed for each period, listing, and counterparty, based on
        path receipts net of fees. Results are newest period first and use cursor pagination.
      x-riptide-rbac: [billing.read]
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - name: period
          in: query
          required: false
          description: Only settlements of this calendar month (YYYY-MM).
          schema: { type: string, pattern: '^[0-9]{4}-(0[1-9]|1[0-2])$' }
        - name: listing_id
          in: query
          required: false
          description: Only settlements of this listing.
          schema: { type: string, format: uuid }
        - name: status
          in: query
          required: false
          description: Only settlements in this status.
          schema: { $ref: '#/components/schemas/MarketplaceSettlementStatus' }
      responses:
        '200':
          description: A page of marketplace settlements.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/MarketplaceSettlementList' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/marketplace-settlements/run:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    post:
      tags: [billing, marketplace]
      operationId: runMarketplaceSettlement
      summary: >
        Settle marketplace activity for a period using receipts added since the previous run. Writes a
        settlement row per listing and counterparty and advances the receipt cursor. Returns 202 while
        processing continues. Repeating a period processes only receipts not included in earlier runs.
      x-riptide-rbac: [billing.write]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/MarketplaceSettlementRunRequest' }
      responses:
        '202':
          description: The settlement run summary.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/MarketplaceSettlementRun' }
        default: { $ref: '#/components/responses/Error' }

  /v1/operator/rate-plans:
    get:
      tags: [billing, operator]
      operationId: listRatePlans
      summary: >
        List platform rate plans. Operator access required.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of rate plans.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PlatformRatePlanList' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [billing, operator]
      operationId: createRatePlan
      summary: >
        Add a platform rate plan. Operator access required. The name is unique; repeating a request with
        the same name returns the existing plan.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/RatePlanCreate' }
      responses:
        '201':
          description: Created (or the existing plan of that name).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PlatformRatePlan' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/users:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [iam]
      operationId: listUsers
      summary: List a tenant's console users.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of users.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/UserList' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [iam]
      operationId: createUser
      summary: Create a console user (email/name). Prefer invites for role assignment + magic link.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/UserCreate' }
      responses:
        '201':
          description: Created.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/User' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/invites:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [iam]
      operationId: listInvites
      summary: >
        List pending, expired, and revoked tenant invitations. Requires tenant_admin.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - name: status
          in: query
          required: false
          schema: { $ref: '#/components/schemas/InviteStatus' }
      responses:
        '200':
          description: A page of invites, newest first.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/InviteList' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [iam]
      operationId: createInvite
      summary: Invite a user by email with a role; sends a magic link (tenant_admin).
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/InviteCreate' }
      responses:
        '200':
          description: Invite accepted (enumerate-safe ok body).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AuthAccepted' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/invites/{invite_id}/resend:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/InviteId'
    post:
      tags: [iam]
      operationId: resendInvite
      summary: Re-issue a pending invite's magic link (a fresh challenge; the previous link stops working).
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: The re-issued invite.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Invite' }
        default: { $ref: '#/components/responses/Error' }
  /v1/tenants/{tenant_id}/invites/{invite_id}/revoke:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/InviteId'
    post:
      tags: [iam]
      operationId: revokeInvite
      summary: Revoke a pending invite; its magic link can no longer be redeemed.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: The revoked invite.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Invite' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/users/{user_id}:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/UserId'
    get:
      tags: [iam]
      operationId: getUser
      summary: Get a console user.
      responses:
        '200':
          description: The user.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/User' }
        default: { $ref: '#/components/responses/Error' }
    put:
      tags: [iam]
      operationId: updateUser
      summary: Update a console user's name and/or role set. email is immutable after creation.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/UserUpdate' }
      responses:
        '200':
          description: Updated.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/User' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/users/{user_id}/suspend:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/UserId'
    post:
      tags: [iam]
      operationId: suspendUser
      summary: >
        Suspend a console user.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: Suspended.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/User' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/users/{user_id}/reactivate:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/UserId'
    post:
      tags: [iam]
      operationId: reactivateUser
      summary: >
        Reactivate a suspended console user.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: Reactivated.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/User' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/users/status-counts:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [iam]
      operationId: getUserStatusCounts
      summary: >
        Per-status row counts for the tenant's console users.
      responses:
        '200':
          description: Status counts.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/StatusCounts' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/audiences:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [targeting]
      operationId: listAudiences
      summary: List a tenant's audiences (reusable targeting definitions).
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of audiences.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AudienceList' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [targeting]
      operationId: createAudience
      summary: >
        Create an audience with keys, duration, custom_rules, and inclusion and exclusion rule arrays.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/AudienceCreate' }
      responses:
        '201':
          description: Created.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Audience' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/audiences/{audience_id}:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/AudienceId'
    get:
      tags: [targeting]
      operationId: getAudience
      summary: Get an audience.
      responses:
        '200':
          description: The audience.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Audience' }
        default: { $ref: '#/components/responses/Error' }
    put:
      tags: [targeting]
      operationId: updateAudience
      summary: >
        Update an audience. Supplied rule arrays replace the complete stored array; omitted arrays
        remain unchanged.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/AudienceUpdate' }
      responses:
        '200':
          description: Updated.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Audience' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/audiences/{audience_id}/archive:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/AudienceId'
    post:
      tags: [targeting]
      operationId: archiveAudience
      summary: >
        Archive an audience.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: Archived.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Audience' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/audiences/status-counts:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [targeting]
      operationId: getAudienceStatusCounts
      summary: >
        Per-status row counts for the tenant's audiences.
      responses:
        '200':
          description: Status counts.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/StatusCounts' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/reports/catalog:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [reporting]
      operationId: getReportsCatalog
      summary: >
        Read available reporting metrics and dimensions, including units, descriptions, supported
        storage tiers, and query limits. Use this catalog to populate metric and dimension choices in
        clients.
      responses:
        '200':
          description: Catalog.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ReportCatalog' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/reports/run:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    post:
      tags: [reporting]
      operationId: runReport
      summary: >
        Run a report using the reporting catalog. The platform compiles the query and selects an
        eligible hot, warm, or cold table. With ClickHouse configured, it executes the query and returns
        rows. Otherwise, it returns the compiled query with no result rows.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ReportRunRequest' }
      responses:
        '200':
          description: Report result.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ReportRunResponse' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/reports/chat:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    post:
      tags: [reporting]
      operationId: reportingChat
      summary: >
        Ask the reporting assistant a read-only question. It uses permitted reporting and insight tools
        and returns supporting evidence. Numeric answers come from run_report, ask_question,
        compare_periods, or explain_change results. The assistant does not change configuration or
        accept free-form SQL.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ReportingChatRequest' }
      responses:
        '200':
          description: Assistant reply with evidence and optional ReportSpec to apply in Explore.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ReportingChatReply' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/reports/live:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [reporting]
      operationId: getReportsLive
      summary: >
        Read counters held by the current console process. These counters are not connected to the
        production event stream and remain empty unless updated within the process. For production
        traffic, use reports/run with ClickHouse configured through RIPTIDE_CLICKHOUSE_DSN.
      parameters:
        - name: placement_id
          in: query
          required: false
          description: Restrict the snapshot to one placement id.
          schema: { type: string }
      responses:
        '200':
          description: Live counter snapshot.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ReportLiveResponse' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/reports/stream:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [reporting]
      operationId: streamReportsLive
      summary: >
        Read the current process-local counters as a server-sent event. This operation sends one
        snapshot event and closes the connection. It uses the same counters and has the same
        production-data limitation as getReportsLive.
      responses:
        '200':
          description: One SSE `snapshot` event carrying the live counter snapshot.
          content:
            text/event-stream:
              schema: { type: string }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/attribution-config:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [reporting]
      operationId: getAttributionConfig
      summary: >
        Get the tenant's conversion attribution config (deterministic last-touch: click-through +
        view-through windows). When no row has been written yet, returns the platform defaults
        (7d click window, 1d view window, view-through enabled) with a nil id.
      responses:
        '200':
          description: The attribution config.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AttributionConfig' }
        default: { $ref: '#/components/responses/Error' }
    put:
      tags: [reporting]
      operationId: updateAttributionConfig
      summary: >
        Upsert the tenant's conversion attribution config (one row per tenant in v1). Omitted
        fields keep their current (or default) values.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/AttributionConfigUpdate' }
      responses:
        '200':
          description: Updated.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AttributionConfig' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/forecasts/availability:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    post:
      tags: [forecast]
      operationId: forecastAvailability
      summary: >
        Forecast inventory from recent request volume by placement and day of week, subtracting
        overlapping bookings with an equal or higher PriorityClass. This operation makes no changes.
        Empty history returns zero capacity with LOW confidence.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ForecastAvailabilityRequest' }
      responses:
        '200':
          description: Availability forecast metrics for the requested slice and range.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/AvailabilityForecastResult' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/forecasts/delivery:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    post:
      tags: [forecast]
      operationId: forecastDelivery
      summary: >
        Forecast delivery for a proposed flight using capacity available at its PriorityClass and even
        pacing. Deliverable volume is the smaller of availability and the goal; the response reports any
        shortfall. This operation makes no changes.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ForecastDeliveryRequest' }
      responses:
        '200':
          description: Delivery forecast metrics for the proposed flight.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DeliveryForecastResult' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/profiles/{user_key}:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/UserKey'
    get:
      tags: [targeting]
      operationId: getUserProfile
      summary: >
        Read a user profile from the cell profile store using an opaque user_key. Keep email addresses
        and phone numbers out of the identifier. Returns 404 when the profile is absent.
      responses:
        '200':
          description: Profile snapshot.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/UserProfile' }
        '404':
          description: Profile not found (miss or prior RTBF erase).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Error' }
        default: { $ref: '#/components/responses/Error' }
    put:
      tags: [targeting]
      operationId: upsertUserProfile
      summary: >
        Create or update profile attributes, tags, segments, and opt-out status for a tenant and
        user_key. This control-plane operation is separate from ad serving. Supports up to 64
        attributes, 32 tags, and 128 segments.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/UserProfileUpsert' }
      responses:
        '200':
          description: Upserted profile snapshot.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/UserProfile' }
        default: { $ref: '#/components/responses/Error' }
    delete:
      tags: [targeting]
      operationId: deleteUserProfile
      summary: >
        Right-to-be-forgotten erase for (tenant, user_key): clears attributes, tags,
        and segment membership. Subsequent GET misses. Idempotent on already-absent keys.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '204':
          description: Erased (or already absent).
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/conversions:ingest:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    post:
      tags: [reporting]
      operationId: ingestConversions
      summary: >
        Ingest server-side conversion events from an advertiser's backend. Each accepted event is
        validated and published as a CONVERSION event subject to the tenant's privacy controls. Rejected
        rows are reported by index; accepted rows continue processing. Returns 202. Identical batches
        are deduplicated by their SHA-256 digest.
      x-riptide-rbac: [reporting.write]
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ConversionIngestBatch' }
      responses:
        '202':
          description: The batch receipt (accepted / rejected counts and the batch digest).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ConversionIngestResult' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/conversion-tag:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [reporting]
      operationId: getConversionTag
      summary: >
        Return conversion pixel HTML and server-to-server endpoint templates for the tenant. The
        snippets use the sealed click_ref join key, rt_clkref or clkref, and exclude personally
        identifying data.
      responses:
        '200':
          description: Conversion tag / pixel snippets.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ConversionTag' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/webhooks:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [webhooks]
      operationId: listWebhookSubscriptions
      summary: >
        List the tenant's webhook subscriptions. Signing secrets are excluded from the response.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of webhook subscriptions.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/WebhookSubscriptionList' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [webhooks]
      operationId: createWebhookSubscription
      summary: >
        Create a webhook subscription with HMAC-signed delivery and retries.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/WebhookSubscriptionCreate' }
      responses:
        '201':
          description: Created. The response includes secret exactly once; it is never returned again.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/WebhookSubscriptionCreated' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/fee-schedules/preview:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    post:
      tags: [billing]
      operationId: previewFeeSchedule
      summary: >
        Preview which saved fee schedule applies to specified scope IDs and a fee kind, using the same
        precedence rules as the serving plan. This operation makes no changes.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/FeeSchedulePreviewRequest' }
      responses:
        '200':
          description: The resolved entry, if any.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/FeeSchedulePreviewResponse' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/webhooks/{webhook_subscription_id}:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/WebhookSubscriptionId'
    get:
      tags: [webhooks]
      operationId: getWebhookSubscription
      summary: >
        Read a webhook subscription. Its signing secret is excluded from the response.
      responses:
        '200':
          description: The subscription.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/WebhookSubscription' }
        default: { $ref: '#/components/responses/Error' }
    put:
      tags: [webhooks]
      operationId: updateWebhookSubscription
      summary: Update a webhook subscription's URL, subscribed events, or active flag.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/WebhookSubscriptionUpdate' }
      responses:
        '200':
          description: Updated.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/WebhookSubscription' }
        default: { $ref: '#/components/responses/Error' }
    delete:
      tags: [webhooks]
      operationId: deleteWebhookSubscription
      summary: Delete a webhook subscription.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '204':
          description: Deleted.
        default: { $ref: '#/components/responses/Error' }

  # SR-1214 durable webhooks: delivery history (webhook_outbox), replay, secret rotation.
  /v1/tenants/{tenant_id}/webhooks/{webhook_subscription_id}/deliveries:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/WebhookSubscriptionId'
    get:
      tags: [webhooks]
      operationId: listWebhookDeliveries
      summary: >
        List webhook deliveries, newest first, with cursor pagination. Each record includes its attempt
        count, last status code or error, and next retry time. Deliveries that exhaust their retries
        have status DEAD.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of deliveries.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/WebhookDeliveryList' }
        default: { $ref: '#/components/responses/Error' }
  /v1/tenants/{tenant_id}/webhooks/{webhook_subscription_id}/deliveries/{webhook_delivery_id}/replay:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/WebhookSubscriptionId'
      - $ref: '#/components/parameters/WebhookDeliveryId'
    post:
      tags: [webhooks]
      operationId: replayWebhookDelivery
      summary: >
        Re-enqueue a delivery (DELIVERED or DEAD) as a fresh PENDING delivery with a new delivery
        id and replay_of pointing at the original. The original row is left untouched; the new
        one goes through the normal retry policy.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '201':
          description: The new delivery.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/WebhookDelivery' }
        default: { $ref: '#/components/responses/Error' }
  /v1/tenants/{tenant_id}/webhooks/{webhook_subscription_id}/rotate-secret:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/WebhookSubscriptionId'
    post:
      tags: [webhooks]
      operationId: rotateWebhookSecret
      summary: >
        Rotate the subscription's signing secret. The response carries the new secret exactly
        once. For grace_seconds (default 86400, max 604800) deliveries are signed under both the
        new and the previous secret (multiple v1 entries in X-Riptide-Webhook-Signature-256), so a
        receiver can switch keys without a verification gap.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: false
        content:
          application/json:
            schema: { $ref: '#/components/schemas/WebhookSecretRotateRequest' }
      responses:
        '200':
          description: Rotated; the new secret is returned once.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/WebhookSecretRotated' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/report-configs:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [reporting]
      operationId: listReportConfigs
      summary: >
        List the tenant's saved report definitions.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of report configs.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ReportConfigList' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [reporting]
      operationId: createReportConfig
      summary: >
        Save reporting metrics, dimensions, and grain under a tenant-unique name. The saved definition
        can be run again or scheduled for delivery.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ReportConfigCreate' }
      responses:
        '201':
          description: Created.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ReportConfig' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/report-configs/{report_config_id}:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/ReportConfigId'
    get:
      tags: [reporting]
      operationId: getReportConfig
      summary: Get a saved report config.
      responses:
        '200':
          description: The report config.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ReportConfig' }
        default: { $ref: '#/components/responses/Error' }
    put:
      tags: [reporting]
      operationId: updateReportConfig
      summary: Replace a saved report config's name/metrics/dimensions/grain.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ReportConfigUpdate' }
      responses:
        '200':
          description: Updated.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ReportConfig' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/report-configs/{report_config_id}/archive:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/ReportConfigId'
    post:
      tags: [reporting]
      operationId: archiveReportConfig
      summary: >
        Archive a saved report config.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: Archived.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ReportConfig' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/scheduled-reports:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [reporting]
      operationId: listScheduledReports
      summary: >
        List the tenant's scheduled report deliveries.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of scheduled reports.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduledReportList' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [reporting]
      operationId: createScheduledReport
      summary: >
        Schedule delivery of a saved report to an HTTP destination. Results use the same HMAC signing
        convention as tenant webhooks. The signing secret is returned once in the creation response.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ScheduledReportCreate' }
      responses:
        '201':
          description: Created. The response includes destination_secret exactly once.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduledReportCreated' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/scheduled-reports/{scheduled_report_id}:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/ScheduledReportId'
    get:
      tags: [reporting]
      operationId: getScheduledReport
      summary: >
        Read a scheduled report. The destination_secret is excluded from the response.
      responses:
        '200':
          description: The scheduled report.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduledReport' }
        default: { $ref: '#/components/responses/Error' }
    put:
      tags: [reporting]
      operationId: updateScheduledReport
      summary: Update a scheduled report's cadence or destination URL.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ScheduledReportUpdate' }
      responses:
        '200':
          description: Updated.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduledReport' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/scheduled-reports/{scheduled_report_id}/archive:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/ScheduledReportId'
    post:
      tags: [reporting]
      operationId: archiveScheduledReport
      summary: Archive a scheduled report (lifecycle; stops it from ever being due again).
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: Archived.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduledReport' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/scheduled-reports/{scheduled_report_id}/run:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/ScheduledReportId'
    post:
      tags: [reporting]
      operationId: runScheduledReport
      summary: >
        Run a saved report immediately, regardless of next_run_at, and send the signed result to its
        destination_url. The query uses the same execution as reports/run. A successful delivery updates
        last_run_at and next_run_at.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: Delivery attempt result.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduledReportRunResult' }
        default: { $ref: '#/components/responses/Error' }

  # ---- SR-1011 inventory tooling: platforms, web-domain inventory, app CSV, store lookup ----

  /v1/platforms:
    get:
      tags: [supply]
      operationId: listPlatforms
      summary: >
        List the shared device and app-store platform catalog used by App.platform_id and app-store URL
        lookups. This catalog is available across tenants.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: The platform catalog, name-sorted.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PlatformList' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/inventory-domains:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [supply]
      operationId: listInventoryDomains
      summary: >
        List the tenant's web-domain inventory.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - name: publisher_id
          in: query
          required: false
          schema: { type: string, format: uuid }
          description: Only domains owned by this publisher.
      responses:
        '200':
          description: A page of inventory domains.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/InventoryDomainList' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [supply]
      operationId: createInventoryDomain
      summary: Create a web-domain inventory record (floor, cost model, categories, ads.txt location).
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/InventoryDomainCreate' }
      responses:
        '201':
          description: Created.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/InventoryDomain' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/inventory-domains/status-counts:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [supply]
      operationId: getInventoryDomainStatusCounts
      summary: Per-status row counts for inventory domains.
      responses:
        '200':
          description: Counts keyed by status.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/StatusCounts' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/inventory-domains/csv:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [supply]
      operationId: exportInventoryDomainsCsv
      summary: >
        Export every non-archived inventory domain as CSV. The header is the exact header
        importInventoryDomainsCsv accepts, so an export → edit → import round trip is lossless.
      responses:
        '200':
          description: CSV document.
          content:
            text/csv:
              schema: { type: string }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/inventory-domains/csv-import:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    post:
      tags: [supply]
      operationId: importInventoryDomainsCsv
      summary: >
        Import inventory domains from CSV (header hostname,publisher_id,site_name,ads_txt_url,
        floor,cost_type,cost_value,categories,keywords). Rows keyed by hostname upsert: an existing
        hostname is updated, a new one created. dry_run reports what would happen without
        persisting anything. Per-row failures never abort the batch.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CsvImportRequest' }
      responses:
        '200':
          description: Per-row outcome.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CsvImportResponse' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/inventory-domains/{inventory_domain_id}:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/InventoryDomainId'
    get:
      tags: [supply]
      operationId: getInventoryDomain
      summary: Get one inventory domain.
      responses:
        '200':
          description: The inventory domain.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/InventoryDomain' }
        default: { $ref: '#/components/responses/Error' }
    put:
      tags: [supply]
      operationId: updateInventoryDomain
      summary: >
        Update an inventory domain. Omitted fields retain their stored values.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/InventoryDomainUpdate' }
      responses:
        '200':
          description: Updated.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/InventoryDomain' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/inventory-domains/{inventory_domain_id}/archive:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/InventoryDomainId'
    post:
      tags: [supply]
      operationId: archiveInventoryDomain
      summary: Archive an inventory domain.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: Archived.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/InventoryDomain' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/apps/csv:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [supply]
      operationId: exportAppsCsv
      summary: >
        Export every non-archived app as CSV (header bundle_id,store_id,name,platform,domain,
        language,store_urls,categories,max_ad_duration). Lossless round trip with importAppsCsv.
      responses:
        '200':
          description: CSV document.
          content:
            text/csv:
              schema: { type: string }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/apps/csv-import:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    post:
      tags: [supply]
      operationId: importAppsCsv
      summary: >
        Import apps from CSV. Rows keyed by bundle_id (or store_id when bundle_id is empty)
        upsert; platform is a platform catalog name (GET /v1/platforms); store_urls and
        categories are pipe-separated. dry_run validates without persisting.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CsvImportRequest' }
      responses:
        '200':
          description: Per-row outcome.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CsvImportResponse' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/apps/store-lookup:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    post:
      tags: [supply]
      operationId: lookupAppStoreUrl
      summary: >
        Look up app metadata from a supported public app-store URL. The platform and store ID are parsed
        from the URL; name, bundle, and categories are fetched when the store provides a public lookup
        API. HTML pages are not scraped. Nothing is saved; pass the result to createApp or
        importAppsCsv.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/StoreLookupRequest' }
      responses:
        '200':
          description: Parsed / resolved metadata.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/StoreLookupResult' }
        default: { $ref: '#/components/responses/Error' }

  # ---- SR-1011 API-key management (iam) ----

  /v1/tenants/{tenant_id}/api-keys:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [iam]
      operationId: listApiKeys
      summary: >
        List API key prefixes, roles, scopes, and status for the tenant. Key secrets are excluded.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of API keys.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ApiKeyList' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [iam]
      operationId: createApiKey
      summary: >
        Mint a tenant API key granting one or more tenant roles (optionally bound to one
        publisher or advertiser for persona roles). Only the SHA-256 hash is stored; the
        plaintext secret is returned exactly once in the create response.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ApiKeyCreate' }
      responses:
        '201':
          description: Created. The response includes secret exactly once.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ApiKeyCreated' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/api-keys/{api_key_id}:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/ApiKeyId'
    get:
      tags: [iam]
      operationId: getApiKey
      summary: Get one API key's metadata.
      responses:
        '200':
          description: The API key (no secret).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ApiKey' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/api-keys/{api_key_id}/revoke:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/ApiKeyId'
    post:
      tags: [iam]
      operationId: revokeApiKey
      summary: Revoke an API key. Revocation is immediate and permanent; the row is kept for audit.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '200':
          description: Revoked (idempotent).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ApiKey' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/api-keys/{api_key_id}/rotate:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/ApiKeyId'
    post:
      tags: [iam]
      operationId: rotateApiKey
      summary: >
        Create a successor API key with the same name, roles, persona binding, and scopes. Its plaintext
        secret is returned once. The previous key expires after the specified grace period. Only ACTIVE,
        unexpired keys can rotate; other keys return 409 CONFLICT. A retry with the same idempotency key
        and payload returns the same successor with an empty secret. A changed payload returns 409
        idempotency_key_reused.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
        - name: grace
          in: query
          required: false
          description: >
            How long the previous key keeps authenticating after the rotation, as a duration
            string such as `24h`, `90m` or `2h30m`. Omitted = the platform default grace; a value
            above the platform maximum is rejected 400.
          schema: { type: string, maxLength: 32 }
      responses:
        '201':
          description: Rotated. The response includes the successor's secret exactly once.
          headers:
            Cache-Control:
              description: Always `no-store` — the secret must never be served from a cache.
              schema: { type: string }
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ApiKeyRotated' }
        default: { $ref: '#/components/responses/Error' }

  # ---- SR-1010 publisher persona: publisher-scoped portal surface ----

  /v1/tenants/{tenant_id}/publishers/{publisher_id}/placements:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/PublisherId'
    get:
      tags: [supply]
      operationId: listPublisherPlacements
      summary: >
        Placements owned by this publisher (Placement.publisher_id). A publisher-role actor may
        only call this for its bound publisher_id.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of placements.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PlacementList' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/publishers/{publisher_id}/report-summary:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/PublisherId'
    get:
      tags: [reporting]
      operationId: getPublisherReportSummary
      summary: >
        Publisher portal dashboard tile: requests / impressions / fills / net revenue / publisher
        payout for this publisher over the trailing window, from the reporting semantic layer with
        the publisher filter injected server-side.
      parameters:
        - name: days
          in: query
          required: false
          schema: { type: integer, minimum: 1, maximum: 90, default: 7 }
      responses:
        '200':
          description: Publisher-scoped KPI summary.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PublisherReportSummary' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/publishers/{publisher_id}/reports/run:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/PublisherId'
    post:
      tags: [reporting]
      operationId: runPublisherReport
      summary: >
        Run a report using ReportRunRequest with the publisher's public_id enforced as a server-side
        filter. A supplied publisher filter naming another publisher returns BAD_INPUT.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ReportRunRequest' }
      responses:
        '200':
          description: Compiled (and, when a warehouse is configured, executed) report.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ReportRunResponse' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/publishers/{publisher_id}/report-configs:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/PublisherId'
    get:
      tags: [reporting]
      operationId: listPublisherReportConfigs
      summary: Saved report definitions scoped to this publisher (ReportConfig.publisher_id).
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of report configs.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ReportConfigList' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [reporting]
      operationId: createPublisherReportConfig
      summary: >
        Save a publisher-scoped report definition. The stored config carries publisher_id; every
        run (ad hoc or scheduled) injects the publisher filter.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ReportConfigCreate' }
      responses:
        '201':
          description: Created.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ReportConfig' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/publishers/{publisher_id}/scheduled-reports:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/PublisherId'
    get:
      tags: [reporting]
      operationId: listPublisherScheduledReports
      summary: Scheduled deliveries over this publisher's report configs.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of scheduled reports.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduledReportList' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [reporting]
      operationId: createPublisherScheduledReport
      summary: >
        Schedule delivery of one of this publisher's report configs. report_config_id must be a
        config scoped to the same publisher; the schedule inherits the scope.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ScheduledReportCreate' }
      responses:
        '201':
          description: Created. The response includes destination_secret exactly once.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ScheduledReportCreated' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/publishers/{publisher_id}/payout-statements:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/PublisherId'
    get:
      tags: [billing]
      operationId: listPublisherPayoutStatements
      summary: >
        List publisher payout statements, newest period first.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of statements.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PublisherPayoutStatementList' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [billing]
      operationId: generatePublisherPayoutStatement
      summary: >
        Close a period for this publisher: aggregate the reporting layer's publisher_payout leg
        over [period_start, period_end), apply the signed adjustment, and issue the statement
        through the configured payout provider. Idempotent per (publisher, period): a repeated
        close returns the existing statement.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/PublisherPayoutStatementGenerate' }
      responses:
        '201':
          description: Statement issued (or the existing one for that period).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PublisherPayoutStatement' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/publishers/{publisher_id}/payout-statements/{statement_id}:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/PublisherId'
      - $ref: '#/components/parameters/StatementId'
    get:
      tags: [billing]
      operationId: getPublisherPayoutStatement
      summary: Get one payout statement.
      responses:
        '200':
          description: The statement.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PublisherPayoutStatement' }
        default: { $ref: '#/components/responses/Error' }

  /v1/tenants/{tenant_id}/publishers/{publisher_id}/payout-statements/{statement_id}/csv:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/PublisherId'
      - $ref: '#/components/parameters/StatementId'
    get:
      tags: [billing]
      operationId: exportPublisherPayoutStatementCsv
      summary: >
        Download a publisher payout statement as CSV.
      responses:
        '200':
          description: CSV document.
          content:
            text/csv:
              schema: { type: string }
        default: { $ref: '#/components/responses/Error' }


  # ---- Wave 8 round 3 contract batch (Step 0) ----
  # SR-1221 tenant -> cells (docs/design/16-infrastructure.md "Tenant->cell routing seam";
  # migration 0113). Operator scope, like every /v1/operator/tenants/{tenant_id} operation.
  /v1/operator/tenants/{tenant_id}/cells:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [operator]
      operationId: getTenantCells
      summary: >
        Read the tenant's home cell and serving cells. Operator access required.
      responses:
        '200':
          description: The tenant's cell assignment.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TenantCells' }
        default: { $ref: '#/components/responses/Error' }
    put:
      tags: [operator]
      operationId: setTenantCells
      summary: >
        Replace a tenant's cell assignment: the home cell (control-plane residency) and the full
        serving-cell set (the home cell is always a serving cell; it is added when omitted).
        The next compile of every affected cell carries or drops the tenant. Operator only.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/TenantCellsUpdate' }
      responses:
        '200':
          description: The stored assignment.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TenantCells' }
        default: { $ref: '#/components/responses/Error' }
  /v1/operator/cells/{cell_id}:
    parameters:
      - $ref: '#/components/parameters/CellId'
    get:
      tags: [operator]
      operationId: getCell
      summary: >
        Read a cell's registry record. Operator access required.
      responses:
        '200':
          description: The cell.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Cell' }
        default: { $ref: '#/components/responses/Error' }
  /v1/operator/cells/{cell_id}/tenants:
    parameters:
      - $ref: '#/components/parameters/CellId'
    get:
      tags: [operator]
      operationId: listCellTenants
      summary: >
        List tenants assigned to serve in the cell, sorted by tenant ID. This is the tenant set included
        in the cell's next serving-plan compile. Operator access required.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of tenant references.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/TenantCellRefList' }
        default: { $ref: '#/components/responses/Error' }

  # SR-1218 demand-partner approvals (docs/design/06-demand.md "Outbound request construction";
  # migration 0112). Bundle approvals live on App.route_approvals (app_route_approval); domain
  # approvals are the site-side twin. The CSV round-trips both.
  /v1/tenants/{tenant_id}/demand-partners/{demand_partner_id}/domain-approvals:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/DemandPartnerId'
    get:
      tags: [demand]
      operationId: listDemandPartnerDomainApprovals
      summary: A partner's site-domain approvals (demand_partner_domain_approval), sorted by domain.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - name: status
          in: query
          required: false
          schema: { $ref: '#/components/schemas/ApprovalStatus' }
      responses:
        '200':
          description: A page of domain approvals.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DomainApprovalList' }
        default: { $ref: '#/components/responses/Error' }
  /v1/tenants/{tenant_id}/demand-partners/{demand_partner_id}/domain-approvals/{domain}:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/DemandPartnerId'
      - $ref: '#/components/parameters/ApprovalDomain'
    put:
      tags: [demand]
      operationId: setDemandPartnerDomainApproval
      summary: >
        Upsert one site-domain approval for the partner (PENDING | APPROVED | REJECTED). APPROVED
        rows project onto the plan (DemandPartner.approved_domains) at the next compile.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/DomainApprovalUpdate' }
      responses:
        '200':
          description: The stored approval.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/DomainApproval' }
        default: { $ref: '#/components/responses/Error' }
    delete:
      tags: [demand]
      operationId: deleteDemandPartnerDomainApproval
      summary: Remove one site-domain approval (the domain reverts to "no record").
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '204': { description: Removed (or never existed). }
        default: { $ref: '#/components/responses/Error' }
  /v1/tenants/{tenant_id}/demand-partners/{demand_partner_id}/approval-counts:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/DemandPartnerId'
    get:
      tags: [demand]
      operationId: getDemandPartnerApprovalCounts
      summary: >
        Read per-partner approval counts by status for app bundles and site domains.
      responses:
        '200':
          description: Counts.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ApprovalCounts' }
        default: { $ref: '#/components/responses/Error' }
  /v1/tenants/{tenant_id}/demand-partners/{demand_partner_id}/approvals/csv:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/DemandPartnerId'
    get:
      tags: [demand]
      operationId: exportDemandPartnerApprovalsCsv
      summary: >
        Export a partner's approvals as CSV (header kind,key,status,note): one row per app
        bundle approval (kind=bundle, key=bundle id) and per site-domain approval (kind=domain,
        key=domain). Lossless round trip with importDemandPartnerApprovalsCsv.
      responses:
        '200':
          description: CSV document.
          content:
            text/csv:
              schema: { type: string }
        default: { $ref: '#/components/responses/Error' }
  /v1/tenants/{tenant_id}/demand-partners/{demand_partner_id}/approvals/csv-import:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/DemandPartnerId'
    post:
      tags: [demand]
      operationId: importDemandPartnerApprovalsCsv
      summary: >
        Import approvals from CSV (header kind,key,status,note). kind=bundle rows upsert the
        app_route_approval row of the tenant's app with that bundle id (an unknown bundle is an
        error row); kind=domain rows upsert demand_partner_domain_approval. dry_run validates
        without persisting.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CsvImportRequest' }
      responses:
        '200':
          description: Per-row outcome.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CsvImportResponse' }
        default: { $ref: '#/components/responses/Error' }

  # SR-1212 crawler tunables and the partner sellers-list import (docs/design/06-demand.md
  # "Supply chain"; migration 0110).
  /v1/tenants/{tenant_id}/supply-transparency/crawler-config:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [supply_transparency]
      operationId: getCrawlerConfig
      summary: >
        Read the tenant's ads.txt and app-ads.txt crawler settings. Returns platform defaults when no
        settings have been saved.
      responses:
        '200':
          description: Effective crawler config.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CrawlerConfig' }
        default: { $ref: '#/components/responses/Error' }
    put:
      tags: [supply_transparency]
      operationId: updateCrawlerConfig
      summary: Upsert the tenant's crawler tunables (partial update by presence).
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/CrawlerConfigUpdate' }
      responses:
        '200':
          description: Effective crawler config after the update.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/CrawlerConfig' }
        default: { $ref: '#/components/responses/Error' }
  /v1/tenants/{tenant_id}/demand-partners/{demand_partner_id}/sellers-imports:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/DemandPartnerId'
    get:
      tags: [demand]
      operationId: listPartnerSellersImports
      summary: Imports of the partner's sellers.json (partner_sellers_import), newest first.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
      responses:
        '200':
          description: A page of imports (without rows).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PartnerSellersImportList' }
        default: { $ref: '#/components/responses/Error' }
  /v1/tenants/{tenant_id}/demand-partners/{demand_partner_id}/sellers-imports/{import_id}:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/DemandPartnerId'
      - $ref: '#/components/parameters/PartnerSellersImportId'
    get:
      tags: [demand]
      operationId: getPartnerSellersImport
      summary: One sellers.json import with its parsed rows and compliance verdicts.
      responses:
        '200':
          description: The import.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PartnerSellersImport' }
        default: { $ref: '#/components/responses/Error' }
  /v1/tenants/{tenant_id}/demand-partners/{demand_partner_id}/sellers-import/run:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/DemandPartnerId'
    post:
      tags: [demand]
      operationId: runPartnerSellersImport
      summary: >
        Create a PENDING sellers.json import with a source URL. The daily import job fetches, parses,
        and checks the document, then updates its status, counts, and rows. Returns the created import
        record.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/PartnerSellersImportRunRequest' }
      responses:
        '201':
          description: The recorded import (status PENDING until the tick runs it).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PartnerSellersImport' }
        default: { $ref: '#/components/responses/Error' }

  # SR-1213 program-guide content store (riptide.content.v1; docs/design/14-enrichment.md
  # "Enrichment pipeline"; migration 0111).
  /v1/tenants/{tenant_id}/content-programs:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [targeting]
      operationId: listContentPrograms
      summary: A tenant's program-guide entries (content_program), sorted by id; filter by provider / channel.
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Cursor'
        - name: provider
          in: query
          required: false
          schema: { type: string }
        - name: channel
          in: query
          required: false
          schema: { type: string }
      responses:
        '200':
          description: A page of programs.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ContentProgramList' }
        default: { $ref: '#/components/responses/Error' }
    post:
      tags: [targeting]
      operationId: createContentProgram
      summary: Add a program-guide entry; (provider, external_id) is unique per tenant (409 on a duplicate).
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ContentProgramCreate' }
      responses:
        '201':
          description: Created.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ContentProgram' }
        default: { $ref: '#/components/responses/Error' }
  /v1/tenants/{tenant_id}/content-programs/dictionary:
    parameters:
      - $ref: '#/components/parameters/TenantId'
    get:
      tags: [targeting]
      operationId: getContentDictionary
      summary: >
        List distinct genres, keywords, providers, and channels in the tenant's program guide, with
        program counts. These values can be used in targeting rules and the content_keyword and
        content_genre report dimensions.
      responses:
        '200':
          description: Dictionary.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ContentDictionary' }
        default: { $ref: '#/components/responses/Error' }
  /v1/tenants/{tenant_id}/content-programs/{program_id}:
    parameters:
      - $ref: '#/components/parameters/TenantId'
      - $ref: '#/components/parameters/ContentProgramId'
    get:
      tags: [targeting]
      operationId: getContentProgram
      summary: One program-guide entry.
      responses:
        '200':
          description: The program.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ContentProgram' }
        default: { $ref: '#/components/responses/Error' }
    put:
      tags: [targeting]
      operationId: updateContentProgram
      summary: Partial update by presence; arrays present replace the whole set.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/ContentProgramUpdate' }
      responses:
        '200':
          description: The updated program.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ContentProgram' }
        default: { $ref: '#/components/responses/Error' }
    delete:
      tags: [targeting]
      operationId: deleteContentProgram
      summary: Remove a program-guide entry.
      parameters:
        - $ref: '#/components/parameters/IdempotencyKey'
      responses:
        '204': { description: Removed. }
        default: { $ref: '#/components/responses/Error' }

  /v1/webhooks/payment/{provider}:
    parameters:
      - name: provider
        in: path
        required: true
        description: Registered payment-provider adapter key (libs/billing payment seam).
        schema: { type: string, pattern: '^[a-z][a-z0-9_]{1,31}$' }
    post:
      tags: [billing, webhooks]
      operationId: paymentProviderWebhook
      summary: >
        Receive a payment-provider event for a successful or failed payment, refund, or dispute.
        Authentication uses the provider's signature header and secret to verify the raw request body
        before parsing; missing or invalid signatures return 401 UNAUTHORIZED. Bearer credentials and
        API keys are not used. Each provider and external_id pair is recorded once and applied to its
        invoice. Successful payments mark invoices PAID; failed charges advance payment collection.
        Duplicate events return 200 with duplicate: true and make no changes. The provider should retry
        5xx responses. Idempotency-Key is not required.
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/PaymentProviderEvent' }
      responses:
        '200':
          description: Event recorded (or recognised as a redelivery).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/PaymentWebhookAck' }
        default: { $ref: '#/components/responses/Error' }

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: Opaque console session (`rt_sess_…`) or a signed JWT (libs/authz).
    apiKeyAuth:
      type: apiKey
      in: header
      name: X-Riptide-Api-Key
      description: >
        Durable tenant / operator API key minted by createApiKey (`rt_key_…`) or a bootstrap key
        from RIPTIDE_API_KEYS_JSON. Resolved by services/console rbacMiddleware.
  parameters:
    TenantId:
      description: The tenant the resource belongs to (the path is the tenant boundary; a body never names a tenant).
      example: "7a1d3c5e-2b4f-4c6d-8e9f-0a1b2c3d4e5f"
      name: tenant_id
      in: path
      required: true
      schema: { type: string, format: uuid }
    PublisherId:
      description: Publisher id (uuid; the public id is a separate, human-chosen key).
      example: "7a1d3c5e-2b4f-4c6d-8e9f-0a1b2c3d4e5f"
      name: publisher_id
      in: path
      required: true
      schema: { type: string, format: uuid }
    InviteId:
      description: Invite id.
      example: "7a1d3c5e-2b4f-4c6d-8e9f-0a1b2c3d4e5f"
      name: invite_id
      in: path
      required: true
      schema: { type: string, format: uuid }
    DeliveryAlertEventId:
      description: Delivery alert event id.
      example: "7a1d3c5e-2b4f-4c6d-8e9f-0a1b2c3d4e5f"
      name: event_id
      in: path
      required: true
      schema: { type: string, format: uuid }
    ReconciliationStatementId:
      description: Reconciliation statement id.
      example: "7a1d3c5e-2b4f-4c6d-8e9f-0a1b2c3d4e5f"
      name: statement_id
      in: path
      required: true
      schema: { type: string, format: uuid }
    ReconciliationReportId:
      description: Reconciliation report id.
      example: "7a1d3c5e-2b4f-4c6d-8e9f-0a1b2c3d4e5f"
      name: report_id
      in: path
      required: true
      schema: { type: string, format: uuid }
    PlacementId:
      description: Placement id (uuid; the public id is the tag key).
      example: "7a1d3c5e-2b4f-4c6d-8e9f-0a1b2c3d4e5f"
      name: placement_id
      in: path
      required: true
      schema: { type: string, format: uuid }
    CampaignOrderId:
      description: Campaign order id.
      example: "7a1d3c5e-2b4f-4c6d-8e9f-0a1b2c3d4e5f"
      name: campaign_order_id
      in: path
      required: true
      schema: { type: string, format: uuid }
    LineItemId:
      description: Line item id.
      example: "7a1d3c5e-2b4f-4c6d-8e9f-0a1b2c3d4e5f"
      name: line_item_id
      in: path
      required: true
      schema: { type: string, format: uuid }
    CreativeId:
      description: Creative id.
      example: "7a1d3c5e-2b4f-4c6d-8e9f-0a1b2c3d4e5f"
      name: creative_id
      in: path
      required: true
      schema: { type: string, format: uuid }
    CreativeMediaUploadId:
      example: "7a1d3c5e-2b4f-4c6d-8e9f-0a1b2c3d4e5f"
      name: upload_id
      in: path
      required: true
      description: Media upload record id (SR-307), minted by createCreativeMediaUpload.
      schema: { type: string, format: uuid }
    AudienceId:
      description: Audience id.
      example: "7a1d3c5e-2b4f-4c6d-8e9f-0a1b2c3d4e5f"
      name: audience_id
      in: path
      required: true
      schema: { type: string, format: uuid }
    DemandPartnerId:
      description: Demand partner id.
      example: "7a1d3c5e-2b4f-4c6d-8e9f-0a1b2c3d4e5f"
      name: demand_partner_id
      in: path
      required: true
      schema: { type: string, format: uuid }
    AppId:
      description: App id.
      example: "7a1d3c5e-2b4f-4c6d-8e9f-0a1b2c3d4e5f"
      name: app_id
      in: path
      required: true
      schema: { type: string, format: uuid }
    AppLookupCacheKey:
      example: "acme-pub%7Ccom.example.app"
      name: cache_key
      in: path
      required: true
      description: >
        App-resolution cache key (publisher_public_id|bundle_id). Path-encode `|` as `%7C`.
      schema: { type: string, minLength: 1 }
    VerificationVendorConfigId:
      description: Verification vendor configuration id.
      example: "7a1d3c5e-2b4f-4c6d-8e9f-0a1b2c3d4e5f"
      name: config_id
      in: path
      required: true
      schema: { type: string, format: uuid }
    IpListId:
      description: IP list id.
      example: "7a1d3c5e-2b4f-4c6d-8e9f-0a1b2c3d4e5f"
      name: list_id
      in: path
      required: true
      schema: { type: string, format: uuid }
    BidModifierId:
      description: Bid modifier id.
      example: "7a1d3c5e-2b4f-4c6d-8e9f-0a1b2c3d4e5f"
      name: modifier_id
      in: path
      required: true
      schema: { type: string, format: uuid }
    CustomListId:
      description: Custom list id.
      example: "7a1d3c5e-2b4f-4c6d-8e9f-0a1b2c3d4e5f"
      name: list_id
      in: path
      required: true
      schema: { type: string, format: uuid }
    CreativeTemplateId:
      description: Creative template id.
      example: "7a1d3c5e-2b4f-4c6d-8e9f-0a1b2c3d4e5f"
      name: template_id
      in: path
      required: true
      schema: { type: string, format: uuid }
    DeliveryExperimentId:
      description: Delivery experiment id.
      example: "7a1d3c5e-2b4f-4c6d-8e9f-0a1b2c3d4e5f"
      name: experiment_id
      in: path
      required: true
      schema: { type: string, format: uuid }
    DeliveryAlertRuleId:
      description: Delivery alert rule id.
      example: "7a1d3c5e-2b4f-4c6d-8e9f-0a1b2c3d4e5f"
      name: rule_id
      in: path
      required: true
      schema: { type: string, format: uuid }
    ItemCatalogId:
      description: Item catalog id.
      example: "7a1d3c5e-2b4f-4c6d-8e9f-0a1b2c3d4e5f"
      name: catalog_id
      in: path
      required: true
      schema: { type: string, format: uuid }
    ExportDestinationId:
      description: Export destination id.
      example: "7a1d3c5e-2b4f-4c6d-8e9f-0a1b2c3d4e5f"
      name: destination_id
      in: path
      required: true
      schema: { type: string, format: uuid }
    ExportJobId:
      description: Export job id.
      example: "7a1d3c5e-2b4f-4c6d-8e9f-0a1b2c3d4e5f"
      name: job_id
      in: path
      required: true
      schema: { type: string, format: uuid }
    SupplyDocumentCheckId:
      description: Supply document check id.
      example: "7a1d3c5e-2b4f-4c6d-8e9f-0a1b2c3d4e5f"
      name: check_id
      in: path
      required: true
      schema: { type: string, format: uuid }
    SupplyDocumentSnapshotId:
      description: Supply document snapshot id.
      example: "7a1d3c5e-2b4f-4c6d-8e9f-0a1b2c3d4e5f"
      name: snapshot_id
      in: path
      required: true
      schema: { type: string, format: uuid }
    DemandRouteId:
      description: Demand route id.
      example: "7a1d3c5e-2b4f-4c6d-8e9f-0a1b2c3d4e5f"
      name: demand_route_id
      in: path
      required: true
      schema: { type: string, format: uuid }
    DealId:
      description: Deal id.
      example: "7a1d3c5e-2b4f-4c6d-8e9f-0a1b2c3d4e5f"
      name: deal_id
      in: path
      required: true
      schema: { type: string, format: uuid }
    MarketplaceId:
      description: Marketplace id.
      example: "7a1d3c5e-2b4f-4c6d-8e9f-0a1b2c3d4e5f"
      name: marketplace_id
      in: path
      required: true
      schema: { type: string, format: uuid }
    MarketplaceListingId:
      description: Marketplace listing id.
      example: "7a1d3c5e-2b4f-4c6d-8e9f-0a1b2c3d4e5f"
      name: listing_id
      in: path
      required: true
      schema: { type: string, format: uuid }
    AdvertiserId:
      description: Advertiser id.
      example: "7a1d3c5e-2b4f-4c6d-8e9f-0a1b2c3d4e5f"
      name: advertiser_id
      in: path
      required: true
      schema: { type: string, format: uuid }
    FeeScheduleId:
      description: Fee schedule id.
      example: "7a1d3c5e-2b4f-4c6d-8e9f-0a1b2c3d4e5f"
      name: fee_schedule_id
      in: path
      required: true
      schema: { type: string, format: uuid }
    ReportConfigId:
      description: Report config id.
      example: "7a1d3c5e-2b4f-4c6d-8e9f-0a1b2c3d4e5f"
      name: report_config_id
      in: path
      required: true
      schema: { type: string, format: uuid }
    ScheduledReportId:
      description: Scheduled report id.
      example: "7a1d3c5e-2b4f-4c6d-8e9f-0a1b2c3d4e5f"
      name: scheduled_report_id
      in: path
      required: true
      schema: { type: string, format: uuid }
    InventoryDomainId:
      description: Inventory domain id.
      example: "7a1d3c5e-2b4f-4c6d-8e9f-0a1b2c3d4e5f"
      name: inventory_domain_id
      in: path
      required: true
      schema: { type: string, format: uuid }
    ApiKeyId:
      description: API key id (the row, never the secret).
      example: "7a1d3c5e-2b4f-4c6d-8e9f-0a1b2c3d4e5f"
      name: api_key_id
      in: path
      required: true
      schema: { type: string, format: uuid }
    StatementId:
      description: Statement id.
      example: "7a1d3c5e-2b4f-4c6d-8e9f-0a1b2c3d4e5f"
      name: statement_id
      in: path
      required: true
      schema: { type: string, format: uuid }
    UserId:
      description: User id.
      example: "7a1d3c5e-2b4f-4c6d-8e9f-0a1b2c3d4e5f"
      name: user_id
      in: path
      required: true
      schema: { type: string, format: uuid }
    SealingKeyId:
      example: "acme-2026-09"
      name: key_id
      in: path
      required: true
      description: Opaque sealing key id embedded in sealed payloads (not the row uuid).
      schema: { type: string, minLength: 1 }
    WebhookSubscriptionId:
      description: Webhook subscription id.
      example: "7a1d3c5e-2b4f-4c6d-8e9f-0a1b2c3d4e5f"
      name: webhook_subscription_id
      in: path
      required: true
      schema: { type: string, format: uuid }
    WebhookDeliveryId:
      description: Webhook delivery id.
      example: "7a1d3c5e-2b4f-4c6d-8e9f-0a1b2c3d4e5f"
      name: webhook_delivery_id
      in: path
      required: true
      schema: { type: string, format: uuid }
    Limit:
      example: 100
      name: limit
      in: query
      required: false
      description: Page size (default 100, max 1000); larger values are clamped to 1000.
      schema: { type: integer, minimum: 1, maximum: 1000, default: 100 }
    Cursor:
      example: "next_01J9Z3Q7R8S9T0V1W2X3Y4Z5A6"
      name: cursor
      in: query
      required: false
      description: >
        Opaque keyset cursor from the previous page's `next_cursor`. Clients must not construct
        or parse it; a cursor is only valid for the operation (and filters) that issued it and a
        malformed one is answered 400 BAD_INPUT.
      schema: { type: string }
    CellId:
      description: Cell id as registered by the operator (for example the region and ordinal).
      example: "cell-us-east-1"
      name: cell_id
      in: path
      required: true
      schema: { type: string, minLength: 1 }
    ApprovalDomain:
      example: "publisher.example"
      name: domain
      in: path
      required: true
      description: Site domain (lower-case, no scheme) the approval is keyed by.
      schema: { type: string, minLength: 4, maxLength: 253 }
    PartnerSellersImportId:
      description: Partner sellers.json import id.
      example: "7a1d3c5e-2b4f-4c6d-8e9f-0a1b2c3d4e5f"
      name: import_id
      in: path
      required: true
      schema: { type: string, format: uuid }
    ContentProgramId:
      description: Content program id.
      example: "7a1d3c5e-2b4f-4c6d-8e9f-0a1b2c3d4e5f"
      name: program_id
      in: path
      required: true
      schema: { type: string, format: uuid }
    IdempotencyKey:
      example: "5f0b4c1e-6d7a-4b8c-9d0e-1f2a3b4c5d6e"
      name: Idempotency-Key
      in: header
      required: true
      description: >
        Client-chosen key that makes the mutation safe to retry. The console records a hash of
        the request payload under (actor, method, path, key): a replay with an identical payload
        returns the original response, a replay with a different payload is answered 409
        CONFLICT (`hint: idempotency_key_reused`).
      schema: { type: string, minLength: 1, maxLength: 200 }
    InvoiceId:
      description: Invoice id.
      example: "7a1d3c5e-2b4f-4c6d-8e9f-0a1b2c3d4e5f"
      name: invoice_id
      in: path
      required: true
      schema: { type: string, format: uuid }
    AgentIdentityId:
      description: Agent identity id.
      example: "7a1d3c5e-2b4f-4c6d-8e9f-0a1b2c3d4e5f"
      name: agent_identity_id
      in: path
      required: true
      schema: { type: string, format: uuid }
    PrivacyRequestId:
      description: Privacy request id.
      example: "7a1d3c5e-2b4f-4c6d-8e9f-0a1b2c3d4e5f"
      name: privacy_request_id
      in: path
      required: true
      schema: { type: string, format: uuid }
    UserKey:
      example: "u_9f2c1e7a"
      name: user_key
      in: path
      required: true
      description: >
        Opaque first-party user key (design 33). Not a raw email/phone; max 256 chars;
        alphanumeric plus . _ : @ + - (no whitespace or path separators).
      schema:
        type: string
        minLength: 1
        maxLength: 256
        pattern: '^[A-Za-z0-9._:@+-]{1,256}$'
  responses:
    Error:
      description: Typed error.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Error' }
  schemas:
    Error:
      example:
        code: BAD_INPUT
        message: name is required
        hint: missing_field:name
        request_id: req_01J9Z3Q7R8S9T0V1W2X3Y4Z5A6
        details:
          - field: name
            code: required
            message: name is required
      type: object
      required: [code, message]
      properties:
        code:
          type: string
          enum: [BAD_INPUT, UNAUTHORIZED, FORBIDDEN, NOT_ENTITLED, NOT_FOUND, LIMITING, TIMEOUT, CONFLICT, INTERNAL]
        message: { type: string }
        hint:
          type: string
          description: >
            A stable, dotted, machine-actionable code a client can branch on programmatically
            (e.g. "missing_field:name", "cross_tenant_denied", "retryable") without parsing
            `message` — see docs/spec/api-fundamentals.md and libs/apierrors. Optional; not every
            error carries one yet (a documented, ratcheting rollout, not a contract requirement).
        request_id: { type: string }
        details:
          type: array
          description: Per-field errors for BAD_INPUT responses (UX-41); absent when the error is not field-scoped.
          items: { $ref: '#/components/schemas/FieldError' }
    FieldError:
      type: object
      required: [field, message]
      properties:
        field: { type: string, description: "JSON pointer-ish property path (e.g. `name`, `stitch.max_ads`)." }
        code: { type: string, description: "Stable machine code (`required`, `invalid`, `conflict`)." }
        message: { type: string }
    AuditLogEnvelope:
      type: object
      description: >
        One immutable AuditEnvelope row from audit_log, exposed read-only for operability. The
        before_json and after_json fields are JSON strings to match proto/riptide/agent/v1's
        AuditEnvelope contract and preserve arbitrary resource payloads exactly.
      required: [id, tenant_id, actor_label, action, rationale, model_id, model_version, before_json, after_json, outcome, at]
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        actor_label:
          type: string
          description: Opaque actor label for API-key/JWT/operator callers; empty when only actor_agent_id is populated.
        actor_agent_id:
          type: string
          format: uuid
          description: Registered agent identity id when the audit row was written by an agent identity.
        action: { type: string }
        rationale: { type: string }
        model_id: { type: string }
        model_version: { type: string }
        before_json: { type: string }
        after_json: { type: string }
        outcome: { type: string }
        at: { type: string, format: date-time }
    AuditLogList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/AuditLogEnvelope' }
        next_cursor:
          type: string
          description: Opaque cursor for the next page; absent when no more rows are available.
    LifecycleStatus:
      type: string
      enum: [ACTIVE, INACTIVE, ARCHIVED]
    GeoReferenceLevel:
      type: string
      description: Geo dictionary hierarchy level (libs/enrich GeoLevel*), country → region → metro → city.
      enum: [country, region, metro, city]
      # Pinned Go constant names (see DemandRouteIntegration for rationale).
      x-enum-varnames: [GeoReferenceLevelCountry, GeoReferenceLevelRegion, GeoReferenceLevelMetro, GeoReferenceLevelCity]
    GeoDictEntry:
      type: object
      description: >
        One distinct value of the loaded dataset at a level (libs/enrich.GeoDictEntry). country /
        region are set for every level below country; metro is the metro code at the metro level
        and, for a city, the metro its rows fall under (absent = none). code is the numeric value a
        targeting rule stores (metro code at the metro level, the stable city code at the city
        level; absent otherwise); name is the display label.
      required: [level, country, name]
      properties:
        level: { $ref: '#/components/schemas/GeoReferenceLevel' }
        country: { type: string, description: "ISO 3166-1 alpha-2 country code." }
        region: { type: string, description: "Region code within the country." }
        metro: { type: integer, format: int32, description: "Metro (DMA) code." }
        code: { type: integer, format: int32, description: "Numeric value a targeting rule stores for this entry." }
        name: { type: string, description: "Display label (country code, region code, metro code as text, or city name)." }
    GeoReferenceDataset:
      type: object
      description: Which dataset version the page's labels came from, so a picker can show it.
      required: [version, loaded_at, rows]
      properties:
        version: { type: string, description: "Dataset identifier: blob:<sha256 prefix>/<rows>." }
        loaded_at: { type: string, format: date-time }
        rows: { type: integer, description: "IP ranges in the loaded dataset." }
    GeoReferencePage:
      type: object
      description: One page of dictionary entries (docs/spec/api-fundamentals.md "Cursor pagination").
      required: [items, total, dataset]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/GeoDictEntry' }
        total: { type: integer, description: "Entries matching the query across every page." }
        next_cursor: { type: string, description: "Opaque cursor for the next page; absent on the last page." }
        dataset: { $ref: '#/components/schemas/GeoReferenceDataset' }
    AudioToVideoConfig:
      type: object
      description: >
        One level of the audio-to-video hierarchical configuration (docs/design/11-stitching.md's
        tenant -> publisher -> brand -> platform-default hierarchy), mirrored field-for-field from
        plan.v1.AudioToVideoConfig. Enabling requires the tenant `audio_to_video` entitlement.
      properties:
        id: { type: string }
        version:
          type: integer
          description: Bump to invalidate cache entries rendered under a prior config.
        background_url:
          type: string
          description: The visual composited behind the audio.
        width: { type: integer, minimum: 1 }
        height: { type: integer, minimum: 1 }
    Tenant:
      type: object
      required: [id, name, slug, region, cell_id, status]
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        slug: { type: string }
        region: { type: string }
        cell_id: { type: string }
        status: { $ref: '#/components/schemas/LifecycleStatus' }
        retention_tier:
          readOnly: true
          description: >
            Telemetry retention tier every event of the tenant is stamped with at publish
            (SR-1239b; Event.retention_tier): derived from the plan's telemetry tier — full →
            extended, sampled → standard, minimal → basic, unspecified → standard. The event
            store's TTL reads it.
          allOf:
            - $ref: '#/components/schemas/RetentionTier'
        sandbox: { type: boolean, readOnly: true, description: "Sandbox tenant (SR-1240): accepts loadSampleData, never bills, and is labelled as such in the console." }
        sample_data_loaded_at: { type: string, format: date-time, nullable: true, readOnly: true, description: "When loadSampleData last completed; null when never loaded." }
        timezone: { type: string, description: "IANA time zone for the console's default reporting / flight display (UX-41). Defaults to UTC; serving day-buckets stay UTC." }
        default_delivery_window_seconds:
          type: integer
          minimum: 60
          maximum: 86400
          description: Tenant default supply-side delivery window (seconds); see delivery-timing.md.
        audio_to_video: { $ref: '#/components/schemas/AudioToVideoConfig' }
        audio_to_video_by_brand:
          type: object
          additionalProperties: { $ref: '#/components/schemas/AudioToVideoConfig' }
          description: >
            Per-advertiser-domain ("brand") audio-to-video override, keyed by advertiser domain
            (auction.Bid.AdvertiserDomain). Consulted after tenant, before the platform default.
        branding: { $ref: '#/components/schemas/Branding' }
        entitlements:
          type: array
          items: { $ref: '#/components/schemas/Entitlement' }
          description: >
            Feature gates for this tenant (docs/glossary.md: `ssp`|`dsp`|`ad_server` plus
            sub-features like `warm_pool`|`stitch`|`audio_to_video`|`verification`|`deals`) —
            mirrors plan.v1.Tenant.entitlements / the `tenant_entitlement` table. The console
            (P3-04) uses the `ssp`/`dsp`/`ad_server` features to show/hide its three modules.
        settlement_currency:
          type: string
          description: >
            ISO 4217 settlement currency for bid normalization. Empty means process-level FX
            config applies; RIPTIDE_FX_SETTLEMENT_CURRENCY_JSON remains an emergency override.
        governor: { $ref: '#/components/schemas/GovernorLimit' }
        supply_transparency: { $ref: '#/components/schemas/SupplyTransparencyPolicy' }
        supply_contact_email:
          type: string
          description: >
            Contact email published in the tenant's sellers.json `contact_email` field
            (IAB sellers.json 1.0). Empty means the document omits the field.
        creative_auto_approve:
          type: boolean
          default: true
          description: >
            Creative review policy (design 31 §2C). True keeps auto-approve when the pipeline
            reaches READY; false requires manual approve/reject via reviewCreative, and material
            content changes re-audit (APPROVED resets to PENDING).
        sanctioned_countries:
          type: array
          items: { type: string, minLength: 2, maxLength: 2 }
          description: >
            ISO-3166 alpha-2 countries this tenant must never monetise (compliance gate,
            docs/design/08-economics.md "Request-level gates"): a request whose country is listed
            is answered no-bid with reason `sanctioned_geo` before any demand is dialed.
        request_quota_per_day:
          type: integer
          format: int64
          minimum: 0
          description: >
            Rate-plan request quota per UTC day; 0/omitted = unlimited. serve meters accepted
            requests and answers `quota_exceeded` at the quota (fail-open when the meter is down).
        serving_suspended:
          type: boolean
          description: >
            Control-plane serving kill switch (prepaid balance exhausted / overdue rate plan): serve
            answers every request `serving_suspended`.
        bad_input_http_errors:
          type: boolean
          description: >
            Bad player input posture (SR-1217, design 09 "Bad player input posture"): false (default) answers malformed tag / OpenRTB
            input the way a player degrades gracefully (an empty VAST document on /vast, a
            body-less 204 on /ortb); true opts into typed 4xx errors for integration debugging.
        home_cell_id:
          type: string
          description: >
            Cell that owns the tenant's control-plane residency (SR-1221, design 16 "Tenant->cell
            routing seam"; tenant_cell role `home`). Read-only here; set through
            setTenantCells. Equals `cell_id` until the tenant is re-homed.
        serving_cell_ids:
          type: array
          items: { type: string }
          description: >
            Every cell whose serving plan carries the tenant (home included; tenant_cell roles
            `home` | `serving`), sorted. Read-only here; set through setTenantCells.
        tag_id:
          type: string
          description: >
            Certification-authority id (16 hex characters) rendered on the tenant's own ads.txt /
            app-ads.txt lines and as a sellers.json identifier (SR-1212, design 06 "Supply
            chain"). Empty = omitted from the documents.
        contact_address:
          type: string
          description: >
            Postal contact rendered into the tenant's sellers.json `contact_address` (SR-1212).
            Empty = omitted.
        stale_plan_fail_open:
          type: boolean
          description: >
            Stale-plan posture (SR-1235, design 03 "Converge"): true keeps serving the last-known
            plan past the stale ceiling (with the PlanStale alert firing) instead of the default
            house-only / out-of-rotation posture.
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    Branding:
      type: object
      description: >
        A tenant's white-label identity (docs/design/01-platform.md, docs/glossary.md): the
        product name shown in the console/reports, and the hostnames a white-label deployment
        serves ads/events/user-sync from — mirrors plan.v1.Branding / the `tenant.ad_system_name`+
        `serving_domain`+`event_domain`+`sync_domain` columns. `serving_domain` doubles as the
        console's host->tenant resolution key (P3-04's white-label hostname hook).
      properties:
        ad_system_name: { type: string, description: "Product/brand name shown in the console UI." }
        serving_domain: { type: string, description: "Hostname the tenant serves ads from." }
        event_domain: { type: string, description: "Hostname the tenant serves tracking events from." }
        sync_domain: { type: string, description: "Hostname the tenant uses for user-sync." }
        logo_url: { type: string, description: "HTTPS URL of the console / report logo (UX-41)." }
        favicon_url: { type: string, description: "HTTPS URL of the console favicon." }
        primary_color: { type: string, pattern: '^#[0-9a-fA-F]{6}$', description: "Primary brand colour (#rrggbb) applied to the console theme tokens." }
        accent_color: { type: string, pattern: '^#[0-9a-fA-F]{6}$', description: "Accent brand colour (#rrggbb)." }
        email_sender: { type: string, description: "Requested From address (display name allowed) for tenant-branded mail. Magic links and invites use this mailbox only when it matches the operator's tenant-specific RIPTIDE_AUTH_EMAIL_SENDERS_JSON authorization; otherwise they use the platform mailbox with the tenant display name." }
    Entitlement:
      type: object
      required: [feature, enabled]
      description: >
        One feature gate on a tenant (plan.v1.Entitlement / `tenant_entitlement` row). `feature` is
        a free-form key (docs/glossary.md): `ssp`|`dsp`|`ad_server`|`warm_pool`|`stitch`|
        `audio_to_video`|`verification`|`deals`|`demand_rate_types` (default off — gates non-CPM
        demand buy rates on line items; design 32 / D51)|`roas_autobid` (default off — gates
        ROAS/GMV optimization_goal on line items; design 35 §6 / CP-13).
      properties:
        feature: { type: string, minLength: 1 }
        enabled: { type: boolean }
        limits:
          type: object
          additionalProperties: { type: string }
    GovernorLimit:
      type: object
      description: >
        Compiled admission governor for tenant-scope QPS/share-of-voice. Zero or omitted values are
        unlimited; share_of_voice is a decimal fraction string such as "0.250000".
      properties:
        qps: { type: integer, minimum: 0 }
        burst: { type: integer, minimum: 0 }
        share_of_voice:
          type: string
          pattern: '^(0(\.\d{1,6})?|1(\.0{1,6})?)$'
    SupplyTransparencyMode:
      type: string
      description: Inheritable verify-and-decide mode for ads.txt/app-ads.txt/sellers.json facts.
      enum: ['off', observe, require]
    SupplyTransparencyPolicy:
      type: object
      description: >
        Inheritable ads.txt/app-ads.txt/sellers.json policy. Omitted fields inherit from the next
        broader scope; mode=require makes failed compiled authorization facts bid-time blocking.
      properties:
        mode: { $ref: '#/components/schemas/SupplyTransparencyMode' }
        max_hops: { type: integer, minimum: 0 }
        require_ads_txt: { type: boolean }
        require_sellers_json: { type: boolean }
        require_direct_only: { type: boolean }
    SupplyDocumentKind:
      type: string
      enum: [ads_txt, app_ads_txt, sellers_json]
    SupplyDocumentSubjectType:
      type: string
      enum: [publisher, tenant, demand_partner, domain]
    SupplyDocumentCheckStatus:
      type: string
      enum: [ok, missing, stale, malformed, unauthorized, error]
    SupplyDocumentPreview:
      type: object
      required: [kind, content_type, content]
      properties:
        kind: { $ref: '#/components/schemas/SupplyDocumentKind' }
        publisher_id: { type: string, format: uuid }
        url:
          type: string
          description: Canonical document path or derived crawl URL for the previewed subject.
        content_type: { type: string }
        content: { type: string }
        generated_at: { type: string, format: date-time }
    SupplyDocumentVerifyRequest:
      type: object
      required: [kind, subject_type, subject_id]
      properties:
        kind: { $ref: '#/components/schemas/SupplyDocumentKind' }
        subject_type: { $ref: '#/components/schemas/SupplyDocumentSubjectType' }
        subject_id:
          type: string
          minLength: 1
          description: Publisher uuid, tenant uuid, demand-partner uuid, or domain host.
        url:
          type: string
          description: Optional explicit crawl URL; omitted means derive from kind and subject.
    SupplyDocumentCheck:
      type: object
      required: [id, tenant_id, kind, subject_type, subject_id, url, status, missing_count, checked_at]
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        kind: { $ref: '#/components/schemas/SupplyDocumentKind' }
        subject_type: { $ref: '#/components/schemas/SupplyDocumentSubjectType' }
        subject_id: { type: string }
        url: { type: string }
        status: { $ref: '#/components/schemas/SupplyDocumentCheckStatus' }
        content_hash: { type: string }
        owner_domain: { type: string }
        manager_domain: { type: string }
        missing_count: { type: integer, minimum: 0 }
        error_summary: { type: string }
        checked_at: { type: string, format: date-time }
        expires_at: { type: string, format: date-time }
        details_json:
          type: object
          additionalProperties: true
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    SupplyDocumentCheckList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/SupplyDocumentCheck' }
        next_cursor: { type: string }
    SupplyDocumentFinding:
      type: object
      description: One structural finding from analyzing a crawled supply document.
      required: [code, severity, message]
      properties:
        code:
          type: string
          description: >
            Stable machine-readable finding code (for example `duplicate_lines`,
            `conflicting_relationships`, `missing_owner_domain`, `duplicate_seller_ids`,
            `confidential_heavy`, `missing_seller_domains`).
        severity:
          type: string
          enum: [info, warning, error]
        message: { type: string }
        count:
          type: integer
          minimum: 0
          description: How many document entries this finding covers (0 when not countable).
    SupplyDocumentAnalysis:
      type: object
      description: >
        Deterministic structural analysis of one crawled ads.txt/app-ads.txt/sellers.json
        document. Counters irrelevant to the document kind are zero. risk_score is 0 (clean)
        to 100 (severely defective), derived only from the document's own structure.
      required: [kind, risk_score, findings]
      properties:
        kind: { $ref: '#/components/schemas/SupplyDocumentKind' }
        risk_score: { type: integer, minimum: 0, maximum: 100 }
        line_count: { type: integer, minimum: 0 }
        direct_count: { type: integer, minimum: 0 }
        reseller_count: { type: integer, minimum: 0 }
        ad_system_count:
          type: integer
          minimum: 0
          description: Distinct advertising-system domains named by ads.txt data lines.
        duplicate_count: { type: integer, minimum: 0 }
        conflict_count:
          type: integer
          minimum: 0
          description: Domain+account pairs listed as both DIRECT and RESELLER.
        malformed_count: { type: integer, minimum: 0 }
        seller_count: { type: integer, minimum: 0 }
        publisher_seller_count: { type: integer, minimum: 0 }
        intermediary_seller_count: { type: integer, minimum: 0 }
        both_seller_count: { type: integer, minimum: 0 }
        confidential_count: { type: integer, minimum: 0 }
        passthrough_count: { type: integer, minimum: 0 }
        duplicate_seller_id_count: { type: integer, minimum: 0 }
        missing_domain_count:
          type: integer
          minimum: 0
          description: Non-confidential sellers.json entries with no domain.
        has_owner_domain: { type: boolean }
        has_manager_domain: { type: boolean }
        has_contact: { type: boolean }
        findings:
          type: array
          items: { $ref: '#/components/schemas/SupplyDocumentFinding' }
    SupplyDocumentDiff:
      type: object
      description: >
        Deterministic drift diff between one snapshot and the previous snapshot of the same
        document. All entry lists are rendered canonically and sorted. `unchanged` is true when
        the content hash matched the previous snapshot; a first snapshot has no previous_* fields
        and empty lists.
      required: [unchanged]
      properties:
        unchanged: { type: boolean }
        previous_snapshot_id: { type: string, format: uuid }
        previous_fetched_at: { type: string, format: date-time }
        added_lines:
          type: array
          items: { type: string }
        removed_lines:
          type: array
          items: { type: string }
        variable_changes:
          type: array
          items: { type: string }
        added_sellers:
          type: array
          items: { type: string }
        removed_sellers:
          type: array
          items: { type: string }
        changed_sellers:
          type: array
          items: { type: string }
    SupplyDocumentSnapshot:
      type: object
      description: >
        One crawled supply document snapshot. List responses omit `body`; the single-snapshot
        read includes it.
      required: [id, tenant_id, kind, subject_type, subject_id, url, content_hash, fetched_at]
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        kind: { $ref: '#/components/schemas/SupplyDocumentKind' }
        subject_type: { $ref: '#/components/schemas/SupplyDocumentSubjectType' }
        subject_id: { type: string }
        url: { type: string }
        content_hash: { type: string }
        fetched_at: { type: string, format: date-time }
        body: { type: string }
        diff: { $ref: '#/components/schemas/SupplyDocumentDiff' }
        analysis: { $ref: '#/components/schemas/SupplyDocumentAnalysis' }
        created_at: { type: string, format: date-time }
    SupplyDocumentSnapshotList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/SupplyDocumentSnapshot' }
        next_cursor: { type: string }
    SupplyHealthItem:
      type: object
      description: Latest verification + drift + structural risk for one verified subject.
      required: [kind, subject_type, subject_id, status]
      properties:
        kind: { $ref: '#/components/schemas/SupplyDocumentKind' }
        subject_type: { $ref: '#/components/schemas/SupplyDocumentSubjectType' }
        subject_id: { type: string }
        subject_label:
          type: string
          description: Human-readable subject name (publisher/partner name) when resolvable.
        url: { type: string }
        status: { $ref: '#/components/schemas/SupplyDocumentCheckStatus' }
        checked_at: { type: string, format: date-time }
        stale:
          type: boolean
          description: True when the check has passed its expires_at freshness window.
        risk_score: { type: integer, minimum: 0, maximum: 100 }
        drift_detected:
          type: boolean
          description: True when the latest snapshot's diff against its predecessor is non-empty.
        missing_count: { type: integer, minimum: 0 }
        error_summary: { type: string }
        top_findings:
          type: array
          items: { $ref: '#/components/schemas/SupplyDocumentFinding' }
    SupplyTransparencyHealthReport:
      type: object
      description: >
        Aggregate supply-chain transparency posture for a tenant - verification coverage over
        publishers and demand partners, per-subject latest status/risk/drift, and the totals a
        console dashboard or agent needs to prioritize fixes.
      required: [tenant_id, generated_at, items, checks_total, checks_ok, checks_failing, checks_stale]
      properties:
        tenant_id: { type: string, format: uuid }
        generated_at: { type: string, format: date-time }
        checks_total: { type: integer, minimum: 0 }
        checks_ok: { type: integer, minimum: 0 }
        checks_failing: { type: integer, minimum: 0 }
        checks_stale: { type: integer, minimum: 0 }
        drift_count:
          type: integer
          minimum: 0
          description: Subjects whose most recent snapshot shows drift from its predecessor.
        publishers_total: { type: integer, minimum: 0 }
        publishers_verified:
          type: integer
          minimum: 0
          description: Publishers with at least one non-stale ads.txt/app-ads.txt check.
        demand_partners_total: { type: integer, minimum: 0 }
        demand_partners_verified:
          type: integer
          minimum: 0
          description: Demand partners with at least one non-stale sellers.json check.
        items:
          type: array
          items: { $ref: '#/components/schemas/SupplyHealthItem' }
    TenantUpdate:
      type: object
      description: >
        Body for PUT /v1/operator/tenants/{tenant_id}. Each top-level field independently replaces
        its respective state when present; an omitted field leaves that state unchanged. `branding`
        replaces all four white-label columns together (omitted sub-fields clear to null);
        `entitlements`, when present, replaces the tenant's *entire* entitlement set (not a merge).
      properties:
        branding: { $ref: '#/components/schemas/Branding' }
        entitlements:
          type: array
          items: { $ref: '#/components/schemas/Entitlement' }
        settlement_currency: { type: string, minLength: 3, maxLength: 3 }
        governor: { $ref: '#/components/schemas/GovernorLimit' }
        supply_transparency: { $ref: '#/components/schemas/SupplyTransparencyPolicy' }
        supply_contact_email:
          type: string
          description: sellers.json `contact_email`; an empty string clears it.
        creative_auto_approve: { type: boolean, description: "Creative review policy; see Tenant.creative_auto_approve." }
        sanctioned_countries:
          type: array
          items: { type: string, minLength: 2, maxLength: 2 }
          description: "Replaces the sanctioned-country list (ISO-3166 alpha-2); [] clears it. See Tenant.sanctioned_countries."
        request_quota_per_day: { type: integer, format: int64, minimum: 0, description: "Requests per UTC day; 0 = unlimited. See Tenant.request_quota_per_day." }
        serving_suspended: { type: boolean, description: "Serving kill switch. See Tenant.serving_suspended." }
        timezone: { type: string, description: "IANA time zone (validated); an empty string resets to UTC. See Tenant.timezone." }
        bad_input_http_errors:
          type: boolean
          description: >
            Bad player input posture (SR-1217): false (default) answers malformed tag / OpenRTB
            input the way a player degrades gracefully (an empty VAST document on /vast, a
            body-less 204 on /ortb); true opts into typed 4xx errors for integration debugging.
        tag_id:
          type: string
          maxLength: 16
          description: "Certification-authority id (16 hex characters); an empty string clears it. See Tenant.tag_id (SR-1212)."
        contact_address:
          type: string
          maxLength: 512
          description: "sellers.json `contact_address`; an empty string clears it. See Tenant.contact_address (SR-1212)."
        stale_plan_fail_open: { type: boolean, description: "Stale-plan fail-open posture. See Tenant.stale_plan_fail_open (SR-1235)." }
    TenantAudioToVideoUpdate:
      type: object
      description: >
        Body for PUT /v1/tenants/{tenant_id}/audio-to-video. Each field independently replaces its
        respective column when present; an omitted field leaves that level unchanged (same partial-
        update convention as PlacementUpdate.stitch).
      properties:
        audio_to_video: { $ref: '#/components/schemas/AudioToVideoConfig' }
        audio_to_video_by_brand:
          type: object
          additionalProperties: { $ref: '#/components/schemas/AudioToVideoConfig' }
    TenantRatePlanAssign:
      type: object
      required: [plan_id]
      properties:
        plan_id: { type: string, format: uuid, description: "rate_plan.id to assign." }
        apply_entitlement_pack: { type: boolean, description: "Replace the tenant's entitlements with the plan's pack (default true)." }
    Cell:
      type: object
      required: [id, region, tier, status, serve_replicas, created_at, updated_at]
      properties:
        id: { type: string }
        region: { type: string }
        tier:
          type: string
          enum: [shared, dedicated]
          x-enum-varnames: [CellTierShared, CellTierDedicated]
        status:
          type: string
          enum: [ACTIVE, CORDONED, DRAINING]
          x-enum-varnames: [CellStatusACTIVE, CellStatusCORDONED, CellStatusDRAINING]
        serve_replicas: { type: integer }
        endpoints:
          type: object
          additionalProperties: { type: string }
          description: Service endpoints keyed by role (`serve`, `pland`).
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    CellList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/Cell' }
        next_cursor: { type: string }
    TenantCreate:
      example:
        name: Acme Media
        slug: acme
        region: us
        cell_id: cell-us-east-1
        settlement_currency: USD
        creative_auto_approve: true
      type: object
      required: [name, slug, region, cell_id]
      properties:
        name: { type: string, minLength: 1 }
        slug: { type: string, minLength: 1 }
        region: { type: string, minLength: 1 }
        timezone: { type: string, description: "IANA time zone; defaults to UTC. See Tenant.timezone." }
        cell_id: { type: string, minLength: 1 }
        default_delivery_window_seconds: { type: integer, minimum: 60, maximum: 86400 }
        sandbox: { type: boolean, default: false, description: "Create a sandbox tenant (SR-1240 developer platform); see Tenant.sandbox. Immutable after creation." }
        settlement_currency: { type: string, minLength: 3, maxLength: 3 }
        governor: { $ref: '#/components/schemas/GovernorLimit' }
        supply_transparency: { $ref: '#/components/schemas/SupplyTransparencyPolicy' }
        creative_auto_approve: { type: boolean, default: true, description: "Creative review policy; see Tenant.creative_auto_approve." }
        sanctioned_countries:
          type: array
          items: { type: string, minLength: 2, maxLength: 2 }
          description: "ISO-3166 alpha-2 countries answered no-bid (sanctioned_geo). See Tenant.sanctioned_countries."
        request_quota_per_day: { type: integer, format: int64, minimum: 0, description: "Requests per UTC day; 0 = unlimited." }
        serving_suspended: { type: boolean, description: "Serving kill switch (default false)." }
        bad_input_http_errors:
          type: boolean
          description: >
            Bad player input posture (SR-1217): false (default) answers malformed tag / OpenRTB
            input the way a player degrades gracefully (an empty VAST document on /vast, a
            body-less 204 on /ortb); true opts into typed 4xx errors for integration debugging.
    TenantList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/Tenant' }
        next_cursor: { type: string }
    OnboardTenantRequest:
      type: object
      required: [name, slug, region, admin_email]
      description: >
        Request body for POST /v1/operator/onboarding/tenants. Idempotency is carried by the
        Idempotency-Key header; optional cell_id pins the target cell, otherwise the onboarding
        pipeline chooses a healthy cell in the requested region.
      properties:
        name: { type: string, minLength: 1 }
        slug: { type: string, minLength: 1 }
        region: { type: string, minLength: 1 }
        cell_id: { type: string }
        entitlements:
          type: array
          items: { $ref: '#/components/schemas/Entitlement' }
        default_delivery_window_seconds: { type: integer, minimum: 60, maximum: 86400 }
        admin_email: { type: string, format: email, minLength: 1 }
        admin_name: { type: string }
        hostname: { type: string, description: "Optional custom serving domain to provision." }
        cname_target: { type: string, description: "Expected CNAME target for hostname verification." }
    OnboardDomain:
      type: object
      required: [id, tenant_id, hostname, status]
      properties:
        id: { type: string }
        tenant_id: { type: string, format: uuid }
        hostname: { type: string }
        cname_target: { type: string }
        status: { type: string }
        cert_ref: { type: string }
        failure_reason: { type: string }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    TenantOnboardingResponse:
      type: object
      required: [tenant, sealing_key, admin_user]
      properties:
        tenant: { $ref: '#/components/schemas/Tenant' }
        sealing_key: { $ref: '#/components/schemas/SealingKey' }
        admin_user: { $ref: '#/components/schemas/User' }
        domain: { $ref: '#/components/schemas/OnboardDomain' }
    Publisher:
      type: object
      required: [id, tenant_id, public_id, name, status]
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        public_id: { type: string }
        name: { type: string }
        status: { $ref: '#/components/schemas/LifecycleStatus' }
        pricing: { $ref: '#/components/schemas/AdminPricing' }
        whitelist_apps:
          type: boolean
          description: >
            When true, serve no-bids unless the request resolves to a non-blocked AppLookup
            (`inventory_not_allowlisted` / `app_blocked`; docs/design/22-ip-list-and-identity-envelope.md).
        whitelist_domains:
          type: boolean
          description: >
            When true, serve no-bids unless the request site/app domain matches publisher.domain
            (`inventory_not_allowlisted`; docs/design/22-ip-list-and-identity-envelope.md).
        serving_fee: { type: string, description: "Flat serving fee (decimal CPM) subtracted on the publisher side of every demand win (docs/design/08-economics.md)." }
        audio_partner_cost:
          type: string
          description: >
            Audio-partner cost (decimal CPM, 4 dp): added to the floor sent to AUDIO demand routes
            and deducted from AUDIO bids (docs/design/08-economics.md "Outbound floor gross-up").
            Empty string clears it (no cost); new publishers seed the platform default 0.3500.
        domain: { type: string, description: "Publisher ads.txt / sellers.json domain." }
        inventory_partner_domains:
          type: array
          items: { type: string }
          description: "Inventory partner root domains for ads.txt INVENTORYPARTNERDOMAIN= (IAB inventory sharing)."
        sellers:
          type: array
          items: { $ref: '#/components/schemas/PublisherSeller' }
        delivery_window_seconds:
          type: integer
          minimum: 60
          maximum: 86400
          description: Supply-side delivery window override (seconds); see delivery-timing.md.
        audio_to_video: { $ref: '#/components/schemas/AudioToVideoConfig' }
        allowed_advertisers:
          type: array
          items: { type: string, minLength: 1, maxLength: 253 }
          description: >
            Advertiser allow-list (SR-1217): when non-empty, only a bid whose adomain matches an
            entry (exact or parent domain) may serve on this publisher's placements; every other
            bid loses with `advertiser_not_allowed`. A placement's own allowed_advertisers takes
            precedence when set. Empty / [] = no allow-list.
        blocked_media_url_substrings:
          type: array
          items: { type: string, minLength: 1, maxLength: 512 }
          description: >
            Media-URL substring blocklist (SR-1217): a bid whose creative media URL contains any
            entry (case-insensitive) loses with `blocked_media_url`. Empty / [] = off.
        demand_tmax_cap_ms:
          type: integer
          minimum: 0
          maximum: 10000
          description: >
            Demand latency cap (ms, SR-1217): the demand fan-out budget of every route dialed for a
            request under this publisher is capped at this value regardless of the request's
            tmax. 0 / omitted = no cap beyond tmax.
        vast_serving_paused:
          type: boolean
          description: "Kill switch (SR-1217): /vast answers an empty VAST document (reason `vast_paused`) for this publisher."
        ortb_serving_paused:
          type: boolean
          description: "Kill switch (SR-1217): /ortb answers no-bid (reason `ortb_paused`) for this publisher."
        mask_vast_trackers:
          type: boolean
          description: >
            Mask buyer trackers (SR-1217): a buyer VAST's own Impression / Tracking / Error URLs are
            removed from the document the player receives, cached server-side and fired by the
            platform when the player hits the masked tracker.
        updated_at: { type: string, format: date-time }
    PublisherCreate:
      example:
        public_id: acme-pub
        name: Acme Publisher
        domain: publisher.acme.example
        pricing:
          type: REVSHARE
          value: "0.20"
      type: object
      required: [public_id, name]
      properties:
        public_id: { type: string, minLength: 1 }
        name: { type: string, minLength: 1 }
        pricing: { $ref: '#/components/schemas/AdminPricing' }
        whitelist_apps: { type: boolean }
        whitelist_domains: { type: boolean }
        serving_fee: { type: string, description: "Flat serving fee (decimal CPM) subtracted on the publisher side of every demand win (docs/design/08-economics.md)." }
        audio_partner_cost:
          type: string
          description: >
            Audio-partner cost (decimal CPM, 4 dp): added to the floor sent to AUDIO demand routes
            and deducted from AUDIO bids (docs/design/08-economics.md "Outbound floor gross-up").
            Empty string clears it (no cost); new publishers seed the platform default 0.3500.
        domain: { type: string }
        inventory_partner_domains:
          type: array
          items: { type: string }
          description: "Inventory partner root domains for ads.txt INVENTORYPARTNERDOMAIN= (IAB inventory sharing)."
        sellers:
          type: array
          items: { $ref: '#/components/schemas/PublisherSeller' }
        delivery_window_seconds: { type: integer, minimum: 60, maximum: 86400 }
        audio_to_video: { $ref: '#/components/schemas/AudioToVideoConfig' }
        allowed_advertisers:
          type: array
          items: { type: string, minLength: 1, maxLength: 253 }
          description: >
            Advertiser allow-list (SR-1217): when non-empty, only a bid whose adomain matches an
            entry (exact or parent domain) may serve on this publisher's placements; every other
            bid loses with `advertiser_not_allowed`. A placement's own allowed_advertisers takes
            precedence when set. Empty / [] = no allow-list.
        blocked_media_url_substrings:
          type: array
          items: { type: string, minLength: 1, maxLength: 512 }
          description: >
            Media-URL substring blocklist (SR-1217): a bid whose creative media URL contains any
            entry (case-insensitive) loses with `blocked_media_url`. Empty / [] = off.
        demand_tmax_cap_ms:
          type: integer
          minimum: 0
          maximum: 10000
          description: >
            Demand latency cap (ms, SR-1217): the demand fan-out budget of every route dialed for a
            request under this publisher is capped at this value regardless of the request's
            tmax. 0 / omitted = no cap beyond tmax.
        vast_serving_paused:
          type: boolean
          description: "Kill switch (SR-1217): /vast answers an empty VAST document (reason `vast_paused`) for this publisher."
        ortb_serving_paused:
          type: boolean
          description: "Kill switch (SR-1217): /ortb answers no-bid (reason `ortb_paused`) for this publisher."
        mask_vast_trackers:
          type: boolean
          description: >
            Mask buyer trackers (SR-1217): a buyer VAST's own Impression / Tracking / Error URLs are
            removed from the document the player receives, cached server-side and fired by the
            platform when the player hits the masked tracker.
    PublisherUpdate:
      type: object
      required: [name]
      properties:
        name: { type: string, minLength: 1 }
        pricing: { $ref: '#/components/schemas/AdminPricing' }
        whitelist_apps: { type: boolean }
        whitelist_domains: { type: boolean }
        serving_fee: { type: string, description: "Flat serving fee (decimal CPM) subtracted on the publisher side of every demand win (docs/design/08-economics.md)." }
        audio_partner_cost:
          type: string
          description: >
            Audio-partner cost (decimal CPM, 4 dp): added to the floor sent to AUDIO demand routes
            and deducted from AUDIO bids (docs/design/08-economics.md "Outbound floor gross-up").
            Empty string clears it (no cost); new publishers seed the platform default 0.3500.
        domain: { type: string }
        inventory_partner_domains:
          type: array
          items: { type: string }
          description: "Inventory partner root domains for ads.txt INVENTORYPARTNERDOMAIN= (IAB inventory sharing)."
        sellers:
          type: array
          items: { $ref: '#/components/schemas/PublisherSeller' }
        delivery_window_seconds: { type: integer, minimum: 60, maximum: 86400 }
        audio_to_video: { $ref: '#/components/schemas/AudioToVideoConfig' }
        allowed_advertisers:
          type: array
          items: { type: string, minLength: 1, maxLength: 253 }
          description: >
            Advertiser allow-list (SR-1217): when non-empty, only a bid whose adomain matches an
            entry (exact or parent domain) may serve on this publisher's placements; every other
            bid loses with `advertiser_not_allowed`. A placement's own allowed_advertisers takes
            precedence when set. Empty / [] = no allow-list.
        blocked_media_url_substrings:
          type: array
          items: { type: string, minLength: 1, maxLength: 512 }
          description: >
            Media-URL substring blocklist (SR-1217): a bid whose creative media URL contains any
            entry (case-insensitive) loses with `blocked_media_url`. Empty / [] = off.
        demand_tmax_cap_ms:
          type: integer
          minimum: 0
          maximum: 10000
          description: >
            Demand latency cap (ms, SR-1217): the demand fan-out budget of every route dialed for a
            request under this publisher is capped at this value regardless of the request's
            tmax. 0 / omitted = no cap beyond tmax.
        vast_serving_paused:
          type: boolean
          description: "Kill switch (SR-1217): /vast answers an empty VAST document (reason `vast_paused`) for this publisher."
        ortb_serving_paused:
          type: boolean
          description: "Kill switch (SR-1217): /ortb answers no-bid (reason `ortb_paused`) for this publisher."
        mask_vast_trackers:
          type: boolean
          description: >
            Mask buyer trackers (SR-1217): a buyer VAST's own Impression / Tracking / Error URLs are
            removed from the document the player receives, cached server-side and fired by the
            platform when the player hits the masked tracker.
    PublisherList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/Publisher' }
        next_cursor: { type: string }
    StitchConfig:
      type: object
      description: >
        Real-time stitch configuration for a placement (docs/design/11-stitching.md). Enabling
        stitch requires the tenant `stitch` entitlement.
      properties:
        enabled: { type: boolean }
        max_ads: { type: integer, minimum: 2 }
        max_video: { type: integer, minimum: 0 }
        max_audio: { type: integer, minimum: 0 }
        floor: { type: string, description: "Decimal CPM floor for the combined stitch bid." }
        fee: { type: string, description: "Flat stitch fee (decimal CPM) subtracted once from the combined bid." }
        accept_floor_pct:
          type: string
          description: >
            Accept-% of floor under real-time stitch (fraction 0-1 as a decimal string, e.g.
            "0.80"): partners may bid this fraction of the floor because the stitched slate
            clears the floor as a whole (docs/design/08-economics.md "Floor precedence"). Empty = 1.
        ignore_duration: { type: boolean }
        dedupe: { type: boolean }
        dedupe_media: { type: boolean }
        dedupe_advertiser: { type: boolean }
        dedupe_category: { type: boolean }
        max_duration_sec: { type: integer, minimum: 0 }
        cpm_cap:
          type: object
          properties:
            enabled: { type: boolean }
            min: { type: string }
            max: { type: string }
            increment: { type: string }
    AdminPricing:
      type: object
      description: Economics pricing (REVSHARE|CPM) as decimal string value (4dp).
      properties:
        type: { type: string, enum: [REVSHARE, CPM] }
        value: { type: string }
    AdminCpmCap:
      type: object
      description: CPM cap band applied on the response-to-supply net price path.
      properties:
        enabled: { type: boolean }
        min: { type: string }
        max: { type: string }
        increment: { type: string }
    WarmPoolConfig:
      type: object
      description: Warm-pool (bid cache) media flags for a placement; requires warm_pool entitlement.
      properties:
        audio: { type: boolean }
        video: { type: boolean }
        only_cache: { type: boolean, description: "When true, live demand is async-warmed; sync path uses cache." }
        ttl_class:
          type: string
          enum: ["", default, short, long, buyer_exp]
          description: >
            Placement-level cache lifetime class for warmed bids (docs/design/17-warm-pool.md
            "TTL classes"): ''/default = 40 min, short = 10 min, long = 60 min, buyer_exp = the
            buyer's own bid.exp (capped at 60 min). A partner's warm_pool_ttl_class overrides it.
    DemandAttachmentConfig:
      type: object
      description: >
        Entitlement-gated demand attachment for a placement (docs/design/26-demand-attachment.md).
        Enabling attachment requires the tenant `demand_attachment` entitlement.
      properties:
        enabled: { type: boolean }
        max_attachments: { type: integer, minimum: 0 }
        share_of_voice: { type: integer, minimum: 0, maximum: 100 }
        allow_duplicate_seat_pct: { type: integer, minimum: 0, maximum: 100 }
        max_pmp_in_bundle: { type: integer, minimum: 0 }
        partner_allow_list:
          type: array
          items: { type: string, format: uuid }
        attach_budget_ms: { type: integer, minimum: 0 }
    DualModeConfig:
      type: object
      description: >
        Dual-mode auction: one demand fanout at long_timeout_ms; bids arriving after
        short_timeout_ms are discarded for this request (not a second pass).
      properties:
        enabled: { type: boolean }
        short_timeout_ms: { type: integer, minimum: 1 }
        long_timeout_ms: { type: integer, minimum: 1 }
    EntityVerification:
      type: object
      description: Per-entity verification binding (vendor slot A–D, PIXEL|WRAPPER).
      properties:
        enabled: { type: boolean }
        vendor: { type: string, enum: [A, B, C, D] }
        mode: { type: string, enum: [PIXEL, WRAPPER] }
        vendor_key: { type: string }
    FrequencyCap:
      type: object
      description: >
        Sliding-window frequency cap (HOUR|DAY|WEEK|MONTH|LIFETIME); multiple caps AND'd.
        Across scopes the most restrictive wins (creative association → line item → order).
      properties:
        id: { type: string, format: uuid }
        duration: { type: integer, minimum: 1 }
        limit: { type: integer, minimum: 1 }
        unit: { type: string, enum: [HOUR, DAY, WEEK, MONTH, LIFETIME] }
    FrequencyCapInput:
      type: object
      required: [duration, limit, unit]
      properties:
        duration: { type: integer, minimum: 1 }
        limit: { type: integer, minimum: 1 }
        unit: { type: string, enum: [HOUR, DAY, WEEK, MONTH, LIFETIME] }
    Daypart:
      type: object
      description: >
        One day-of-week window (docs/decisions/D33-line-item-dayparting.md). day_of_week is
        0=Sunday..6=Saturday. Both hours unset = whole day; else [start_hour, end_hour).
      required: [day_of_week]
      properties:
        day_of_week: { type: integer, minimum: 0, maximum: 6 }
        start_hour: { type: integer, minimum: 0, maximum: 23 }
        end_hour: { type: integer, minimum: 1, maximum: 24 }
    LineItemFlight:
      type: object
      description: >
        One sequential budget segment under a line item (GC-05, design 32 §10). Sibling flights
        never overlap and subdivide the line item's flight window + lifetime/daily envelope —
        they never widen it. id is the flight's stable identity; delivery counters key on it.
      required: [start_at, end_at]
      properties:
        id:
          type: string
          format: uuid
          description: >
            Server-minted flight identity. Omit on create; carry it on update to edit the
            flight in place, preserving its delivered counters.
        sequence:
          type: integer
          readOnly: true
          description: 1-based position derived from start_at order; server-computed and returned.
        start_at: { type: string, format: date-time }
        end_at: { type: string, format: date-time, description: "Exclusive end; must be after start_at." }
        goal:
          type: integer
          format: int64
          minimum: 0
          description: >
            Count of the line item's goal_type metric for this segment; omit for uncapped.
            Only accepted when goal_type is IMPRESSIONS, COMPLETIONS, CLICKS, or SPEND.
            For SPEND (and REVENUE) the money goal IS the flight budget: the budget stop
            covers it and a goal set here is never enforced or carried — cap money delivery
            per flight with budget instead.
        budget: { type: string, description: "Decimal money string (4dp), e.g. \"5000.0000\"; omit for uncapped spend." }
        carryover_policy:
          type: string
          enum: [NONE, CARRY_FORWARD]
          x-enum-varnames: [FlightCarryoverNONE, FlightCarryoverCARRYFORWARD]
          description: >
            Whether this flight receives the previous flight's unmet envelope: CARRY_FORWARD
            adds max(0, prior effective goal/budget - prior delivered) to this flight's own
            goal/budget (only when both flights define the value). Default NONE.
    FanoutConfig:
      type: object
      description: Floor-exploration route multiplication (docs/design/06-demand.md).
      properties:
        enabled: { type: boolean }
        count: { type: integer, minimum: 1 }
        use_default_floor: { type: boolean }
        min_floor: { type: string }
        max_floor: { type: string }
        increment: { type: string }
    PublisherSeller:
      type: object
      required: [seller_id]
      properties:
        id: { type: string, format: uuid }
        seller_id: { type: string, minLength: 1 }
        name: { type: string }
        domain: { type: string }
        seller_type: { type: string, enum: [PUBLISHER, INTERMEDIARY, BOTH] }
        is_confidential: { type: boolean }
        is_passthrough:
          type: boolean
          description: >
            sellers.json 1.0 `is_passthrough` - the seller passes the request through without
            taking ownership of the inventory.
        identifiers:
          type: array
          maxItems: 50
          items: { $ref: '#/components/schemas/SellerIdentifier' }
          description: sellers.json 1.0 per-seller `identifiers` (TAG-ID, DUNS, and similar).
    SellerIdentifier:
      type: object
      required: [name, value]
      properties:
        name: { type: string, minLength: 1, maxLength: 120 }
        value: { type: string, minLength: 1, maxLength: 256 }
    WeightedCreativeRef:
      type: object
      required: [creative_id, weight]
      description: >
        One line-item↔creative association: rotation weight plus association-level state,
        optional flight window, and click override (design 30 §2E).
      properties:
        creative_id: { type: string, format: uuid }
        weight: { type: integer, minimum: 1 }
        active: { type: boolean, default: true, description: "Inactive associations never rotate; the creative itself is untouched." }
        start_at: { type: string, format: date-time, description: "Association flight start; omit for no start bound." }
        end_at: { type: string, format: date-time, description: "Association flight end; omit for no end bound." }
        click_url: { type: string, description: "Click-through override for this association; empty inherits the creative's click_url." }
    AudienceStringRule:
      type: object
      required: [value]
      properties:
        value: { type: string }
        excluded: { type: boolean }
    AudienceIntRule:
      type: object
      required: [value]
      properties:
        value: { type: integer }
        excluded: { type: boolean }
    AudienceGeoRule:
      type: object
      required: [type]
      description: Geo targeting row (proto GeoRule).
      properties:
        type: { type: string, description: "Granularity: continent, country, region, or city." }
        excluded: { type: boolean }
        continent: { type: string }
        country: { type: string }
        region: { type: string }
        city: { type: integer, format: int32 }
    AudienceMetroRule:
      type: object
      required: [country, metro]
      description: DMA/metro targeting row (proto MetroRule).
      properties:
        country: { type: string }
        metro: { type: integer, format: int32 }
        excluded: { type: boolean }
    AudiencePlacementRule:
      type: object
      required: [placement_id]
      description: Placement allow/block row (proto PlacementRule).
      properties:
        placement_id: { type: string, format: uuid }
        excluded: { type: boolean }
    AudienceContentRules:
      type: object
      description: Content metadata targeting (proto ContentRules).
      properties:
        languages: { type: array, items: { type: string } }
        languages_excluded: { type: boolean }
        channels: { type: array, items: { type: string } }
        channels_excluded: { type: boolean }
        genres: { type: array, items: { type: string } }
        genres_excluded: { type: boolean }
        ratings: { type: array, items: { type: string } }
        ratings_excluded: { type: boolean }
        titles: { type: array, items: { type: string } }
        titles_excluded: { type: boolean }
        categories: { type: array, items: { type: string } }
        categories_excluded: { type: boolean }
        keywords: { type: array, items: { type: string } }
        keywords_excluded: { type: boolean }
    ProximityTarget:
      type: object
      properties:
        lat: { type: number }
        lon: { type: number }
        radius_km: { type: number, minimum: 0 }
    AppPricingEntry:
      type: object
      properties:
        floor: { type: string }
        pricing: { $ref: '#/components/schemas/AdminPricing' }
        upstream_cpm_cap: { $ref: '#/components/schemas/AdminCpmCap' }
    RouteApprovalEntry:
      type: object
      properties:
        status: { type: string }
        macros:
          type: object
          additionalProperties: { type: string }
    AppLookup:
      type: object
      description: >
        App-resolution cache row (docs/design/02-data-model.md). Keyed by cache_key
        (publisher_public_id|bundle_id via plan.AppLookupKey).
      required: [tenant_id, cache_key, publisher_public_id, blocked]
      properties:
        tenant_id: { type: string, format: uuid }
        cache_key: { type: string }
        publisher_public_id: { type: string }
        bundle_id: { type: string }
        platform_id: { type: string, format: uuid }
        store_url: { type: string }
        app_id: { type: string, format: uuid }
        blocked: { type: boolean }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    AppLookupCreate:
      type: object
      required: [publisher_public_id]
      properties:
        publisher_public_id: { type: string, minLength: 1 }
        bundle_id: { type: string }
        platform_id: { type: string, format: uuid }
        store_url: { type: string }
        app_id: { type: string, format: uuid }
        blocked: { type: boolean }
    AppLookupUpdate:
      type: object
      properties:
        platform_id: { type: string, format: uuid }
        store_url: { type: string }
        app_id: { type: string, format: uuid }
        blocked: { type: boolean }
    AppLookupList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/AppLookup' }
        next_cursor: { type: string }
    VerificationVendorConfig:
      type: object
      description: >
        Tenant-configured templates for one verification vendor slot (A–D). Distinct from
        per-entity EntityVerification references (RIP-133 WS10).
      required: [id, tenant_id, vendor]
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        vendor: { type: string, enum: [A, B, C, D] }
        pixel_url: { type: string }
        wrapper_url: { type: string }
        js_resource_url: { type: string }
        api_framework: { type: string }
        prebid_url: { type: string }
        prebid_timeout_ms: { type: integer, minimum: 0 }
        sampling_pct: { type: string, description: "Decimal 0-100; omit/empty = always measure." }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    VerificationVendorConfigCreate:
      type: object
      required: [vendor]
      properties:
        vendor: { type: string, enum: [A, B, C, D] }
        pixel_url: { type: string }
        wrapper_url: { type: string }
        js_resource_url: { type: string }
        api_framework: { type: string }
        prebid_url: { type: string }
        prebid_timeout_ms: { type: integer, minimum: 0 }
        sampling_pct: { type: string }
    VerificationVendorConfigUpdate:
      type: object
      properties:
        pixel_url: { type: string }
        wrapper_url: { type: string }
        js_resource_url: { type: string }
        api_framework: { type: string }
        prebid_url: { type: string }
        prebid_timeout_ms: { type: integer, minimum: 0 }
        sampling_pct: { type: string }
    VerificationVendorConfigList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/VerificationVendorConfig' }
        next_cursor: { type: string }
    AttributionConfig:
      type: object
      description: >
        Per-tenant conversion attribution settings (migrations/postgres/0059). Deterministic
        last-touch in v1: a conversion attributes to the most recent click within
        click_window_seconds, else (when view_through_enabled) the most recent impression within
        view_window_seconds. One config per tenant; no per-line-item overrides in v1.
      required: [tenant_id, click_window_seconds, view_window_seconds, view_through_enabled, enabled]
      properties:
        id:
          type: string
          format: uuid
          description: Absent when the tenant is still on platform defaults (no row written).
        tenant_id: { type: string, format: uuid }
        click_window_seconds:
          type: integer
          minimum: 1
          maximum: 3888000
          description: >
            Click-through lookback window in seconds (default 604800 = 7 days; max 3888000 = 45
            days, the click_ref token acceptance horizon).
        view_window_seconds:
          type: integer
          minimum: 1
          maximum: 2592000
          description: View-through lookback window in seconds (default 86400 = 1 day; max 30 days).
        view_through_enabled:
          type: boolean
          description: Whether impressions are eligible touchpoints (view-through) at all.
        enabled:
          type: boolean
          description: Master switch — when false, conversions are ingested but not attributed.
        product_attribution_match_type:
          # description (moved from a $ref sibling, invalid in OAS 3.0):
          $ref: '#/components/schemas/ProductAttributionMatchType'
          # Catalog-aware attribution match (design 35 §5 / CP-8 / PG 0073). Default ANY.
          # When set to a non-ANY type and the conversion carries product keys, candidate
          # touchpoints are filtered via libs/catalog.MatchesAttribution. Missing product keys
          # on the conversion degrade to ANY (do not drop the conversion).
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    ConversionTag:
      type: object
      description: >
        Advertiser conversion ingest snippets (pixel + S2S). Placeholders use curly braces;
        replace {click_ref} with the sealed rt_clkref value from the landing URL and {label}
        with a conversion_label matching [a-z0-9_-]{1,64}.
      required: [pixel_url_template, pixel_html, s2s_url, s2s_body_example]
      properties:
        pixel_url_template:
          type: string
          description: GET pixel URL template including clkref and label query params.
        pixel_html:
          type: string
          description: 1x1 image tag ready to paste on a thank-you / conversion page.
        s2s_url:
          type: string
          description: POST /conversion endpoint for server-to-server fires.
        s2s_body_example:
          type: string
          description: Example JSON body for S2S ingest.
        notes:
          type: string
          description: Operator-facing setup notes (no secrets).
    UserProfile:
      type: object
      description: >
        Cell-local user profile snapshot (design 33 / CP-4). Opaque user_key; attributes are
        typed scalars (string|int|bool|decimal). Used for targeting expressions and RTBF.
      required: [tenant_id, user_key, found]
      properties:
        tenant_id: { type: string, format: uuid }
        user_key: { type: string, minLength: 1, maxLength: 256 }
        found:
          type: boolean
          description: false on miss / after RTBF
        opt_out:
          type: boolean
          description: "When true, targeting treats the profile as empty"
        attributes:
          type: object
          additionalProperties: { $ref: '#/components/schemas/UserProfileAttribute' }
          description: At most 64 keys; key ≤64 chars; string/decimal values ≤256 chars.
        tags:
          type: array
          maxItems: 32
          items: { type: string, maxLength: 256 }
        segments:
          type: array
          maxItems: 128
          items: { type: string, maxLength: 256 }
          description: Audience segment ids carried on the profile.
        audit_id:
          type: string
          description: AuditEnvelope id for the mutation that produced this snapshot (writes only).
    UserProfileAttribute:
      type: object
      required: [kind, value]
      properties:
        kind:
          type: string
          enum: [string, int, bool, decimal]
        value:
          type: string
          description: >
            Canonical string form — int digits, bool "true"|"false", decimal money-safe string,
            or free-form string. Parsed according to kind.
    UserProfileUpsert:
      type: object
      description: Replace-style upsert of profile fields (omitted collections clear to empty).
      properties:
        opt_out: { type: boolean }
        attributes:
          type: object
          additionalProperties: { $ref: '#/components/schemas/UserProfileAttribute' }
        tags:
          type: array
          maxItems: 32
          items: { type: string, maxLength: 256 }
        segments:
          type: array
          maxItems: 128
          items: { type: string, maxLength: 256 }
    AttributionConfigUpdate:
      type: object
      description: Upsert payload; omitted fields keep their current (or default) values.
      properties:
        click_window_seconds: { type: integer, minimum: 1, maximum: 3888000 }
        view_window_seconds: { type: integer, minimum: 1, maximum: 2592000 }
        view_through_enabled: { type: boolean }
        enabled: { type: boolean }
        product_attribution_match_type:
          $ref: '#/components/schemas/ProductAttributionMatchType'
    EventTracker:
      type: object
      description: >
        One third-party event tracker (docs/design/29-event-trackers.md; D47). url is HTTPS +
        macro-templated. vendor_key/verification_params/fallback_url apply only to
        verification_script kind.
      required: [event, kind, url]
      properties:
        id: { type: string, format: uuid }
        event:
          type: string
          enum: [IMPRESSION, BILLABLE_IMPRESSION, CLICK, START, FIRST_QUARTILE, MIDPOINT, THIRD_QUARTILE, COMPLETE, MUTE, PAUSE, FULLSCREEN, SKIP, VIEWABLE_MRC50, VIEWABLE_MRC100, COMPANION_VIEW, ERROR, VIEW_UNDETERMINED]
        kind:
          type: string
          enum: [IMAGE_PIXEL, JS_URL, TRACKING_URL, CLICK_REDIRECT, CLICK_PARALLEL, VERIFICATION_SCRIPT]
        url: { type: string, description: "HTTPS, macro-templated." }
        vendor_key: { type: string }
        verification_params: { type: string, description: "Opaque OMID VerificationParameters JSON." }
        fallback_url: { type: string, description: "HTTPS no-JS fallback pixel (verification_script only)." }
    EventTrackerSet:
      type: object
      description: The full replacement set of an entity's event trackers.
      required: [trackers]
      properties:
        trackers:
          type: array
          items: { $ref: '#/components/schemas/EventTracker' }
    EventTrackerList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/EventTracker' }
        next_cursor: { type: string }
    IvtPolicy:
      type: object
      description: >
        Per-tenant IVT detection/enforcement policy (riptide.ivt.v1.IvtPolicy;
        docs/design/28-ivt-engine.md). Zero/empty values mean "library default".
      required: [id, tenant_id, mode]
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        mode: { type: string, enum: ['', OFF, MONITOR, ENFORCE] }
        flag_threshold: { type: integer, minimum: 0, maximum: 1000 }
        block_threshold: { type: integer, minimum: 0, maximum: 1000 }
        allow_givt: { type: boolean }
        disabled_detectors:
          type: array
          items: { type: string }
        max_requests_per_ip_min: { type: integer, minimum: 0 }
        max_fires_per_ip_min: { type: integer, minimum: 0 }
        min_click_delay_ms: { type: integer, minimum: 0 }
        supply_tolerance_pct:
          type: string
          description: "Decimal percentage (e.g. \"1.5\"); empty disables tolerance blocking."
        supply_min_requests: { type: integer, format: int64, minimum: 0 }
        min_supply_grade: { type: string, enum: ['', A, B, C, D, F] }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    IvtPolicyUpdate:
      type: object
      properties:
        mode: { type: string, enum: ['', OFF, MONITOR, ENFORCE] }
        flag_threshold: { type: integer, minimum: 0, maximum: 1000 }
        block_threshold: { type: integer, minimum: 0, maximum: 1000 }
        allow_givt: { type: boolean }
        disabled_detectors:
          type: array
          items: { type: string }
        max_requests_per_ip_min: { type: integer, minimum: 0 }
        max_fires_per_ip_min: { type: integer, minimum: 0 }
        min_click_delay_ms: { type: integer, minimum: 0 }
        supply_tolerance_pct: { type: string }
        supply_min_requests: { type: integer, format: int64, minimum: 0 }
        min_supply_grade: { type: string, enum: ['', A, B, C, D, F] }
    SupplyScorecard:
      type: object
      description: >
        Computed quality fact for one supply key (publisher x domain/bundle) over one trailing
        window (riptide.ivt.v1.SupplyScorecard; docs/design/28-ivt-engine.md).
      required: [id, tenant_id, publisher_public_id, supply_id, requests, ivt_rate_pct, grade]
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        publisher_public_id: { type: string }
        supply_id: { type: string, description: "Canonical domain-or-bundle half of the supply key." }
        window_start: { type: string, format: date-time }
        window_end: { type: string, format: date-time }
        requests: { type: integer, format: int64 }
        flagged: { type: integer, format: int64 }
        blocked: { type: integer, format: int64 }
        givt: { type: integer, format: int64 }
        sivt: { type: integer, format: int64 }
        ivt_rate_pct: { type: string, description: "Decimal percentage, 4dp." }
        grade: { type: string, enum: ['', A, B, C, D, F] }
        top_reasons:
          type: array
          items: { type: string }
        computed_at: { type: string, format: date-time }
    SupplyScorecardList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/SupplyScorecard' }
        next_cursor: { type: string }
    IpList:
      type: object
      required: [id, tenant_id, name, mode, scope, status, version, entry_count, created_at, updated_at]
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        name: { type: string }
        mode:
          type: string
          enum: [block, allow]
          description: >
            block = exclude members (blocklist); allow = include-only when bound (allowlist).
            Primary binding is line-item targeting via ip_block_list_ids / ip_allow_list_ids.
        scope:
          type: string
          enum: [tenant, publisher, cell]
          description: >
            Optional request-wide shed: tenant/publisher post-resolve; cell at the serve edge
            (operator-only). Line items bind lists regardless of this field.
        status:
          # description (moved from a $ref sibling, invalid in OAS 3.0): ACTIVE lists may publish into the plan; ARCHIVED lists are excluded.
          $ref: '#/components/schemas/LifecycleStatus'
        publisher_id:
          type: string
          format: uuid
          nullable: true
          description: Required when scope=publisher; must be null otherwise.
        blob_uri:
          type: string
          nullable: true
          description: Object-storage URI of the compiled membership blob (set on publish).
        content_hash:
          type: string
          nullable: true
          description: Hex SHA-256 of the compiled blob bytes.
        version:
          type: integer
          format: int64
          minimum: 0
        entry_count:
          type: integer
          format: int64
          minimum: 0
          description: Approximate membership cardinality after last publish (hosts + CIDRs).
        staging_count:
          type: integer
          format: int64
          minimum: 0
          description: Uncompiled staging lines awaiting publish (not part of the live set).
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    IpListCreate:
      type: object
      required: [name, mode, scope]
      properties:
        name: { type: string, minLength: 1 }
        mode: { type: string, enum: [block, allow] }
        scope:
          type: string
          enum: [tenant, publisher, cell]
          description: cell is operator-only; publisher requires publisher_id owned by the tenant.
        publisher_id:
          type: string
          format: uuid
          description: Required when scope=publisher; must belong to the tenant.
    IpListPage:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/IpList' }
        next_cursor: { type: string }
    IpListBulkImportRequest:
      type: object
      required: [lines]
      properties:
        lines:
          type: array
          minItems: 1
          maxItems: 10000
          items: { type: string }
          description: >
            One IP or CIDR per line (IPv4/IPv6). Empty lines and `#` comments are ignored server-side.
        replace:
          type: boolean
          default: false
          description: When true, clear existing staging for this list before appending.
    BidModifierTerm:
      type: object
      required: [dimension, multiplier]
      description: >
        One sparse match term on a bid modifier. Matching multipliers multiply; no match ⇒ 1.0;
        result clamped to [0,10]; 0 ⇒ no-bid (design 32 §4.3).
      properties:
        id: { type: string, format: uuid, description: "Set on read; ignored on write (server-assigned)." }
        dimension: { type: string, minLength: 1 }
        key: { type: string, default: "" }
        value: { type: string, default: "" }
        multiplier:
          type: string
          description: Decimal string in [0, 10], e.g. "1.2500".
    BidModifierTermInput:
      type: object
      required: [dimension, multiplier]
      properties:
        dimension: { type: string, minLength: 1 }
        key: { type: string, default: "" }
        value: { type: string, default: "" }
        multiplier:
          type: string
          description: "Decimal string in [0, 10]."
    BidModifier:
      type: object
      required: [id, tenant_id, name, active, terms, status, created_at, updated_at]
      description: >
        Tenant-scoped sparse multiplicative bid adjustment set (design 32 §4.3 / D51). Max 1000
        terms; attach via line_item.bid_modifier_id.
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        name: { type: string }
        description: { type: string }
        active: { type: boolean }
        terms:
          type: array
          maxItems: 1000
          items: { $ref: '#/components/schemas/BidModifierTerm' }
        status:
          # description (moved from a $ref sibling, invalid in OAS 3.0): ACTIVE modifiers may compile into the plan; ARCHIVED are excluded.
          $ref: '#/components/schemas/LifecycleStatus'
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    BidModifierCreate:
      type: object
      required: [name]
      properties:
        name: { type: string, minLength: 1 }
        description: { type: string }
        active: { type: boolean, default: true }
        terms:
          type: array
          maxItems: 1000
          items: { $ref: '#/components/schemas/BidModifierTermInput' }
    BidModifierUpdate:
      type: object
      required: [name]
      properties:
        name: { type: string, minLength: 1 }
        description: { type: string }
        active: { type: boolean }
    BidModifierTermsReplace:
      type: object
      required: [terms]
      properties:
        terms:
          type: array
          maxItems: 1000
          items: { $ref: '#/components/schemas/BidModifierTermInput' }
    BidModifierPage:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/BidModifier' }
        next_cursor: { type: string }
    CustomListKind:
      type: string
      description: >
        Request dimension a custom list matches (design 32 §6.1). LAT_LONG is exact "lat,lon"
        string match in v1.
      enum: [DOMAIN, APP_BUNDLE, SITE, PUBLISHER_REF, DEAL_REF, ZIP, LAT_LONG]
    CustomList:
      type: object
      required: [id, tenant_id, name, kind, items, entry_count, status, created_at, updated_at]
      description: >
        Tenant membership set for non-IP targeting (design 32 §6.1 / BP-LISTS). Max 10000 items
        embedded in the serving plan; attach via line_item.custom_block_list_ids /
        custom_allow_list_ids.
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        name: { type: string }
        kind: { $ref: '#/components/schemas/CustomListKind' }
        items:
          type: array
          maxItems: 10000
          items: { type: string, minLength: 1 }
        entry_count: { type: integer, format: int64, minimum: 0 }
        usage:
          readOnly: true
          description: Where the list is referenced (line items via custom_block_list_ids / custom_allow_list_ids; creatives and placements are 0 for lists); counted at read time.
          allOf:
            - $ref: '#/components/schemas/EntityUsage'
        status:
          # description (moved from a $ref sibling, invalid in OAS 3.0): ACTIVE lists may compile into the plan; ARCHIVED are excluded.
          $ref: '#/components/schemas/LifecycleStatus'
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    CustomListCreate:
      type: object
      required: [name, kind]
      properties:
        name: { type: string, minLength: 1 }
        kind: { $ref: '#/components/schemas/CustomListKind' }
        items:
          type: array
          maxItems: 10000
          items: { type: string, minLength: 1 }
    CustomListUpdate:
      type: object
      required: [name]
      properties:
        name: { type: string, minLength: 1 }
    CustomListItemsReplace:
      type: object
      required: [items]
      properties:
        items:
          type: array
          maxItems: 10000
          items: { type: string, minLength: 1 }
    CustomListPage:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/CustomList' }
        next_cursor: { type: string }
    CreativeTemplateKind:
      type: string
      enum: [DISPLAY, NATIVE, VAST]
    CreativeTemplate:
      type: object
      required: [id, tenant_id, name, kind, markup_template, status, created_at, updated_at]
      description: >
        Tenant creative template (design 32 §6.2 / CP-5 thin). markup_template uses {{MACRO}}
        tokens; optional schema_json documents required template_fields keys.
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        name: { type: string }
        kind: { $ref: '#/components/schemas/CreativeTemplateKind' }
        markup_template: { type: string }
        schema_json:
          type: string
          description: Optional shallow JSON Schema text; empty = none.
        refresh_interval_seconds:
          type: integer
          nullable: true
          minimum: 300
          description: >
            Dynamic-creative refresh cadence in seconds (SR-1218 DCO refresh; minimum 300). Null /
            omitted = no scheduled refresh; refreshCreativeTemplate still runs on demand.
        refresh_source_url:
          type: string
          format: uri
          nullable: true
          maxLength: 2048
          description: >
            HTTPS feed the refresh run fetches to re-expand template variants (SR-1218). Null /
            omitted = the template is static and cannot be refreshed.
        refresh_status:
          $ref: '#/components/schemas/CreativeTemplateRefreshStatus'
        last_refreshed_at:
          type: string
          format: date-time
          nullable: true
          readOnly: true
          description: When the last refresh run finished successfully; null until one has.
        status: { $ref: '#/components/schemas/LifecycleStatus' }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    CreativeTemplateCreate:
      type: object
      required: [name, kind]
      properties:
        name: { type: string, minLength: 1 }
        kind: { $ref: '#/components/schemas/CreativeTemplateKind' }
        markup_template: { type: string }
        schema_json: { type: string }
        refresh_interval_seconds:
          type: integer
          nullable: true
          minimum: 300
          description: >
            Dynamic-creative refresh cadence in seconds (SR-1218 DCO refresh; minimum 300). Null /
            omitted = no scheduled refresh; refreshCreativeTemplate still runs on demand.
        refresh_source_url:
          type: string
          format: uri
          nullable: true
          maxLength: 2048
          description: >
            HTTPS feed the refresh run fetches to re-expand template variants (SR-1218). Null /
            omitted = the template is static and cannot be refreshed.
    CreativeTemplateUpdate:
      type: object
      required: [name]
      properties:
        name: { type: string, minLength: 1 }
        kind: { $ref: '#/components/schemas/CreativeTemplateKind' }
        markup_template: { type: string }
        schema_json: { type: string }
        refresh_interval_seconds:
          type: integer
          nullable: true
          minimum: 300
          description: >
            Dynamic-creative refresh cadence in seconds (SR-1218 DCO refresh; minimum 300). Null /
            omitted = no scheduled refresh; refreshCreativeTemplate still runs on demand.
        refresh_source_url:
          type: string
          format: uri
          nullable: true
          maxLength: 2048
          description: >
            HTTPS feed the refresh run fetches to re-expand template variants (SR-1218). Null /
            omitted = the template is static and cannot be refreshed.
    CreativeTemplatePage:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/CreativeTemplate' }
        next_cursor: { type: string }
    CreativeTemplateRefreshStatus:
      type: string
      description: >
        Dynamic-creative refresh state of a template (SR-1218), read-only (advanced by
        refreshCreativeTemplate and the scheduled cadence): IDLE when no source is configured or
        no run is pending, SCHEDULED when the cadence has queued a run, RUNNING while one runs,
        FAILED when the last run failed (last_refreshed_at keeps the previous success).
      enum: [IDLE, SCHEDULED, RUNNING, FAILED]
      # Pinned Go constant names (see DemandRouteIntegration for rationale).
      x-enum-varnames: [CreativeTemplateRefreshStatusIDLE, CreativeTemplateRefreshStatusSCHEDULED, CreativeTemplateRefreshStatusRUNNING, CreativeTemplateRefreshStatusFAILED]
    CreativeTemplateRefreshRunStatus:
      type: string
      description: Outcome of one refresh run (SR-1218).
      enum: [RUNNING, SUCCEEDED, FAILED]
      x-enum-varnames: [CreativeTemplateRefreshRunStatusRUNNING, CreativeTemplateRefreshRunStatusSUCCEEDED, CreativeTemplateRefreshRunStatusFAILED]
    CreativeTemplateRefreshRun:
      type: object
      description: >
        One dynamic-creative refresh run (SR-1218; creative_template_refresh_run): started by
        refreshCreativeTemplate or the scheduled cadence, it re-expands every creative variant
        rendered from the template against the fresh refresh_source_url feed.
      required: [id, creative_template_id, started_at, status, variants_refreshed]
      properties:
        id: { type: string, format: uuid, description: "Run id." }
        creative_template_id: { type: string, format: uuid, description: "The template refreshed." }
        started_at: { type: string, format: date-time, description: "When the run started." }
        finished_at:
          type: string
          format: date-time
          nullable: true
          description: When the run finished (SUCCEEDED or FAILED); null while RUNNING.
        status: { $ref: '#/components/schemas/CreativeTemplateRefreshRunStatus' }
        error:
          type: string
          nullable: true
          description: Failure reason when status is FAILED (fetch error, feed shape, variant render); null otherwise.
        variants_refreshed:
          type: integer
          minimum: 0
          description: Number of creative variants re-expanded so far (final on SUCCEEDED).
    DeliveryExperimentArm:
      type: string
      enum: [CONTROL, TREATMENT]
      description: Sticky user arm (design 32 §8.1). CONTROL suppresses TREATMENT subjects.
    DeliveryExperimentSubjectKind:
      type: string
      enum: [CAMPAIGN_ORDER, LINE_ITEM]
    DeliveryExperimentSubject:
      type: object
      required: [subject_id, kind, arm]
      properties:
        subject_id: { type: string, format: uuid }
        kind: { $ref: '#/components/schemas/DeliveryExperimentSubjectKind' }
        arm: { $ref: '#/components/schemas/DeliveryExperimentArm' }
    DeliveryExperiment:
      type: object
      required: [id, tenant_id, name, holdout_pct, subjects, status, created_at, updated_at]
      description: >
        Tenant holdout/campaign experiment (design 32 §8.1 / BP-EXP). holdout_pct is the
        percent of sticky users in CONTROL; control arm suppresses TREATMENT subjects.
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        name: { type: string }
        description: { type: string }
        holdout_pct:
          type: string
          description: Decimal percent 0–100 (4 dp money style), e.g. "10.0000".
        subjects:
          type: array
          items: { $ref: '#/components/schemas/DeliveryExperimentSubject' }
        status: { $ref: '#/components/schemas/LifecycleStatus' }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    DeliveryExperimentCreate:
      type: object
      required: [name, holdout_pct]
      properties:
        name: { type: string, minLength: 1 }
        description: { type: string }
        holdout_pct: { type: string, minLength: 1 }
        subjects:
          type: array
          items: { $ref: '#/components/schemas/DeliveryExperimentSubject' }
    DeliveryExperimentUpdate:
      type: object
      required: [name, holdout_pct]
      properties:
        name: { type: string, minLength: 1 }
        description: { type: string }
        holdout_pct: { type: string, minLength: 1 }
    DeliveryExperimentSubjectsReplace:
      type: object
      required: [subjects]
      properties:
        subjects:
          type: array
          items: { $ref: '#/components/schemas/DeliveryExperimentSubject' }
    DeliveryExperimentPage:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/DeliveryExperiment' }
        next_cursor: { type: string }
    DeliveryAlertMetric:
      type: string
      enum: [SPEND, IMPRESSIONS, PACE]
      description: >
        SPEND/IMPRESSIONS are window sums; PACE is (actual/expected)*100 percent
        (design 32 §8.2).
    DeliveryAlertComparison:
      type: string
      enum: [ABOVE, BELOW]
    DeliveryAlertRule:
      type: object
      required:
        - id
        - tenant_id
        - name
        - metric
        - comparison
        - threshold
        - window_seconds
        - enabled
        - cooldown_seconds
        - status
        - created_at
        - updated_at
      description: >
        Tenant delivery/spend alert rule (design 32 §8.2 / BP-ALERT). HMAC secrets are stored
        only as webhook_secret_ref (secret store / RIPTIDE_WEBHOOK_SECRETS); never returned on GET/list.
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        name: { type: string }
        metric: { $ref: '#/components/schemas/DeliveryAlertMetric' }
        comparison: { $ref: '#/components/schemas/DeliveryAlertComparison' }
        threshold:
          type: string
          description: Decimal threshold (4 dp); money for SPEND, count for IMPRESSIONS, percent for PACE.
        window_seconds: { type: integer, minimum: 1 }
        campaign_order_id: { type: string, format: uuid, nullable: true }
        line_item_id: { type: string, format: uuid, nullable: true }
        webhook_url: { type: string }
        webhook_configured:
          type: boolean
          description: True when a webhook_secret_ref is stored (secret itself is never returned).
        enabled: { type: boolean }
        cooldown_seconds: { type: integer, minimum: 0 }
        last_fired_at: { type: string, format: date-time, nullable: true }
        muted_until: { type: string, format: date-time, nullable: true, description: "While in the future the rule is evaluated but never fires (UX-41 mute)." }
        status: { $ref: '#/components/schemas/LifecycleStatus' }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    DeliveryAlertMuteRequest:
      type: object
      properties:
        muted_until:
          type: string
          format: date-time
          nullable: true
          description: Mute until this instant (must be in the future); null or omitted unmutes.
    DeliveryAlertRuleCreated:
      allOf:
        - $ref: '#/components/schemas/DeliveryAlertRule'
        - type: object
          properties:
            webhook_secret_ref:
              type: string
              description: Echoed on create when a new secret_ref is supplied (not the secret value).
    DeliveryAlertRuleCreate:
      type: object
      required: [name, metric, comparison, threshold, window_seconds]
      properties:
        name: { type: string, minLength: 1 }
        metric: { $ref: '#/components/schemas/DeliveryAlertMetric' }
        comparison: { $ref: '#/components/schemas/DeliveryAlertComparison' }
        threshold: { type: string, minLength: 1 }
        window_seconds: { type: integer, minimum: 1 }
        campaign_order_id: { type: string, format: uuid, nullable: true }
        line_item_id: { type: string, format: uuid, nullable: true }
        webhook_url:
          type: string
          description: HTTPS URL only; localhost/RFC1918/metadata hosts are rejected.
        webhook_secret_ref:
          type: string
          description: >
            Secret-store reference resolved at fire time via SecretResolver
            (RIPTIDE_WEBHOOK_SECRETS JSON map). Never persist the raw HMAC secret in the rule row.
        enabled: { type: boolean }
        cooldown_seconds: { type: integer, minimum: 0 }
    DeliveryAlertRuleUpdate:
      type: object
      required: [name, metric, comparison, threshold, window_seconds]
      properties:
        name: { type: string, minLength: 1 }
        metric: { $ref: '#/components/schemas/DeliveryAlertMetric' }
        comparison: { $ref: '#/components/schemas/DeliveryAlertComparison' }
        threshold: { type: string, minLength: 1 }
        window_seconds: { type: integer, minimum: 1 }
        campaign_order_id: { type: string, format: uuid, nullable: true }
        line_item_id: { type: string, format: uuid, nullable: true }
        webhook_url:
          type: string
          description: HTTPS URL only; localhost/RFC1918/metadata hosts are rejected.
        webhook_secret_ref:
          type: string
          description: Omit or empty to leave the stored secret_ref unchanged.
        enabled: { type: boolean }
        cooldown_seconds: { type: integer, minimum: 0 }
    DeliveryAlertRulePage:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/DeliveryAlertRule' }
        next_cursor: { type: string }
    DeliveryAlertWebhookStatus:
      type: string
      enum: [SENT, SKIPPED, FAILED]
    DeliveryAlertEvent:
      type: object
      required:
        - id
        - tenant_id
        - rule_id
        - metric
        - observed_value
        - threshold
        - comparison
        - fired_at
        - webhook_status
      description: Append-only alert firing record (design 32 §8.2).
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        rule_id: { type: string, format: uuid }
        metric: { $ref: '#/components/schemas/DeliveryAlertMetric' }
        observed_value: { type: string }
        threshold: { type: string }
        comparison: { $ref: '#/components/schemas/DeliveryAlertComparison' }
        fired_at: { type: string, format: date-time }
        webhook_status: { $ref: '#/components/schemas/DeliveryAlertWebhookStatus' }
        acknowledged_at: { type: string, format: date-time, description: "Set by acknowledgeDeliveryAlertEvent (UX-41)." }
        acknowledged_by: { type: string, description: "Actor label that acknowledged the firing." }
    DeliveryAlertEventPage:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/DeliveryAlertEvent' }
        next_cursor: { type: string }
    ProductAttributionMatchType:
      type: string
      description: >
        Catalog-aware attribution match (design 35 §5 / CP-8). Mirrors
        riptide.common.v1.ProductAttributionMatchType.
      enum: [ANY, PRODUCT, BRAND, CATEGORY, MERCHANT]
    ItemCatalog:
      type: object
      required: [id, tenant_id, name, currency, locale, status, created_at, updated_at]
      description: >
        Tenant product/SKU catalog container (design 35 / CP-6). Distinct from
        MarketplaceListing (open-marketplace discovery).
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        name: { type: string }
        currency: { type: string, minLength: 3, maxLength: 3 }
        locale: { type: string }
        status: { $ref: '#/components/schemas/LifecycleStatus' }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    ItemCatalogCreate:
      type: object
      required: [name]
      properties:
        name: { type: string, minLength: 1 }
        currency: { type: string, minLength: 3, maxLength: 3 }
        locale: { type: string }
    ItemCatalogUpdate:
      type: object
      required: [name]
      properties:
        name: { type: string, minLength: 1 }
        currency: { type: string, minLength: 3, maxLength: 3 }
        locale: { type: string }
    ItemCatalogPage:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/ItemCatalog' }
        next_cursor: { type: string }
    CatalogItem:
      type: object
      required: [id, tenant_id, catalog_id, external_item_id, title, available, status, created_at, updated_at]
      description: >
        SKU/product row in an ItemCatalog (design 35). external_item_id is the
        merchant/product id unique per (tenant, catalog).
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        catalog_id: { type: string, format: uuid }
        external_item_id: { type: string, minLength: 1 }
        title: { type: string }
        brand: { type: string }
        category: { type: string }
        merchant_id: { type: string }
        price: { type: string, description: "Decimal string money (4 dp)." }
        currency: { type: string }
        available: { type: boolean }
        image_url: { type: string }
        product_url: { type: string }
        attributes:
          type: object
          additionalProperties: { type: string }
        status: { $ref: '#/components/schemas/LifecycleStatus' }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    CatalogItemInput:
      type: object
      required: [external_item_id]
      properties:
        external_item_id: { type: string, minLength: 1 }
        title: { type: string }
        brand: { type: string }
        category: { type: string }
        merchant_id: { type: string }
        price: { type: string }
        currency: { type: string }
        available: { type: boolean }
        image_url: { type: string }
        product_url: { type: string }
        attributes:
          type: object
          additionalProperties: { type: string }
        status: { $ref: '#/components/schemas/LifecycleStatus' }
    CatalogItemUpsert:
      type: object
      required: [items]
      properties:
        items:
          type: array
          minItems: 1
          maxItems: 1000
          items: { $ref: '#/components/schemas/CatalogItemInput' }
    CatalogItemPage:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/CatalogItem' }
        next_cursor: { type: string }
    ExportDestinationKind:
      type: string
      description: Object-store destination kind (design 37 §2 / CP-9).
      enum: [S3, GCS]
    ExportDestination:
      type: object
      required: [id, tenant_id, name, kind, bucket, format, enabled, status, created_at, updated_at]
      description: >
        Tenant-owned event export destination. Credentials via secret_ref (secret store).
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        name: { type: string }
        kind: { $ref: '#/components/schemas/ExportDestinationKind' }
        bucket: { type: string, minLength: 1 }
        prefix: { type: string }
        region: { type: string }
        secret_ref:
          type: string
          description: Secret-store reference for destination credentials (never plaintext).
        format:
          type: string
          enum: [JSONL, PARQUET]
          description: >
            File encoding (docs/spec/export-record.md). Files are written hourly under
            {prefix}/{tenant_id}/dt=YYYY-MM-DD/hour=HH/ in either format.
        event_kinds:
          type: array
          items: { type: string }
          description: Empty = all kinds; otherwise filter (e.g. IMPRESSION, CLICK).
        enabled: { type: boolean }
        status: { $ref: '#/components/schemas/LifecycleStatus' }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    ExportDestinationCreate:
      type: object
      required: [name, kind, bucket]
      properties:
        name: { type: string, minLength: 1 }
        kind: { $ref: '#/components/schemas/ExportDestinationKind' }
        bucket: { type: string, minLength: 1 }
        prefix: { type: string }
        region: { type: string }
        secret_ref: { type: string }
        format: { type: string, enum: [JSONL, PARQUET] }
        event_kinds:
          type: array
          items: { type: string }
        enabled: { type: boolean }
    ExportDestinationUpdate:
      type: object
      required: [name]
      properties:
        name: { type: string, minLength: 1 }
        bucket: { type: string, minLength: 1 }
        prefix: { type: string }
        region: { type: string }
        secret_ref: { type: string }
        format: { type: string, enum: [JSONL, PARQUET] }
        event_kinds:
          type: array
          items: { type: string }
        enabled: { type: boolean }
    ExportDestinationPage:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/ExportDestination' }
        next_cursor: { type: string }
    ExportJobKind:
      type: string
      description: Customer log export kind (maps to telemetry EventKind / attributed_conversion).
      enum: [WINS, IMPRESSIONS, CLICKS, CONVERSIONS, BIDS]
    ExportJobStatus:
      type: string
      enum: [PENDING, RUNNING, SUCCEEDED, FAILED, ARCHIVED]
    ExportJobFormat:
      type: string
      enum: [JSONL, CSV]
    ExportJob:
      type: object
      required: [id, tenant_id, kind, format, range_start, range_end, status, row_count, created_at, updated_at]
      description: >
        On-demand customer log export job (BP-LOGS). Worker writes JSONL/CSV to blobstore and
        sets blob_uri (mem:// or s3://). Empty history is SUCCEEDED with row_count 0.
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        kind: { $ref: '#/components/schemas/ExportJobKind' }
        format: { $ref: '#/components/schemas/ExportJobFormat' }
        range_start: { type: string, format: date-time }
        range_end: { type: string, format: date-time }
        status: { $ref: '#/components/schemas/ExportJobStatus' }
        blob_uri:
          type: string
          description: Object-store URI after SUCCEEDED (empty while PENDING/RUNNING).
        row_count: { type: integer, format: int64, minimum: 0 }
        error_message: { type: string }
        destination_id:
          type: string
          format: uuid
          description: >
            The tenant export_destination the file was written through (SR-1214); absent when
            the job wrote to the platform blobstore.
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        completed_at: { type: string, format: date-time }
    ExportJobCreate:
      type: object
      required: [kind, range_start, range_end]
      properties:
        kind: { $ref: '#/components/schemas/ExportJobKind' }
        format: { $ref: '#/components/schemas/ExportJobFormat' }
        range_start: { type: string, format: date-time }
        range_end: { type: string, format: date-time }
        destination_id:
          type: string
          format: uuid
          description: >
            Optional tenant export_destination (same tenant, ACTIVE) to write the file to through
            libs/export.ResolveWriter — its credentials come from the destination's secret_ref.
            Omit to write to the platform blobstore (mem:// or s3:// blob_uri).
    ExportJobPage:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/ExportJob' }
        next_cursor: { type: string }
    AdvertiserReportSummary:
      type: object
      required: [tenant_id, advertiser_id, impressions, clicks, spend]
      description: >
        Minimal onsite advertiser portal read model (design 37 §3 / CP-10). Counts are
        zero when no reporting source is wired; scaffold for branded portal surfaces.
      properties:
        tenant_id: { type: string, format: uuid }
        advertiser_id: { type: string, format: uuid }
        impressions: { type: integer, format: int64, minimum: 0 }
        clicks: { type: integer, format: int64, minimum: 0 }
        spend:
          type: string
          description: Decimal string money (4 dp) in tenant reporting currency.
        currency: { type: string }
    TenantOIDCConfig:
      type: object
      required: [tenant_id, enabled]
      description: >
        Tenant OIDC SSO config entity (design 37 §5 / CP-15). Client secret via
        client_secret_ref in the secret store — never plaintext in this API.
      properties:
        tenant_id: { type: string, format: uuid }
        enabled: { type: boolean }
        issuer_url: { type: string }
        client_id: { type: string }
        client_secret_ref: { type: string }
        scopes:
          type: array
          items: { type: string }
        role_claim: { type: string, description: "IdP claim mapped to Riptide roles." }
        updated_at: { type: string, format: date-time }
    TenantOIDCConfigUpdate:
      type: object
      properties:
        enabled: { type: boolean }
        issuer_url: { type: string }
        client_id: { type: string }
        client_secret_ref: { type: string }
        scopes:
          type: array
          items: { type: string }
        role_claim: { type: string }
    TenantSAMLConfig:
      type: object
      required: [tenant_id, enabled]
      description: >
        Tenant SAML SSO config scaffold (design 37 §5 / CP-15). IdP signing certificate via
        cert_ref in the secret store — never plaintext in this API. Assertion login residual.
      properties:
        tenant_id: { type: string, format: uuid }
        enabled: { type: boolean }
        entity_id: { type: string, description: "IdP entity ID / issuer." }
        sso_url: { type: string, description: "IdP HTTP-Redirect SSO URL." }
        cert_ref: { type: string, description: "Secret-store ref for IdP X.509 cert PEM." }
        role_claim: { type: string, description: "IdP attribute mapped to Riptide roles." }
        email_attribute: { type: string, description: "IdP attribute carrying the user's email; empty = the assertion NameID (email format)." }
        sp_entity_id: { type: string, description: "This deployment's SP entity id (the metadata URL), read-only." }
        updated_at: { type: string, format: date-time }
    TenantSAMLConfigUpdate:
      type: object
      properties:
        enabled: { type: boolean }
        entity_id: { type: string }
        sso_url: { type: string }
        cert_ref: { type: string }
        role_claim: { type: string }
        email_attribute: { type: string }
    AdvertiserLineItemWrite:
      type: object
      description: >
        Limited self-serve line-item fields for the onsite advertiser portal (CP-10):
        name, owning campaign_order_id (create), flight window, delivery goal, and rate.
      properties:
        campaign_order_id:
          type: string
          format: uuid
          description: Required on create; must belong to the path advertiser_id.
        name: { type: string }
        start_at: { type: string, format: date-time }
        end_at: { type: string, format: date-time }
        goal_type: { $ref: '#/components/schemas/DeliveryGoalType' }
        lifetime_goal: { type: integer, format: int64 }
        daily_goal: { type: integer, format: int64 }
        rate_amount: { type: string, description: "Buy rate (4dp decimal string)." }
        demand_rate_type: { $ref: '#/components/schemas/DemandRateType' }
        cpm: { type: string, description: "Legacy CPM alias when demand_rate_type is CPM." }
    IpListBulkImportResponse:
      type: object
      required: [accepted, rejected, staging_count]
      properties:
        accepted: { type: integer, minimum: 0 }
        rejected: { type: integer, minimum: 0 }
        staging_count:
          type: integer
          format: int64
          minimum: 0
          description: Staging lines present after this import.
        errors:
          type: array
          items:
            type: object
            required: [index, message]
            properties:
              index: { type: integer }
              message: { type: string }
    Placement:
      type: object
      required: [id, tenant_id, public_id, name, delivery, status]
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        public_id: { type: string }
        name: { type: string }
        delivery:
          type: string
          enum: [WEB, CTV, APP, DOOH]
        media: { type: string, enum: [VIDEO, AUDIO, BANNER] }
        status: { $ref: '#/components/schemas/LifecycleStatus' }
        floor: { type: string }
        pod_floor: { type: string }
        pricing: { $ref: '#/components/schemas/AdminPricing' }
        line_item_margin: { type: string }
        cpm_cap: { $ref: '#/components/schemas/AdminCpmCap' }
        upstream_cpm_cap: { $ref: '#/components/schemas/AdminCpmCap' }
        qps_limit: { type: integer, minimum: 0 }
        share_of_voice: { type: integer, minimum: 0, maximum: 100 }
        pod_enabled: { type: boolean }
        max_ad_duration: { type: integer, minimum: 0 }
        max_pod_slots: { type: integer, minimum: 0 }
        min_pod_duration: { type: integer, minimum: 0 }
        max_pod_duration: { type: integer, minimum: 0 }
        dedup_advertiser: { type: boolean }
        dedup_media: { type: boolean }
        dedup_category: { type: boolean }
        multi_imp: { type: boolean }
        packed_pixels:
          type: boolean
          description: >
            Packed pixels (SR-1210 / SR-902, design 29 "Rendering"): true renders one platform
            beacon per event and the beacon fans the vendor tracker set out server-side from
            the sealed payload; false (default) renders every vendor URL into the document.
        compliance_exempt:
          type: boolean
          description: >
            When true (the default for new placements) the placement is exempt from the partner
            approval gate (route_approval / app approvals): demand routes and deals serve on it
            without a per-partner approval record. False makes the gate blocking.
        ignore_compliance:
          type: boolean
          deprecated: true
          description: Deprecated since Wave 8 round 2 - alias of `compliance_exempt`, accepted and echoed for one release after CHANGELOG-api.md lists it; use `compliance_exempt`.
        blocked_advertisers: { type: array, items: { type: string } }
        blocked_categories: { type: array, items: { type: string } }
        allowed_advertisers:
          type: array
          items: { type: string, minLength: 1, maxLength: 253 }
          description: >
            Advertiser allow-list for this placement (SR-1217; exact or parent-domain match).
            Non-empty overrides the publisher's allowed_advertisers; empty falls through to it.
        vast_serving_paused:
          type: boolean
          description: "Kill switch (SR-1217): /vast answers an empty VAST document (reason `vast_paused`) for this placement."
        ortb_serving_paused:
          type: boolean
          description: "Kill switch (SR-1217): /ortb answers no-bid (reason `ortb_paused`) for this placement."
        blocked_exclusion_labels:
          type: array
          items: { type: string, minLength: 1, maxLength: 128 }
          description: >
            Competitive exclusion labels blocked on this placement (GC-03): a line item whose
            effective exclusion labels (union of advertiser, campaign order, and line item)
            intersect this set is ineligible here.
        verification: { $ref: '#/components/schemas/EntityVerification' }
        warm_pool: { $ref: '#/components/schemas/WarmPoolConfig' }
        dual_mode: { $ref: '#/components/schemas/DualModeConfig' }
        stitch: { $ref: '#/components/schemas/StitchConfig' }
        demand_attachment: { $ref: '#/components/schemas/DemandAttachmentConfig' }
        audience_id: { type: string, format: uuid }
        delivery_window_seconds:
          type: integer
          minimum: 60
          maximum: 86400
          description: Supply-side delivery window override (seconds); precedence over publisher/tenant.
        line_item_ttl_minutes:
          type: integer
          minimum: 5
          maximum: 1440
          description: Direct line-item beacon validity (minutes); default 60 when unset.
        remainder_pct:
          type: integer
          minimum: 0
          maximum: 99
          description: >
            CP-17 selection exploration percent (0–99). With that probability the single-winner
            path serves a uniform draw from non-primary eligible candidates. Omit/null = unset
            (no placement override of AuctionParams.RemainderPct).
        auction_type:
          $ref: '#/components/schemas/DealAuctionType'
        companion_slots:
          type: array
          maxItems: 8
          items: { $ref: '#/components/schemas/CompanionSlotSpec' }
          description: >
            Companion ad slots the placement's player can render (SR-1211): a creative's
            companions are matched to these by size; a `required` slot with no matching
            companion makes the creative ineligible here. Arrays present on update replace the
            full set.
        live_stream:
          nullable: true
          description: Live-stream insertion settings (SR-1211 / design 38). On update, omitted = unchanged and an explicit null clears them (the DemandPartner nullable-clears rule).
          allOf:
            - $ref: '#/components/schemas/LiveStreamConfig'
        publisher_id:
          type: string
          format: uuid
          description: Owning publisher (SR-1010); scopes the publisher portal, per-publisher reporting and payout statements.
        updated_at: { type: string, format: date-time }
    PlacementCreate:
      example:
        public_id: acme-preroll
        name: Acme web pre-roll
        publisher_id: 7a1d3c5e-2b4f-4c6d-8e9f-0a1b2c3d4e5f
        delivery: WEB
        media: VIDEO
        floor: "1.00"
        max_ad_duration: 30
      type: object
      required: [public_id, name, delivery]
      properties:
        public_id: { type: string, minLength: 1 }
        name: { type: string, minLength: 1 }
        publisher_id: { type: string, format: uuid, description: "Owning publisher; must be an active publisher in the same tenant." }
        delivery:
          type: string
          enum: [WEB, CTV, APP, DOOH]
        media: { type: string, enum: [VIDEO, AUDIO, BANNER] }
        floor: { type: string }
        pod_floor: { type: string }
        pricing: { $ref: '#/components/schemas/AdminPricing' }
        line_item_margin: { type: string }
        cpm_cap: { $ref: '#/components/schemas/AdminCpmCap' }
        upstream_cpm_cap: { $ref: '#/components/schemas/AdminCpmCap' }
        qps_limit: { type: integer, minimum: 0 }
        share_of_voice: { type: integer, minimum: 0, maximum: 100 }
        pod_enabled: { type: boolean }
        max_ad_duration: { type: integer, minimum: 0 }
        max_pod_slots: { type: integer, minimum: 0 }
        min_pod_duration: { type: integer, minimum: 0 }
        max_pod_duration: { type: integer, minimum: 0 }
        dedup_advertiser: { type: boolean }
        dedup_media: { type: boolean }
        dedup_category: { type: boolean }
        multi_imp: { type: boolean }
        packed_pixels:
          type: boolean
          description: >
            Packed pixels (SR-1210 / SR-902, design 29 "Rendering"): true renders one platform
            beacon per event and the beacon fans the vendor tracker set out server-side from
            the sealed payload; false (default) renders every vendor URL into the document.
        compliance_exempt: { type: boolean, description: "Partner-approval gate exemption; see Placement.compliance_exempt." }
        ignore_compliance: { type: boolean, deprecated: true, description: "Deprecated since Wave 8 round 2: alias of `compliance_exempt`, accepted and echoed for one release after CHANGELOG-api.md lists it; use `compliance_exempt`." }
        blocked_advertisers: { type: array, items: { type: string } }
        blocked_categories: { type: array, items: { type: string } }
        allowed_advertisers:
          type: array
          items: { type: string, minLength: 1, maxLength: 253 }
          description: >
            Advertiser allow-list for this placement (SR-1217; exact or parent-domain match).
            Non-empty overrides the publisher's allowed_advertisers; empty falls through to it.
        vast_serving_paused:
          type: boolean
          description: "Kill switch (SR-1217): /vast answers an empty VAST document (reason `vast_paused`) for this placement."
        ortb_serving_paused:
          type: boolean
          description: "Kill switch (SR-1217): /ortb answers no-bid (reason `ortb_paused`) for this placement."
        blocked_exclusion_labels:
          type: array
          items: { type: string, minLength: 1, maxLength: 128 }
          description: >
            Competitive exclusion labels blocked on this placement (GC-03): a line item whose
            effective exclusion labels (union of advertiser, campaign order, and line item)
            intersect this set is ineligible here.
        verification: { $ref: '#/components/schemas/EntityVerification' }
        warm_pool: { $ref: '#/components/schemas/WarmPoolConfig' }
        dual_mode: { $ref: '#/components/schemas/DualModeConfig' }
        stitch: { $ref: '#/components/schemas/StitchConfig' }
        demand_attachment: { $ref: '#/components/schemas/DemandAttachmentConfig' }
        audience_id: { type: string, format: uuid }
        delivery_window_seconds:
          type: integer
          minimum: 60
          maximum: 86400
          description: Supply-side delivery window override (seconds); precedence over publisher/tenant.
        line_item_ttl_minutes:
          type: integer
          minimum: 5
          maximum: 1440
          description: Direct line-item beacon validity (minutes); default 60 when unset.
        remainder_pct:
          type: integer
          minimum: 0
          maximum: 99
          description: >
            CP-17 selection exploration percent (0–99). Omit/null = unset (no placement override).
        auction_type: { $ref: '#/components/schemas/DealAuctionType' }
        companion_slots:
          type: array
          maxItems: 8
          items: { $ref: '#/components/schemas/CompanionSlotSpec' }
          description: >
            Companion ad slots the placement's player can render (SR-1211): a creative's
            companions are matched to these by size; a `required` slot with no matching
            companion makes the creative ineligible here. Arrays present on update replace the
            full set.
        live_stream:
          nullable: true
          description: Live-stream insertion settings (SR-1211 / design 38). On update, omitted = unchanged and an explicit null clears them (the DemandPartner nullable-clears rule).
          allOf:
            - $ref: '#/components/schemas/LiveStreamConfig'
    PlacementUpdate:
      type: object
      description: >
        Partial update — omitted optional objects/fields leave stored values untouched
        (same convention as stitch and demand_attachment). Arrays present replace the full set.
      required: [name]
      properties:
        name: { type: string, minLength: 1 }
        publisher_id: { type: string, format: uuid, description: "Owning publisher; must be an active publisher in the same tenant." }
        media:
          type: string
          enum: [VIDEO, AUDIO, BANNER]
          # Pinned Go constant names (see DemandRouteIntegration for rationale).
          x-enum-varnames: [PlacementUpdateMediaVIDEO, PlacementUpdateMediaAUDIO, PlacementUpdateMediaBANNER]
        floor: { type: string }
        pod_floor: { type: string }
        pricing: { $ref: '#/components/schemas/AdminPricing' }
        line_item_margin: { type: string }
        cpm_cap: { $ref: '#/components/schemas/AdminCpmCap' }
        upstream_cpm_cap: { $ref: '#/components/schemas/AdminCpmCap' }
        qps_limit: { type: integer, minimum: 0 }
        share_of_voice: { type: integer, minimum: 0, maximum: 100 }
        pod_enabled: { type: boolean }
        max_ad_duration: { type: integer, minimum: 0 }
        max_pod_slots: { type: integer, minimum: 0 }
        min_pod_duration: { type: integer, minimum: 0 }
        max_pod_duration: { type: integer, minimum: 0 }
        dedup_advertiser: { type: boolean }
        dedup_media: { type: boolean }
        dedup_category: { type: boolean }
        multi_imp: { type: boolean }
        packed_pixels:
          type: boolean
          description: >
            Packed pixels (SR-1210 / SR-902, design 29 "Rendering"): true renders one platform
            beacon per event and the beacon fans the vendor tracker set out server-side from
            the sealed payload; false (default) renders every vendor URL into the document.
        compliance_exempt: { type: boolean, description: "Partner-approval gate exemption; see Placement.compliance_exempt." }
        ignore_compliance: { type: boolean, deprecated: true, description: "Deprecated since Wave 8 round 2: alias of `compliance_exempt`, accepted and echoed for one release after CHANGELOG-api.md lists it; use `compliance_exempt`." }
        blocked_advertisers: { type: array, items: { type: string } }
        blocked_categories: { type: array, items: { type: string } }
        allowed_advertisers:
          type: array
          items: { type: string, minLength: 1, maxLength: 253 }
          description: >
            Advertiser allow-list for this placement (SR-1217; exact or parent-domain match).
            Non-empty overrides the publisher's allowed_advertisers; empty falls through to it.
        vast_serving_paused:
          type: boolean
          description: "Kill switch (SR-1217): /vast answers an empty VAST document (reason `vast_paused`) for this placement."
        ortb_serving_paused:
          type: boolean
          description: "Kill switch (SR-1217): /ortb answers no-bid (reason `ortb_paused`) for this placement."
        blocked_exclusion_labels:
          type: array
          items: { type: string, minLength: 1, maxLength: 128 }
          description: >
            Competitive exclusion labels blocked on this placement (GC-03): a line item whose
            effective exclusion labels (union of advertiser, campaign order, and line item)
            intersect this set is ineligible here.
        verification: { $ref: '#/components/schemas/EntityVerification' }
        warm_pool: { $ref: '#/components/schemas/WarmPoolConfig' }
        dual_mode: { $ref: '#/components/schemas/DualModeConfig' }
        stitch: { $ref: '#/components/schemas/StitchConfig' }
        demand_attachment: { $ref: '#/components/schemas/DemandAttachmentConfig' }
        audience_id: { type: string, format: uuid }
        delivery_window_seconds:
          type: integer
          minimum: 60
          maximum: 86400
          description: Supply-side delivery window override (seconds); precedence over publisher/tenant.
        line_item_ttl_minutes:
          type: integer
          minimum: 5
          maximum: 1440
          description: Direct line-item beacon validity (minutes); default 60 when unset.
        remainder_pct:
          type: integer
          minimum: 0
          maximum: 99
          description: >
            CP-17 selection exploration percent (0–99). Omit to leave the stored value
            untouched (same partial-update convention as other optional scalars).
        auction_type: { $ref: '#/components/schemas/DealAuctionType' }
        companion_slots:
          type: array
          maxItems: 8
          items: { $ref: '#/components/schemas/CompanionSlotSpec' }
          description: >
            Companion ad slots the placement's player can render (SR-1211): a creative's
            companions are matched to these by size; a `required` slot with no matching
            companion makes the creative ineligible here. Arrays present on update replace the
            full set.
        live_stream:
          nullable: true
          description: Live-stream insertion settings (SR-1211 / design 38). On update, omitted = unchanged and an explicit null clears them (the DemandPartner nullable-clears rule).
          allOf:
            - $ref: '#/components/schemas/LiveStreamConfig'
    CompanionSlotSpec:
      type: object
      description: One companion ad slot a placement's player renders (SR-1211).
      required: [width, height]
      properties:
        width: { type: integer, minimum: 1, description: "Slot width in pixels." }
        height: { type: integer, minimum: 1, description: "Slot height in pixels." }
        required:
          type: boolean
          description: True when a creative without a companion of this size is ineligible on the placement.
    LiveStreamBreakSource:
      type: string
      description: >
        How ad breaks are detected in a live HLS stream (SR-1211): CUE (EXT-X-CUE-OUT/IN tags),
        SCTE35 (EXT-X-DATERANGE SCTE-35 markers), PDT (EXT-X-PROGRAM-DATE-TIME schedule match),
        SCHEDULE (operator-defined break times).
      enum: [CUE, SCTE35, PDT, SCHEDULE]
      x-enum-varnames: [LiveStreamBreakSourceCUE, LiveStreamBreakSourceSCTE35, LiveStreamBreakSourcePDT, LiveStreamBreakSourceSCHEDULE]
    LiveStreamConfig:
      type: object
      description: >
        Live-stream ad insertion for an HLS placement (SR-1211): the stitcher rewrites the
        origin's sliding-window manifest, fills breaks from the auction, and serves predicted
        segment URLs ahead of the break.
      properties:
        enabled:
          type: boolean
          description: True to serve this placement through live-stream insertion; false leaves the origin manifest untouched.
        window_segments:
          type: integer
          minimum: 1
          maximum: 600
          description: Number of media segments kept in the rewritten sliding window.
        break_source: { $ref: '#/components/schemas/LiveStreamBreakSource' }
        predicted_url_template:
          type: string
          maxLength: 2048
          description: URL template for predicted ad segments served before the break is filled ({{SESSION}} / {{SEQ}} macros).
        aes_key_uri:
          type: string
          maxLength: 2048
          description: EXT-X-KEY URI the rewritten manifest advertises when the origin is AES-128 encrypted; omit for clear streams.
    PlacementList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/Placement' }
        next_cursor: { type: string }
    SealingKeyStatus:
      type: string
      enum: [ACTIVE, RETIRED]
      # Pinned Go constant names: oapi-codegen's automatic conflict-prefixing is order-sensitive
      # (ApiKeyStatus, added by SR-1011, shares ACTIVE) — pin so existing code keeps compiling.
      x-enum-varnames: [SealingKeyStatusACTIVE, SealingKeyStatusRETIRED]
    SealingKey:
      type: object
      required: [id, tenant_id, key_id, status, created_at]
      properties:
        id: { type: string, format: uuid, description: Row id (internal). }
        tenant_id: { type: string, format: uuid }
        key_id:
          type: string
          description: Opaque id embedded in sealed payloads; used in retire path.
        status: { $ref: '#/components/schemas/SealingKeyStatus' }
        created_at: { type: string, format: date-time }
        retired_at: { type: string, format: date-time }
    SealingKeyCreate:
      type: object
      properties:
        key_id:
          type: string
          minLength: 1
          description: Optional opaque key id; server generates when omitted.
    SealingKeyList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/SealingKey' }
        next_cursor: { type: string }
    DemandPartner:
      type: object
      required: [id, tenant_id, name, adapter, status]
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        name: { type: string }
        adapter: { type: string }
        approvals_required: { type: boolean }
        timeout_ms: { type: integer, minimum: 1 }
        max_concurrency:
          type: integer
          nullable: true
          minimum: 1
          description: Maximum concurrent outbound calls a serving node keeps in flight to this partner; omitted = unlimited.
        rev_share: { type: string }
        flat_cost: { type: string }
        sync_redirect: { type: string }
        tcf_vendor_id: { type: integer, minimum: 1, description: "IAB TCF vendor id for vendor-scoped GDPR forwards." }
        bid_ttl_seconds:
          type: integer
          minimum: 60
          maximum: 86400
          description: Default buyer notice tolerance (seconds) when bid.exp is omitted.
        openrtb_version:
          type: string
          enum: ["", "2.5", "2.6"]
          description: >
            OpenRTB version the partner speaks (SR-1207, design 06 "Outbound request
            construction"): empty = 2.6 (default). 2.5 downgrades the 2.6-only objects on the
            outbound request and parses the response as 2.5.
        warm_pool_no_cache: { type: boolean, description: "Opt every bid of this partner out of the warm pool (docs/design/17-warm-pool.md Eligibility)." }
        warm_pool_allow_deals: { type: boolean, description: "Allow this partner's deal/PMP bids to be cached in the warm pool (never cached otherwise)." }
        warm_pool_ttl_class:
          type: string
          enum: ["", default, short, long, buyer_exp]
          description: Warm-pool TTL class override for this partner; empty uses the placement class.
        status: { $ref: '#/components/schemas/LifecycleStatus' }
        updated_at: { type: string, format: date-time }
    DemandPartnerCreate:
      type: object
      required: [name, adapter]
      properties:
        name: { type: string, minLength: 1 }
        adapter: { type: string, minLength: 1 }
        approvals_required: { type: boolean }
        timeout_ms: { type: integer, minimum: 1 }
        max_concurrency:
          type: integer
          nullable: true
          minimum: 1
          description: Maximum concurrent outbound calls a serving node keeps in flight to this partner; omitted = unlimited.
        rev_share: { type: string }
        flat_cost: { type: string }
        sync_redirect: { type: string }
        tcf_vendor_id: { type: integer, minimum: 1 }
        bid_ttl_seconds: { type: integer, minimum: 60, maximum: 86400 }
        openrtb_version:
          type: string
          enum: ["", "2.5", "2.6"]
          description: >
            OpenRTB version the partner speaks (SR-1207, design 06 "Outbound request
            construction"): empty = 2.6 (default). 2.5 downgrades the 2.6-only objects on the
            outbound request and parses the response as 2.5.
        warm_pool_no_cache: { type: boolean, description: "Opt every bid of this partner out of the warm pool (docs/design/17-warm-pool.md Eligibility)." }
        warm_pool_allow_deals: { type: boolean, description: "Allow this partner's deal/PMP bids to be cached in the warm pool (never cached otherwise)." }
        warm_pool_ttl_class:
          type: string
          enum: ["", default, short, long, buyer_exp]
          description: Warm-pool TTL class override for this partner; empty uses the placement class.
    DemandPartnerUpdate:
      type: object
      description: >
        Partial update by presence. Optional integers (`timeout_ms`, `max_concurrency`,
        `tcf_vendor_id`) follow the nullable-clears rule (docs/spec/api-fundamentals.md
        "Nullable fields"): omitted leaves the stored value, an explicit `null` clears it back
        to unset.
      required: [name]
      properties:
        name: { type: string, minLength: 1 }
        approvals_required: { type: boolean }
        timeout_ms:
          type: integer
          nullable: true
          minimum: 1
          description: Partner timeout; omitted = unchanged, null = clear to the process default.
        max_concurrency:
          type: integer
          nullable: true
          minimum: 1
          description: Maximum concurrent outbound calls a serving node keeps in flight to this partner; omitted = unchanged, null = unlimited.
        rev_share: { type: string }
        flat_cost: { type: string }
        sync_redirect: { type: string }
        tcf_vendor_id:
          type: integer
          nullable: true
          minimum: 1
          description: IAB TCF vendor id; omitted = unchanged, null = clear (no vendor-scoped deny path).
        bid_ttl_seconds: { type: integer, minimum: 60, maximum: 86400 }
        openrtb_version:
          type: string
          enum: ["", "2.5", "2.6"]
          description: >
            OpenRTB version the partner speaks (SR-1207, design 06 "Outbound request
            construction"): empty = 2.6 (default). 2.5 downgrades the 2.6-only objects on the
            outbound request and parses the response as 2.5.
        warm_pool_no_cache: { type: boolean, description: "Opt every bid of this partner out of the warm pool (docs/design/17-warm-pool.md Eligibility)." }
        warm_pool_allow_deals: { type: boolean, description: "Allow this partner's deal/PMP bids to be cached in the warm pool (never cached otherwise)." }
        warm_pool_ttl_class:
          type: string
          enum: ["", default, short, long, buyer_exp]
          description: Warm-pool TTL class override for this partner; empty uses the placement class.
    DemandPartnerList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/DemandPartner' }
        next_cursor: { type: string }
    App:
      type: object
      description: >
        App inventory — docs/design/02-data-model.md "Supply". Includes publisher_pricing and
        route_approvals maps (RIP-133).
      required: [id, tenant_id, name, status]
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        bundle_id: { type: string }
        store_id: { type: string }
        domain: { type: string }
        name: { type: string }
        platform_id: { type: string, format: uuid, description: "Global device/CTV platform catalog reference." }
        language: { type: string }
        categories: { type: array, items: { type: string } }
        store_urls: { type: array, items: { type: string } }
        max_ad_duration: { type: integer, minimum: 0 }
        publisher_pricing:
          type: object
          additionalProperties: { $ref: '#/components/schemas/AppPricingEntry' }
          description: "Keyed by publisher id."
        route_approvals:
          type: object
          additionalProperties: { $ref: '#/components/schemas/RouteApprovalEntry' }
          description: "Keyed by demand partner id."
        status: { $ref: '#/components/schemas/LifecycleStatus' }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    AppCreate:
      type: object
      required: [name]
      properties:
        bundle_id: { type: string }
        store_id: { type: string }
        domain: { type: string }
        name: { type: string, minLength: 1 }
        platform_id: { type: string, format: uuid }
        language: { type: string }
        categories: { type: array, items: { type: string } }
        store_urls: { type: array, items: { type: string } }
        max_ad_duration: { type: integer, minimum: 0 }
        publisher_pricing:
          type: object
          additionalProperties: { $ref: '#/components/schemas/AppPricingEntry' }
        route_approvals:
          type: object
          additionalProperties: { $ref: '#/components/schemas/RouteApprovalEntry' }
    AppUpdate:
      type: object
      required: [name]
      properties:
        name: { type: string, minLength: 1 }
        domain: { type: string }
        language: { type: string }
        categories: { type: array, items: { type: string } }
        store_urls: { type: array, items: { type: string } }
        max_ad_duration: { type: integer, minimum: 0 }
        publisher_pricing:
          type: object
          additionalProperties: { $ref: '#/components/schemas/AppPricingEntry' }
        route_approvals:
          type: object
          additionalProperties: { $ref: '#/components/schemas/RouteApprovalEntry' }
    AppList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/App' }
        next_cursor: { type: string }
    DemandRouteIntegration:
      type: string
      description: Demand route integration kind (riptide.common.v1.IntegrationKind).
      enum: [ORTB, VAST, ADAPTER, CUSTOM]
      # Pinned Go constant names: oapi-codegen's automatic conflict-prefixing is order-sensitive
      # across the whole spec; pinning keeps the generated adminv1 identifiers stable as enums
      # are added elsewhere (design 30 added DeliveryStatus/CreativeKind values).
      x-enum-varnames: [DemandRouteIntegrationORTB, DemandRouteIntegrationVAST, DemandRouteIntegrationADAPTER, DemandRouteIntegrationCUSTOM]
    DemandRoute:
      type: object
      description: >
        A configured path to a partner's demand — docs/design/06-demand.md. Includes endpoints,
        seats, deals, params, fanout, and audience binding (RIP-133 settings flexibility).
        Request rewrites remain a dedicated sub-resource.
      required: [id, tenant_id, demand_partner_id, integration, status]
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        demand_partner_id: { type: string, format: uuid }
        integration: { $ref: '#/components/schemas/DemandRouteIntegration' }
        name: { type: string, maxLength: 200, description: "Operator-facing route name shown in lists and pickers (REM-J); the id stays the key." }
        media: { type: string, enum: [VIDEO, AUDIO, BANNER] }
        endpoints:
          type: object
          additionalProperties: { type: string }
          description: "Region → bid endpoint URI map."
        target_cpm: { type: string, description: "When set, bids on this route skip the partner revenue-share deflation (the flat cost still applies) — docs/design/08-economics.md \"Bid pre-process\"." }
        floor: { type: string, description: "Route floor (decimal CPM): REPLACES the placement floor as the base of the floor sent to this partner; the request floor only raises it." }
        flat_cost: { type: string, description: "Route-level IO/flat cost (decimal CPM) subtracted from every bid after the revenue-share deflation; overrides the partner's flat_cost." }
        qps: { type: integer, minimum: 0 }
        share_of_voice: { type: integer, minimum: 0 }
        gzip: { type: boolean }
        require_advertiser: { type: boolean }
        require_category: { type: boolean }
        supply_chain_asi: { type: string }
        seller_id: { type: string }
        private_auction: { type: boolean }
        params:
          type: object
          additionalProperties: { type: string }
          description: Adapter macros / partner params.
        fanout: { $ref: '#/components/schemas/FanoutConfig' }
        seats: { type: array, items: { type: string } }
        deals: { type: array, items: { type: string, format: uuid }, description: "Deal ids bound to this route." }
        audience_id: { type: string, format: uuid }
        marketplace_id: { type: string, format: uuid }
        marketplace_rev_share: { type: string }
        supply_transparency: { $ref: '#/components/schemas/SupplyTransparencyPolicy' }
        unwrap_depth:
          type: integer
          minimum: 0
          maximum: 10
          description: >
            <Wrapper> hops the hub follows for this route's VAST bids before serving the bid as
            is (SR-1210, design 10 "Demand bid unwrap"); omitted = the process default, 0 =
            never unwrap.
        fixed_cpm:
          type: string
          description: >
            Fixed CPM (decimal, 4 dp) a VAST-tag route (integration VAST) bids when the
            partner's tag carries no <Pricing> (SR-1235, design 06 "Adapters"); omitted = such a
            tag is a no-bid.
        status: { $ref: '#/components/schemas/LifecycleStatus' }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    DemandRouteCreate:
      type: object
      required: [demand_partner_id, integration]
      properties:
        demand_partner_id: { type: string, format: uuid }
        integration: { $ref: '#/components/schemas/DemandRouteIntegration' }
        name: { type: string, maxLength: 200, description: "Operator-facing route name; see DemandRoute.name." }
        media: { type: string, enum: [VIDEO, AUDIO, BANNER] }
        endpoints:
          type: object
          additionalProperties: { type: string }
        target_cpm: { type: string, description: "When set, bids on this route skip the partner revenue-share deflation (the flat cost still applies) — docs/design/08-economics.md \"Bid pre-process\"." }
        floor: { type: string, description: "Route floor (decimal CPM): REPLACES the placement floor as the base of the floor sent to this partner; the request floor only raises it." }
        flat_cost: { type: string, description: "Route-level IO/flat cost (decimal CPM) subtracted from every bid after the revenue-share deflation; overrides the partner's flat_cost." }
        qps: { type: integer, minimum: 0 }
        share_of_voice: { type: integer, minimum: 0 }
        gzip: { type: boolean }
        require_advertiser: { type: boolean }
        require_category: { type: boolean }
        supply_chain_asi: { type: string }
        seller_id: { type: string }
        private_auction: { type: boolean }
        params:
          type: object
          additionalProperties: { type: string }
        fanout: { $ref: '#/components/schemas/FanoutConfig' }
        seats: { type: array, items: { type: string } }
        deals: { type: array, items: { type: string, format: uuid } }
        audience_id: { type: string, format: uuid }
        marketplace_id: { type: string, format: uuid }
        marketplace_rev_share: { type: string }
        supply_transparency: { $ref: '#/components/schemas/SupplyTransparencyPolicy' }
        unwrap_depth:
          type: integer
          minimum: 0
          maximum: 10
          description: >
            <Wrapper> hops the hub follows for this route's VAST bids before serving the bid as
            is (SR-1210, design 10 "Demand bid unwrap"); omitted = the process default, 0 =
            never unwrap.
        fixed_cpm:
          type: string
          description: >
            Fixed CPM (decimal, 4 dp) a VAST-tag route (integration VAST) bids when the
            partner's tag carries no <Pricing> (SR-1235, design 06 "Adapters"); omitted = such a
            tag is a no-bid.
    DemandRouteUpdate:
      type: object
      properties:
        media: { type: string, enum: [VIDEO, AUDIO, BANNER] }
        name: { type: string, maxLength: 200, description: "Operator-facing route name; see DemandRoute.name. Omitted = unchanged." }
        endpoints:
          type: object
          additionalProperties: { type: string }
        target_cpm: { type: string, description: "When set, bids on this route skip the partner revenue-share deflation (the flat cost still applies) — docs/design/08-economics.md \"Bid pre-process\"." }
        floor: { type: string, description: "Route floor (decimal CPM): REPLACES the placement floor as the base of the floor sent to this partner; the request floor only raises it." }
        flat_cost: { type: string, description: "Route-level IO/flat cost (decimal CPM) subtracted from every bid after the revenue-share deflation; overrides the partner's flat_cost." }
        qps: { type: integer, minimum: 0 }
        share_of_voice: { type: integer, minimum: 0 }
        gzip: { type: boolean }
        require_advertiser: { type: boolean }
        require_category: { type: boolean }
        supply_chain_asi: { type: string }
        seller_id: { type: string }
        private_auction: { type: boolean }
        params:
          type: object
          additionalProperties: { type: string }
        fanout: { $ref: '#/components/schemas/FanoutConfig' }
        seats: { type: array, items: { type: string } }
        deals: { type: array, items: { type: string, format: uuid } }
        audience_id: { type: string, format: uuid }
        marketplace_id: { type: string, format: uuid }
        marketplace_rev_share: { type: string }
        supply_transparency: { $ref: '#/components/schemas/SupplyTransparencyPolicy' }
        unwrap_depth:
          type: integer
          minimum: 0
          maximum: 10
          description: >
            <Wrapper> hops the hub follows for this route's VAST bids before serving the bid as
            is (SR-1210, design 10 "Demand bid unwrap"); omitted = the process default, 0 =
            never unwrap.
        fixed_cpm:
          type: string
          description: >
            Fixed CPM (decimal, 4 dp) a VAST-tag route (integration VAST) bids when the
            partner's tag carries no <Pricing> (SR-1235, design 06 "Adapters"); omitted = such a
            tag is a no-bid.
    DemandRouteList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/DemandRoute' }
        next_cursor: { type: string }
    RouteRewrite:
      type: object
      description: >
        A single request-rewrite rule on a demand route (route_rewrite, docs/design/06-demand.md) —
        P3-03's admin surface for the table 0002_demand always carried. Exactly one of `value`
        (when overwriting/appending a field) or `clear` (removing it) applies; a rule cannot be both.
      required: [id, path]
      properties:
        id: { type: string, format: uuid }
        path: { type: string, minLength: 1, description: "The bid-request field path to rewrite." }
        value: { type: string, description: "Replacement value; empty when clear is true." }
        overwrite: { type: boolean, description: "Replace an existing value rather than only filling an empty one." }
        clear: { type: boolean, description: "Remove the field instead of setting a value." }
    RouteRewriteCreate:
      type: object
      required: [path]
      properties:
        path: { type: string, minLength: 1 }
        value: { type: string }
        overwrite: { type: boolean }
        clear: { type: boolean }
    RouteRewriteSet:
      type: object
      required: [rewrites]
      properties:
        rewrites:
          type: array
          items: { $ref: '#/components/schemas/RouteRewriteCreate' }
    RouteRewriteList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/RouteRewrite' }
        next_cursor: { type: string }
    DemandRouteRewritePreviewRequest:
      type: object
      required: [bid_request]
      description: >
        Sample OpenRTB bid request to run through the route's saved rewrite rules. The request must
        contain exactly one imp, matching Riptide's current one-opportunity-per-request outbound
        builder.
      properties:
        bid_request:
          type: object
          additionalProperties: true
    DemandRouteRewritePreviewResponse:
      type: object
      required: [original_bid_request, rewritten_bid_request, applied_rewrites]
      properties:
        original_bid_request:
          type: object
          additionalProperties: true
        rewritten_bid_request:
          type: object
          additionalProperties: true
        applied_rewrites:
          type: array
          items: { $ref: '#/components/schemas/RouteRewrite' }
    DealAuctionType:
      type: string
      description: riptide.common.v1.AuctionType.
      enum: [FIRST_PRICE, SECOND_PRICE, BID_FLOOR]
    DealKind:
      type: string
      description: >
        Negotiated access kind. Omit on create/update to inherit from guaranteed
        (true -> GUARANTEED, false -> PREFERRED).
      enum: [PREFERRED, GUARANTEED, PRIVATE]
    Deal:
      type: object
      description: >
        A negotiated buy — docs/design/06-demand.md. Full surface includes seats, advertiser
        domains, kind, flight, verification, audience, and per-deal frequency caps (RIP-133).
      required: [id, tenant_id, external_id, name, status]
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        external_id: { type: string, description: "The negotiated deal id sent to demand." }
        name: { type: string }
        auction_type: { $ref: '#/components/schemas/DealAuctionType' }
        floor: { type: string }
        currency: { type: string }
        guaranteed: { type: boolean, description: "Wins outright when matched." }
        kind: { $ref: '#/components/schemas/DealKind' }
        flight_start: { type: string, format: date-time, description: "Inclusive UTC flight start." }
        flight_end: { type: string, format: date-time, description: "Exclusive UTC flight end; omit for open-ended." }
        priority: { type: integer, format: int32, default: 0, description: "Preferred ranking hint; higher first." }
        committed_impressions:
          type: integer
          format: int64
          minimum: 0
          description: >
            Volume commitment in impressions (GC-06, RIPTIDE-87, ADR D54 D-02). Exactly one of
            committed_impressions / committed_spend may be set; a commitment requires
            guaranteed=true AND a bounded flight (flight_end set). The commitment paces the
            deal's guaranteed outright wins; under-delivery surfaces as a makegood
            recommendation, never a credit. Omit both for no commitment (today's behavior).
        committed_spend:
          type: string
          description: >
            Volume commitment as spend — decimal money string (4dp), e.g. "5000.0000", in the
            deal's currency (GC-06, RIPTIDE-87, ADR D54 D-02). Exactly one of
            committed_impressions / committed_spend may be set; a commitment requires
            guaranteed=true AND a bounded flight (flight_end set). The commitment paces the
            deal's guaranteed outright wins; under-delivery surfaces as a makegood
            recommendation, never a credit. Omit both for no commitment (today's behavior).
        format: { type: string }
        advertiser_id: { type: string, format: uuid }
        seats: { type: array, items: { type: string } }
        advertiser_domains: { type: array, items: { type: string } }
        verification: { $ref: '#/components/schemas/EntityVerification' }
        audience_id: { type: string, format: uuid }
        first_impression: { type: boolean }
        all_pods: { type: boolean }
        frequency_caps:
          type: array
          items: { $ref: '#/components/schemas/FrequencyCap' }
        supply_transparency: { $ref: '#/components/schemas/SupplyTransparencyPolicy' }
        status: { $ref: '#/components/schemas/LifecycleStatus' }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    DealMakegoodRecommendation:
      type: object
      description: >
        libs/auction/direct.RecommendMakegood's answer for one committed guaranteed deal
        (design 31 "Pacing parity" deal commitments; ADR D54 D-02): the commitment in its primary
        unit, what the deal-scoped counters delivered, the shortfall, the delivery percent against
        the linear pace, and the status. counters_available=false means the hot store is not
        configured (or unreachable) and delivered reads zero — reported honestly, never guessed.
      required: [deal_id, external_id, unit, committed, delivered, shortfall, delivery_pct, expected_pct, status, recommend, counters_available, computed_at]
      properties:
        deal_id: { type: string, format: uuid }
        external_id: { type: string, description: "The negotiated deal id sent to demand." }
        unit:
          type: string
          enum: [impressions, spend]
          x-enum-varnames: [DealMakegoodUnitImpressions, DealMakegoodUnitSpend]
          description: >
            The commitment's primary unit: impressions (committed_impressions) or spend
            (committed_spend). committed / delivered / shortfall are counts for impressions and
            integer micros of the deal currency for spend — the counters' own unit.
        committed: { type: integer, format: int64 }
        delivered: { type: integer, format: int64 }
        shortfall: { type: integer, format: int64, description: "committed − delivered when positive; 0 once delivered." }
        delivery_pct: { type: number, format: double, description: "delivered ÷ committed × 100 (not clamped, so over-delivery shows)." }
        expected_pct:
          type: number
          format: double
          description: >
            The linear pace by now over the flight: 0 before the start, 100 at/after the end,
            proportional in between; equal to delivery_pct when the flight is unbounded (an
            open-ended deal is never behind).
        status:
          type: string
          enum: [on_track, behind, delivered, shortfall]
          x-enum-varnames: [DealMakegoodStatusOnTrack, DealMakegoodStatusBehind, DealMakegoodStatusDelivered, DealMakegoodStatusShortfall]
          description: >
            on_track = delivery within 10 percentage points of the linear pace; behind = in flight
            and more than 10 points under pace; delivered = commitment met (100 % or more);
            shortfall = the flight ended under 100 % — a makegood is recommended.
        recommend: { type: boolean, description: "True only for shortfall: offer a makegood (extension or bonus inventory)." }
        flight_end: { type: string, format: date-time, description: "Exclusive UTC flight end; absent for an open-ended deal." }
        counters_available: { type: boolean }
        computed_at: { type: string, format: date-time }
    DealCreate:
      type: object
      required: [external_id, name]
      properties:
        external_id: { type: string, minLength: 1 }
        name: { type: string, minLength: 1 }
        auction_type: { $ref: '#/components/schemas/DealAuctionType' }
        floor: { type: string }
        currency: { type: string }
        guaranteed: { type: boolean }
        kind: { $ref: '#/components/schemas/DealKind' }
        flight_start: { type: string, format: date-time }
        flight_end: { type: string, format: date-time }
        priority: { type: integer, format: int32, default: 0 }
        committed_impressions:
          type: integer
          format: int64
          minimum: 0
          description: >
            Volume commitment in impressions (GC-06, RIPTIDE-87, ADR D54 D-02). Exactly one of
            committed_impressions / committed_spend may be set (both set is rejected); a
            commitment requires guaranteed=true AND a bounded flight (flight_end set). The
            commitment paces the deal's guaranteed outright wins; under-delivery surfaces as a
            makegood recommendation, never a credit. Omit both for no commitment (today's
            behavior).
        committed_spend:
          type: string
          description: >
            Volume commitment as spend — decimal money string (4dp), e.g. "5000.0000", in the
            deal's currency (GC-06, RIPTIDE-87, ADR D54 D-02). Exactly one of
            committed_impressions / committed_spend may be set (both set is rejected); a
            commitment requires guaranteed=true AND a bounded flight (flight_end set). The
            commitment paces the deal's guaranteed outright wins; under-delivery surfaces as a
            makegood recommendation, never a credit. Omit both for no commitment (today's
            behavior).
        format: { type: string }
        advertiser_id: { type: string, format: uuid }
        seats: { type: array, items: { type: string } }
        advertiser_domains: { type: array, items: { type: string } }
        verification: { $ref: '#/components/schemas/EntityVerification' }
        audience_id: { type: string, format: uuid }
        first_impression: { type: boolean }
        all_pods: { type: boolean }
        frequency_caps:
          type: array
          items: { $ref: '#/components/schemas/FrequencyCapInput' }
        supply_transparency: { $ref: '#/components/schemas/SupplyTransparencyPolicy' }
    DealUpdate:
      type: object
      required: [name]
      properties:
        name: { type: string, minLength: 1 }
        auction_type: { $ref: '#/components/schemas/DealAuctionType' }
        floor: { type: string }
        currency: { type: string }
        guaranteed: { type: boolean }
        kind: { $ref: '#/components/schemas/DealKind' }
        flight_start: { type: string, format: date-time }
        flight_end: { type: string, format: date-time }
        priority: { type: integer, format: int32, default: 0 }
        committed_impressions:
          type: integer
          format: int64
          minimum: 0
          description: >
            Volume commitment in impressions (GC-06, RIPTIDE-87, ADR D54 D-02). Exactly one of
            committed_impressions / committed_spend may be set (both set is rejected); a
            commitment requires guaranteed=true AND a bounded flight (flight_end set). The
            commitment paces the deal's guaranteed outright wins; under-delivery surfaces as a
            makegood recommendation, never a credit. Omit both to clear the commitment (today's
            behavior).
        committed_spend:
          type: string
          description: >
            Volume commitment as spend — decimal money string (4dp), e.g. "5000.0000", in the
            deal's currency (GC-06, RIPTIDE-87, ADR D54 D-02). Exactly one of
            committed_impressions / committed_spend may be set (both set is rejected); a
            commitment requires guaranteed=true AND a bounded flight (flight_end set). The
            commitment paces the deal's guaranteed outright wins; under-delivery surfaces as a
            makegood recommendation, never a credit. Omit both to clear the commitment (today's
            behavior).
        format: { type: string }
        advertiser_id: { type: string, format: uuid }
        seats: { type: array, items: { type: string } }
        advertiser_domains: { type: array, items: { type: string } }
        verification: { $ref: '#/components/schemas/EntityVerification' }
        audience_id: { type: string, format: uuid }
        first_impression: { type: boolean }
        all_pods: { type: boolean }
        frequency_caps:
          type: array
          items: { $ref: '#/components/schemas/FrequencyCapInput' }
        supply_transparency: { $ref: '#/components/schemas/SupplyTransparencyPolicy' }
    DealList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/Deal' }
        next_cursor: { type: string }
    Marketplace:
      type: object
      description: >
        A packaged bundle of demand routes attached to supply — docs/design/06-demand.md. Placement
        membership is managed separately via PUT .../placements/{placement_id}/marketplaces.
      required: [id, tenant_id, code, name, status]
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        code: { type: integer, description: "Stable int identity exposed to the runtime plan proto." }
        name: { type: string }
        rev_share: { type: string }
        status: { $ref: '#/components/schemas/LifecycleStatus' }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    MarketplaceCreate:
      type: object
      required: [code, name]
      properties:
        code: { type: integer }
        name: { type: string, minLength: 1 }
        rev_share: { type: string }
    MarketplaceUpdate:
      type: object
      required: [name]
      properties:
        name: { type: string, minLength: 1 }
        rev_share: { type: string }
    MarketplaceList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/Marketplace' }
        next_cursor: { type: string }
    MarketplaceDealMembership:
      type: object
      required: [deal_ids]
      properties:
        deal_ids:
          type: array
          items: { type: string, format: uuid }
        next_cursor: { type: string, description: "Opaque cursor for the next page of ids; absent on the last page." }

    ListingVisibility:
      type: string
      description: Discovery scope for a MarketplaceListing (design 24 §4.1).
      enum: [PRIVATE, TENANT, FEDERATED]
      x-enum-varnames: [ListingVisibilityPRIVATE, ListingVisibilityTENANT, ListingVisibilityFEDERATED]
    ListingStatus:
      type: string
      description: Lifecycle status for a MarketplaceListing.
      enum: [DRAFT, PUBLISHED, PAUSED, ARCHIVED]
      x-enum-varnames: [ListingStatusDRAFT, ListingStatusPUBLISHED, ListingStatusPAUSED, ListingStatusARCHIVED]
    ListingKind:
      type: string
      description: Commercial role of a MarketplaceListing (design 25 §5). UNSPECIFIED treated as SUPPLY_PACK.
      enum: [UNSPECIFIED, SUPPLY_PACK, DEMAND_CAPACITY, BUYER_PACK, STACK_PRODUCT]
      x-enum-varnames: [ListingKindUNSPECIFIED, ListingKindSUPPLYPACK, ListingKindDEMANDCAPACITY, ListingKindBUYERPACK, ListingKindSTACKPRODUCT]
    ListingPaymentProfile:
      type: string
      description: Payment obligation for activating a listing (design 25). Default NET_30.
      enum: [UNSPECIFIED, PREPAY, NET_30, ESCROW_HOSTED]
      x-enum-varnames: [ListingPaymentProfileUNSPECIFIED, ListingPaymentProfilePREPAY, ListingPaymentProfileNET30, ListingPaymentProfileESCROWHOSTED]
    ListingTermsSummary:
      type: object
      description: Cached discovery DTO computed at publish (design 24 §5.2).
      required: [listing_id, code, name]
      properties:
        listing_id: { type: string, format: uuid }
        code: { type: string }
        name: { type: string }
        deal_kind_set: { type: array, items: { type: string } }
        floor_cpm: { type: string }
        currency: { type: string }
        flight_start_unix: { type: integer, format: int64 }
        flight_end_unix: { type: integer, format: int64 }
        media_types: { type: array, items: { type: string } }
        formats: { type: array, items: { type: string } }
        max_hops: { type: integer, format: int32 }
        allow_reseller: { type: boolean }
        private_auction_required: { type: boolean }
        seat_restricted: { type: boolean }
        domain_restricted: { type: boolean }
        marketplace_code: { type: string }
        deal_count: { type: integer, format: int32 }
        kind: { $ref: '#/components/schemas/ListingKind' }
        payment_profile: { $ref: '#/components/schemas/ListingPaymentProfile' }
        min_prepay: { type: string, description: "Decimal string; PREPAY listings only." }
        tenant_restricted: { type: boolean, description: "True when buyer_tenant_allowlist is non-empty." }
    MarketplaceListing:
      type: object
      description: >
        Publishable commercial package for buyer discovery/activation (design 24 §4.1,
        design 25 §5). Does not replace Marketplace or Deal.
      required: [id, tenant_id, code, name, visibility, status]
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        code: { type: string }
        name: { type: string }
        visibility: { $ref: '#/components/schemas/ListingVisibility' }
        status: { $ref: '#/components/schemas/ListingStatus' }
        kind: { $ref: '#/components/schemas/ListingKind' }
        marketplace_id: { type: string, format: uuid }
        placement_ids: { type: array, items: { type: string, format: uuid } }
        deal_ids: { type: array, items: { type: string, format: uuid } }
        media_types: { type: array, items: { type: string } }
        floor_cpm: { type: string }
        currency: { type: string }
        flight_start_unix: { type: integer, format: int64 }
        flight_end_unix: { type: integer, format: int64 }
        max_hops: { type: integer, format: int32 }
        allow_reseller: { type: boolean }
        buyer_seat_allowlist: { type: array, items: { type: string } }
        buyer_tenant_allowlist: { type: array, items: { type: string, format: uuid } }
        require_proposal: { type: boolean }
        payment_profile: { $ref: '#/components/schemas/ListingPaymentProfile' }
        terms_summary: { $ref: '#/components/schemas/ListingTermsSummary' }
        published_at: { type: string, format: date-time }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
        stack_product: { $ref: '#/components/schemas/StackProductPublic' }
    StackProductPublic:
      type: object
      description: Public stack-product catalog metadata (design 25 §8.1). Never includes capability tokens or resolved endpoints.
      properties:
        extension_kinds:
          type: array
          description: >
            Kinds the product delivers. REPORT_FIELD and DECISIONER were retired by ADR D57
            (their proto values are reserved); a write naming either is rejected with BAD_INPUT.
            They stay in the enum for one compatibility release so existing clients keep
            validating, then leave it.
          items:
            type: string
            enum: [BID_SOURCE, DEMAND_ADAPTER, ENRICHER, AUDIENCE_EVALUATOR, PRICING_MODULE, CREATIVE_RENDERER, VERIFICATION_VENDOR, REPORT_FIELD, DECISIONER, DEMAND_ATTACHMENT]
        distribution:
          type: string
          enum: [SIDECAR_ENDPOINT]
        endpoint_ref: { type: string }
        config_schema: { type: string }
        latency_budget_ms: { type: integer, format: int32 }
        entitlement_key: { type: string }
        conformance_run_id: { type: string, format: uuid }
        trust_tier_required:
          type: string
          enum: [LIST, ATTACH, AUTONOMOUS_AGENT]
        pricing:
          type: object
          properties:
            model:
              type: string
              enum: [FLAT_MONTHLY, PER_INVOKE, PER_ASSISTED_FILL, REVSHARE_ASSISTED_SPEND]
            value: { type: string }
            currency: { type: string }
    TenantExtension:
      type: object
      description: >
        Installed stack-product ExtensionService projection (design 25 §8.3 / OM-22).
        Control-plane read model for bidding-agent UX — no capability tokens or dial secrets.
      required: [id, tenant_id, activation_id, product_listing_id, extension_kind, latency_budget_ms, entitlement_key, created_at, updated_at]
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        activation_id: { type: string, format: uuid }
        product_listing_id: { type: string, format: uuid }
        product_listing_code: { type: string, description: "Seller listing code when joinable; empty when listing archived." }
        product_listing_name: { type: string, description: "Seller listing name when joinable; empty when listing archived." }
        extension_kind:
          type: string
          description: >
            REPORT_FIELD and DECISIONER are retired (ADR D57): rejected on write with BAD_INPUT,
            kept in the enum for one compatibility release.
          enum: [BID_SOURCE, DEMAND_ADAPTER, ENRICHER, AUDIENCE_EVALUATOR, PRICING_MODULE, CREATIVE_RENDERER, VERIFICATION_VENDOR, REPORT_FIELD, DECISIONER, DEMAND_ATTACHMENT]
        latency_budget_ms: { type: integer, format: int32 }
        entitlement_key: { type: string }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    TenantExtensionList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/TenantExtension' }
        next_cursor: { type: string }
    StackProductInstallRequest:
      type: object
      required: [listing_id]
      properties:
        listing_id: { type: string, format: uuid }
        config_json: { type: string, description: "Validated install config JSON object." }
    RunExtensionConformanceRequest:
      type: object
      required: [vendor_tenant_id, extension_kind, harness_version]
      properties:
        vendor_tenant_id: { type: string, format: uuid }
        extension_kind: { type: string }
        harness_version: { type: string }
        artifact_uri: { type: string }
    # Named distinctly from oapi-codegen's client wrapper RunExtensionConformanceResponse.
    ExtensionConformanceRunResult:
      type: object
      required: [conformance_run_id, result, operator_verified]
      properties:
        conformance_run_id: { type: string, format: uuid }
        result: { type: string, enum: [PASS, FAIL] }
        operator_verified: { type: boolean }
    MarketplaceListingCreate:
      type: object
      required: [code, name, visibility]
      properties:
        code: { type: string, minLength: 1 }
        name: { type: string, minLength: 1 }
        visibility: { $ref: '#/components/schemas/ListingVisibility' }
        kind: { $ref: '#/components/schemas/ListingKind' }
        marketplace_id: { type: string, format: uuid }
        placement_ids: { type: array, items: { type: string, format: uuid } }
        deal_ids: { type: array, items: { type: string, format: uuid } }
        media_types: { type: array, items: { type: string } }
        floor_cpm: { type: string }
        currency: { type: string }
        flight_start_unix: { type: integer, format: int64 }
        flight_end_unix: { type: integer, format: int64 }
        max_hops: { type: integer, format: int32 }
        allow_reseller: { type: boolean }
        buyer_seat_allowlist: { type: array, items: { type: string } }
        buyer_tenant_allowlist: { type: array, items: { type: string, format: uuid } }
        require_proposal: { type: boolean }
        payment_profile: { $ref: '#/components/schemas/ListingPaymentProfile' }
    MarketplaceListingUpdate:
      type: object
      required: [name]
      properties:
        name: { type: string, minLength: 1 }
        visibility: { $ref: '#/components/schemas/ListingVisibility' }
        kind: { $ref: '#/components/schemas/ListingKind' }
        marketplace_id: { type: string, format: uuid }
        placement_ids: { type: array, items: { type: string, format: uuid } }
        deal_ids: { type: array, items: { type: string, format: uuid } }
        media_types: { type: array, items: { type: string } }
        floor_cpm: { type: string }
        currency: { type: string }
        flight_start_unix: { type: integer, format: int64 }
        flight_end_unix: { type: integer, format: int64 }
        max_hops: { type: integer, format: int32 }
        allow_reseller: { type: boolean }
        buyer_seat_allowlist: { type: array, items: { type: string } }
        buyer_tenant_allowlist: { type: array, items: { type: string, format: uuid } }
        require_proposal: { type: boolean }
        payment_profile: { $ref: '#/components/schemas/ListingPaymentProfile' }
    MarketplaceListingList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/MarketplaceListing' }
        next_cursor: { type: string }

    ListingActivationEffect:
      type: string
      enum: [MERGE_DEALS_ONTO_ROUTE, OPEN_DEMAND_EDGE, ATTACH_TO_BUYER_PACK, INSTALL_STACK_PRODUCT]
    ListingActivationStatus:
      type: string
      enum: [ACTIVE, PAUSED, REVOKED]
      # Pinned Go constant names (see SealingKeyStatus): ApiKeyStatus shares ACTIVE / REVOKED.
      x-enum-varnames: [ListingActivationStatusACTIVE, ListingActivationStatusPAUSED, ListingActivationStatusREVOKED]
    ListingActivation:
      type: object
      required: [id, activator_tenant_id, owner_tenant_id, listing_id, effect, status, idempotency_key]
      properties:
        id: { type: string, format: uuid }
        activator_tenant_id: { type: string, format: uuid }
        owner_tenant_id: { type: string, format: uuid }
        listing_id: { type: string, format: uuid }
        effect: { $ref: '#/components/schemas/ListingActivationEffect' }
        demand_route_id: { type: string, format: uuid }
        demand_partner_id: { type: string, format: uuid }
        buyer_seat: { type: string }
        status: { $ref: '#/components/schemas/ListingActivationStatus' }
        actor_label: { type: string }
        idempotency_key: { type: string }
        activated_at: { type: string, format: date-time }
        revoked_at: { type: string, format: date-time }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    ListingActivationCreate:
      type: object
      required: [listing_id]
      properties:
        listing_id: { type: string, format: uuid }
        demand_route_id: { type: string, format: uuid, description: Required for supply-pack / demand-edge effects; omit for INSTALL_STACK_PRODUCT. }
        demand_partner_id: { type: string, format: uuid, description: Required for supply-pack / demand-edge effects; omit for INSTALL_STACK_PRODUCT. }
        buyer_seat: { type: string }
        actor_label: { type: string }
        terms_snapshot: { type: string, description: Opaque terms or stack-product install config JSON. }
        effect:
          # default (moved from a $ref sibling, invalid in OAS 3.0): MERGE_DEALS_ONTO_ROUTE
          $ref: '#/components/schemas/ListingActivationEffect'
    ListingActivationList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/ListingActivation' }
        next_cursor: { type: string }

    PasteSiteCreate:
      type: object
      required: [site]
      properties:
        site:
          type: string
          description: Bare domain or URL pasted by the publisher (normalized to hostname).
    PasteSiteResult:
      type: object
      required: [domain, placement, listing]
      properties:
        domain: { type: string }
        placement: { $ref: '#/components/schemas/Placement' }
        listing: { $ref: '#/components/schemas/MarketplaceListing' }

    PathReceiptNodeRole:
      type: string
      enum: [SUPPLY, RESELLER, DEMAND, BUYER, VENDOR]
    PathReceiptFeeKind:
      type: string
      enum: [LAKE_DISCOVERY, CLEARING, SUPPLY_TERMS, DEMAND_MARGIN, VENDOR_PRODUCT, BUNDLE_PREMIUM, PLATFORM]
    PathReceiptDealContext:
      type: string
      enum: [OPEN, MARKETPLACE, DEAL_PREFERRED, DEAL_GUARANTEED, DEAL_PRIVATE, FEDERATED]
    PathReceiptNode:
      type: object
      required: [role, hop_index]
      properties:
        role: { $ref: '#/components/schemas/PathReceiptNodeRole' }
        tenant_id: { type: string }
        display_name: { type: string }
        schain_asi: { type: string }
        schain_sid: { type: string }
        hop_index: { type: integer, format: int32, description: "-1 for receipt-only VENDOR nodes" }
        attestation_id: { type: string }
    PathReceiptFeeLine:
      type: object
      required: [payee_role, kind, fee_type, value, amount]
      properties:
        payee_role: { $ref: '#/components/schemas/PathReceiptNodeRole' }
        payee_tenant_id: { type: string }
        kind: { $ref: '#/components/schemas/PathReceiptFeeKind' }
        fee_type:
          type: string
          enum: [REVSHARE, CPM, FLAT]
          # Pinned Go constant names (see SealingKeyStatus): InventoryCostType shares these values.
          x-enum-varnames: [PathReceiptFeeLineFeeTypeREVSHARE, PathReceiptFeeLineFeeTypeCPM, PathReceiptFeeLineFeeTypeFLAT]
        value: { type: string }
        amount: { type: string }
    PathReceipt:
      type: object
      required: [request_id, fill_id, tenant_id, occurred_at, path_nodes, fee_lines, deal_context, hop_count, gross_cpm, net_cpm, currency]
      description: Per-fill path + economics projection (design 25 §7).
      properties:
        request_id: { type: string }
        fill_id: { type: string }
        tenant_id: { type: string, format: uuid }
        occurred_at: { type: string, format: date-time }
        path_nodes:
          type: array
          items: { $ref: '#/components/schemas/PathReceiptNode' }
        fee_lines:
          type: array
          items: { $ref: '#/components/schemas/PathReceiptFeeLine' }
        deal_context: { $ref: '#/components/schemas/PathReceiptDealContext' }
        privacy_mode: { type: string }
        seller_of_record:
          type: object
          properties:
            tenant_id: { type: string }
            sellers_json_seller_id: { type: string }
        hop_count: { type: integer, format: int32 }
        effective_max_hops: { type: integer, format: int32 }
        gross_cpm: { type: string }
        net_cpm: { type: string }
        currency: { type: string }

    ProposalState:
      type: string
      enum: [OPEN, COUNTERED, APPROVED, REJECTED, WITHDRAWN, EXPIRED]
    ProposalPartyRole:
      type: string
      enum: [PROPOSER, OWNER]
    ListingProposalTerms:
      type: object
      description: Bilateral terms payload stored as jsonb on listing_proposal (design 25 §6.2).
      properties:
        fee_type:
          type: string
          enum: [REVSHARE, CPM, FLAT]
          # Pinned Go constant names: without the pin, pinning InventoryCostType's shared values
          # flips oapi-codegen's conflict-based prefixing for this enum (see DemandRouteIntegration).
          x-enum-varnames: [ListingProposalTermsFeeTypeREVSHARE, ListingProposalTermsFeeTypeCPM, ListingProposalTermsFeeTypeFLAT]
        fee_value: { type: string, description: Decimal string. }
        max_qps: { type: integer, format: int32 }
        categories: { type: array, items: { type: string } }
        hop_policy:
          type: object
          properties:
            max_hops: { type: integer, format: int32 }
            allow_reseller: { type: boolean }
        seller_of_record:
          type: object
          properties:
            tenant_id: { type: string, format: uuid }
            sellers_json_seller_id: { type: string }
        flight:
          type: object
          properties:
            start_unix: { type: integer, format: int64 }
            end_unix: { type: integer, format: int64 }
        payment_profile: { $ref: '#/components/schemas/ListingPaymentProfile' }
    ListingProposal:
      type: object
      required: [id, tenant_id, counterparty_tenant_id, party_role, listing_id, thread_id, revision, state, idempotency_key]
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        counterparty_tenant_id: { type: string, format: uuid }
        party_role: { $ref: '#/components/schemas/ProposalPartyRole' }
        listing_id: { type: string, format: uuid }
        thread_id: { type: string, format: uuid }
        revision: { type: integer, format: int32 }
        last_actor_tenant_id: { type: string, format: uuid }
        state: { $ref: '#/components/schemas/ProposalState' }
        terms: { $ref: '#/components/schemas/ListingProposalTerms' }
        expires_at: { type: string, format: date-time }
        idempotency_key: { type: string }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    ListingProposalCreate:
      type: object
      required: [listing_id, terms, reason]
      properties:
        listing_id: { type: string, format: uuid }
        terms: { $ref: '#/components/schemas/ListingProposalTerms' }
        reason: { type: string, description: Recorded in the audit envelope. }
    ListingProposalMutate:
      type: object
      required: [expected_revision, reason]
      properties:
        expected_revision: { type: integer, format: int32 }
        reason: { type: string }
    ListingProposalCounter:
      allOf:
        - $ref: '#/components/schemas/ListingProposalMutate'
        - type: object
          required: [terms]
          properties:
            terms: { $ref: '#/components/schemas/ListingProposalTerms' }
    ListingProposalApprove:
      allOf:
        - $ref: '#/components/schemas/ListingProposalMutate'
        - type: object
          properties:
            demand_route_id: { type: string, format: uuid }
            demand_partner_id: { type: string, format: uuid }
            buyer_seat: { type: string }
            actor_label: { type: string }
            activation_idempotency_key:
              type: string
              description: Idempotency key for the ListingActivation row created on approve.
    ListingProposalApproveResult:
      type: object
      required: [proposal, audit_id]
      properties:
        proposal: { $ref: '#/components/schemas/ListingProposal' }
        activation: { $ref: '#/components/schemas/ListingActivation' }
        audit_id: { type: string, description: AuditEnvelope id for this mutation. }

    PlacementMarketplaceMembership:
      type: object
      required: [marketplace_ids]
      properties:
        marketplace_ids:
          type: array
          items: { type: string, format: uuid }
        next_cursor: { type: string, description: "Opaque cursor for the next page of ids; absent on the last page." }
    PlacementDemandRouteMembership:
      type: object
      required: [demand_route_ids]
      properties:
        demand_route_ids:
          type: array
          items: { type: string, format: uuid }
        next_cursor: { type: string, description: "Opaque cursor for the next page of ids; absent on the last page." }
    ResolvedMarketplaceRoutes:
      type: object
      description: >
        The result of resolvePlacementMarketplaces's dry run (P3-03 "marketplace resolve"): every
        ACTIVE demand route id that would be merged onto the placement at compile time via its
        marketplace membership.
      required: [demand_route_ids]
      properties:
        demand_route_ids:
          type: array
          items: { type: string, format: uuid }
    Advertiser:
      type: object
      required: [id, tenant_id, ref, name, status]
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        ref: { type: string, description: "Short per-tenant reference code." }
        name: { type: string }
        exclusion_labels:
          type: array
          items: { type: string, minLength: 1, maxLength: 128 }
          description: >
            Competitive exclusion labels (GC-03). At serve time a line item's effective label
            set is the union of its advertiser's, campaign order's, and its own labels; no two
            winners sharing a label serve into one slate/pod or one multi-opportunity request.
        agency: { type: string, description: "Buying agency (SR-1011 commercial metadata)." }
        sales_owner: { type: string, description: "Account / sales owner on the seller side." }
        account_owner_user_id:
          type: string
          format: uuid
          nullable: true
          description: >
            The app user who owns the account relationship (SR-1218): a user of the same tenant
            (references app_user). Null / omitted = unassigned; sales_owner stays the free-text
            seller-side name for commercial metadata.
        notes: { type: string }
        billing_contact_name: { type: string }
        billing_contact_email: { type: string }
        status: { $ref: '#/components/schemas/LifecycleStatus' }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    AdvertiserCreate:
      type: object
      required: [ref, name]
      properties:
        ref: { type: string, minLength: 1 }
        name: { type: string, minLength: 1 }
        exclusion_labels:
          type: array
          items: { type: string, minLength: 1, maxLength: 128 }
          description: >
            Competitive exclusion labels (GC-03). At serve time a line item's effective label
            set is the union of its advertiser's, campaign order's, and its own labels; no two
            winners sharing a label serve into one slate/pod or one multi-opportunity request.
        agency: { type: string, description: "Buying agency (SR-1011 commercial metadata)." }
        sales_owner: { type: string, description: "Account / sales owner on the seller side." }
        account_owner_user_id:
          type: string
          format: uuid
          nullable: true
          description: >
            The app user who owns the account relationship (SR-1218): a user of the same tenant
            (references app_user). Null / omitted = unassigned; sales_owner stays the free-text
            seller-side name for commercial metadata.
        notes: { type: string }
        billing_contact_name: { type: string }
        billing_contact_email: { type: string }
    AdvertiserUpdate:
      type: object
      required: [name]
      properties:
        name: { type: string, minLength: 1 }
        exclusion_labels:
          type: array
          items: { type: string, minLength: 1, maxLength: 128 }
          description: >
            Competitive exclusion labels (GC-03). At serve time a line item's effective label
            set is the union of its advertiser's, campaign order's, and its own labels; no two
            winners sharing a label serve into one slate/pod or one multi-opportunity request.
        agency: { type: string, description: "Buying agency (SR-1011 commercial metadata)." }
        sales_owner: { type: string, description: "Account / sales owner on the seller side." }
        account_owner_user_id:
          type: string
          format: uuid
          nullable: true
          description: >
            The app user who owns the account relationship (SR-1218): a user of the same tenant
            (references app_user). Null / omitted = unassigned; sales_owner stays the free-text
            seller-side name for commercial metadata.
        notes: { type: string }
        billing_contact_name: { type: string }
        billing_contact_email: { type: string }
    AdvertiserList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/Advertiser' }
        next_cursor: { type: string }
    FeeScheduleScope:
      type: string
      description: >
        The resolution level a fee schedule applies at (riptide.common.v1.FeeScope). Precedence,
        most to least specific: PLACEMENT > PUBLISHER > MARKETPLACE > DEMAND_PARTNER > DEFAULT
        (docs/design/08-economics.md). At most one schedule per tenant+scope+scope_id.
      enum: [PLACEMENT, PUBLISHER, MARKETPLACE, DEMAND_PARTNER, DEFAULT, STACK_PRODUCT, LISTING]
    FeeKind:
      type: string
      description: >
        Which economics-phase leg a fee-schedule line funds within its scope bundle
        (riptide.common.v1.FeeKind, docs/design/08-economics.md): DEMAND is the buyer-side revenue
        share/cost, MARKETPLACE the marketplace cut (platform take), PUBLISHER the publisher
        revenue share/fixed CPM, SERVING the flat serving/stitch-adjacent fee.
      enum: [platform, demand, publisher, serving, vendor, lake, io]
    FeeMediaKind:
      type: string
      description: >
        Media kind a fee-schedule line is scoped to (riptide.common.v1.FeeMediaKind, SR-1204).
        Omitted = any media. Within one scope level a line whose media kind matches the priced
        opportunity wins over the any-media line; a line for another media kind never matches.
        COMPANION is a display creative filling a companion slot of a video/audio opportunity.
      enum: [VIDEO, AUDIO, COMPANION, DISPLAY]
    FeeType:
      type: string
      description: riptide.common.v1.FeeType — the basis a fee line's value is expressed in.
      enum: [REVSHARE, CPM, FLAT]
    FeeLine:
      type: object
      description: >
        One economics-phase leg's value within a fee-schedule bundle — at most one line per kind
        (docs/design/08-economics.md "the same engine produces both the publisher payout and the
        platform's take": resolving one scope bundle and reading each kind's line).
      required: [kind, fee_type, value]
      properties:
        kind: { $ref: '#/components/schemas/FeeKind' }
        fee_type: { $ref: '#/components/schemas/FeeType' }
        value: { type: string, description: "Decimal string; a fraction for REVSHARE (e.g. \"0.15\"), money for CPM/FLAT." }
        gross_net: { type: string, enum: [GROSS, NET], default: NET }
        cpm_metric: { type: string, enum: [IMPRESSIONS, FILLS], default: IMPRESSIONS, description: "Only meaningful when fee_type is CPM." }
        media_kind: { $ref: '#/components/schemas/FeeMediaKind' }
    FeeSchedule:
      type: object
      description: >
        Tenant-configured pricing rules by scope — docs/design/08-economics.md — already consumed
        by the runtime pricing engine's scope-precedence resolver (libs/plan.ResolveFeeSchedule,
        P3-06).
      required: [id, tenant_id, name, scope, status]
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        name: { type: string }
        scope: { $ref: '#/components/schemas/FeeScheduleScope' }
        scope_id:
          type: string
          description: "Id of the scoped entity (placement/publisher/marketplace/demand partner); empty for DEFAULT."
        lines:
          type: array
          items: { $ref: '#/components/schemas/FeeLine' }
        status: { $ref: '#/components/schemas/LifecycleStatus' }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    FeeScheduleCreate:
      type: object
      required: [name, lines]
      properties:
        name: { type: string, minLength: 1 }
        scope: { $ref: '#/components/schemas/FeeScheduleScope' }
        scope_id: { type: string }
        lines:
          type: array
          minItems: 1
          items: { $ref: '#/components/schemas/FeeLine' }
    FeeScheduleUpdate:
      type: object
      required: [name, lines]
      description: scope and scope_id are immutable after creation (create a new bundle instead).
      properties:
        name: { type: string, minLength: 1 }
        lines:
          type: array
          minItems: 1
          items: { $ref: '#/components/schemas/FeeLine' }
    FeeScheduleList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/FeeSchedule' }
        next_cursor: { type: string }
    FeeSchedulePreviewRequest:
      type: object
      required: [kind]
      description: A scope-precedence dry run (P3-06) — which saved fee schedule would apply.
      properties:
        kind: { $ref: '#/components/schemas/FeeKind' }
        placement_id: { type: string, format: uuid }
        publisher_id: { type: string, format: uuid }
        marketplace_id: { type: string, format: uuid }
        demand_partner_id: { type: string, format: uuid }
        media_kind: { $ref: '#/components/schemas/FeeMediaKind' }
    FeeSchedulePreviewResponse:
      type: object
      required: [resolved]
      properties:
        resolved: { type: boolean, description: "false when no fee schedule (not even a DEFAULT) matched." }
        fee_schedule_id: { type: string, format: uuid }
        scope: { $ref: '#/components/schemas/FeeScheduleScope' }
        line: { $ref: '#/components/schemas/FeeLine' }

    ModelKind:
      type: string
      description: Decisioner integration kind (proto decisionv1's INTEGRATION_KIND family, mirrored as plain text in model_registry.kind).
      enum: [BASELINE, SIDECAR, EXTERNAL_ENDPOINT]
    ModelAssignmentMode:
      type: string
      description: How a model may currently serve for a decision point (docs/spec/decisioners/README.md "Shadow, live, and replay").
      enum: [SHADOW, BOUNDED_AB, LIVE]
    ModelRegistryEntry:
      type: object
      description: >
        AI-0C model registry entry (migrations/postgres/0023_model_registry). tenant_id absent
        means a platform-shared model (Riptide-shipped baseline/model), visible to every tenant
        but writable only by the operator.
      required: [id, decision_point, name, version, kind, created_at, updated_at]
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid, description: "Absent for a platform-shared model." }
        decision_point:
          type: string
          description: "One of docs/spec/decisioners/README.md's decision points: bid, floor, traffic_shape, pace, creative, audience, insight, anomaly."
        name: { type: string }
        version: { type: string }
        entitlement: { type: string, description: "Feature gating this model; empty means not entitlement-gated." }
        kind: { $ref: '#/components/schemas/ModelKind' }
        config:
          type: object
          additionalProperties: true
          description: "Endpoint/weights config for this model — never a secret (500-security-privacy)."
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    ModelCreateRequest:
      type: object
      required: [decision_point, name, version, kind]
      properties:
        decision_point:
          type: string
          description: "One of docs/spec/decisioners/README.md's decision points: bid, floor, traffic_shape, pace, creative, audience, insight, anomaly."
        name: { type: string, minLength: 1 }
        version: { type: string, minLength: 1 }
        entitlement: { type: string }
        kind: { $ref: '#/components/schemas/ModelKind' }
        config:
          type: object
          additionalProperties: true
          description: "Endpoint/weights config for this model — never a secret."
    ModelAssignmentRequest:
      type: object
      required: [decision_point, mode]
      properties:
        decision_point: { type: string }
        mode: { $ref: '#/components/schemas/ModelAssignmentMode' }
        traffic_pct:
          type: string
          description: "Decimal percentage as a string in [0,100], stored in model_assignment.traffic_pct."
    ModelAssignment:
      type: object
      required: [id, tenant_id, model_id, decision_point, mode, traffic_pct]
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        model_id: { type: string, format: uuid }
        decision_point: { type: string }
        mode: { $ref: '#/components/schemas/ModelAssignmentMode' }
        traffic_pct:
          type: string
          description: "Decimal percentage as a string."
    ModelEvaluationResult:
      type: string
      enum: [PENDING, PASSED, FAILED]
    ModelEvaluationRunRequest:
      type: object
      required: [decision_point, objective, result]
      properties:
        decision_point: { type: string }
        objective: { type: string, minLength: 1 }
        baseline_metric: { type: string, description: "Decimal metric value as a string." }
        candidate_metric: { type: string, description: "Decimal metric value as a string." }
        lift_pct: { type: string, description: "Decimal lift percentage as a string." }
        result: { $ref: '#/components/schemas/ModelEvaluationResult' }
        criteria_id:
          type: string
          format: uuid
          description: >
            The model evaluation criteria row (model_eval_criteria) the run was judged by
            (SR-1213 / SR-005): a PASSED result is never self-attested — the evaluation writer
            checks the metrics against the named criteria. Omit only for runs that record a
            baseline without a promotion gate.
    ModelEvaluationRun:
      type: object
      required: [id, tenant_id, model_id, decision_point, objective, result, recorded_at]
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        model_id: { type: string, format: uuid }
        decision_point: { type: string }
        objective: { type: string }
        baseline_metric: { type: string }
        candidate_metric: { type: string }
        lift_pct: { type: string }
        result: { $ref: '#/components/schemas/ModelEvaluationResult' }
        recorded_at: { type: string, format: date-time }
        criteria_id:
          type: string
          format: uuid
          nullable: true
          description: The model_eval_criteria row the run was judged by; null for runs recorded before the criteria gate (SR-1213).
        evaluated_by:
          type: string
          readOnly: true
          description: Actor label of the evaluation writer that judged the run (SR-1213); empty for legacy rows.
        audit_id:
          type: string
          format: uuid
          nullable: true
          readOnly: true
          description: The audit_log envelope the evaluation writer produced for this run (SR-1213); null when no envelope was written.
    PlatformRatePlan:
      type: object
      description: Platform rate plan assigned to a tenant (migrations/postgres/0005 + 0017).
      required: [id, name, model, currency]
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        model:
          type: string
          description: FLAT|PER_SEAT|PER_IMPRESSION|PCT_SPEND|INFRA_PASSTHROUGH|HYBRID
        currency: { type: string }
        config_json: { type: string, description: "Opaque plan config JSON (decimal money as strings)." }
    RatePlanCreate:
      type: object
      description: Request body for POST /v1/operator/rate-plans (RIP-148 commercial catalog).
      required: [name, model, currency]
      properties:
        name: { type: string }
        model:
          type: string
          description: FLAT|PER_SEAT|PER_IMPRESSION|PCT_SPEND|INFRA_PASSTHROUGH|HYBRID
        currency: { type: string }
        config_json: { type: string, description: "Opaque plan config JSON (decimal money as strings)." }
    PlatformRatePlanList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/PlatformRatePlan' }
        next_cursor: { type: string }
    UsageRecord:
      type: object
      required: [tenant_id, metric, quantity, unit]
      properties:
        tenant_id: { type: string, format: uuid }
        period_start: { type: string, format: date-time }
        period_end: { type: string, format: date-time }
        metric: { type: string }
        quantity: { type: string, description: "Decimal string." }
        unit: { type: string }
        source_hash: { type: string }
        sources:
          type: array
          maxItems: 20000
          description: Stable source contributions for ingestion; omitted from aggregate reports.
          items: { $ref: '#/components/schemas/UsageSource' }
        prepaid_assessed:
          type: boolean
          readOnly: true
          description: Usage already assessed through prepaid billing, including exhausted or waived charges; excluded from later monthly usage collection.
    UsageSource:
      type: object
      required: [source_id, quantity]
      properties:
        source_id: { type: string, minLength: 1, maxLength: 256 }
        quantity:
          type: string
          description: Nonnegative decimal contribution with at most 18 fractional digits.
          pattern: '^(0|[1-9][0-9]{0,13})(\.[0-9]{1,18})?$'
    ReconciliationPartnerKind:
      type: string
      enum: [DEMAND_PARTNER, PUBLISHER]
      x-enum-varnames: [ReconciliationPartnerKindDEMANDPARTNER, ReconciliationPartnerKindPUBLISHER]
    ReconciliationStatementRow:
      type: object
      required: [day, impressions, amount]
      properties:
        day: { type: string, format: date, description: "Statement day (partner's reporting day, UTC)." }
        impressions: { type: integer, format: int64 }
        amount: { type: string, description: "Decimal string in the statement currency (4 dp)." }
        external_ref: { type: string, description: "Partner-side line reference (deal id, seat, placement)." }
    ReconciliationStatementImport:
      type: object
      required: [partner_kind, partner_id, period_start, period_end, currency, rows]
      properties:
        partner_kind: { $ref: '#/components/schemas/ReconciliationPartnerKind' }
        partner_id: { type: string, format: uuid, description: "demand_partner.id or publisher.id in this tenant." }
        period_start: { type: string, format: date }
        period_end: { type: string, format: date, description: "Inclusive." }
        currency: { type: string, minLength: 3, maxLength: 3 }
        source:
          type: string
          enum: [CSV, API]
          x-enum-varnames: [ReconciliationStatementSourceCSV, ReconciliationStatementSourceAPI]
          description: Where the rows came from; informational.
        rows:
          type: array
          items: { $ref: '#/components/schemas/ReconciliationStatementRow' }
    ReconciliationStatement:
      type: object
      required: [id, tenant_id, partner_kind, partner_id, period_start, period_end, currency, row_count, total_impressions, total_amount, created_at]
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        partner_kind: { $ref: '#/components/schemas/ReconciliationPartnerKind' }
        partner_id: { type: string, format: uuid }
        period_start: { type: string, format: date }
        period_end: { type: string, format: date }
        currency: { type: string }
        source: { type: string }
        row_count: { type: integer }
        total_impressions: { type: integer, format: int64 }
        total_amount: { type: string, description: "Decimal string (4 dp)." }
        rows:
          type: array
          description: Present on getReconciliationStatement only.
          items: { $ref: '#/components/schemas/ReconciliationStatementRow' }
        created_at: { type: string, format: date-time }
    ReconciliationStatementList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/ReconciliationStatement' }
        next_cursor: { type: string }
    ReconciliationCompareRequest:
      type: object
      required: [statement_id]
      properties:
        statement_id: { type: string, format: uuid }
        tolerance_pct: { type: string, description: "Decimal percent (e.g. \"2.5\"); a day whose |delta| / platform amount is within it is MATCHED. Default 0 (exact)." }
        tolerance_amount: { type: string, description: "Absolute decimal tolerance in the statement currency; a day within either tolerance is MATCHED." }
    ReconciliationLine:
      type: object
      required: [day, statement_impressions, platform_impressions, statement_amount, platform_amount, delta_amount, within_tolerance]
      properties:
        day: { type: string, format: date }
        statement_impressions: { type: integer, format: int64 }
        platform_impressions: { type: integer, format: int64 }
        statement_amount: { type: string }
        platform_amount: { type: string }
        delta_amount: { type: string, description: "statement − platform (decimal, 4 dp)." }
        within_tolerance: { type: boolean }
    ReconciliationReportStatus:
      type: string
      enum: [MATCHED, DISCREPANT]
      x-enum-varnames: [ReconciliationReportStatusMATCHED, ReconciliationReportStatusDISCREPANT]
    ReconciliationReport:
      type: object
      required: [id, tenant_id, statement_id, partner_kind, partner_id, status, currency, statement_total, platform_total, delta_total, discrepant_days, created_at]
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        statement_id: { type: string, format: uuid }
        partner_kind: { $ref: '#/components/schemas/ReconciliationPartnerKind' }
        partner_id: { type: string, format: uuid }
        status: { $ref: '#/components/schemas/ReconciliationReportStatus' }
        currency: { type: string }
        tolerance_pct: { type: string }
        tolerance_amount: { type: string }
        statement_total: { type: string }
        platform_total: { type: string }
        delta_total: { type: string }
        discrepant_days: { type: integer }
        lines:
          type: array
          description: Per-day comparison; present on getReconciliationReport and the compare response.
          items: { $ref: '#/components/schemas/ReconciliationLine' }
        created_at: { type: string, format: date-time }
        adjustment_memo:
          type: string
          nullable: true
          maxLength: 2000
          readOnly: true
          description: Human explanation of how the discrepancy was settled (SR-1218); set via setReconciliationAdjustmentMemo, null until then.
        adjustment_memo_by:
          type: string
          format: uuid
          nullable: true
          readOnly: true
          description: The app user who recorded the adjustment memo; null until a memo is set.
        adjustment_memo_at:
          type: string
          format: date-time
          nullable: true
          readOnly: true
          description: When the adjustment memo was recorded; null until a memo is set.
    ReconciliationReportList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/ReconciliationReport' }
        next_cursor: { type: string }
    ReconciliationAdjustmentMemoRequest:
      type: object
      description: Body for setReconciliationAdjustmentMemo (SR-1218).
      required: [memo]
      properties:
        memo:
          type: string
          minLength: 1
          maxLength: 2000
          description: The adjustment memo text; replaces any earlier memo on the report.
    UsageRecordList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/UsageRecord' }
        next_cursor: { type: string }
    CostTelemetry:
      type: object
      description: >
        Derived per-usage-record cost telemetry. id/source_hash is the usage_record source_hash;
        cost_usd is computed from libs/billing.DefaultCostRates over the persisted metered quantity.
      required: [id, tenant_id, period_start, period_end, metric, quantity, unit, cost_usd, currency, source_hash]
      properties:
        id: { type: string, description: "Stable id equal to source_hash." }
        tenant_id: { type: string, format: uuid }
        period_start: { type: string, format: date-time }
        period_end: { type: string, format: date-time }
        metric: { type: string }
        quantity: { type: string, description: "Decimal string from the usage record." }
        unit: { type: string }
        cost_usd: { type: string, description: "Decimal USD cost string with 4 decimal places." }
        currency: { type: string, enum: [USD] }
        source_hash: { type: string }
    CostTelemetryList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/CostTelemetry' }
        next_cursor: { type: string }
    PrepaidBalance:
      type: object
      required: [tenant_id, currency, balance]
      properties:
        tenant_id: { type: string, format: uuid }
        currency: { type: string }
        balance: { type: string, description: "Decimal string; sum of signed ledger amounts (4 dp)." }
    PrepaidTopUpCreate:
      type: object
      required: [amount, currency]
      properties:
        amount: { type: string, description: "Positive decimal string credit (4 dp)." }
        currency: { type: string, minLength: 3, maxLength: 3 }
        reason: { type: string, description: "Optional memo for the ledger row." }
    PrepaidLedgerEntry:
      type: object
      required: [id, tenant_id, amount, currency, created_at, idempotency_key]
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        amount: { type: string, description: "Signed decimal string (4 dp)." }
        currency: { type: string }
        reason: { type: string }
        actor_label: { type: string }
        created_at: { type: string, format: date-time }
        idempotency_key: { type: string }
        balance_after: { type: string, description: "Balance for currency after this entry." }
    PrepaidTopUpResponse:
      type: object
      required: [entry]
      properties:
        entry: { $ref: '#/components/schemas/PrepaidLedgerEntry' }
        audit_id: { type: string, description: "AuditEnvelope id for this mutation." }
    TenantQuotaState:
      type: object
      description: >
        The meter's serving verdict for a tenant (SR-1204; tenant_quota_state). state is what
        planc projects onto Tenant.serving_state: serve answers quota_exceeded / serving_suspended
        from it. used and quota_limit are decimal strings in the metric's unit (requests for
        requests_per_day; settlement money for prepaid_balance).
      required: [tenant_id, state]
      properties:
        tenant_id: { type: string, format: uuid }
        state: { type: string, enum: [ok, quota_exceeded, suspended] }
        metric: { type: string, description: "requests_per_day | prepaid_balance; empty when nothing is metered." }
        used: { type: string, description: "Decimal string." }
        quota_limit: { type: string, description: "Decimal string; 0 = unlimited." }
        period_start: { type: string, format: date-time }
        period_end: { type: string, format: date-time }
        reason: { type: string }
        updated_at: { type: string, format: date-time }
    FxRate:
      type: object
      required: [currency, rate_to_usd, as_of]
      properties:
        currency: { type: string, minLength: 3, maxLength: 3, description: ISO 4217 code. }
        rate_to_usd: { type: string, description: "Decimal string: one unit of currency in USD." }
        as_of: { type: string, format: date-time, description: The source snapshot date. }
        source: { type: string }
        fetched_at: { type: string, format: date-time }
    FxRateList:
      type: object
      required: [items, age_seconds]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/FxRate' }
        age_seconds: { type: integer, format: int64, description: "now - newest as_of over the whole cache; -1 when the cache is empty." }
        stale: { type: boolean, description: "age_seconds above the configured RIPTIDE_FX_RATES_MAX_AGE_SECONDS (default 48h)." }
        next_cursor: { type: string }
    FxRefreshResult:
      type: object
      required: [currencies, as_of]
      properties:
        currencies: { type: integer, format: int32, description: Rates written. }
        as_of: { type: string, format: date-time }
        source: { type: string }
    PlatformInvoice:
      type: object
      required: [id, tenant_id, period_start, period_end, currency, total, status]
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        period_start: { type: string, format: date-time }
        period_end: { type: string, format: date-time }
        currency: { type: string }
        total: { type: string, description: "Decimal string." }
        status: { $ref: '#/components/schemas/InvoiceStatus' }
        external_ref: { type: string }
        number:
          type: string
          nullable: true
          description: Sequential invoice number assigned at ISSUED (per tenant, gap-free); null before.
        issued_at: { type: string, format: date-time, nullable: true, description: "When the invoice became ISSUED; null before." }
        due_at: { type: string, format: date-time, nullable: true, description: "Payment due date from the tenant's terms; null before ISSUED." }
        paid_at: { type: string, format: date-time, nullable: true, description: "When the invoice became PAID; null otherwise." }
        voided_at: { type: string, format: date-time, nullable: true, description: "When the invoice became VOID; null otherwise." }
        fx_snapshot:
          nullable: true
          description: FX rates frozen at ISSUED for lines billed in another currency; null before ISSUED or when no conversion applied.
          allOf:
            - $ref: '#/components/schemas/FxSnapshot'
        tax:
          nullable: true
          description: Tax computed at ISSUED through the tax seam; null before ISSUED or when no tax applies.
          allOf:
            - $ref: '#/components/schemas/InvoiceTax'
        lines:
          type: array
          items: { $ref: '#/components/schemas/PlatformInvoiceLine' }
    PlatformInvoiceLine:
      type: object
      required: [description, amount]
      properties:
        description: { type: string }
        metric: { type: string }
        quantity: { type: string }
        unit_price: { type: string }
        amount: { type: string, description: "Decimal string." }
        rate_plan_id: { type: string, format: uuid, description: "Catalog plan id stamped at invoice generation (RIP-148)." }
        resolved_rate: { type: string, description: "L2 rate after ratchet/floor (decimal string)." }
        config_hash: { type: string, description: "Digest of rate_plan.config_json used for this line." }
    PlatformInvoiceList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/PlatformInvoice' }
        next_cursor: { type: string }
    ModelRegistryEntryList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/ModelRegistryEntry' }
        next_cursor: { type: string }
    ModelPromoteRequest:
      type: object
      required: [decision_point, to_mode, rationale]
      properties:
        decision_point: { type: string }
        from_mode:
          allOf: [{ $ref: '#/components/schemas/ModelAssignmentMode' }]
          description: "Omitted when this promotion has no specific prior mode (e.g. the model's first assignment for this decision point)."
        to_mode: { $ref: '#/components/schemas/ModelAssignmentMode' }
        evaluation_run_id:
          type: string
          format: uuid
          description: "The model_evaluation_run this promotion is based on, if any."
        rationale: { type: string, minLength: 1, description: "Why this promotion is happening — recorded on the model_promotion_event row." }
    ModelPromotionEvent:
      type: object
      description: An append-only model_promotion_event row (never edited/deleted after the fact).
      required: [id, model_id, decision_point, to_mode, at]
      properties:
        id: { type: string, format: uuid }
        model_id: { type: string, format: uuid }
        decision_point: { type: string }
        from_mode: { $ref: '#/components/schemas/ModelAssignmentMode' }
        to_mode: { $ref: '#/components/schemas/ModelAssignmentMode' }
        evaluation_run_id: { type: string, format: uuid }
        rationale: { type: string }
        at: { type: string, format: date-time }
    DecisionFactSummary:
      type: object
      required: [request_id, occurred_at, stage_count]
      properties:
        request_id: { type: string }
        occurred_at: { type: string, format: date-time }
        stage_count: { type: integer }
        winner_rationale: { type: string }
    DecisionFunnelResponse:
      type: object
      required: [source_configured, stages, facts]
      properties:
        source_configured:
          type: boolean
          description: false when the console has no ClickHouse decision-funnel query source wired.
        stages:
          type: array
          items: { $ref: '#/components/schemas/DecisionStageFilter' }
        facts:
          type: array
          items: { $ref: '#/components/schemas/DecisionFactSummary' }
    UserStatus:
      type: string
      enum: [ACTIVE, SUSPENDED]
      # Pinned Go constant names (see DemandRouteIntegration for rationale).
      x-enum-varnames: [UserStatusACTIVE, UserStatusSUSPENDED]
    User:
      type: object
      description: A console user — docs/design/02-data-model.md; roles from user_role (RIP-131).
      required: [id, tenant_id, email, status]
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        email: { type: string, format: email }
        name: { type: string }
        status: { $ref: '#/components/schemas/UserStatus' }
        roles:
          type: array
          items: { type: string }
          description: authz.Role names (docs/spec/authz-roles.md).
        publisher_id:
          type: string
          format: uuid
          description: Bound publisher for users holding the publisher role (SR-1010); scopes every portal call.
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    UserCreate:
      type: object
      required: [email]
      properties:
        email: { type: string, format: email, minLength: 1 }
        name: { type: string }
        publisher_id: { type: string, format: uuid }
    UserUpdate:
      type: object
      properties:
        name: { type: string }
        roles:
          type: array
          items: { type: string }
          description: >
            Replaces the user's tenant role set (UX-41). Only tenant-assignable roles
            (docs/spec/authz-roles.md) are accepted; `operator` and unknown names are rejected
            400. An empty array leaves the user with tenant_viewer.
    UserList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/User' }
        next_cursor: { type: string }
    AuthConfig:
      type: object
      required: [public_signup]
      properties:
        public_signup: { type: boolean }
    AuthAccepted:
      type: object
      required: [ok]
      properties:
        ok: { type: boolean }
    AuthSignupRequest:
      type: object
      required: [email, tenant_name]
      properties:
        email: { type: string, format: email }
        name: { type: string }
        tenant_name: { type: string, minLength: 1 }
        slug: { type: string }
        region: { type: string }
        cell_id: { type: string }
    AuthMagicLinkRequest:
      type: object
      required: [email]
      properties:
        email: { type: string, format: email }
        tenant_slug: { type: string }
    AuthVerifyRequest:
      type: object
      required: [token]
      properties:
        token: { type: string, minLength: 1 }
    AuthSession:
      type: object
      required: [access_token, expires_at, me]
      properties:
        access_token: { type: string, description: "Opaque rt_sess_ bearer." }
        expires_at: { type: string, format: date-time }
        me: { $ref: '#/components/schemas/AuthMe' }
        memberships:
          type: array
          description: >
            Every tenant the verified e-mail is a member of (REM-J tenant choice). Present only on
            the authVerify response; a single-tenant user gets a one-element list. The session is
            issued for `me.tenant`; choosing another membership re-verifies into that tenant.
          items: { $ref: '#/components/schemas/AuthMembership' }
        tenant_choice_required:
          type: boolean
          description: >
            True when memberships has more than one entry and the console must ask which tenant
            to enter before using the session. Never set by authMagicLink (enumerate-safe).
    AuthMembership:
      type: object
      description: One tenant membership of the verified user (REM-J).
      required: [tenant_id, tenant_name, role]
      properties:
        tenant_id: { type: string, format: uuid, description: "The tenant." }
        tenant_name: { type: string, description: "Display name of the tenant." }
        tenant_slug: { type: string, description: "Workspace slug for an explicitly tenant-bound magic-link request after email verification." }
        role: { type: string, description: "The user's role in that tenant (tenant_admin | tenant_editor | tenant_viewer | publisher | advertiser | ...)." }
    SessionsRevoked:
      type: object
      required: [revoked]
      properties:
        revoked:
          type: integer
          description: Sessions ended, the caller's included.
    AuthMe:
      type: object
      required: [operator, roles]
      properties:
        operator: { type: boolean }
        user: { $ref: '#/components/schemas/User' }
        tenant: { $ref: '#/components/schemas/Tenant' }
        roles:
          type: array
          items: { type: string }
        entitlements:
          type: array
          items: { $ref: '#/components/schemas/Entitlement' }
    InviteStatus:
      type: string
      enum: [PENDING, ACCEPTED, EXPIRED, REVOKED]
      x-enum-varnames: [InviteStatusPENDING, InviteStatusACCEPTED, InviteStatusEXPIRED, InviteStatusREVOKED]
    Invite:
      type: object
      description: One invite challenge (auth_login_challenge purpose=invite) as seen by a tenant admin; the magic-link token is never returned.
      required: [id, tenant_id, email, role, status, created_at, expires_at]
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        email: { type: string, format: email }
        role: { type: string }
        publisher_id: { type: string, format: uuid, description: "Publisher binding for publisher-role invites (SR-1010)." }
        status: { $ref: '#/components/schemas/InviteStatus' }
        created_at: { type: string, format: date-time }
        expires_at: { type: string, format: date-time }
        accepted_at: { type: string, format: date-time }
        revoked_at: { type: string, format: date-time }
    InviteList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/Invite' }
        next_cursor: { type: string }
    InviteCreate:
      type: object
      required: [email, role]
      properties:
        email: { type: string, format: email }
        name: { type: string }
        role:
          type: string
          enum: [tenant_admin, tenant_editor, tenant_viewer, advertiser, publisher, sales_rep, campaign_manager, creative_designer, revenue_ops]
          # Pinned Go constant names (see DemandRouteIntegration for rationale).
          x-enum-varnames: [InviteCreateRoleTenantAdmin, InviteCreateRoleTenantEditor, InviteCreateRoleTenantViewer, InviteCreateRoleAdvertiser, InviteCreateRolePublisher, InviteCreateRoleSalesRep, InviteCreateRoleCampaignManager, InviteCreateRoleCreativeDesigner, InviteCreateRoleRevenueOps]
        publisher_id:
          type: string
          format: uuid
          description: >
            Required when role is publisher. The publisher must be non-archived in the same tenant
            at redemption. An existing invited user must already have this same non-null
            publisher binding; a missing or different binding returns 409 CONFLICT with a
            publisher_id field hint. Redemption never implicitly rebinds an existing user.
    StatusCounts:
      type: object
      description: >
        Per-status row counts for a resource collection (console summary widgets) — the general
        status-counts operation from docs/spec/admin-api.md's resource conventions.
      required: [counts]
      properties:
        counts:
          type: object
          additionalProperties: { type: integer, format: int64 }
          description: Map of status value (LifecycleStatus, or the resource's own status enum) to count.
    DeliveryStatus:
      type: string
      description: >
        riptide.common.v1.DeliveryStatus — the richer delivery lifecycle. Only ACTIVE serves;
        DRAFT/PAUSED/ENDED/ARCHIVED never serve (docs/spec/ad-server-delivery.md).
      enum: [DRAFT, ACTIVE, PAUSED, ENDED, ARCHIVED]
    CampaignOrder:
      type: object
      description: >
        Booked order (advertiser, budget, dates, owners) — docs/design/02-data-model.md "Ad server"
        and docs/design/30-dsp-completeness.md. ref is auto-minted when omitted on create (P3-03
        "auto ref"); advertiser_id is the owner constraint (NOT NULL, immutable after creation —
        validated to exist in-tenant on create); archiving an order cascades to archive every line
        item it owns (P3-03 "status cascade"). The delivery envelope is the serve-time order gate:
        a line item owned by an order serves only while the order is ACTIVE, inside the order
        flight (in the order's timezone), and under the order budgets — orders with defaulted
        delivery fields are an open gate (pre-existing behavior). currency is ISO 4217, immutable
        after create; line items inherit it. Cross-entity mismatches (line-item flight outside the
        order flight) surface as eligibility warnings, never silent drops.
      required: [id, tenant_id, ref, name, advertiser_id, status]
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        ref: { type: string, description: "Short per-tenant reference code; auto-minted when omitted on create." }
        name: { type: string }
        advertiser_id: { type: string, format: uuid, description: "Owning advertiser (campaign_order.advertiser_id is NOT NULL, immutable)." }
        delivery_status: { $ref: '#/components/schemas/DeliveryStatus' }
        start_at: { type: string, format: date-time, description: "Order flight start (campaign_order.flight_start); omit for open start." }
        end_at: { type: string, format: date-time, description: "Order flight end (campaign_order.flight_end); omit for open end." }
        timezone: { type: string, description: "IANA timezone for the flight window and EVEN-pacing day fraction; spend/pacing counter day-buckets are UTC (design 31 §6). Defaults to UTC." }
        currency: { type: string, description: "ISO 4217 order currency; immutable after create. Line items inherit it." }
        lifetime_budget: { type: string, description: "Decimal money string (4dp), e.g. \"5000.0000\"; omit for uncapped." }
        daily_budget: { type: string, description: "Decimal money string (4dp); omit for uncapped." }
        frequency_caps:
          type: array
          items: { $ref: '#/components/schemas/FrequencyCap' }
          description: Order-level caps; most restrictive wins across association/line-item/order scopes.
        exclusion_labels:
          type: array
          items: { type: string, minLength: 1, maxLength: 128 }
          description: >
            Competitive exclusion labels (GC-03), additive to the advertiser's own labels for
            every line item this order owns. Arrays present on update replace the full set.
        status: { $ref: '#/components/schemas/LifecycleStatus' }
        supply_transparency: { $ref: '#/components/schemas/SupplyTransparencyPolicy' }
        trafficker: { type: string, description: "Person trafficking the order (SR-1011 commercial metadata)." }
        agency: { type: string, description: "Buying agency name." }
        sales_owner: { type: string, description: "Account / sales owner on the seller side." }
        account_owner_user_id:
          type: string
          format: uuid
          nullable: true
          description: >
            The app user who owns the account relationship (SR-1218): a user of the same tenant
            (references app_user). Null / omitted = unassigned; sales_owner stays the free-text
            seller-side name for commercial metadata.
        po_number: { type: string, description: "Purchase-order reference." }
        notes: { type: string }
        billing_contact_name: { type: string }
        billing_contact_email: { type: string }
        monthly_goal_impressions: { type: integer, format: int64, minimum: 0 }
        monthly_goal_budget: { type: string, description: "Decimal money string (4 dp)." }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    CampaignOrderCreate:
      type: object
      required: [name, advertiser_id]
      properties:
        ref: { type: string, minLength: 1, description: "Auto-minted when omitted." }
        name: { type: string, minLength: 1 }
        advertiser_id: { type: string, format: uuid }
        supply_transparency: { $ref: '#/components/schemas/SupplyTransparencyPolicy' }
        delivery_status: { $ref: '#/components/schemas/DeliveryStatus' }
        start_at: { type: string, format: date-time }
        end_at: { type: string, format: date-time }
        timezone: { type: string }
        currency: { type: string, minLength: 3, maxLength: 3, description: "ISO 4217; immutable after create (omit for USD)." }
        lifetime_budget: { type: string }
        daily_budget: { type: string }
        frequency_caps:
          type: array
          items: { $ref: '#/components/schemas/FrequencyCapInput' }
        exclusion_labels:
          type: array
          items: { type: string, minLength: 1, maxLength: 128 }
          description: >
            Competitive exclusion labels (GC-03), additive to the advertiser's own labels for
            every line item this order owns. Arrays present on update replace the full set.
        trafficker: { type: string, description: "Person trafficking the order (SR-1011 commercial metadata)." }
        agency: { type: string, description: "Buying agency name." }
        sales_owner: { type: string, description: "Account / sales owner on the seller side." }
        account_owner_user_id:
          type: string
          format: uuid
          nullable: true
          description: >
            The app user who owns the account relationship (SR-1218): a user of the same tenant
            (references app_user). Null / omitted = unassigned; sales_owner stays the free-text
            seller-side name for commercial metadata.
        po_number: { type: string, description: "Purchase-order reference." }
        notes: { type: string }
        billing_contact_name: { type: string }
        billing_contact_email: { type: string }
        monthly_goal_impressions: { type: integer, format: int64, minimum: 0 }
        monthly_goal_budget: { type: string, description: "Decimal money string (4 dp)." }
    CampaignOrderUpdate:
      type: object
      required: [name]
      description: >
        currency is immutable after create and intentionally absent here. Pausing or ending the
        order gates every owned line item at serve time (order gate); archiving cascades the
        archive to owned line items.
      properties:
        name: { type: string, minLength: 1 }
        supply_transparency: { $ref: '#/components/schemas/SupplyTransparencyPolicy' }
        delivery_status: { $ref: '#/components/schemas/DeliveryStatus' }
        start_at: { type: string, format: date-time }
        end_at: { type: string, format: date-time }
        timezone: { type: string }
        lifetime_budget: { type: string }
        daily_budget: { type: string }
        frequency_caps:
          type: array
          items: { $ref: '#/components/schemas/FrequencyCapInput' }
        exclusion_labels:
          type: array
          items: { type: string, minLength: 1, maxLength: 128 }
          description: >
            Competitive exclusion labels (GC-03), additive to the advertiser's own labels for
            every line item this order owns. Arrays present on update replace the full set.
        trafficker: { type: string, description: "Person trafficking the order (SR-1011 commercial metadata)." }
        agency: { type: string, description: "Buying agency name." }
        sales_owner: { type: string, description: "Account / sales owner on the seller side." }
        account_owner_user_id:
          type: string
          format: uuid
          nullable: true
          description: >
            The app user who owns the account relationship (SR-1218): a user of the same tenant
            (references app_user). Null / omitted = unassigned; sales_owner stays the free-text
            seller-side name for commercial metadata.
        po_number: { type: string, description: "Purchase-order reference." }
        notes: { type: string }
        billing_contact_name: { type: string }
        billing_contact_email: { type: string }
        monthly_goal_impressions: { type: integer, format: int64, minimum: 0 }
        monthly_goal_budget: { type: string, description: "Decimal money string (4 dp)." }
    CampaignOrderDuplicateRequest:
      type: object
      required: [name]
      description: >
        Body for duplicateCampaignOrder. Deep copy: line items and their weighted-creative
        associations are copied (creatives referenced, not copied), refs are freshly auto-minted,
        delivery statuses reset to DRAFT, and delivery counters start at zero.
      properties:
        name: { type: string, minLength: 1, description: "Name for the new order." }
        include_line_items: { type: boolean, default: true, description: "Copy owned line items + associations; false copies the order shell only." }
    CampaignOrderList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/CampaignOrder' }
        next_cursor: { type: string }
    LineItemKind:
      type: string
      description: DIRECT (booked/guaranteed) or HOUSE (no-cost filler).
      enum: [DIRECT, HOUSE]
    PacingMode:
      type: string
      enum: [EVEN, ASAP, MANUAL_WEIGHT]
    PriorityClass:
      type: string
      enum: [SPONSORSHIP, STANDARD, HOUSE, BACKFILL]
    DeliveryGoalType:
      type: string
      description: >
        riptide.common.v1.DeliveryGoalType — what lifetime/daily goals count. CONVERSIONS /
        REVENUE / SHARE_OF_VOICE are buyer-parity multi-metric goals (design 32 §4.4).
      enum: [IMPRESSIONS, COMPLETIONS, CLICKS, SPEND, CONVERSIONS, REVENUE, SHARE_OF_VOICE]
    DemandRateType:
      type: string
      description: >
        riptide.common.v1.DemandRateType — advertiser buy-rate model on a line item. Distinct
        from supply PricingType. Non-CPM values require tenant entitlement `demand_rate_types`.
      enum: [CPM, CPC, CPA, FLAT]
    BiddingStrategy:
      type: string
      description: >
        riptide.common.v1.BiddingStrategy — optimization target. VCR optimizes toward video
        completion rate under a max CPM (deterministic baseline + Decisioner override).
      enum: [CPM, CPC, CPA, VCR]
    OptimizationGoal:
      type: string
      description: >
        riptide.common.v1.OptimizationGoal — ROAS/GMV autobid objective (design 35 §6 / CP-13).
        NONE leaves the base bid unchanged. ROAS/GMV require tenant entitlement `roas_autobid`.
        Optimization revenue/GMV feeds the bid Decisioner only; platform meter is unchanged.
      enum: [NONE, ROAS, GMV]
    FreqCapNoKeyPolicy:
      type: string
      description: >
        riptide.common.v1.FreqCapNoKeyPolicy — behavior when a request carries no user key for
        frequency capping. SERVE keeps the fail-open behavior (caps skipped without a key);
        SKIP drops the line item from the candidate set instead.
      enum: [SERVE, SKIP]
    HourlyShape:
      type: string
      enum: [OFF, LEVEL, ACCELERATED]
      x-enum-varnames: [HourlyShapeOFF, HourlyShapeLEVEL, HourlyShapeACCELERATED]
      description: >
        Hourly shaping of the remaining goal (docs/design/31-dsp-completeness.md "Pacing
        parity"): LEVEL spreads the remaining goal over the scheduled hours with a 1.05 catch-up
        factor, ACCELERATED with 1.40; the hourly weight max((hourly goal − delivered this hour)
        ÷ priority, 0) weights selection and a met hourly goal withholds the line item for the
        rest of the hour. OFF (default) disables hourly shaping.
    LineItemDeliveryState:
      type: object
      required: [line_item_id, state, eligible_to_bid, delivered, remaining, spend, computed_at, counters_available]
      description: >
        Server-computed delivery state of a line item over the live pacing counters (design 31
        "Pacing parity" / SR-1202): PENDING, SERVING, GOAL_REACHED, FLIGHT_ENDED or INACTIVE (the
        pacing_state vocabulary), derived by the same pure gates the serving engine runs.
        counters_available=false means the hot store is not configured and every counter reads
        zero (reported honestly, never guessed).
      properties:
        line_item_id: { type: string, format: uuid }
        state:
          type: string
          enum: [PENDING, SERVING, GOAL_REACHED, FLIGHT_ENDED, INACTIVE]
          # Pinned Go constant names (PENDING / INACTIVE collide with other enums).
          x-enum-varnames: [LineItemDeliveryStateStatePENDING, LineItemDeliveryStateStateSERVING, LineItemDeliveryStateStateGOALREACHED, LineItemDeliveryStateStateFLIGHTENDED, LineItemDeliveryStateStateINACTIVE]
          description: "One of PENDING | SERVING | GOAL_REACHED | FLIGHT_ENDED | INACTIVE (libs/auction/direct.DeliveryState)."
        eligible_to_bid: { type: boolean, description: "True when the line item would bid right now." }
        lifetime_goal_effective: { type: integer, format: int64, description: "Lifetime goal with the delivery buffer applied." }
        daily_goal_effective: { type: integer, format: int64 }
        delivered: { type: integer, format: int64 }
        daily_delivered: { type: integer, format: int64 }
        remaining: { type: integer, format: int64 }
        spend: { type: string, description: "Booked spend so far (money decimal, 4 dp) in the order currency." }
        hourly_shaping: { type: boolean }
        hourly_goal: { type: integer, format: int64 }
        hourly_weight: { type: number, format: double, description: "Hourly shaping weight max((hourly goal − delivered this hour) ÷ priority, 0)." }
        bid_rate_cap: { type: integer, format: int64, description: "Render-rate bid budget left this hour (when hourly shaping applies)." }
        render_throttled: { type: boolean }
        dynamic_pricing: { type: boolean, description: "True when the line item bids the dynamic eCPM (budget + goal)." }
        dynamic_ecpm: { type: string, description: "The dynamic eCPM it bids with now (money decimal)." }
        counters_available: { type: boolean }
        computed_at: { type: string, format: date-time }
        pacing_state:
          type: string
          enum: [ON_TRACK, GOAL_REACHED, FLIGHT_ENDED]
          x-enum-varnames: [LineItemDeliveryStatePacingStateONTRACK, LineItemDeliveryStatePacingStateGOALREACHED, LineItemDeliveryStatePacingStateFLIGHTENDED]
          description: >
            The pacer's control-plane projection of the state (SR-1202; libs/auction/direct
            PacingStateFor): ON_TRACK while under goals and budgets (PENDING / SERVING /
            INACTIVE), GOAL_REACHED at a goal or budget hard stop, FLIGHT_ENDED once the flight
            end has passed or the line item is ENDED / ARCHIVED. The pacing cycle
            (services/console pacing_tick.go) writes the same value — with the effective goals,
            delivered count, hourly goal and bid-rate cap — to the line item of record
            (line_item.pacing_state, migration 0103) every minute, so this read and the stored
            projection can only ever differ by one cycle.
    LineItem:
      type: object
      description: >
        Deliverable within a campaign order — docs/design/02-data-model.md "Ad server". kind is
        immutable after creation; ref/audience auto-mint on create. Full delivery surface
        (pacing/goals/budgets/caps/creatives/dayparts) is admin-editable (RIP-133).
      required: [id, tenant_id, ref, name, kind, status]
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        campaign_order_id: { type: string, format: uuid, description: "Owning campaign order, if any." }
        ref: { type: string, description: "Short per-tenant reference code; auto-minted when omitted on create." }
        name: { type: string }
        kind: { $ref: '#/components/schemas/LineItemKind' }
        cpm: { type: string, description: "Decimal CPM (4dp string), e.g. \"8.5000\". Legacy alias when demand_rate_type=CPM." }
        demand_rate_type: { $ref: '#/components/schemas/DemandRateType' }
        rate_amount:
          type: string
          description: "Buy rate in order currency (4dp decimal string). Required for non-CPM demand rates when entitlement demand_rate_types is on."
        bidding_strategy: { $ref: '#/components/schemas/BiddingStrategy' }
        bid_modifier_id:
          type: string
          format: uuid
          nullable: true
          description: Optional tenant bid modifier applied after base bid, before predicted eCPM.
        optimization_goal:
          $ref: '#/components/schemas/OptimizationGoal'
        target_roas:
          type: string
          description: >
            Autobid target as a 4dp decimal string (e.g. "2.5000"). Required when
            optimization_goal is ROAS or GMV (design 35 §6 autobid_target).
        max_cpm:
          type: string
          description: >
            Hard autobid CPM cap as a 4dp decimal string (design 35 §6 autobid_max_cpm).
            Distinct from the line item's base `cpm` / rate_amount.
        priority: { type: integer }
        pacing_mode: { $ref: '#/components/schemas/PacingMode' }
        pacing_weight: { type: number }
        goal_type: { $ref: '#/components/schemas/DeliveryGoalType' }
        lifetime_goal: { type: integer, format: int64 }
        daily_goal: { type: integer, format: int64 }
        lifetime_budget: { type: string }
        daily_budget: { type: string }
        priority_class: { $ref: '#/components/schemas/PriorityClass' }
        guaranteed: { type: boolean }
        timezone: { type: string, description: "IANA timezone for daypart/flight evaluation." }
        pacing_tolerance_pct:
          type: string
          description: >
            EVEN-pacing soft-throttle tolerance as a decimal percent string in 0-100
            (e.g. "12.500000"). Omit for the engine default (10).
        goal_buffer_pct:
          type: string
          description: >
            Delivery buffer as a decimal percent string 0-100 (e.g. "5.000000"): the pacer
            delivers against goal × (1 + buffer/100) so the booked goal is met after discrepancy
            (docs/design/31-dsp-completeness.md "Pacing parity"). Empty = no buffer.
        hourly_shape: { $ref: '#/components/schemas/HourlyShape' }
        freq_cap_no_key: { $ref: '#/components/schemas/FreqCapNoKeyPolicy' }
        frequency_caps:
          type: array
          items: { $ref: '#/components/schemas/FrequencyCap' }
        advertiser_domain: { type: string }
        categories: { type: array, items: { type: string } }
        exclusion_labels:
          type: array
          items: { type: string, minLength: 1, maxLength: 128 }
          description: >
            Line-item scope competitive exclusion labels (GC-03). Serving enforces the union of
            advertiser + campaign order + line item labels; this field carries only the labels
            declared directly on the line item. Arrays present on update replace the full set.
        creatives:
          type: array
          items: { $ref: '#/components/schemas/WeightedCreativeRef' }
        dayparts:
          type: array
          items: { $ref: '#/components/schemas/Daypart' }
        flights:
          type: array
          items: { $ref: '#/components/schemas/LineItemFlight' }
          description: >
            Sequential budget segments (GC-05, design 32 §10), sorted by sequence. Empty means
            legacy behavior: the whole line-item window is one implicit flight. On update the
            array is upserted by id, not replaced — see LineItemUpdate.flights.
        ip_block_list_ids:
          type: array
          items: { type: string, format: uuid }
          description: >
            Tenant IP lists in block mode (ADR D36). Request IP matching any list
            excludes this line item (`ip_blocked`).
        ip_allow_list_ids:
          type: array
          items: { type: string, format: uuid }
          description: >
            Tenant IP lists in allow mode (ADR D36). When set, request IP must be a
            member of every listed allow set or the line item is excluded (`ip_not_allowed`).
        custom_block_list_ids:
          type: array
          items: { type: string, format: uuid }
          description: >
            Tenant custom lists in block mode (design 32 §6.1). Matching kind membership
            excludes this line item.
        custom_allow_list_ids:
          type: array
          items: { type: string, format: uuid }
          description: >
            Tenant custom lists in allow mode (design 32 §6.1). When set, the request must
            match every listed allow set or the line item is excluded.
        catalog_id:
          type: string
          format: uuid
          nullable: true
          description: >
            Optional item catalog (design 35 / CP-6) this line item draws sponsored-product
            candidates from. Distinct from MarketplaceListing. Null/omit = not a listing LI.
        catalog_external_item_ids:
          type: array
          items: { type: string }
          description: >
            External item ids (SKU keys) eligible under catalog_id. Empty with catalog_id set
            means all available catalog items may compete.
        relevancy_weight:
          type: integer
          format: int64
          description: >
            Listing composite-score weight (design 35). predicted_ecpm * max(1, weight) +
            relevancy. Omit or 0 treated as 1 by libs/catalog.Rank.
        audience_id: { type: string, format: uuid, description: "Targeting audience; auto-created and auto-named when omitted on create." }
        start_at: { type: string, format: date-time, description: "Flight window start (line_item.flight_start)." }
        end_at: { type: string, format: date-time, description: "Flight window end (line_item.flight_end)." }
        supply_transparency: { $ref: '#/components/schemas/SupplyTransparencyPolicy' }
        status: { $ref: '#/components/schemas/LifecycleStatus' }
        delivery_status:
          $ref: '#/components/schemas/DeliveryStatus'
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    LineItemCreate:
      type: object
      required: [name, kind]
      properties:
        campaign_order_id: { type: string, format: uuid }
        ref: { type: string, minLength: 1, description: "Auto-minted when omitted." }
        name: { type: string, minLength: 1 }
        kind: { $ref: '#/components/schemas/LineItemKind' }
        cpm: { type: string }
        demand_rate_type: { $ref: '#/components/schemas/DemandRateType' }
        rate_amount: { type: string }
        bidding_strategy: { $ref: '#/components/schemas/BiddingStrategy' }
        bid_modifier_id: { type: string, format: uuid, nullable: true }
        optimization_goal: { $ref: '#/components/schemas/OptimizationGoal' }
        target_roas: { type: string }
        max_cpm: { type: string }
        priority: { type: integer }
        pacing_mode: { $ref: '#/components/schemas/PacingMode' }
        pacing_weight: { type: number }
        goal_type: { $ref: '#/components/schemas/DeliveryGoalType' }
        lifetime_goal: { type: integer, format: int64 }
        daily_goal: { type: integer, format: int64 }
        lifetime_budget: { type: string }
        daily_budget: { type: string }
        priority_class: { $ref: '#/components/schemas/PriorityClass' }
        guaranteed: { type: boolean }
        timezone: { type: string }
        pacing_tolerance_pct: { type: string, description: "Decimal percent string 0-100; omit for the engine default (10)." }
        goal_buffer_pct: { type: string, description: "Delivery buffer percent 0-100; see LineItem.goal_buffer_pct." }
        hourly_shape: { $ref: '#/components/schemas/HourlyShape' }
        freq_cap_no_key: { $ref: '#/components/schemas/FreqCapNoKeyPolicy' }
        frequency_caps:
          type: array
          items: { $ref: '#/components/schemas/FrequencyCapInput' }
        advertiser_domain: { type: string }
        categories: { type: array, items: { type: string } }
        exclusion_labels:
          type: array
          items: { type: string, minLength: 1, maxLength: 128 }
          description: >
            Line-item scope competitive exclusion labels (GC-03). Serving enforces the union of
            advertiser + campaign order + line item labels; this field carries only the labels
            declared directly on the line item. Arrays present on update replace the full set.
        creatives:
          type: array
          items: { $ref: '#/components/schemas/WeightedCreativeRef' }
        dayparts:
          type: array
          items: { $ref: '#/components/schemas/Daypart' }
        flights:
          type: array
          items: { $ref: '#/components/schemas/LineItemFlight' }
          description: >
            Sequential, non-overlapping budget segments (GC-05, design 32 §10). Omit ids —
            the server mints them and derives sequence from start_at order. Empty/omitted
            means legacy behavior (whole line-item window is one implicit flight).
        ip_block_list_ids:
          type: array
          items: { type: string, format: uuid }
          description: Published tenant IP lists with mode=block.
        ip_allow_list_ids:
          type: array
          items: { type: string, format: uuid }
          description: Published tenant IP lists with mode=allow.
        custom_block_list_ids:
          type: array
          items: { type: string, format: uuid }
        custom_allow_list_ids:
          type: array
          items: { type: string, format: uuid }
        catalog_id: { type: string, format: uuid, nullable: true }
        catalog_external_item_ids: { type: array, items: { type: string } }
        relevancy_weight: { type: integer, format: int64 }
        audience_id: { type: string, format: uuid, description: "Omit to auto-create an auto-named audience for this line item." }
        start_at: { type: string, format: date-time }
        end_at: { type: string, format: date-time }
        supply_transparency: { $ref: '#/components/schemas/SupplyTransparencyPolicy' }
    LineItemUpdate:
      type: object
      required: [name]
      properties:
        name: { type: string, minLength: 1 }
        cpm: { type: string }
        demand_rate_type: { $ref: '#/components/schemas/DemandRateType' }
        rate_amount: { type: string }
        bidding_strategy: { $ref: '#/components/schemas/BiddingStrategy' }
        bid_modifier_id: { type: string, format: uuid, nullable: true }
        optimization_goal: { $ref: '#/components/schemas/OptimizationGoal' }
        target_roas: { type: string }
        max_cpm: { type: string }
        priority: { type: integer }
        pacing_mode: { $ref: '#/components/schemas/PacingMode' }
        pacing_weight: { type: number }
        goal_type: { $ref: '#/components/schemas/DeliveryGoalType' }
        lifetime_goal: { type: integer, format: int64 }
        daily_goal: { type: integer, format: int64 }
        lifetime_budget: { type: string }
        daily_budget: { type: string }
        priority_class: { $ref: '#/components/schemas/PriorityClass' }
        guaranteed: { type: boolean }
        timezone: { type: string }
        start_at: { type: string, format: date-time, description: "Flight window start (line_item.flight_start); omit to leave unchanged." }
        end_at: { type: string, format: date-time, description: "Flight window end (line_item.flight_end); omit to leave unchanged." }
        pacing_tolerance_pct: { type: string, description: "Decimal percent string 0-100; omit for the engine default (10)." }
        goal_buffer_pct: { type: string, description: "Delivery buffer percent 0-100; see LineItem.goal_buffer_pct." }
        hourly_shape: { $ref: '#/components/schemas/HourlyShape' }
        freq_cap_no_key: { $ref: '#/components/schemas/FreqCapNoKeyPolicy' }
        frequency_caps:
          type: array
          items: { $ref: '#/components/schemas/FrequencyCapInput' }
        advertiser_domain: { type: string }
        categories: { type: array, items: { type: string } }
        exclusion_labels:
          type: array
          items: { type: string, minLength: 1, maxLength: 128 }
          description: >
            Line-item scope competitive exclusion labels (GC-03). Serving enforces the union of
            advertiser + campaign order + line item labels; this field carries only the labels
            declared directly on the line item. Arrays present on update replace the full set.
        creatives:
          type: array
          items: { $ref: '#/components/schemas/WeightedCreativeRef' }
        dayparts:
          type: array
          items: { $ref: '#/components/schemas/Daypart' }
        flights:
          type: array
          items: { $ref: '#/components/schemas/LineItemFlight' }
          description: >
            Sequential, non-overlapping budget segments (GC-05, design 32 §10). When present the
            array is upserted by id (not plain-replaced, because flight identity keys delivery
            counters): entries carrying a known id UPDATE that flight in place preserving its
            identity and delivered counters; existing flights whose id is absent from the
            payload are DELETED; entries without id INSERT as new flights. Omit the field to
            leave flights unchanged; send [] to delete all flights.
        ip_block_list_ids:
          type: array
          items: { type: string, format: uuid }
          description: Replaces the line item's block-list bindings when present.
        ip_allow_list_ids:
          type: array
          items: { type: string, format: uuid }
          description: Replaces the line item's allow-list bindings when present.
        custom_block_list_ids:
          type: array
          items: { type: string, format: uuid }
          description: Replaces the line item's custom block-list bindings when present.
        custom_allow_list_ids:
          type: array
          items: { type: string, format: uuid }
          description: Replaces the line item's custom allow-list bindings when present.
        catalog_id: { type: string, format: uuid, nullable: true }
        catalog_external_item_ids:
          type: array
          items: { type: string }
          description: Replaces the line item's catalog SKU keys when present.
        relevancy_weight: { type: integer, format: int64 }
        audience_id: { type: string, format: uuid }
        supply_transparency: { $ref: '#/components/schemas/SupplyTransparencyPolicy' }
    LineItemPeriod:
      type: object
      description: The new flight window for extendLineItem's replacement line item.
      properties:
        start_at: { type: string, format: date-time }
        end_at: { type: string, format: date-time }
    LineItemList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/LineItem' }
        next_cursor: { type: string }
    EligibilityConditionSeverity:
      type: string
      description: >
        BLOCKING conditions stop serving; WARNING conditions do not stop serving but signal
        likely misconfiguration (e.g. line-item flight outside the order flight); INFO reports
        honest unknowns (e.g. COUNTERS_UNAVAILABLE) and context.
      enum: [BLOCKING, WARNING, INFO]
    EligibilityCondition:
      type: object
      required: [code, severity, message]
      description: >
        One condition from the serve engine's own eligibility functions (design 30 §2H). code is
        a stable machine-readable identifier (e.g. order_not_active, flight_not_started,
        no_servable_creative, budget_exhausted, COUNTERS_UNAVAILABLE); entity/entity_id name the
        entity the condition was evaluated on (line_item, campaign_order, creative, ...).
      properties:
        code: { type: string }
        severity: { $ref: '#/components/schemas/EligibilityConditionSeverity' }
        message: { type: string, description: "Human-readable explanation of the condition." }
        entity: { type: string, description: "Entity type the condition applies to (line_item, campaign_order, creative, ...)." }
        entity_id: { type: string, description: "Id of the entity the condition applies to." }
        budget_mode:
          $ref: '#/components/schemas/BudgetMode'
    EligibilityReport:
      type: object
      required: [serving, conditions]
      description: >
        Deterministic servability report for a line item, computed by the same pure functions the
        serve engine runs — order gate, flight, dayparts, approvals, associations, and (where the
        counter store is reachable) budgets/pacing/caps. Counter-dependent checks report
        COUNTERS_UNAVAILABLE (INFO) when the counter store is not configured. serving is true iff
        no BLOCKING condition is present.
      properties:
        serving: { type: boolean }
        conditions:
          type: array
          items: { $ref: '#/components/schemas/EligibilityCondition' }
        budget_mode:
          $ref: '#/components/schemas/BudgetMode'
    BudgetMode:
      type: string
      nullable: true
      description: >
        How a line item's budget is admitted across cells (SR-1223, riptide.budget.v1.BudgetMode):
        LOCAL_ALLOWANCE (the settler's per-cell allowance), TOKEN_BLOCK (near-goal token blocks),
        LOCAL_EVEN (no allowance yet — even split by the tenant's cell weights). Null when the
        line item has no money goal or budgets were not evaluated.
      enum: [LOCAL_ALLOWANCE, TOKEN_BLOCK, LOCAL_EVEN]
      x-enum-varnames: [BudgetModeLOCALALLOWANCE, BudgetModeTOKENBLOCK, BudgetModeLOCALEVEN]
    LineItemBulkStatusAction:
      type: string
      description: Delivery-status transition to apply (DeliveryStatus target ACTIVE|PAUSED|ARCHIVED).
      enum: [ACTIVATE, PAUSE, ARCHIVE]
    LineItemBulkStatusRequest:
      type: object
      required: [ids, status]
      properties:
        ids:
          type: array
          minItems: 1
          maxItems: 100
          items: { type: string, format: uuid }
        status: { $ref: '#/components/schemas/LineItemBulkStatusAction' }
    LineItemBulkStatusItemResult:
      type: object
      required: [id, ok]
      properties:
        id: { type: string, format: uuid }
        ok: { type: boolean }
        error_code: { type: string, description: "Stable machine-readable code when ok=false (e.g. not_found, no_servable_creative)." }
        error_message: { type: string, description: "Human-readable explanation when ok=false." }
    LineItemBulkStatusResult:
      type: object
      required: [results]
      description: >
        Multi-status result (design 30 §2I): one entry per requested id, in request order — never
        all-or-nothing, never a silent partial failure.
      properties:
        results:
          type: array
          items: { $ref: '#/components/schemas/LineItemBulkStatusItemResult' }
    CreativeKind:
      type: string
      description: >
        Creative media kind (design 30 §2D). VAST and MEDIA are legacy kinds accepted for
        compatibility and normalized to VIDEO by the platform (riptide.common.v1.CreativeKind);
        DISPLAY/VIDEO/AUDIO/NATIVE map 1:1 to the normalized enum.
      enum: [VAST, MEDIA, DISPLAY, VIDEO, AUDIO, NATIVE]
    CreativePipelineStatus:
      type: string
      description: >
        upload→store→watermark→transcode→ready (P3-03); advanced one step at a time via
        POST .../creatives/{id}/pipeline/advance.
      enum: [UPLOADED, STORED, WATERMARKED, TRANSCODED, READY]
    CreativeApprovalStatus:
      type: string
      description: Compliance review gate; auto-approved on reaching READY when still PENDING (P3-03).
      enum: [PENDING, APPROVED, REJECTED]
    Creative:
      type: object
      description: >
        VAST tag / hosted media / display / native creative — docs/design/02-data-model.md
        "Ad server" and docs/design/30-dsp-completeness.md. pipeline_status/approval_status and
        the review audit fields (approval_reason/approval_feedback/reviewed_at/reviewed_by) are
        read-only (advanced via pipeline/advance and the review endpoint). Content, measurement,
        and proximity fields are admin-editable (RIP-133). When the tenant's creative_auto_approve
        is off, a material content change (markup, click, media) resets APPROVED → PENDING.
      required: [id, tenant_id, ref, name, kind, status]
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        ref: { type: string, description: "Short per-tenant reference code." }
        name: { type: string }
        kind: { $ref: '#/components/schemas/CreativeKind' }
        vast_url: { type: string }
        vast_xml: { type: string }
        duration: { type: integer, format: int64, minimum: 0 }
        click_url: { type: string }
        width: { type: integer, minimum: 0, description: "Creative width in pixels (display/native slot fitting); 0 = unset." }
        height: { type: integer, minimum: 0, description: "Creative height in pixels; 0 = unset." }
        markup: { type: string, description: "Display HTML/tag markup; empty for hosted-image/video creatives." }
        native_payload: { type: string, description: "OpenRTB Native 1.2 response JSON document (string); empty = none." }
        adomain: { type: string, description: "Advertiser domain carried into bid responses and blocklist checks." }
        expires_at: { type: string, format: date-time, description: "Hard serving stop after expiry; omit for no expiry." }
        verification: { $ref: '#/components/schemas/EntityVerification' }
        proximity: { $ref: '#/components/schemas/ProximityTarget' }
        audience_id: { type: string, format: uuid }
        template_id:
          type: string
          format: uuid
          nullable: true
          description: Optional creative template (design 32 §6.2); render expands markup_template.
        template_fields:
          type: object
          additionalProperties: { type: string }
          description: Field values substituted into the template's {{MACRO}} tokens.
        companions:
          type: array
          maxItems: 8
          items: { $ref: '#/components/schemas/CompanionSlot' }
          description: >
            Companion ads rendered alongside the primary creative (VAST 4.x Companion; SR-1211).
            Each slot is filled into a placement companion_slots entry of the same size. Arrays
            present on update replace the full set.
        native_privacy_url:
          type: string
          format: uri
          maxLength: 2048
          nullable: true
          description: >
            HTTPS privacy-policy URL for native creatives (REM-G / SR-1239b): fills the Native
            response `privacy` field when the buyer payload lacks one. Null = none.
        interactive_file:
          nullable: true
          description: Interactive layer of a video creative (SR-1211). On update, omitted = unchanged and an explicit null clears it (the DemandPartner nullable-clears rule).
          allOf:
            - $ref: '#/components/schemas/InteractiveCreativeFile'
        pipeline_status: { $ref: '#/components/schemas/CreativePipelineStatus' }
        approval_status: { $ref: '#/components/schemas/CreativeApprovalStatus' }
        approval_reason: { type: string, readOnly: true, description: "Machine-readable review reason code (set via reviewCreative)." }
        approval_feedback: { type: string, readOnly: true, description: "Reviewer free-text feedback (set via reviewCreative)." }
        reviewed_at: { type: string, format: date-time, readOnly: true }
        reviewed_by: { type: string, readOnly: true, description: "Actor label of the reviewer." }
        status: { $ref: '#/components/schemas/LifecycleStatus' }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    CreativeCreate:
      type: object
      required: [ref, name, kind]
      properties:
        ref: { type: string, minLength: 1 }
        name: { type: string, minLength: 1 }
        kind: { $ref: '#/components/schemas/CreativeKind' }
        vast_url: { type: string }
        vast_xml: { type: string }
        duration: { type: integer, format: int64, minimum: 0 }
        click_url: { type: string }
        width: { type: integer, minimum: 0 }
        height: { type: integer, minimum: 0 }
        markup: { type: string }
        native_payload: { type: string, description: "OpenRTB Native 1.2 response JSON document (string)." }
        adomain: { type: string }
        expires_at: { type: string, format: date-time }
        verification: { $ref: '#/components/schemas/EntityVerification' }
        proximity: { $ref: '#/components/schemas/ProximityTarget' }
        audience_id: { type: string, format: uuid }
        template_id: { type: string, format: uuid, nullable: true }
        template_fields:
          type: object
          additionalProperties: { type: string }
        companions:
          type: array
          maxItems: 8
          items: { $ref: '#/components/schemas/CompanionSlot' }
          description: >
            Companion ads rendered alongside the primary creative (VAST 4.x Companion; SR-1211).
            Each slot is filled into a placement companion_slots entry of the same size. Arrays
            present on update replace the full set.
        native_privacy_url:
          type: string
          format: uri
          maxLength: 2048
          nullable: true
          description: >
            HTTPS privacy-policy URL for native creatives (REM-G / SR-1239b): fills the Native
            response `privacy` field when the buyer payload lacks one. Null = none.
        interactive_file:
          nullable: true
          description: Interactive layer of a video creative (SR-1211). On update, omitted = unchanged and an explicit null clears it (the DemandPartner nullable-clears rule).
          allOf:
            - $ref: '#/components/schemas/InteractiveCreativeFile'
    CreativeUpdate:
      type: object
      required: [name]
      properties:
        name: { type: string, minLength: 1 }
        vast_url: { type: string }
        vast_xml: { type: string }
        duration: { type: integer, format: int64, minimum: 0 }
        click_url: { type: string }
        width: { type: integer, minimum: 0 }
        height: { type: integer, minimum: 0 }
        markup: { type: string }
        native_payload: { type: string, description: "OpenRTB Native 1.2 response JSON document (string)." }
        adomain: { type: string }
        expires_at: { type: string, format: date-time }
        verification: { $ref: '#/components/schemas/EntityVerification' }
        proximity: { $ref: '#/components/schemas/ProximityTarget' }
        audience_id: { type: string, format: uuid }
        template_id: { type: string, format: uuid, nullable: true }
        template_fields:
          type: object
          additionalProperties: { type: string }
        companions:
          type: array
          maxItems: 8
          items: { $ref: '#/components/schemas/CompanionSlot' }
          description: >
            Companion ads rendered alongside the primary creative (VAST 4.x Companion; SR-1211).
            Each slot is filled into a placement companion_slots entry of the same size. Arrays
            present on update replace the full set.
        native_privacy_url:
          type: string
          format: uri
          maxLength: 2048
          nullable: true
          description: >
            HTTPS privacy-policy URL for native creatives (REM-G / SR-1239b): fills the Native
            response `privacy` field when the buyer payload lacks one. Null = none.
        interactive_file:
          nullable: true
          description: Interactive layer of a video creative (SR-1211). On update, omitted = unchanged and an explicit null clears it (the DemandPartner nullable-clears rule).
          allOf:
            - $ref: '#/components/schemas/InteractiveCreativeFile'
    CompanionSlotTrackers:
      type: object
      description: Third-party trackers for one companion slot (SR-1211); every URL is HTTPS and macro-templated.
      properties:
        creative_view:
          type: array
          maxItems: 16
          items: { type: string, maxLength: 2048 }
          description: Fired when the companion is rendered (VAST creativeView).
        click:
          type: array
          maxItems: 16
          items: { type: string, maxLength: 2048 }
          description: Fired on companion click (VAST CompanionClickTracking).
    CompanionSlot:
      type: object
      description: >
        One companion ad carried by a creative (VAST 4.x Companion; SR-1211): exactly one of
        static_url (image), html (HTMLResource), or iframe_url (IFrameResource) is the resource.
      required: [width, height]
      properties:
        id:
          type: string
          maxLength: 64
          description: Stable slot id within the creative (rendered as the Companion id); minted when omitted.
        width: { type: integer, minimum: 1, description: "Companion width in pixels." }
        height: { type: integer, minimum: 1, description: "Companion height in pixels." }
        static_url:
          type: string
          maxLength: 2048
          description: HTTPS image resource (StaticResource) when the companion is an image.
        html:
          type: string
          maxLength: 65536
          description: Inline HTML resource (HTMLResource) when the companion is markup.
        iframe_url:
          type: string
          maxLength: 2048
          description: HTTPS iframe resource (IFrameResource) when the companion is a hosted page.
        click_url:
          type: string
          maxLength: 2048
          description: Companion click-through (CompanionClickThrough); omit to inherit the creative's click_url.
        alt_text:
          type: string
          maxLength: 256
          description: Alternative text (AltText) for image companions.
        trackers: { $ref: '#/components/schemas/CompanionSlotTrackers' }
    InteractiveApiFramework:
      type: string
      description: >
        Interactive creative API the file implements (SR-1211): VPAID 2.0, OMID 1.x verification,
        SIMID 1.0 or 1.1 — rendered only when the request declares support for the framework.
      enum: [VPAID_2, OMID_1, SIMID_1_0, SIMID_1_1]
      x-enum-varnames: [InteractiveApiFrameworkVPAID2, InteractiveApiFrameworkOMID1, InteractiveApiFrameworkSIMID10, InteractiveApiFrameworkSIMID11]
    InteractiveCreativeFile:
      type: object
      description: >
        VAST 4.x InteractiveCreativeFile carried by a video creative (SR-1211): the interactive
        layer (SIMID / VPAID) rendered alongside the media file when the player negotiates the
        api_framework.
      required: [api_framework, url]
      properties:
        type:
          type: string
          maxLength: 128
          description: MIME type of the interactive file (e.g. text/html for SIMID, application/javascript for VPAID).
        api_framework: { $ref: '#/components/schemas/InteractiveApiFramework' }
        url: { type: string, maxLength: 2048, description: "HTTPS URL of the interactive file." }
        variable_duration:
          type: boolean
          description: True when the interactive layer may change the ad duration (VAST variableDuration).
    CreativeMediaUploadStatus:
      type: string
      description: Processing state of a media upload (SR-307); RECEIVED → VALIDATED → TRANSCODING → READY, or FAILED.
      enum: [RECEIVED, VALIDATED, TRANSCODING, READY, FAILED]
      x-enum-varnames: [CreativeMediaUploadStatusRECEIVED, CreativeMediaUploadStatusVALIDATED, CreativeMediaUploadStatusTRANSCODING, CreativeMediaUploadStatusREADY, CreativeMediaUploadStatusFAILED]
    CreativeMediaUploadForm:
      type: object
      description: multipart/form-data body of createCreativeMediaUpload (SR-307).
      required: [file]
      properties:
        file:
          type: string
          format: binary
          description: The media bytes (video, audio, or image) for the creative.
        content_type:
          type: string
          maxLength: 128
          description: Override of the file part's Content-Type (e.g. video/mp4) when the client cannot set it on the part.
    CreativeMediaUpload:
      type: object
      description: >
        One media upload record (SR-307; creative_media_upload): the bytes are stored on receipt
        and validated / transcoded asynchronously; READY links the resulting media variant and a
        preview URL onto the creative.
      required: [id, creative_id, status, content_type, bytes, sha256, created_at, updated_at]
      properties:
        id: { type: string, format: uuid, description: "Upload record id." }
        creative_id: { type: string, format: uuid, description: "The creative the media belongs to." }
        status: { $ref: '#/components/schemas/CreativeMediaUploadStatus' }
        content_type: { type: string, description: "Validated media MIME type (from the part or the content_type override)." }
        bytes: { type: integer, format: int64, minimum: 0, description: "Size of the received file in bytes." }
        sha256: { type: string, description: "Hex SHA-256 of the received bytes (dedup key and integrity check)." }
        media_variant_id:
          type: string
          format: uuid
          nullable: true
          description: The creative media variant produced from this upload; null until READY.
        preview_url:
          type: string
          nullable: true
          description: HTTPS preview of the processed media; null until READY.
        error:
          type: string
          nullable: true
          description: Failure reason when status is FAILED (unsupported type, corrupt file, transcode error); null otherwise.
        created_at: { type: string, format: date-time, description: "When the upload was received." }
        updated_at: { type: string, format: date-time, description: "When the status last changed." }
    CreativeReviewAction:
      type: string
      enum: [APPROVE, REJECT]
    CreativeReviewRequest:
      type: object
      required: [action]
      description: >
        Body for reviewCreative (design 30 §2C). REJECT requires reason_code; APPROVE may carry
        one. The decision, actor, and time are recorded on the creative's read-only review audit
        fields.
      properties:
        action: { $ref: '#/components/schemas/CreativeReviewAction' }
        reason_code: { type: string, description: "Machine-readable reason code; required when action=REJECT." }
        feedback: { type: string, description: "Free-text feedback for the creative owner." }
    CreativeList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/Creative' }
        next_cursor: { type: string }
    Audience:
      type: object
      description: >
        Reusable targeting definition — docs/design/02-data-model.md "Targeting". Full rule set
        is admin-editable. On update, omit a field to leave it unchanged; when present, each rule
        array (geo, metros, placements, app_ids, …) and the content object fully replace the
        stored set (RIP-133, RIPTIDE-20).
      required: [id, tenant_id, name]
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        name: { type: string }
        k1: { type: string }
        origin:
          readOnly: true
          description: How the audience came to exist (MANUAL by default); set by the system, never by the API caller.
          allOf:
            - $ref: '#/components/schemas/AudienceOrigin'
        usage:
          readOnly: true
          description: Where the audience is referenced (REM-J "Used by"); counted at read time.
          allOf:
            - $ref: '#/components/schemas/EntityUsage'
        k2: { type: string }
        k3: { type: string }
        k4: { type: string }
        keys_excluded: { type: boolean }
        ad_duration_enabled: { type: boolean }
        ad_duration_sec: { type: string }
        custom_rules: { type: string, description: "JSON custom targeting rules." }
        first_impression: { type: boolean }
        all_pods: { type: boolean }
        app_ids: { type: array, items: { $ref: '#/components/schemas/AudienceStringRule' } }
        store_ids: { type: array, items: { $ref: '#/components/schemas/AudienceStringRule' } }
        device_types: { type: array, items: { $ref: '#/components/schemas/AudienceIntRule' } }
        hardware: { type: array, items: { $ref: '#/components/schemas/AudienceStringRule' } }
        domains: { type: array, items: { $ref: '#/components/schemas/AudienceStringRule' } }
        zips: { type: array, items: { $ref: '#/components/schemas/AudienceStringRule' } }
        segments: { type: array, items: { $ref: '#/components/schemas/AudienceStringRule' } }
        app_segments: { type: array, items: { $ref: '#/components/schemas/AudienceStringRule' } }
        deal_ids: { type: array, items: { $ref: '#/components/schemas/AudienceStringRule' } }
        brand_safety: { type: array, items: { $ref: '#/components/schemas/AudienceStringRule' } }
        geo: { type: array, items: { $ref: '#/components/schemas/AudienceGeoRule' } }
        metros: { type: array, items: { $ref: '#/components/schemas/AudienceMetroRule' } }
        placements: { type: array, items: { $ref: '#/components/schemas/AudiencePlacementRule' } }
        content: { $ref: '#/components/schemas/AudienceContentRules' }
        event_membership:
          type: array
          description: >
            Event-fed membership rules (design 32 Phase 2 / design 33 §2.2). Matching events add
            this audience to the user's hot membership store for targeting.
          items: { $ref: '#/components/schemas/AudienceEventMembershipRule' }
        targeting_expression:
          type: string
          maxLength: 8192
          description: >
            Finite targeting expression (design 33 §3 / CP-12). JSON AST or compact S-expression
            over request.* and profile.* paths. Operators: and/or/not, eq/neq/lt/lte/gt/gte,
            in/not_in/contains, exists/not_exists, in_segment. Hard limits: depth≤8, nodes≤64,
            string≤256, in-set≤32 — rejected at write. Empty = no expression gate.
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    AudienceEventMembershipRule:
      type: object
      required: [event_kind]
      properties:
        event_kind:
          type: string
          enum: [FILL, IMPRESSION, CLICK, CONVERSION]
          description: Delivery or conversion event that seeds membership.
        conversion_label:
          type: string
          pattern: '^[a-z0-9_-]{0,64}$'
          description: Optional CONVERSION label filter; empty matches any label.
        ttl_seconds:
          type: integer
          minimum: 0
          maximum: 7776000
          description: Membership TTL in seconds; 0 = platform default (30 days); max 90 days.
        excluded:
          type: boolean
          description: When true, membership fails the audience (suppression / exclude-if-member).
    AudienceCreate:
      type: object
      required: [name]
      properties:
        name: { type: string, minLength: 1 }
        k1: { type: string }
        k2: { type: string }
        k3: { type: string }
        k4: { type: string }
        keys_excluded: { type: boolean }
        ad_duration_enabled: { type: boolean }
        ad_duration_sec: { type: string }
        custom_rules: { type: string }
        first_impression: { type: boolean }
        all_pods: { type: boolean }
        app_ids: { type: array, items: { $ref: '#/components/schemas/AudienceStringRule' } }
        store_ids: { type: array, items: { $ref: '#/components/schemas/AudienceStringRule' } }
        device_types: { type: array, items: { $ref: '#/components/schemas/AudienceIntRule' } }
        hardware: { type: array, items: { $ref: '#/components/schemas/AudienceStringRule' } }
        domains: { type: array, items: { $ref: '#/components/schemas/AudienceStringRule' } }
        zips: { type: array, items: { $ref: '#/components/schemas/AudienceStringRule' } }
        segments: { type: array, items: { $ref: '#/components/schemas/AudienceStringRule' } }
        app_segments: { type: array, items: { $ref: '#/components/schemas/AudienceStringRule' } }
        deal_ids: { type: array, items: { $ref: '#/components/schemas/AudienceStringRule' } }
        brand_safety: { type: array, items: { $ref: '#/components/schemas/AudienceStringRule' } }
        geo: { type: array, items: { $ref: '#/components/schemas/AudienceGeoRule' } }
        metros: { type: array, items: { $ref: '#/components/schemas/AudienceMetroRule' } }
        placements: { type: array, items: { $ref: '#/components/schemas/AudiencePlacementRule' } }
        content: { $ref: '#/components/schemas/AudienceContentRules' }
        event_membership:
          type: array
          items: { $ref: '#/components/schemas/AudienceEventMembershipRule' }
        targeting_expression:
          type: string
          maxLength: 8192
          description: Finite targeting expression AST (design 33 §3 / CP-12); empty clears.
    AudienceUpdate:
      type: object
      required: [name]
      description: >
        Omit a property to leave it unchanged. When present, each rule array and the content object
        fully replace the stored set (including empty arrays or an empty content object to clear).
      properties:
        name: { type: string, minLength: 1 }
        k1: { type: string }
        k2: { type: string }
        k3: { type: string }
        k4: { type: string }
        keys_excluded: { type: boolean }
        ad_duration_enabled: { type: boolean }
        ad_duration_sec: { type: string }
        custom_rules: { type: string }
        first_impression: { type: boolean }
        all_pods: { type: boolean }
        app_ids: { type: array, items: { $ref: '#/components/schemas/AudienceStringRule' } }
        store_ids: { type: array, items: { $ref: '#/components/schemas/AudienceStringRule' } }
        device_types: { type: array, items: { $ref: '#/components/schemas/AudienceIntRule' } }
        hardware: { type: array, items: { $ref: '#/components/schemas/AudienceStringRule' } }
        domains: { type: array, items: { $ref: '#/components/schemas/AudienceStringRule' } }
        zips: { type: array, items: { $ref: '#/components/schemas/AudienceStringRule' } }
        segments: { type: array, items: { $ref: '#/components/schemas/AudienceStringRule' } }
        app_segments: { type: array, items: { $ref: '#/components/schemas/AudienceStringRule' } }
        deal_ids: { type: array, items: { $ref: '#/components/schemas/AudienceStringRule' } }
        brand_safety: { type: array, items: { $ref: '#/components/schemas/AudienceStringRule' } }
        geo: { type: array, items: { $ref: '#/components/schemas/AudienceGeoRule' } }
        metros: { type: array, items: { $ref: '#/components/schemas/AudienceMetroRule' } }
        placements: { type: array, items: { $ref: '#/components/schemas/AudiencePlacementRule' } }
        content: { $ref: '#/components/schemas/AudienceContentRules' }
        event_membership:
          type: array
          items: { $ref: '#/components/schemas/AudienceEventMembershipRule' }
        targeting_expression:
          type: string
          maxLength: 8192
          description: Finite targeting expression AST (design 33 §3 / CP-12); omit to leave unchanged; empty string clears.
    AudienceList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/Audience' }
        next_cursor: { type: string }
    ReportGrain:
      type: string
      description: Time-bucket granularity (libs/reporting.Grain).
      enum: [minute, hour, day]
    ReportFilterOp:
      type: string
      description: Dimension filter predicate (libs/reporting.FilterOp).
      enum: [eq, neq, in, not_in]
    ReportFilter:
      type: object
      required: [dimension, op, values]
      properties:
        dimension: { type: string, description: "Catalog dimension name." }
        op: { $ref: '#/components/schemas/ReportFilterOp' }
        values:
          type: array
          minItems: 1
          items: { type: string }
    ReportOrderDir:
      type: string
      enum: [asc, desc]
    ReportOrderBy:
      type: object
      required: [field]
      properties:
        field: { type: string, description: "period, a requested dimension, or a requested metric." }
        dir: { $ref: '#/components/schemas/ReportOrderDir' }
    ReportRunRequest:
      type: object
      required: [metrics, from, to]
      properties:
        metrics:
          type: array
          minItems: 1
          items:
            type: string
            description: >
              A metric name from the reporting semantic-layer catalog (e.g. requests, bids, fills,
              impressions, completes, clicks, win_rate, fill_rate, ecpm, revenue_gross, revenue_net,
              profit) — see libs/reporting.Metrics().
        dimensions:
          type: array
          items:
            type: string
            description: >
              A dimension name from the catalog (e.g. tenant_id, publisher, placement, campaign_order,
              line_item, creative, deal, partner, country, device_class, cell_id) — see
              libs/reporting.Dimensions().
        filters:
          type: array
          items: { $ref: '#/components/schemas/ReportFilter' }
          description: Dimension value filters applied before aggregation (tenant_id always forced).
        order_by:
          type: array
          items: { $ref: '#/components/schemas/ReportOrderBy' }
        limit:
          type: integer
          minimum: 1
          maximum: 10000
          description: Row cap (default 1000 when omitted).
        from: { type: string, format: date-time }
        to: { type: string, format: date-time }
        grain: { $ref: '#/components/schemas/ReportGrain' }
    ReportCatalogMetric:
      type: object
      required: [name, unit, description, available, money, temperatures]
      properties:
        name: { type: string }
        unit: { type: string, description: "count | ratio | money" }
        description: { type: string }
        available: { type: boolean }
        unavailable_reason: { type: string }
        money: { type: boolean }
        temperatures:
          type: array
          items: { type: string, description: "hot | warm | cold | conversion" }
    ReportCatalogDimension:
      type: object
      required: [name, description, available, temperatures]
      properties:
        name: { type: string, description: "Dimension key (libs/reporting.Dimensions()); includes campaign_order, which expands line_item through the order map (SR-1233)." }
        description: { type: string }
        available: { type: boolean }
        unavailable_reason: { type: string }
        temperatures:
          type: array
          items: { type: string }
    ReportCatalog:
      type: object
      required: [metrics, dimensions, grains, default_limit, max_limit]
      properties:
        metrics:
          type: array
          items: { $ref: '#/components/schemas/ReportCatalogMetric' }
        dimensions:
          type: array
          items: { $ref: '#/components/schemas/ReportCatalogDimension' }
        grains:
          type: array
          items: { $ref: '#/components/schemas/ReportGrain' }
        default_limit: { type: integer }
        max_limit: { type: integer }
    ReportingChatRequest:
      type: object
      required: [message]
      properties:
        session_id: { type: string, description: "Opaque session id; omit to start a new session." }
        message: { type: string, minLength: 1 }
        financial_allowed:
          type: boolean
          description: >
            When true, money-unit metrics may be queried (insight financial gate). Default false.
    ReportingChatEvidence:
      type: object
      required: [tool, summary]
      properties:
        tool: { type: string }
        summary: { type: string }
        table: { type: string }
        sql_hash: { type: string }
        methodology: { type: string }
    ReportingChatReply:
      type: object
      required: [session_id, reply, evidence]
      properties:
        session_id: { type: string }
        reply: { type: string }
        evidence:
          type: array
          items: { $ref: '#/components/schemas/ReportingChatEvidence' }
        apply_spec:
          # description (moved from a $ref sibling, invalid in OAS 3.0): Spec the Explore UI can load from this answer (when tools produced one).
          $ref: '#/components/schemas/ReportRunRequest'
    ReportRow:
      type: object
      description: One result row; keys are the requested dimension/metric names plus "period".
      additionalProperties: true
    ReportRunResponse:
      type: object
      required: [rows, table, grain, sql]
      properties:
        rows:
          type: array
          items: { $ref: '#/components/schemas/ReportRow' }
        table:
          type: string
          description: The physical table the query router selected (hot/warm/cold).
        grain: { $ref: '#/components/schemas/ReportGrain' }
        sql:
          type: string
          description: >
            The compiled, parameterized SQL (values are bound as args, never interpolated) —
            returned for debugging/audit; safe to expose since it carries no literal values.
        executed:
          type: boolean
          description: >
            True if this ran against a live ClickHouse (RIPTIDE_CLICKHOUSE_DSN configured); false
            means rows is always empty and only the compiled query is returned.
    ReportLiveCounter:
      type: object
      required: [placement_id, kind, count]
      properties:
        placement_id: { type: string }
        kind: { type: string }
        count: { type: integer, format: int64 }
    ReportLiveResponse:
      type: object
      required: [items, window_minutes]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/ReportLiveCounter' }
        window_minutes:
          type: integer
          description: Sliding window size, in minutes, the counters are summed over.
    ReportConfig:
      type: object
      description: >
        A saved, reusable report definition over the semantic layer (P3-07 / RIPTIDE-58) — a name
        plus the same metrics/dimensions/filters/order/limit/grain shape POST .../reports/run
        accepts, so it can be re-run or scheduled for delivery without re-specifying the query.
      required: [id, tenant_id, name, metrics, grain, status]
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        name: { type: string }
        metrics:
          type: array
          minItems: 1
          items: { type: string, description: "A metric name from libs/reporting.Metrics()." }
        dimensions:
          type: array
          items: { type: string, description: "A dimension name from libs/reporting.Dimensions()." }
        filters:
          type: array
          items: { $ref: '#/components/schemas/ReportFilter' }
        order_by:
          type: array
          items: { $ref: '#/components/schemas/ReportOrderBy' }
        limit: { type: integer, minimum: 1, maximum: 10000 }
        grain: { $ref: '#/components/schemas/ReportGrain' }
        publisher_id:
          type: string
          format: uuid
          description: Set when the config was created through the publisher portal (SR-1010); every run injects `publisher = <public_id>`.
        status: { $ref: '#/components/schemas/LifecycleStatus' }
        tile: { type: boolean, description: "Pinned as a dashboard tile (SR-1233 tiles as saved definitions)." }
        tile_position: { type: integer, minimum: 0, description: "Tile order on the dashboard (0 = first); ignored unless tile is true." }
        schedule:
          readOnly: true
          description: The scheduled delivery created alongside the config (SR-1233 save + schedule in one dialog); absent when none was requested.
          allOf:
            - $ref: '#/components/schemas/ReportConfigSchedule'
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    ReportConfigCreate:
      type: object
      required: [name, metrics]
      properties:
        name: { type: string, minLength: 1 }
        metrics:
          type: array
          minItems: 1
          items: { type: string }
        dimensions:
          type: array
          items: { type: string }
        filters:
          type: array
          items: { $ref: '#/components/schemas/ReportFilter' }
        order_by:
          type: array
          items: { $ref: '#/components/schemas/ReportOrderBy' }
        limit: { type: integer, minimum: 1, maximum: 10000 }
        grain: { $ref: '#/components/schemas/ReportGrain' }
        tile: { type: boolean, description: "Pin as a dashboard tile; see ReportConfig.tile." }
        tile_position: { type: integer, minimum: 0, description: "Tile order on the dashboard; see ReportConfig.tile_position." }
        schedule:
          description: Create a scheduled delivery for the new config in the same call (SR-1233); the created schedule is returned as ReportConfig.schedule with its secret exactly once.
          allOf:
            - $ref: '#/components/schemas/ReportConfigScheduleCreate'
    ReportConfigUpdate:
      type: object
      required: [name, metrics]
      properties:
        name: { type: string, minLength: 1 }
        metrics:
          type: array
          minItems: 1
          items: { type: string }
        dimensions:
          type: array
          items: { type: string }
        filters:
          type: array
          items: { $ref: '#/components/schemas/ReportFilter' }
        order_by:
          type: array
          items: { $ref: '#/components/schemas/ReportOrderBy' }
        limit: { type: integer, minimum: 1, maximum: 10000 }
        grain: { $ref: '#/components/schemas/ReportGrain' }
        tile: { type: boolean, description: "Pin as a dashboard tile; see ReportConfig.tile." }
        tile_position: { type: integer, minimum: 0, description: "Tile order on the dashboard; see ReportConfig.tile_position." }
    ReportConfigList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/ReportConfig' }
        next_cursor: { type: string }
    ScheduledReportCadence:
      type: string
      description: Recurrence interval (libs/reporting.Cadence).
      enum: [hourly, daily, weekly]
    ScheduledReport:
      type: object
      description: >
        Recurring delivery of a saved report_config's result to a webhook-style destination
        (HMAC-signed via libs/webhooks). destination_secret is write-only — never present on a
        GET/list/update response; only the create response returns it, exactly once.
      required: [id, tenant_id, report_config_id, cadence, destination_url, status, next_run_at]
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        report_config_id: { type: string, format: uuid }
        cadence: { $ref: '#/components/schemas/ScheduledReportCadence' }
        destination_url:
          type: string
          format: uri
          description: >
            Where each run is delivered (SR-1214). https://… — an HMAC-signed webhook POST through
            the durable outbox (retried on the default policy); mailto:a@x,b@y — the result as an
            e-mail with a CSV attachment; export-destination://{export_destination_id} — a JSONL
            or Parquet file written to that tenant export destination (its format/prefix apply).
            An optional tz=<IANA zone> query parameter (e.g. ?tz=America/New_York) makes the
            cadence calendar-aware in that zone: hourly = next top of hour, daily = next local
            midnight, weekly = next local Monday 00:00 (UTC when omitted).
        publisher_id: { type: string, format: uuid, description: "Inherited from a publisher-scoped report config (SR-1010)." }
        status: { $ref: '#/components/schemas/LifecycleStatus' }
        last_run_at: { type: string, format: date-time }
        next_run_at: { type: string, format: date-time }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    ScheduledReportCreate:
      type: object
      required: [report_config_id, cadence, destination_url]
      properties:
        report_config_id: { type: string, format: uuid }
        cadence: { $ref: '#/components/schemas/ScheduledReportCadence' }
        destination_url: { type: string, format: uri, minLength: 1 }
    ScheduledReportCreated:
      description: The created scheduled report plus its signing secret (returned once, at creation only).
      allOf:
        - $ref: '#/components/schemas/ScheduledReport'
        - type: object
          required: [destination_secret]
          properties:
            destination_secret:
              type: string
              description: >
                HMAC-SHA256 signing secret (libs/webhooks.Sign/Verify) for this schedule's
                deliveries. Shown once; store it — the API never returns it again.
    ScheduledReportUpdate:
      type: object
      required: [cadence, destination_url]
      properties:
        cadence: { $ref: '#/components/schemas/ScheduledReportCadence' }
        destination_url: { type: string, format: uri, minLength: 1 }
    ScheduledReportList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/ScheduledReport' }
        next_cursor: { type: string }
    ScheduledReportRunResult:
      type: object
      required: [delivered, row_count, delivered_at]
      properties:
        delivered:
          type: boolean
          description: True if the destination accepted the delivery (a 2xx response).
        row_count:
          type: integer
          description: Number of report rows included in the delivered payload.
        status_code:
          type: integer
          description: The destination's HTTP response status code, if a delivery attempt was made.
        delivered_at: { type: string, format: date-time }
    WebhookEventType:
      type: string
      description: A cataloged webhook event name (libs/webhooks.EventType).
      enum:
        - delivery.impression
        - delivery.click
        - delivery.line_item_ended
        - delivery.alert_fired
        - billing.invoice_created
        - billing.payment_failed
        - creative.approved
        - creative.rejected
        - reporting.scheduled_report_delivered
    WebhookSubscription:
      type: object
      description: >
        A tenant's webhook registration. secret is write-only — never present on a GET/list/update
        response; only the create response returns it, exactly once.
      required: [id, tenant_id, url, events, active]
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        url: { type: string, format: uri }
        events:
          type: array
          items: { $ref: '#/components/schemas/WebhookEventType' }
          minItems: 1
        active: { type: boolean }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    WebhookSubscriptionCreate:
      type: object
      required: [url, events]
      properties:
        url: { type: string, format: uri, minLength: 1 }
        events:
          type: array
          items: { $ref: '#/components/schemas/WebhookEventType' }
          minItems: 1
        active: { type: boolean, default: true }
    WebhookSubscriptionCreated:
      description: The created subscription plus its signing secret (returned once, at creation only).
      allOf:
        - $ref: '#/components/schemas/WebhookSubscription'
        - type: object
          required: [secret]
          properties:
            secret:
              type: string
              description: >
                HMAC-SHA256 signing secret (libs/webhooks.Sign/Verify) for this subscription's
                deliveries. Shown once; store it — the API never returns it again.
    WebhookSubscriptionUpdate:
      type: object
      properties:
        url: { type: string, format: uri, minLength: 1 }
        events:
          type: array
          items: { $ref: '#/components/schemas/WebhookEventType' }
          minItems: 1
        active: { type: boolean }
    WebhookSubscriptionList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/WebhookSubscription' }
        next_cursor: { type: string }
    # SR-1214 durable webhooks (docs/design/webhooks.md): outbox deliveries, replay, rotation.
    WebhookDeliveryStatus:
      type: string
      description: >
        Outbox row state: PENDING (waiting for next_attempt_at), IN_FLIGHT (leased by a runner),
        DELIVERED (a 2xx), DEAD (dead-lettered after the retry policy's max attempts, or the
        target became unresolvable). DEAD rows stay listable and replayable.
      enum: [PENDING, IN_FLIGHT, DELIVERED, DEAD]
    WebhookDelivery:
      type: object
      description: One delivery of one event to one subscription, with its attempt history.
      required: [id, tenant_id, webhook_subscription_id, event_id, event_type, status, attempts, created_at, updated_at]
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        webhook_subscription_id: { type: string, format: uuid }
        event_id:
          type: string
          description: The event's id (X-Riptide-Webhook payload `id`); stable across retries and replays.
        event_type: { $ref: '#/components/schemas/WebhookEventType' }
        status: { $ref: '#/components/schemas/WebhookDeliveryStatus' }
        attempts: { type: integer, minimum: 0 }
        next_attempt_at:
          type: string
          format: date-time
          description: When the next attempt is due (PENDING only).
        last_status_code:
          type: integer
          description: HTTP status of the last attempt (0 when the request never got a response).
        last_error: { type: string }
        delivered_at: { type: string, format: date-time }
        replay_of:
          type: string
          format: uuid
          description: The delivery this one was replayed from, when it is a replay.
        payload:
          type: object
          additionalProperties: true
          description: The exact event payload that is signed and sent.
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    WebhookDeliveryList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/WebhookDelivery' }
        next_cursor: { type: string }
    WebhookSecretRotateRequest:
      type: object
      properties:
        grace_seconds:
          type: integer
          minimum: 0
          maximum: 604800
          default: 86400
          description: How long the previous secret keeps co-signing deliveries after rotation.
    WebhookSecretRotated:
      description: The subscription after rotation plus its new signing secret (returned once).
      allOf:
        - $ref: '#/components/schemas/WebhookSubscription'
        - type: object
          required: [secret, secret_version, previous_secret_expires_at]
          properties:
            secret:
              type: string
              description: The new HMAC-SHA256 signing secret. Shown once; the API never returns it again.
            secret_version: { type: integer }
            previous_secret_expires_at:
              type: string
              format: date-time
              description: Until this instant deliveries also carry a signature under the previous secret.
    TenantHealth:
      type: object
      required: [tenant_id, endpoint_error_rates, governor_shed_counts, window_minutes, checked_at]
      properties:
        tenant_id: { type: string, format: uuid }
        plan_version:
          type: integer
          format: int64
          nullable: true
          description: >
            The serving plan version last observed for this tenant's cell, when this console
            instance has a plan-store binding configured. Null is a documented gap, not a
            fabricated value — see docs/spec/operability.md.
        endpoint_error_rates:
          type: object
          additionalProperties: { type: number }
          description: >
            Per-endpoint error rate (errors/requests) over the retained window
            (libs/health.InMemoryReporter). Empty until something records into this console
            instance's own counters — see docs/spec/operability.md for the cross-process scope note.
        governor_shed_counts:
          type: object
          additionalProperties: { type: integer, format: int64 }
          description: Per-"scope:id" governor shed count over the retained window, when fed.
        window_minutes:
          type: integer
          description: The retained window (minutes) the counts above were summed over.
        checked_at: { type: string, format: date-time }
        requests:
          type: integer
          format: int64
          nullable: true
          description: >
            Sum of requests over the window from riptide.report_event_rollup_minute, when a
            ClickHouse DSN is configured. Null (not zero) when ClickHouse is not configured — a
            documented degrade, see docs/spec/operability.md.
        fills:
          type: integer
          format: int64
          nullable: true
          description: Sum of fills over the window, from the same ClickHouse source as requests.
        impressions:
          type: integer
          format: int64
          nullable: true
          description: Sum of impressions over the window, from the same ClickHouse source as requests.
        revenue_net:
          type: string
          nullable: true
          description: >
            Sum of net-recognized revenue over the window (decimal string, 4dp), from the same
            ClickHouse source as requests.
        fill_rate:
          type: number
          nullable: true
          description: fills / requests over the window (0 when requests is 0), from the same ClickHouse source as requests.
    PublisherBulkImportRequest:
      type: object
      required: [items]
      properties:
        items:
          type: array
          minItems: 1
          maxItems: 500
          items: { $ref: '#/components/schemas/PublisherCreate' }
        dry_run:
          type: boolean
          default: false
          description: When true, validate every row and report what would happen without persisting anything.
    PublisherBulkImportRow:
      type: object
      required: [index, status]
      properties:
        index: { type: integer, description: Position of this row in the request's items array. }
        status:
          type: string
          enum: [would_create, created, error]
        publisher: { $ref: '#/components/schemas/Publisher' }
        error: { type: string }
    PublisherBulkImportResponse:
      type: object
      required: [rows]
      properties:
        rows:
          type: array
          items: { $ref: '#/components/schemas/PublisherBulkImportRow' }
    PlacementBulkImportRequest:
      type: object
      required: [items]
      properties:
        items:
          type: array
          minItems: 1
          maxItems: 500
          items: { $ref: '#/components/schemas/PlacementCreate' }
        dry_run:
          type: boolean
          default: false
          description: When true, validate every row and report what would happen without persisting anything.
    PlacementBulkImportRow:
      type: object
      required: [index, status]
      properties:
        index: { type: integer, description: Position of this row in the request's items array. }
        status:
          type: string
          enum: [would_create, created, error]
        placement: { $ref: '#/components/schemas/Placement' }
        error: { type: string }
    PlacementBulkImportResponse:
      type: object
      required: [rows]
      properties:
        rows:
          type: array
          items: { $ref: '#/components/schemas/PlacementBulkImportRow' }
    DemandPartnerBulkImportRequest:
      type: object
      required: [items]
      properties:
        items:
          type: array
          minItems: 1
          maxItems: 500
          items: { $ref: '#/components/schemas/DemandPartnerCreate' }
        dry_run:
          type: boolean
          default: false
          description: When true, validate every row and report what would happen without persisting anything.
    DemandPartnerBulkImportRow:
      type: object
      required: [index, status]
      properties:
        index: { type: integer, description: Position of this row in the request's items array. }
        status:
          type: string
          enum: [would_create, created, error]
        demand_partner: { $ref: '#/components/schemas/DemandPartner' }
        error: { type: string }
    DemandPartnerBulkImportResponse:
      type: object
      required: [rows]
      properties:
        rows:
          type: array
          items: { $ref: '#/components/schemas/DemandPartnerBulkImportRow' }
    CampaignOrderBulkImportRequest:
      type: object
      required: [items]
      description: >
        CP-11 bulk upsert of campaign orders by `ref` (design 37 §4). Each item must carry a
        non-empty `ref` for idempotent identity. `dry_run` validates without persisting.
      properties:
        items:
          type: array
          minItems: 1
          maxItems: 500
          items: { $ref: '#/components/schemas/CampaignOrderCreate' }
        dry_run:
          type: boolean
          default: false
          description: When true, validate every row and report what would happen without persisting.
    CampaignOrderBulkImportRow:
      type: object
      required: [index, status]
      properties:
        index: { type: integer, description: Position of this row in the request's items array. }
        status:
          type: string
          enum: [would_create, would_update, created, updated, error]
        campaign_order: { $ref: '#/components/schemas/CampaignOrder' }
        error: { type: string }
    CampaignOrderBulkImportResponse:
      type: object
      required: [rows]
      properties:
        rows:
          type: array
          items: { $ref: '#/components/schemas/CampaignOrderBulkImportRow' }
    CreativeBulkImportRequest:
      type: object
      required: [items]
      description: >
        CP-11 bulk upsert of creatives by `ref` (design 37 §4). Each item must carry a
        non-empty `ref` for idempotent identity. `dry_run` validates without persisting.
      properties:
        items:
          type: array
          minItems: 1
          maxItems: 500
          items: { $ref: '#/components/schemas/CreativeCreate' }
        dry_run:
          type: boolean
          default: false
          description: When true, validate every row and report what would happen without persisting.
    CreativeBulkImportRow:
      type: object
      required: [index, status]
      properties:
        index: { type: integer, description: Position of this row in the request's items array. }
        status:
          type: string
          enum: [would_create, would_update, created, updated, error]
        creative: { $ref: '#/components/schemas/Creative' }
        error: { type: string }
    CreativeBulkImportResponse:
      type: object
      required: [rows]
      properties:
        rows:
          type: array
          items: { $ref: '#/components/schemas/CreativeBulkImportRow' }
    DoctorCheck:
      type: object
      required: [name, set]
      properties:
        name: { type: string }
        set: { type: boolean }
    DoctorIssue:
      type: object
      required: [code, message]
      properties:
        code: { type: string }
        message: { type: string }
    DoctorResult:
      type: object
      required: [checks, issues]
      properties:
        checks:
          type: array
          items: { $ref: '#/components/schemas/DoctorCheck' }
        issues:
          type: array
          items: { $ref: '#/components/schemas/DoctorIssue' }
    DecisionStageFilter:
      type: object
      required: [stage, in, out]
      properties:
        stage: { type: string }
        in: { type: integer }
        out: { type: integer }
        removed_by_reason:
          type: object
          additionalProperties: { type: integer }
          description: Count of removed candidates per reason code, for stages that removed any.
    DecisionTraceInput:
      type: object
      properties:
        stages:
          type: array
          items: { $ref: '#/components/schemas/DecisionStageFilter' }
        winner_rationale: { type: string }
    ExplainRequestInput:
      type: object
      properties:
        decision_trace:
          $ref: '#/components/schemas/DecisionTraceInput'
          # Absent/omitted means "no trace recorded" — SummarizeTrace(nil)'s documented behavior.
        request_id:
          type: string
          description: >
            Explain v1 fallback (RIP-96): when decision_trace is omitted and this console is
            configured with a ClickHouse DSN, request_id is looked up directly against
            riptide.ad_event (Tempo+CH join, docs/spec/observability.md) instead of requiring a
            caller-supplied trace. Ignored when decision_trace is present.
    ExplainResult:
      type: object
      required: [summary]
      properties:
        summary:
          type: array
          items: { type: string }
          description: Human-readable stage-by-stage lines, ending with the winner rationale.
        winner_rationale: { type: string }
    DiagnoseDeliveryInput:
      type: object
      required: [line_item_ref]
      properties:
        line_item_ref: { type: string, description: Line item reference code to diagnose. }
    DiagnoseDeliveryResult:
      type: object
      required: [blocking_issues, checked]
      properties:
        blocking_issues:
          type: array
          items: { type: string }
        checked:
          type: array
          items: { type: string }
    PreflightChangeInput:
      type: object
      required: [entity_kind, proposed]
      properties:
        entity_kind:
          type: string
          description: Entity kind the change targets (line_item|placement|route|fee_schedule|campaign_order|creative|audience|...).
        current:
          type: object
          additionalProperties: true
          description: Current field values, as a flat map of field name -> value. Omitted fields are treated as absent (nil).
        proposed:
          type: object
          additionalProperties: true
          description: Proposed field changes, as a flat map of field name -> new value.
    PreflightFieldChange:
      type: object
      description: One before/after value pair. Either side may be any JSON value or absent.
      properties:
        before: {}
        after: {}
    PreflightChangeResult:
      type: object
      required: [would_apply, warnings, diff]
      properties:
        would_apply: { type: boolean, description: Whether the change looks structurally safe to apply. }
        warnings:
          type: array
          items: { type: string }
        diff:
          type: object
          additionalProperties: { $ref: '#/components/schemas/PreflightFieldChange' }

    ForecastConfidence:
      type: string
      description: >
        Coarse confidence from historical sample size (design 36) — not a model probability.
        LOW = fewer than 7 history days; MEDIUM = 7–27; HIGH = 28+.
      enum: [LOW, MEDIUM, HIGH]
    ForecastSlice:
      type: object
      description: Supply slice dimensions assumed for the forecast (placement required in v1).
      required: [placement_id]
      properties:
        placement_id: { type: string, format: uuid }
        country:
          type: string
          description: Optional ISO-3166-1 alpha-2 country filter on the history rollup.
        app_or_site:
          type: string
          description: Optional app/site dimension filter (v1 reserved; ignored when empty).
    ForecastRange:
      type: object
      description: Inclusive UTC calendar-day range for the forecast window.
      required: [start_day, end_day]
      properties:
        start_day: { type: string, format: date, description: "Inclusive start (YYYY-MM-DD, UTC)." }
        end_day: { type: string, format: date, description: "Inclusive end (YYYY-MM-DD, UTC)." }
    ForecastProposal:
      type: object
      description: Draft flight used for deliverable forecast (design 36 §3.1).
      required: [priority_class, goal_impressions, start_day, end_day]
      properties:
        priority_class: { $ref: '#/components/schemas/PriorityClass' }
        goal_impressions:
          type: integer
          format: int64
          minimum: 0
          description: Proposed lifetime impression goal for the flight.
        start_day: { type: string, format: date }
        end_day: { type: string, format: date }
    ForecastAvailabilityRequest:
      type: object
      description: >
        Availability forecast input. Booked demand is loaded from the tenant's ACTIVE/PAUSED
        line items unless bookings are supplied explicitly (agent/test override).
      required: [slice, window, priority_class]
      properties:
        slice: { $ref: '#/components/schemas/ForecastSlice' }
        window: { $ref: '#/components/schemas/ForecastRange' }
        priority_class:
          # description (moved from a $ref sibling, invalid in OAS 3.0): Priority band to compute residual availability against.
          $ref: '#/components/schemas/PriorityClass'
        seasonality_factor:
          type: string
          description: >
            Optional decimal seasonality multiplier (default "1.0"). Applied to replayed
            capacity. Must be > 0 when set.
        lookback_days:
          type: integer
          minimum: 1
          maximum: 90
          description: History window in days ending before as-of (default 28).
        bookings:
          type: array
          description: Optional explicit bookings; when omitted the console loads ACTIVE/PAUSED line items.
          items: { $ref: '#/components/schemas/ForecastBooking' }
    ForecastDeliveryRequest:
      type: object
      description: Delivery forecast input — availability inputs plus a proposal flight.
      required: [slice, proposal]
      properties:
        slice: { $ref: '#/components/schemas/ForecastSlice' }
        proposal: { $ref: '#/components/schemas/ForecastProposal' }
        seasonality_factor: { type: string }
        lookback_days:
          type: integer
          minimum: 1
          maximum: 90
        bookings:
          type: array
          items: { $ref: '#/components/schemas/ForecastBooking' }
        sample_requests:
          type: integer
          minimum: 100
          maximum: 100000
          description: >
            Forecast v2 (SR-1238b): number of sampled requests to replay the proposal's targeting
            against. When set the response carries p10 / p50 / p90, the fill / IVT / viewability
            adjustments and the contention rows; omitted keeps the v1 point estimate.
    ForecastBooking:
      type: object
      description: One booked line item used for contention math.
      required: [ref, priority_class, goal_impressions]
      properties:
        ref: { type: string, description: Line item ref (or draft id) for explainability. }
        priority_class: { $ref: '#/components/schemas/PriorityClass' }
        goal_impressions: { type: integer, format: int64, minimum: 0 }
        start_day: { type: string, format: date }
        end_day: { type: string, format: date }
    ForecastMetrics:
      type: object
      required: [capacity, booked, available, confidence]
      properties:
        capacity:
          type: integer
          format: int64
          description: Estimated eligible requests/impressions in the range.
        booked:
          type: integer
          format: int64
          description: Estimated impressions consumed by higher-or-equal priority bookings.
        available:
          type: integer
          format: int64
          description: Residual capacity at the queried priority band (max 0).
        confidence: { $ref: '#/components/schemas/ForecastConfidence' }
    AvailabilityForecastResult:
      type: object
      required: [slice, window, priority_class, metrics, as_of, history_days]
      description: >
        Availability forecast result body (named distinctly from oapi-codegen's
        ForecastAvailabilityResponse client wrapper).
      properties:
        slice: { $ref: '#/components/schemas/ForecastSlice' }
        window: { $ref: '#/components/schemas/ForecastRange' }
        priority_class: { $ref: '#/components/schemas/PriorityClass' }
        metrics: { $ref: '#/components/schemas/ForecastMetrics' }
        as_of: { type: string, format: date-time, description: Injected clock instant used for the run. }
        history_days:
          type: integer
          description: Count of distinct history days with non-zero request volume used in the replay.
        seasonality_factor: { type: string }
    DeliveryForecastResult:
      type: object
      required: [slice, proposal, metrics, deliverable, shortfall, as_of, history_days]
      description: >
        Delivery forecast result body (named distinctly from oapi-codegen's
        ForecastDeliveryResponse client wrapper).
      properties:
        slice: { $ref: '#/components/schemas/ForecastSlice' }
        proposal: { $ref: '#/components/schemas/ForecastProposal' }
        metrics: { $ref: '#/components/schemas/ForecastMetrics' }
        deliverable:
          type: integer
          format: int64
          description: Expected delivery for the proposal under even pacing (min of available and goal).
        shortfall:
          type: integer
          format: int64
          description: goal_impressions − deliverable (0 when fully deliverable).
        as_of: { type: string, format: date-time }
        history_days: { type: integer }
        seasonality_factor: { type: string }
        p10: { type: integer, format: int64, description: "Forecast v2: 10th-percentile deliverable impressions over the sampled replay (pessimistic)." }
        p50: { type: integer, format: int64, description: "Forecast v2: median deliverable impressions over the sampled replay." }
        p90: { type: integer, format: int64, description: "Forecast v2: 90th-percentile deliverable impressions over the sampled replay (optimistic)." }
        adjustments:
          description: "Forecast v2 (SR-1238b): the multiplicative adjustments applied to raw capacity."
          allOf:
            - $ref: '#/components/schemas/ForecastAdjustments'
        contention:
          type: array
          description: "Forecast v2 (SR-1238b): the bookings whose targeting overlaps the proposal and how much they take."
          items: { $ref: '#/components/schemas/ForecastContention' }

    # ---- SR-1011 inventory tooling ----
    Platform:
      type: object
      description: One row of the global device / app-store platform catalog (migration 0086 seed).
      required: [id, name]
      properties:
        id: { type: string, format: uuid }
        name: { type: string, description: "Stable snake_case platform key (ios, android, roku, web, ...)." }
    PlatformList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/Platform' }
        next_cursor: { type: string }
    InventoryAdsTxtStatus:
      type: string
      description: >
        Last ads.txt verification outcome for a domain: UNCHECKED (never crawled), OK (every
        expected seller line present), MISSING (no ads.txt), MISMATCH (file present but lines
        missing). Written by the supply-transparency verify path; read-only on the API.
      enum: [UNCHECKED, OK, MISSING, MISMATCH]
      # Pinned Go constant names (see DemandRouteIntegration for rationale).
      x-enum-varnames: [InventoryAdsTxtStatusUNCHECKED, InventoryAdsTxtStatusOK, InventoryAdsTxtStatusMISSING, InventoryAdsTxtStatusMISMATCH]
    InventoryCostType:
      type: string
      description: Publisher cost model for a domain — a REVSHARE fraction, or a CPM / FLAT money amount.
      enum: [REVSHARE, CPM, FLAT]
      # Pinned Go constant names (see DemandRouteIntegration for rationale).
      x-enum-varnames: [InventoryCostTypeREVSHARE, InventoryCostTypeCPM, InventoryCostTypeFLAT]
    InventoryDomain:
      type: object
      description: >
        Web-domain inventory record (docs/design/inventory-tooling.md): the site-side counterpart
        of App. floor / cost_value are decimal strings (4 dp); categories are IAB content taxonomy
        ids (docs/spec/taxonomies.md).
      required: [id, tenant_id, hostname, ads_txt_status, status]
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        publisher_id: { type: string, format: uuid, description: "Owning publisher, when assigned." }
        hostname: { type: string, description: "Lower-case registrable host, no scheme or path." }
        site_name: { type: string }
        ads_txt_url: { type: string, description: "Explicit ads.txt location; empty means https://<hostname>/ads.txt." }
        ads_txt_status: { $ref: '#/components/schemas/InventoryAdsTxtStatus' }
        ads_txt_checked_at: { type: string, format: date-time }
        floor: { type: string, description: "CPM floor (decimal string, 4 dp); omitted = inherit." }
        cost_type: { $ref: '#/components/schemas/InventoryCostType' }
        cost_value: { type: string, description: "Fraction for REVSHARE (e.g. \"0.7000\"), money for CPM / FLAT." }
        categories: { type: array, items: { type: string } }
        keywords: { type: string }
        status: { $ref: '#/components/schemas/LifecycleStatus' }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    InventoryDomainCreate:
      type: object
      required: [hostname]
      properties:
        hostname: { type: string, minLength: 1 }
        publisher_id: { type: string, format: uuid }
        site_name: { type: string }
        ads_txt_url: { type: string }
        floor: { type: string }
        cost_type: { $ref: '#/components/schemas/InventoryCostType' }
        cost_value: { type: string }
        categories: { type: array, items: { type: string } }
        keywords: { type: string }
    InventoryDomainUpdate:
      type: object
      description: Partial update — omitted fields keep their stored value; categories present replaces the set.
      properties:
        publisher_id: { type: string, format: uuid }
        site_name: { type: string }
        ads_txt_url: { type: string }
        floor: { type: string }
        cost_type: { $ref: '#/components/schemas/InventoryCostType' }
        cost_value: { type: string }
        categories: { type: array, items: { type: string } }
        keywords: { type: string }
    InventoryDomainList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/InventoryDomain' }
        next_cursor: { type: string }
    CsvImportRequest:
      type: object
      required: [csv]
      properties:
        csv: { type: string, minLength: 1, description: "CSV document with a header row; see the operation summary for the accepted columns." }
        dry_run: { type: boolean, description: "Validate and report per-row outcomes without persisting." }
    CsvImportRowStatus:
      type: string
      enum: [would_create, would_update, created, updated, error]
      # Pinned Go constant names (see DemandRouteIntegration for rationale).
      x-enum-varnames: [CsvImportRowStatusWouldCreate, CsvImportRowStatusWouldUpdate, CsvImportRowStatusCreated, CsvImportRowStatusUpdated, CsvImportRowStatusError]
    CsvImportRow:
      type: object
      required: [index, status]
      properties:
        index: { type: integer, description: "1-based data-row position (header excluded)." }
        key: { type: string, description: "The row's upsert key (hostname / bundle_id)." }
        status: { $ref: '#/components/schemas/CsvImportRowStatus' }
        id: { type: string, format: uuid, description: "Created / updated resource id (not set for dry-run or error rows)." }
        error: { type: string }
    CsvImportResponse:
      type: object
      required: [rows]
      properties:
        rows:
          type: array
          items: { $ref: '#/components/schemas/CsvImportRow' }
    StoreLookupRequest:
      type: object
      required: [store_url]
      properties:
        store_url: { type: string, format: uri, minLength: 1 }
    StoreLookupSource:
      type: string
      description: url = only the URL was parsed (no public lookup API for that store); api = metadata fetched from the store's public lookup API.
      enum: [url, api]
      x-enum-varnames: [StoreLookupSourceUrl, StoreLookupSourceApi]
    StoreLookupResult:
      type: object
      required: [store_url, platform, resolved, source]
      properties:
        store_url: { type: string }
        platform: { type: string, description: "Platform catalog name (GET /v1/platforms); empty when the URL is not a recognised store." }
        platform_id: { type: string, format: uuid }
        store_id: { type: string, description: "Store-assigned numeric / slug id parsed from the URL." }
        bundle_id: { type: string, description: "Package / bundle identifier when the store exposes one." }
        name: { type: string }
        developer: { type: string }
        categories: { type: array, items: { type: string } }
        resolved: { type: boolean, description: "True when name / bundle came back from the store API." }
        source: { $ref: '#/components/schemas/StoreLookupSource' }

    # ---- SR-1011 API keys (iam) ----
    ApiKeyStatus:
      type: string
      enum: [ACTIVE, REVOKED]
      x-enum-varnames: [ApiKeyStatusACTIVE, ApiKeyStatusREVOKED]
    ApiKey:
      type: object
      description: >
        A tenant API key (api_key table). Only prefix (non-secret lookup handle) and the SHA-256
        hash of the secret are stored; the secret itself is returned once by createApiKey.
      required: [id, tenant_id, name, prefix, roles, status, created_at]
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        name: { type: string }
        prefix: { type: string, description: "Non-secret key handle (rt_key_<prefix>) shown in lists and audit." }
        roles:
          type: array
          items: { type: string }
          description: authz.Role names the key grants (docs/spec/authz-roles.md).
        publisher_id: { type: string, format: uuid, description: "Set for publisher-persona keys." }
        advertiser_id: { type: string, format: uuid, description: "Set for advertiser-persona keys." }
        status: { $ref: '#/components/schemas/ApiKeyStatus' }
        last_used_at: { type: string, format: date-time }
        revoked_at: { type: string, format: date-time }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    ApiKeyCreate:
      type: object
      required: [name, roles]
      properties:
        name: { type: string, minLength: 1 }
        roles:
          type: array
          minItems: 1
          items: { type: string }
          description: Tenant-assignable roles only; operator is never grantable.
        publisher_id: { type: string, format: uuid, description: "Required when roles includes publisher." }
        advertiser_id: { type: string, format: uuid, description: "Required when roles includes advertiser." }
    ApiKeyCreated:
      description: The created key plus its plaintext secret (returned once, at creation only).
      allOf:
        - $ref: '#/components/schemas/ApiKey'
        - type: object
          required: [secret]
          properties:
            secret:
              type: string
              description: The full API key (send as X-Riptide-Api-Key). Shown once; never returned again.
    ApiKeyRotated:
      description: >
        The successor key (with its plaintext secret, returned once) plus the rotation lineage:
        the key it replaces and the instant that key stops authenticating.
      allOf:
        - $ref: '#/components/schemas/ApiKeyCreated'
        - type: object
          required: [rotated_from_id, previous_expires_at, grace]
          properties:
            rotated_from_id:
              type: string
              format: uuid
              description: The key this successor replaces; it keeps working until previous_expires_at.
            previous_expires_at:
              type: string
              format: date-time
              description: When the previous key stops authenticating (rotation time + grace).
            grace:
              type: string
              description: The applied grace as a duration string (e.g. `2h0m0s`).
    ApiKeyList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/ApiKey' }
        next_cursor: { type: string }

    # ---- SR-1010 publisher persona ----
    PublisherReportSummary:
      type: object
      description: >
        Publisher portal dashboard read model. Money fields are decimal strings (4 dp) in the
        tenant reporting currency; counts are zero (executed=false) when no warehouse is wired.
      required: [tenant_id, publisher_id, publisher_public_id, from, to, requests, impressions, fills, revenue_net, publisher_payout, executed]
      properties:
        tenant_id: { type: string, format: uuid }
        publisher_id: { type: string, format: uuid }
        publisher_public_id: { type: string }
        from: { type: string, format: date-time }
        to: { type: string, format: date-time }
        requests: { type: integer, format: int64, minimum: 0 }
        impressions: { type: integer, format: int64, minimum: 0 }
        fills: { type: integer, format: int64, minimum: 0 }
        revenue_net: { type: string }
        publisher_payout: { type: string }
        currency: { type: string }
        executed: { type: boolean, description: "False when the reporting warehouse is not configured (counts are then zero, not estimated)." }
    PublisherPayoutStatementStatus:
      type: string
      enum: [DRAFT, ISSUED, PAID, VOID]
      x-enum-varnames: [PublisherPayoutStatementStatusDRAFT, PublisherPayoutStatementStatusISSUED, PublisherPayoutStatementStatusPAID, PublisherPayoutStatementStatusVOID]
    PublisherPayoutStatement:
      type: object
      description: >
        Publisher payout statement (SR-204 seam, docs/design/publisher-persona.md): the
        publisher_payout leg of the reporting layer aggregated over one period, plus a signed
        adjustment. total_due = publisher_share + adjustments. Money is decimal strings (4 dp).
      required: [id, tenant_id, publisher_id, period_start, period_end, currency, impressions, gross_revenue, publisher_share, adjustments, total_due, status, provider, created_at]
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        publisher_id: { type: string, format: uuid }
        period_start: { type: string, format: date, description: "Inclusive." }
        period_end: { type: string, format: date, description: "Exclusive." }
        currency: { type: string }
        impressions: { type: integer, format: int64, minimum: 0 }
        gross_revenue: { type: string, description: "Tenant net revenue on this publisher's supply over the period." }
        publisher_share: { type: string, description: "Publisher-payable amount from the fee schedule (publisher_payout)." }
        adjustments: { type: string, description: "Signed manual adjustment." }
        total_due: { type: string }
        status: { $ref: '#/components/schemas/PublisherPayoutStatementStatus' }
        provider: { type: string, description: "Payout provider the statement was issued through (e.g. manual)." }
        provider_ref: { type: string, description: "Payout provider's reference for the transfer (SR-1238a); empty until handed to the provider." }
        provider_status: { type: string, readOnly: true, description: "Provider-reported transfer state as last synced (SR-1238a); empty until handed to the provider." }
        notes: { type: string }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    PublisherPayoutStatementGenerate:
      type: object
      required: [period_start, period_end]
      properties:
        period_start: { type: string, format: date }
        period_end: { type: string, format: date, description: "Exclusive; must be after period_start." }
        currency: { type: string, minLength: 3, maxLength: 3 }
        adjustments: { type: string, description: "Signed decimal string (4 dp); omitted = 0." }
        notes: { type: string }
    PublisherPayoutStatementList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/PublisherPayoutStatement' }
        next_cursor: { type: string }

    # ---- Wave 8 round 3 contract batch (Step 0) ----
    TenantCells:
      type: object
      description: A tenant's cell assignment (tenant_cell; SR-1221, design 16 "Tenant->cell routing seam").
      required: [tenant_id, home_cell_id, serving_cell_ids]
      properties:
        tenant_id: { type: string, format: uuid }
        home_cell_id: { type: string, description: "Cell owning the tenant's control-plane residency (role home)." }
        serving_cell_ids:
          type: array
          items: { type: string }
          description: Every cell whose plan carries the tenant (home included), sorted.
        updated_at: { type: string, format: date-time }
    TenantCellsUpdate:
      type: object
      required: [home_cell_id]
      properties:
        home_cell_id: { type: string, minLength: 1 }
        serving_cell_ids:
          type: array
          items: { type: string, minLength: 1 }
          description: Replaces the serving set; the home cell is added when omitted. Omitted = home only.
    TenantCellRef:
      type: object
      required: [tenant_id, role]
      properties:
        tenant_id: { type: string, format: uuid }
        role: { $ref: '#/components/schemas/TenantCellRole' }
    TenantCellRole:
      type: string
      description: tenant_cell.role — `home` (control-plane residency, also serving) or `serving`.
      enum: [home, serving]
      x-enum-varnames: [TenantCellRoleHome, TenantCellRoleServing]
    TenantCellRefList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/TenantCellRef' }
        next_cursor: { type: string }
    ApprovalStatus:
      type: string
      description: Partner approval state for an app bundle or a site domain (SR-1218).
      enum: [PENDING, APPROVED, REJECTED]
      x-enum-varnames: [ApprovalStatusPENDING, ApprovalStatusAPPROVED, ApprovalStatusREJECTED]
    DomainApproval:
      type: object
      description: One site-domain approval for a demand partner (demand_partner_domain_approval; SR-1218).
      required: [tenant_id, demand_partner_id, domain, status]
      properties:
        tenant_id: { type: string, format: uuid }
        demand_partner_id: { type: string, format: uuid }
        domain: { type: string, description: "Lower-case site domain, no scheme." }
        status: { $ref: '#/components/schemas/ApprovalStatus' }
        note: { type: string }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    DomainApprovalUpdate:
      type: object
      required: [status]
      properties:
        status: { $ref: '#/components/schemas/ApprovalStatus' }
        note: { type: string, maxLength: 1024, description: "Free-text reason; an empty string clears it." }
    DomainApprovalList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/DomainApproval' }
        next_cursor: { type: string }
    ApprovalStatusCounts:
      type: object
      required: [pending, approved, rejected]
      properties:
        pending: { type: integer, format: int64 }
        approved: { type: integer, format: int64 }
        rejected: { type: integer, format: int64 }
    ApprovalCounts:
      type: object
      description: Per-partner approval counts (SR-1218) — app bundles (App.route_approvals) and site domains.
      required: [demand_partner_id, approvals_required, bundles, domains]
      properties:
        demand_partner_id: { type: string, format: uuid }
        approvals_required: { type: boolean, description: "Mirrors DemandPartner.approvals_required: whether the gate is blocking." }
        bundles: { $ref: '#/components/schemas/ApprovalStatusCounts' }
        domains: { $ref: '#/components/schemas/ApprovalStatusCounts' }
    CrawlerConfig:
      type: object
      description: ads.txt / app-ads.txt crawler tunables (crawler_config; SR-1212). The platform defaults apply until a row is written.
      required: [tenant_id, http_fallback, redirect_hop_cap, fetch_timeout_seconds, batch_size, recrawl_after_hours, app_ads_discovery]
      properties:
        tenant_id: { type: string, format: uuid }
        http_fallback: { type: boolean, description: "Retry over plain HTTP when the HTTPS fetch fails (default true)." }
        redirect_hop_cap: { type: integer, minimum: 0, maximum: 20, description: "In-root-only redirects followed before the crawl gives up (default 5)." }
        fetch_timeout_seconds: { type: integer, minimum: 1, maximum: 600, description: "Per-document fetch timeout (default 180)." }
        batch_size: { type: integer, minimum: 1, maximum: 100, description: "Domains crawled per batch (default 5)." }
        recrawl_after_hours: { type: integer, minimum: 1, maximum: 720, description: "A domain crawled within this window is skipped unless recrawl is forced (default 24)." }
        app_ads_discovery: { type: boolean, description: "Discover app-ads.txt from the store listing's developer site (default true)." }
        updated_at: { type: string, format: date-time }
    CrawlerConfigUpdate:
      type: object
      description: Partial update by presence — an omitted field keeps its current (or default) value.
      properties:
        http_fallback: { type: boolean }
        redirect_hop_cap: { type: integer, minimum: 0, maximum: 20 }
        fetch_timeout_seconds: { type: integer, minimum: 1, maximum: 600 }
        batch_size: { type: integer, minimum: 1, maximum: 100 }
        recrawl_after_hours: { type: integer, minimum: 1, maximum: 720 }
        app_ads_discovery: { type: boolean }
    PartnerSellersImportStatus:
      type: string
      enum: [PENDING, RUNNING, SUCCEEDED, FAILED]
      x-enum-varnames: [PartnerSellersImportStatusPENDING, PartnerSellersImportStatusRUNNING, PartnerSellersImportStatusSUCCEEDED, PartnerSellersImportStatusFAILED]
    SellersImportRowCompliance:
      type: string
      description: Cross-check verdict for one seller row (sellers.json 1.0 name / domain conditional requirements, confidential flag, passthrough resolution).
      enum: [compliant, missing_name, missing_domain, confidential_conflict, passthrough_unresolved]
      x-enum-varnames: [SellersImportRowComplianceCompliant, SellersImportRowComplianceMissingName, SellersImportRowComplianceMissingDomain, SellersImportRowComplianceConfidentialConflict, SellersImportRowCompliancePassthroughUnresolved]
    SellersImportSellerType:
      type: string
      enum: [PUBLISHER, INTERMEDIARY, BOTH]
      x-enum-varnames: [SellersImportSellerTypePUBLISHER, SellersImportSellerTypeINTERMEDIARY, SellersImportSellerTypeBOTH]
    PartnerSellersImportRow:
      type: object
      required: [seller_id, seller_type, compliance]
      properties:
        seller_id: { type: string }
        name: { type: string }
        domain: { type: string }
        seller_type: { $ref: '#/components/schemas/SellersImportSellerType' }
        is_confidential: { type: boolean }
        is_passthrough: { type: boolean }
        compliance: { $ref: '#/components/schemas/SellersImportRowCompliance' }
    PartnerSellersImport:
      type: object
      description: One import of a demand partner's sellers.json (partner_sellers_import; SR-1212).
      required: [id, tenant_id, demand_partner_id, source_url, status, row_count, compliant_count, noncompliant_count, created_at]
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        demand_partner_id: { type: string, format: uuid }
        source_url: { type: string, format: uri }
        status: { $ref: '#/components/schemas/PartnerSellersImportStatus' }
        fetched_at: { type: string, format: date-time }
        row_count: { type: integer }
        compliant_count: { type: integer }
        noncompliant_count: { type: integer }
        error_text: { type: string }
        rows:
          type: array
          items: { $ref: '#/components/schemas/PartnerSellersImportRow' }
          description: Present on getPartnerSellersImport only.
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    PartnerSellersImportList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/PartnerSellersImport' }
        next_cursor: { type: string }
    PartnerSellersImportRunRequest:
      type: object
      required: [source_url]
      properties:
        source_url: { type: string, format: uri, minLength: 12, description: "HTTPS URL of the partner's sellers.json." }
    ContentProgram:
      type: object
      description: One program-guide entry (content_program; riptide.content.v1.Program; SR-1213).
      required: [id, tenant_id, provider, external_id, title, genres, keywords, live, created_at, updated_at]
      properties:
        id: { type: string, format: uuid }
        tenant_id: { type: string, format: uuid }
        provider: { type: string, description: "Guide source the row came from (feed id, EPG vendor key, or manual)." }
        external_id: { type: string, description: "The provider's own program identifier; unique with provider per tenant." }
        title: { type: string }
        series: { type: string }
        season: { type: string }
        episode: { type: string }
        genres: { type: array, maxItems: 64, items: { type: string, maxLength: 64 }, description: "Content Taxonomy 3.1 ids, sorted." }
        keywords: { type: array, maxItems: 256, items: { type: string, maxLength: 128 }, description: "Lower-case keywords, sorted." }
        rating: { type: string }
        language: { type: string, description: "ISO 639-1 code." }
        duration_seconds: { type: integer, minimum: 0 }
        channel: { type: string, description: "Linear channel key; empty for VOD." }
        air_start: { type: string, format: date-time }
        air_end: { type: string, format: date-time }
        live: { type: boolean }
        created_at: { type: string, format: date-time }
        updated_at: { type: string, format: date-time }
    ContentProgramCreate:
      type: object
      required: [provider, external_id, title]
      properties:
        provider: { type: string, minLength: 1, maxLength: 128 }
        external_id: { type: string, minLength: 1, maxLength: 256 }
        title: { type: string, minLength: 1, maxLength: 512 }
        series: { type: string, maxLength: 512 }
        season: { type: string, maxLength: 64 }
        episode: { type: string, maxLength: 64 }
        genres: { type: array, items: { type: string, minLength: 1 } }
        keywords: { type: array, items: { type: string, minLength: 1 } }
        rating: { type: string, maxLength: 32 }
        language: { type: string, minLength: 2, maxLength: 2 }
        duration_seconds: { type: integer, minimum: 0 }
        channel: { type: string, maxLength: 128, description: "Requires air_start / air_end." }
        air_start: { type: string, format: date-time }
        air_end: { type: string, format: date-time }
        live: { type: boolean }
    ContentProgramUpdate:
      type: object
      description: Partial update by presence; arrays present replace the whole set. provider / external_id are immutable.
      properties:
        title: { type: string, minLength: 1, maxLength: 512 }
        series: { type: string, maxLength: 512 }
        season: { type: string, maxLength: 64 }
        episode: { type: string, maxLength: 64 }
        genres: { type: array, items: { type: string, minLength: 1 } }
        keywords: { type: array, items: { type: string, minLength: 1 } }
        rating: { type: string, maxLength: 32 }
        language: { type: string, minLength: 2, maxLength: 2 }
        duration_seconds: { type: integer, minimum: 0 }
        channel: { type: string, maxLength: 128 }
        air_start: { type: string, format: date-time }
        air_end: { type: string, format: date-time }
        live: { type: boolean }
    ContentProgramList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          items: { $ref: '#/components/schemas/ContentProgram' }
        next_cursor: { type: string }
    ContentDictionaryEntry:
      type: object
      required: [value, count]
      properties:
        value: { type: string }
        count: { type: integer, format: int64, description: "Programs carrying the value." }
    ContentDictionary:
      type: object
      description: Distinct values across the tenant's program guide with program counts (SR-1213 dictionary endpoint).
      required: [genres, keywords, providers, channels]
      properties:
        genres:
          type: array
          items: { $ref: '#/components/schemas/ContentDictionaryEntry' }
        keywords:
          type: array
          items: { $ref: '#/components/schemas/ContentDictionaryEntry' }
        providers:
          type: array
          items: { $ref: '#/components/schemas/ContentDictionaryEntry' }
        channels:
          type: array
          items: { $ref: '#/components/schemas/ContentDictionaryEntry' }

    # ---- Round-5 Step 0 contract batch ----
    PublicBranding:
      type: object
      description: The pre-auth subset of Branding a console host may show (getPublicBranding); never hostnames.
      properties:
        ad_system_name: { type: string, description: "Product / brand name shown on the login and verify pages." }
        logo_url: { type: string, description: "HTTPS URL of the logo." }
        favicon_url: { type: string, description: "HTTPS URL of the favicon." }
        primary_color: { type: string, pattern: '^#[0-9a-fA-F]{6}$', description: "Primary brand colour (#rrggbb)." }
        accent_color: { type: string, pattern: '^#[0-9a-fA-F]{6}$', description: "Accent brand colour (#rrggbb)." }
    ListingProposalList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          description: The page of proposal party rows.
          items: { $ref: '#/components/schemas/ListingProposal' }
        next_cursor: { type: string, description: "Cursor for the next page; absent on the last page." }
    AudienceOrigin:
      type: string
      description: How an audience came to exist — MANUAL (created in the console / API), IMPORTED (from a list import), ACTIVATED (bound by a marketplace signal activation), EVENT_MEMBERSHIP (built from event memberships).
      enum: [MANUAL, IMPORTED, ACTIVATED, EVENT_MEMBERSHIP]
      x-enum-varnames: [AudienceOriginMANUAL, AudienceOriginIMPORTED, AudienceOriginACTIVATED, AudienceOriginEVENTMEMBERSHIP]
    EntityUsage:
      type: object
      description: Reference counts for a reusable entity (REM-J "Used by").
      required: [line_items, creatives, placements]
      properties:
        line_items: { type: integer, format: int64, minimum: 0, description: "Line items referencing the entity." }
        creatives: { type: integer, format: int64, minimum: 0, description: "Creatives referencing the entity." }
        placements: { type: integer, format: int64, minimum: 0, description: "Placements referencing the entity." }
    RetentionTier:
      type: string
      description: Event retention tier (Event.retention_tier); the event store's TTL is kind × tier.
      enum: [basic, standard, extended]
      x-enum-varnames: [RetentionTierBasic, RetentionTierStandard, RetentionTierExtended]
    ReportConfigScheduleCreate:
      type: object
      description: Schedule requested alongside a new report config (SR-1233); ScheduledReportCreate minus report_config_id, which is the config being created.
      required: [cadence, destination_url]
      properties:
        cadence: { $ref: '#/components/schemas/ScheduledReportCadence' }
        destination_url: { type: string, format: uri, minLength: 1, description: "Delivery destination; see ScheduledReport.destination_url." }
    ReportConfigSchedule:
      type: object
      description: The scheduled delivery attached to a report config (SR-1233).
      required: [scheduled_report_id, cadence, destination_url]
      properties:
        scheduled_report_id: { type: string, format: uuid, description: "The ScheduledReport row; manage it through the scheduled-reports operations." }
        cadence: { $ref: '#/components/schemas/ScheduledReportCadence' }
        destination_url: { type: string, format: uri, description: "Delivery destination; see ScheduledReport.destination_url." }
        next_run_at: { type: string, format: date-time, description: "Next delivery instant." }
        destination_secret: { type: string, description: "HMAC signing secret — present exactly once, on the createReportConfig response that created the schedule; never on a GET." }

    # ---- SR-1238a money control plane ----
    InvoiceStatus:
      type: string
      description: Invoice lifecycle (design 41) — DRAFT (open, lines accumulate), REVIEW (frozen for approval), ISSUED (numbered, sent, dunning active), PAID, VOID.
      enum: [DRAFT, REVIEW, ISSUED, PAID, VOID]
      x-enum-varnames: [InvoiceStatusDRAFT, InvoiceStatusREVIEW, InvoiceStatusISSUED, InvoiceStatusPAID, InvoiceStatusVOID]
    InvoiceDocumentFormat:
      type: string
      description: Rendered statement format.
      enum: [html, pdf]
      x-enum-varnames: [InvoiceDocumentFormatHtml, InvoiceDocumentFormatPdf]
    FxSnapshot:
      type: object
      description: FX rates frozen on an invoice at ISSUED.
      required: [as_of, base_currency, rates]
      properties:
        as_of: { type: string, format: date-time, description: "Instant the rates were taken." }
        base_currency: { type: string, minLength: 3, maxLength: 3, description: "Invoice currency the rates convert into." }
        rates:
          type: object
          description: Source currency → decimal rate into base_currency.
          additionalProperties: { type: string }
    InvoiceTax:
      type: object
      description: Tax line computed through the tax seam at ISSUED.
      required: [jurisdiction, rate, amount]
      properties:
        jurisdiction: { type: string, description: "Tax jurisdiction key the seam resolved." }
        rate: { type: string, description: "Decimal rate (e.g. 0.2000 for 20%)." }
        amount: { type: string, description: "Decimal tax amount in the invoice currency." }
    InvoiceTransitionRequest:
      type: object
      required: [to, reason]
      properties:
        to: { $ref: '#/components/schemas/InvoiceStatus' }
        reason: { type: string, minLength: 1, maxLength: 2000, description: "Recorded in the audit envelope." }
    InvoiceCredit:
      type: object
      description: A credit recorded against an invoice.
      required: [id, invoice_id, amount, currency, reason, created_at]
      properties:
        id: { type: string, format: uuid, description: "Credit id." }
        invoice_id: { type: string, format: uuid, description: "The invoice credited." }
        amount: { type: string, description: "Positive decimal amount in the invoice currency." }
        currency: { type: string, description: "Invoice currency." }
        reason: { type: string, description: "Why the credit was granted." }
        created_by: { type: string, description: "Actor label of who recorded it." }
        created_at: { type: string, format: date-time, description: "When it was recorded." }
    InvoiceCreditCreate:
      type: object
      required: [amount, reason]
      properties:
        amount: { type: string, pattern: '^[0-9]+(\.[0-9]{1,4})?$', description: "Positive decimal amount in the invoice currency (4 dp)." }
        reason: { type: string, minLength: 1, maxLength: 2000, description: "Why the credit is granted; audited." }
    InvoiceAdjustment:
      type: object
      description: A signed adjustment line on an invoice.
      required: [id, invoice_id, amount, currency, description, reason, created_at]
      properties:
        id: { type: string, format: uuid, description: "Adjustment id." }
        invoice_id: { type: string, format: uuid, description: "The invoice adjusted." }
        amount: { type: string, description: "Signed decimal amount in the invoice currency." }
        currency: { type: string, description: "Invoice currency." }
        description: { type: string, description: "Line text shown on the statement." }
        reason: { type: string, description: "Why the adjustment was made." }
        created_by: { type: string, description: "Actor label of who recorded it." }
        created_at: { type: string, format: date-time, description: "When it was recorded." }
    InvoiceAdjustmentCreate:
      type: object
      required: [amount, description, reason]
      properties:
        amount: { type: string, pattern: '^-?[0-9]+(\.[0-9]{1,4})?$', description: "Signed decimal amount in the invoice currency (4 dp); non-zero." }
        description: { type: string, minLength: 1, maxLength: 500, description: "Line text shown on the statement." }
        reason: { type: string, minLength: 1, maxLength: 2000, description: "Why the adjustment is made; audited." }
    DunningState:
      type: object
      description: Collection state of an ISSUED invoice.
      required: [tenant_id, invoice_id, attempts]
      properties:
        tenant_id: { type: string, format: uuid, description: "The billed tenant." }
        invoice_id: { type: string, format: uuid, description: "The invoice in dunning." }
        attempts: { type: integer, minimum: 0, description: "Charge attempts made so far." }
        next_attempt_at: { type: string, format: date-time, nullable: true, description: "When the next charge retry runs; null when dunning is exhausted or the invoice left ISSUED." }
        suspended_at: { type: string, format: date-time, nullable: true, description: "When serving was suspended for non-payment; null while not suspended." }
    Invoice:
      type: object
      description: An invoice the tenant issued to one of its advertisers (SR-1238a tenant-to-customer billing; D67).
      required: [id, tenant_id, advertiser_id, period_start, period_end, status, currency, totals]
      properties:
        id: { type: string, format: uuid, description: "Invoice id." }
        tenant_id: { type: string, format: uuid, description: "Issuing tenant." }
        advertiser_id: { type: string, format: uuid, description: "Billed advertiser." }
        campaign_order_id: { type: string, format: uuid, nullable: true, description: "The order billed when the invoice covers one order; null for a period invoice across orders." }
        period_start: { type: string, format: date, description: "Inclusive." }
        period_end: { type: string, format: date, description: "Exclusive." }
        status: { $ref: '#/components/schemas/InvoiceStatus' }
        number: { type: string, nullable: true, description: "Sequential number assigned at ISSUED; null before." }
        currency: { type: string, description: "Invoice currency." }
        totals: { $ref: '#/components/schemas/InvoiceTotals' }
        proof_of_performance:
          description: Delivery evidence the invoice is based on.
          allOf:
            - $ref: '#/components/schemas/InvoiceProofOfPerformance'
        issued_at: { type: string, format: date-time, nullable: true, description: "When the invoice became ISSUED; null before." }
        paid_at: { type: string, format: date-time, nullable: true, description: "When the invoice became PAID; null otherwise." }
        created_at: { type: string, format: date-time, description: "Row creation." }
        updated_at: { type: string, format: date-time, description: "Last change." }
    InvoiceTotals:
      type: object
      description: Money on an advertiser invoice (decimal strings, 4 dp).
      required: [subtotal, tax, credits, adjustments, total]
      properties:
        subtotal: { type: string, description: "Sum of the delivery lines." }
        tax: { type: string, description: "Tax on the subtotal." }
        credits: { type: string, description: "Credits applied (non-negative; subtracted)." }
        adjustments: { type: string, description: "Signed adjustments applied." }
        total: { type: string, description: "subtotal + tax + adjustments − credits." }
    InvoiceProofOfPerformance:
      type: object
      description: Per-line delivery evidence behind an advertiser invoice.
      required: [impressions, clicks, lines]
      properties:
        impressions: { type: integer, format: int64, minimum: 0, description: "Billable impressions in the period." }
        clicks: { type: integer, format: int64, minimum: 0, description: "Clicks in the period." }
        completed_views: { type: integer, format: int64, minimum: 0, description: "Completed video views in the period." }
        lines:
          type: array
          description: One row per line item billed.
          items: { $ref: '#/components/schemas/InvoiceProofLine' }
    InvoiceProofLine:
      type: object
      required: [line_item_id, impressions, amount]
      properties:
        line_item_id: { type: string, format: uuid, description: "The line item." }
        impressions: { type: integer, format: int64, minimum: 0, description: "Billable impressions." }
        clicks: { type: integer, format: int64, minimum: 0, description: "Clicks." }
        amount: { type: string, description: "Decimal amount billed for the line." }
    InvoiceList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          description: The page of advertiser invoices.
          items: { $ref: '#/components/schemas/Invoice' }
        next_cursor: { type: string, description: "Cursor for the next page; absent on the last page." }
    WalletBinding:
      type: object
      description: Advertiser → prepaid wallet binding (SR-1238a).
      required: [tenant_id, advertiser_id, wallet_id, auto_pause]
      properties:
        tenant_id: { type: string, format: uuid, description: "Owning tenant." }
        advertiser_id: { type: string, format: uuid, description: "The advertiser." }
        wallet_id: { type: string, format: uuid, description: "The prepaid wallet (prepaid ledger account) the advertiser's delivery draws down." }
        auto_pause: { type: boolean, description: "Pause the advertiser's line items when the wallet balance reaches zero and resume on top-up." }
        created_at: { type: string, format: date-time, description: "Row creation." }
        updated_at: { type: string, format: date-time, description: "Last change." }
    WalletBindingSet:
      type: object
      required: [wallet_id]
      properties:
        wallet_id: { type: string, format: uuid, description: "The prepaid wallet to bind." }
        auto_pause: { type: boolean, default: false, description: "See WalletBinding.auto_pause." }
    WalletBindingList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          description: The page of bindings.
          items: { $ref: '#/components/schemas/WalletBinding' }
        next_cursor: { type: string, description: "Cursor for the next page; absent on the last page." }
    SettlementRunStatus:
      type: string
      description: State of a settlement run.
      enum: [PENDING, RUNNING, COMPLETED, FAILED]
      x-enum-varnames: [SettlementRunStatusPENDING, SettlementRunStatusRUNNING, SettlementRunStatusCOMPLETED, SettlementRunStatusFAILED]
    SettlementTotals:
      type: object
      description: Money folded by a settlement run (decimal strings, 4 dp).
      required: [currency, gross, fees, net]
      properties:
        currency: { type: string, description: "Settlement currency (the tenant's settlement_currency)." }
        gross: { type: string, description: "Gross payable before fees." }
        fees: { type: string, description: "Fees withheld." }
        net: { type: string, description: "Net handed to the payout provider." }
    VendorSettlementRunRequest:
      type: object
      required: [period]
      properties:
        period: { type: string, pattern: '^[0-9]{4}-(0[1-9]|1[0-2])$', description: "Calendar month to settle (YYYY-MM)." }
    VendorSettlementRun:
      type: object
      description: One vendor settlement run (SR-1238a payout provider).
      required: [id, tenant_id, period, status, totals]
      properties:
        id: { type: string, format: uuid, description: "Run id." }
        tenant_id: { type: string, format: uuid, description: "Owning tenant." }
        period: { type: string, description: "Calendar month settled (YYYY-MM)." }
        status: { $ref: '#/components/schemas/SettlementRunStatus' }
        totals: { $ref: '#/components/schemas/SettlementTotals' }
        statements: { type: integer, minimum: 0, description: "Publisher payout statements folded into the run." }
        ran_at: { type: string, format: date-time, nullable: true, description: "When the run finished; null while PENDING / RUNNING." }
        error: { type: string, nullable: true, description: "Provider or fold error when FAILED; null otherwise." }
    PaymentProviderEvent:
      type: object
      description: >
        A payment-provider event as the provider posts it. The adapter registered for the path's
        `{provider}` maps the provider's own shape onto (external_id, kind, invoice reference,
        amount, currency, received_at); the fields below are the adapter-neutral minimum and the
        raw body is kept (payload_sha256) for audit.
      additionalProperties: true
      properties:
        id: { type: string, description: "Provider event id (external_id); the dedup key with the provider." }
        type: { type: string, description: "Provider event type the adapter maps to a payment_event kind." }
    PaymentWebhookAck:
      type: object
      required: [received, duplicate]
      properties:
        received: { type: boolean, description: "Always true on 200." }
        duplicate: { type: boolean, description: "True when the (provider, external_id) event had already been recorded; nothing was applied." }
        payment_event_id: { type: string, format: uuid, nullable: true, description: "The payment_event row recorded (or previously recorded); null when the event kind is ignored by the adapter." }

    # ---- SR-1238b forecast v2 + marketplace settlement ----
    ForecastAdjustments:
      type: object
      description: Multiplicative adjustments (decimal strings in [0, 1]) applied to raw capacity in forecast v2.
      required: [fill, ivt, viewability]
      properties:
        fill: { type: string, description: "Expected fill rate of the sampled requests." }
        ivt: { type: string, description: "Share retained after invalid-traffic filtering (1 − IVT rate)." }
        viewability: { type: string, description: "Expected viewable share (applies to viewable-impression goals only)." }
    ForecastContention:
      type: object
      description: One booking overlapping the proposal in forecast v2.
      required: [ref, priority_class, overlap, contended_impressions]
      properties:
        ref: { type: string, description: "Booking ref (ForecastBooking.ref)." }
        priority_class: { $ref: '#/components/schemas/PriorityClass' }
        overlap: { type: string, description: "Share of the proposal's sampled requests the booking also targets (decimal in [0, 1])." }
        contended_impressions: { type: integer, format: int64, minimum: 0, description: "Impressions the booking takes from the proposal's available capacity." }
    MarketplaceSettlementStatus:
      type: string
      description: State of a marketplace settlement row.
      enum: [PENDING, SETTLED, DISPUTED, VOID]
      x-enum-varnames: [MarketplaceSettlementStatusPENDING, MarketplaceSettlementStatusSETTLED, MarketplaceSettlementStatusDISPUTED, MarketplaceSettlementStatusVOID]
    MarketplaceSettlement:
      type: object
      description: What the tenant owes or is owed for one listing and counterparty in a period, from the path receipts (SR-1238b).
      required: [id, tenant_id, period, listing_id, counterparty_tenant_id, currency, gross, fees, net, status]
      properties:
        id: { type: string, format: uuid, description: "Settlement id." }
        tenant_id: { type: string, format: uuid, description: "Owning tenant." }
        period: { type: string, description: "Calendar month (YYYY-MM)." }
        listing_id: { type: string, format: uuid, description: "The marketplace listing." }
        counterparty_tenant_id: { type: string, format: uuid, description: "The other party (listing owner or activating buyer)." }
        currency: { type: string, description: "Settlement currency." }
        gross: { type: string, description: "Signed gross amount (positive = owed to the tenant)." }
        fees:
          type: object
          description: Fee name → decimal amount withheld (marketplace, platform, ...).
          additionalProperties: { type: string }
        net: { type: string, description: "Signed net after fees." }
        status: { $ref: '#/components/schemas/MarketplaceSettlementStatus' }
        receipts: { type: integer, format: int64, minimum: 0, description: "Path receipts folded into the row." }
        created_at: { type: string, format: date-time, description: "Row creation." }
        updated_at: { type: string, format: date-time, description: "Last change." }
    MarketplaceSettlementList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          description: The page of settlements.
          items: { $ref: '#/components/schemas/MarketplaceSettlement' }
        next_cursor: { type: string, description: "Cursor for the next page; absent on the last page." }
    MarketplaceSettlementRunRequest:
      type: object
      required: [period]
      properties:
        period: { type: string, pattern: '^[0-9]{4}-(0[1-9]|1[0-2])$', description: "Calendar month to settle (YYYY-MM)." }
    MarketplaceSettlementRun:
      type: object
      description: Summary of one marketplace settlement run.
      required: [tenant_id, period, status, settlements]
      properties:
        tenant_id: { type: string, format: uuid, description: "Owning tenant." }
        period: { type: string, description: "Calendar month settled (YYYY-MM)." }
        status: { $ref: '#/components/schemas/SettlementRunStatus' }
        settlements: { type: integer, minimum: 0, description: "Settlement rows written or updated by the run." }
        receipts: { type: integer, format: int64, minimum: 0, description: "Path receipts consumed by the run." }
        ran_at: { type: string, format: date-time, nullable: true, description: "When the run finished; null while PENDING / RUNNING." }

    # ---- SR-1239a agent registry / governance ----
    AgentBand:
      type: string
      description: Autonomy ladder band (design 20; proto agent.v1 AutonomyLevel) — RECOMMEND (proposes only), EXECUTE_WITHIN_BAND (acts within caps), CROSS_SURFACE (acts across surfaces), AGENT_TO_AGENT (may delegate to other agents).
      enum: [RECOMMEND, EXECUTE_WITHIN_BAND, CROSS_SURFACE, AGENT_TO_AGENT]
      x-enum-varnames: [AgentBandRECOMMEND, AgentBandEXECUTEWITHINBAND, AgentBandCROSSSURFACE, AgentBandAGENTTOAGENT]
    AgentWriteDefault:
      type: string
      description: The state an agent's newly created deliverable entities start in (SR-1239a governance by default) — DRAFT, PAUSED or ACTIVE (live immediately; requires the identity to be on EXECUTE_WITHIN_BAND or above).
      enum: [DRAFT, PAUSED, ACTIVE]
      x-enum-varnames: [AgentWriteDefaultDRAFT, AgentWriteDefaultPAUSED, AgentWriteDefaultACTIVE]
    AgentKeyAlg:
      type: string
      description: Signature algorithm of an agent identity key.
      enum: [ED25519, ES256]
      x-enum-varnames: [AgentKeyAlgED25519, AgentKeyAlgES256]
    AgentIdentityKey:
      type: object
      description: One public key bound to an agent identity; the private half never leaves the agent.
      required: [id, alg, public_key, created_at]
      properties:
        id: { type: string, format: uuid, description: "Key id (the `kid` the agent signs with)." }
        alg: { $ref: '#/components/schemas/AgentKeyAlg' }
        public_key: { type: string, description: "Base64 (standard, padded) public key bytes." }
        created_at: { type: string, format: date-time, description: "When the key was registered." }
        revoked_at: { type: string, format: date-time, nullable: true, description: "When the key was revoked by a rotation; null while active." }
    AgentIdentityKeyRegister:
      type: object
      required: [alg, public_key]
      properties:
        alg: { $ref: '#/components/schemas/AgentKeyAlg' }
        public_key: { type: string, minLength: 32, maxLength: 512, description: "Base64 (standard, padded) public key bytes." }
    AgentKillSwitch:
      type: object
      description: Kill-switch state of an agent identity.
      required: [tripped_at, tripped_by, reason]
      properties:
        tripped_at: { type: string, format: date-time, description: "When the switch tripped." }
        tripped_by: { type: string, description: "Actor label, or the anomaly alert rule that tripped it automatically." }
        reason: { type: string, description: "Why it tripped." }
        reset_at: { type: string, format: date-time, nullable: true, description: "When it was reset; null while tripped." }
    AgentIdentity:
      type: object
      description: A registered agent principal (SR-1239a agent registry).
      required: [id, tenant_id, name, status, band, write_default, keys, created_at, updated_at]
      properties:
        id: { type: string, format: uuid, description: "Identity id." }
        tenant_id: { type: string, format: uuid, description: "Owning tenant." }
        name: { type: string, description: "Operator-facing name." }
        status: { $ref: '#/components/schemas/LifecycleStatus' }
        band: { $ref: '#/components/schemas/AgentBand' }
        write_default: { $ref: '#/components/schemas/AgentWriteDefault' }
        shared_cap_id: { type: string, format: uuid, nullable: true, description: "Shared spend / action cap the identity draws from (libs/agentgov cap); null = its own cap." }
        keys:
          type: array
          description: Registered keys, newest first (at most one active).
          items: { $ref: '#/components/schemas/AgentIdentityKey' }
        kill_switch:
          nullable: true
          description: Present while the switch is tripped (or was tripped and reset — see reset_at); null when never tripped.
          allOf:
            - $ref: '#/components/schemas/AgentKillSwitch'
        created_at: { type: string, format: date-time, description: "Row creation." }
        updated_at: { type: string, format: date-time, description: "Last change." }
    AgentIdentityCreate:
      type: object
      required: [name]
      properties:
        name: { type: string, minLength: 1, maxLength: 200, description: "Operator-facing name." }
        band:
          description: Initial autonomy band; RECOMMEND when omitted.
          allOf:
            - $ref: '#/components/schemas/AgentBand'
        write_default:
          description: Initial write default; DRAFT when omitted.
          allOf:
            - $ref: '#/components/schemas/AgentWriteDefault'
        shared_cap_id: { type: string, format: uuid, description: "Shared cap to draw from; omitted = its own cap." }
        key:
          description: The agent's first public key; omitted = registered later with rotateAgentIdentityKey (the identity cannot call tools until it has a key).
          allOf:
            - $ref: '#/components/schemas/AgentIdentityKeyRegister'
    AgentIdentityBandSet:
      type: object
      required: [band, reason]
      properties:
        band: { $ref: '#/components/schemas/AgentBand' }
        reason: { type: string, minLength: 1, maxLength: 2000, description: "Recorded in the audit envelope." }
    AgentKillSwitchRequest:
      type: object
      required: [reason]
      properties:
        reason: { type: string, minLength: 1, maxLength: 2000, description: "Recorded in the audit envelope." }
    AgentIdentityList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          description: The page of identities.
          items: { $ref: '#/components/schemas/AgentIdentity' }
        next_cursor: { type: string, description: "Cursor for the next page; absent on the last page." }
    LlmCallLedgerEntry:
      type: object
      description: One metered language-model call (llm_call_ledger).
      required: [id, tenant_id, surface, provider, model, prompt_id, prompt_sha256, input_tokens, output_tokens, cost, currency, created_at]
      properties:
        id: { type: string, format: uuid, description: "Ledger row id." }
        tenant_id: { type: string, format: uuid, description: "Tenant the call was made for." }
        surface: { type: string, description: "The LLM-backed surface that made the call." }
        provider: { type: string, description: "Provider adapter key (libs/llm seam)." }
        model: { type: string, description: "Provider model key the adapter resolved." }
        prompt_id: { type: string, description: "Pinned prompt id." }
        prompt_sha256: { type: string, description: "Hex SHA-256 of the pinned prompt text." }
        input_tokens: { type: integer, format: int64, minimum: 0, description: "Prompt tokens." }
        output_tokens: { type: integer, format: int64, minimum: 0, description: "Completion tokens." }
        cost: { type: string, description: "Decimal metered cost (4 dp)." }
        currency: { type: string, description: "Cost currency." }
        created_at: { type: string, format: date-time, description: "When the call completed." }
    LlmUsageList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          description: The page of ledger rows.
          items: { $ref: '#/components/schemas/LlmCallLedgerEntry' }
        next_cursor: { type: string, description: "Cursor for the next page; absent on the last page." }

    # ---- SR-1239b deals sync / conversions / privacy ----
    DealSyncDirection:
      type: string
      description: PUSH = sent by this tenant to a partner; RECEIVE = received from a partner.
      enum: [PUSH, RECEIVE]
      x-enum-varnames: [DealSyncDirectionPUSH, DealSyncDirectionRECEIVE]
    DealSyncStatus:
      type: string
      description: Delivery state of a deal sync — PENDING, SENT, ACKNOWLEDGED (partner confirmed), RECEIVED (inbound recorded), FAILED.
      enum: [PENDING, SENT, ACKNOWLEDGED, RECEIVED, FAILED]
      x-enum-varnames: [DealSyncStatusPENDING, DealSyncStatusSENT, DealSyncStatusACKNOWLEDGED, DealSyncStatusRECEIVED, DealSyncStatusFAILED]
    DealSync:
      type: object
      description: One deal exchange with a partner (deal_sync row).
      required: [id, tenant_id, deal_id, direction, partner_id, status, external_ref, payload_sha256, created_at]
      properties:
        id: { type: string, format: uuid, description: "Sync row id." }
        tenant_id: { type: string, format: uuid, description: "Owning tenant." }
        deal_id: { type: string, format: uuid, description: "The local deal." }
        direction: { $ref: '#/components/schemas/DealSyncDirection' }
        partner_id: { type: string, format: uuid, description: "The demand partner (PUSH) or the partner API key's principal (RECEIVE)." }
        status: { $ref: '#/components/schemas/DealSyncStatus' }
        external_ref: { type: string, description: "The partner's reference for the deal." }
        payload_sha256: { type: string, description: "Hex SHA-256 of the exchanged payload." }
        error: { type: string, nullable: true, description: "Partner error when FAILED; null otherwise." }
        created_at: { type: string, format: date-time, description: "When the exchange was recorded." }
        updated_at: { type: string, format: date-time, description: "Last status change." }
    DealPushRequest:
      type: object
      required: [partner_id, reason]
      properties:
        partner_id: { type: string, format: uuid, description: "Demand partner to push the deal to; must have a deal endpoint configured." }
        reason: { type: string, minLength: 1, maxLength: 2000, description: "Recorded in the audit envelope." }
    DealInboundRequest:
      type: object
      required: [external_ref, deal]
      properties:
        external_ref: { type: string, minLength: 1, maxLength: 200, description: "The partner's stable reference for the deal; a second push with the same value updates the deal." }
        deal: { $ref: '#/components/schemas/DealCreate' }
    ConversionIngestEvent:
      type: object
      required: [event_name, occurred_at]
      properties:
        event_name: { type: string, minLength: 1, maxLength: 64, description: "Conversion event name as configured on the conversion tag." }
        user_key: { type: string, maxLength: 256, pattern: '^[A-Za-z0-9._:@+-]{1,256}$', description: "Opaque first-party user key (design 33); omitted for unattributable conversions." }
        occurred_at: { type: string, format: date-time, description: "When the conversion happened." }
        value: { type: string, pattern: '^[0-9]+(\.[0-9]{1,4})?$', description: "Decimal conversion value (4 dp); omitted = no value." }
        currency: { type: string, minLength: 3, maxLength: 3, description: "Currency of value; required when value is set." }
        order_ref: { type: string, maxLength: 200, description: "Advertiser-side order / transaction reference used for dedup." }
        attributes:
          type: object
          description: Additional key → value attributes carried on the event.
          additionalProperties: { type: string }
    ConversionIngestBatch:
      type: object
      required: [source, events]
      properties:
        source: { type: string, minLength: 1, maxLength: 64, description: "Where the batch comes from (e.g. crm, warehouse); recorded on the batch." }
        events:
          type: array
          minItems: 1
          maxItems: 1000
          description: The conversion events.
          items: { $ref: '#/components/schemas/ConversionIngestEvent' }
    ConversionIngestError:
      type: object
      required: [index, message]
      properties:
        index: { type: integer, minimum: 0, description: "Position of the rejected event in the batch." }
        message: { type: string, description: "Why it was rejected." }
    ConversionIngestResult:
      type: object
      required: [batch_id, accepted, rejected, sha256]
      properties:
        batch_id: { type: string, format: uuid, description: "The conversion_ingest_batch row." }
        accepted: { type: integer, minimum: 0, description: "Events published." }
        rejected: { type: integer, minimum: 0, description: "Events rejected." }
        sha256: { type: string, description: "Hex SHA-256 of the batch payload (dedup key)." }
        errors:
          type: array
          description: One row per rejected event.
          items: { $ref: '#/components/schemas/ConversionIngestError' }
    PrivacyRequestKind:
      type: string
      description: Data-subject request kind — ACCESS (export what is held), ERASURE (delete), OPT_OUT (stop processing for targeting).
      enum: [ACCESS, ERASURE, OPT_OUT]
      x-enum-varnames: [PrivacyRequestKindACCESS, PrivacyRequestKindERASURE, PrivacyRequestKindOPTOUT]
    PrivacyRequestStatus:
      type: string
      description: Processing state — RECEIVED, IN_PROGRESS, COMPLETED, REJECTED (with the reason in evidence).
      enum: [RECEIVED, IN_PROGRESS, COMPLETED, REJECTED]
      x-enum-varnames: [PrivacyRequestStatusRECEIVED, PrivacyRequestStatusINPROGRESS, PrivacyRequestStatusCOMPLETED, PrivacyRequestStatusREJECTED]
    PrivacyRequest:
      type: object
      description: One data-subject request (privacy_request row).
      required: [id, tenant_id, kind, subject_key_hash, status, requested_at]
      properties:
        id: { type: string, format: uuid, description: "Request id." }
        tenant_id: { type: string, format: uuid, description: "Owning tenant." }
        kind: { $ref: '#/components/schemas/PrivacyRequestKind' }
        subject_key_hash: { type: string, description: "Hex SHA-256 of the subject's user key; the key itself is never stored." }
        status: { $ref: '#/components/schemas/PrivacyRequestStatus' }
        requested_at: { type: string, format: date-time, description: "When the request was filed." }
        completed_at: { type: string, format: date-time, nullable: true, description: "When processing finished; null while open." }
        evidence:
          type: object
          description: What was done, per store (profiles, memberships, events) — step → outcome.
          additionalProperties: true
    PrivacyRequestCreate:
      type: object
      required: [kind, subject_key, reason]
      properties:
        kind: { $ref: '#/components/schemas/PrivacyRequestKind' }
        subject_key: { type: string, minLength: 1, maxLength: 256, pattern: '^[A-Za-z0-9._:@+-]{1,256}$', description: "Opaque first-party user key of the data subject; hashed at rest." }
        reason: { type: string, minLength: 1, maxLength: 2000, description: "Recorded in the audit envelope." }
    PrivacyRequestList:
      type: object
      required: [items]
      properties:
        items:
          type: array
          description: The page of requests.
          items: { $ref: '#/components/schemas/PrivacyRequest' }
        next_cursor: { type: string, description: "Cursor for the next page; absent on the last page." }

    # ---- SR-1234 diagnostics / SR-1240 developer platform ----
    ServingDiagnosis:
      type: object
      description: Structured serving diagnosis for a line item (SR-1234); `diagnosis` and `summary` are what the MCP diagnose_delivery tool returns.
      required: [line_item_ref, serving, diagnosis, summary, as_of]
      properties:
        line_item_ref: { type: string, description: "The line item diagnosed." }
        line_item_id: { type: string, format: uuid, nullable: true, description: "Resolved id; null when the ref matched nothing (diagnosis then says so)." }
        serving: { type: boolean, description: "Whether the line item is currently eligible to serve." }
        diagnosis: { $ref: '#/components/schemas/DiagnoseDeliveryResult' }
        eligibility:
          nullable: true
          description: The deterministic eligibility report (getLineItemEligibility); null when the ref matched nothing.
          allOf:
            - $ref: '#/components/schemas/EligibilityReport'
        summary: { type: string, description: "The diagnosis rendered as text — byte-for-byte the MCP tool's rendering (console \"Copy diagnosis\")." }
        as_of: { type: string, format: date-time, description: "Injected clock instant of the diagnosis." }
    SampleDataLoadResult:
      type: object
      required: [tenant_id, loaded_at, counts]
      properties:
        tenant_id: { type: string, format: uuid, description: "The sandbox tenant." }
        loaded_at: { type: string, format: date-time, description: "When the load completed." }
        counts:
          type: object
          description: Entity kind → rows created (publisher, placement, advertiser, campaign_order, line_item, creative, audience, fee_schedule).
          additionalProperties: { type: integer }
