openapi: 3.0.3
info:
  title: Meridian Ads API
  version: 1.0.0
  description: |
    Control plane (`api.meridianads.tech`) and adserver (`serve.meridianads.tech`)
    for Meridian Ads from Yelle Software.

    Billing is prepaid USD credits. Campaigns set a CPM; each impression spends
    CPM/1000 USD in integer micro-USD. Clicks do not spend in v1. Hard stop at
    zero. House ads do not debit.

    Live serve indexes only `approved` creatives.
  contact:
    name: Yelle Software
    url: https://yelle.software
servers:
  - url: https://api.meridianads.tech
    description: Production control plane
tags:
  - name: health
  - name: control
  - name: credits
  - name: payouts
  - name: reporting
  - name: admin
  - name: serve
security:
  - bearerAuth: []
paths:
  /health:
    get:
      tags: [health]
      summary: Liveness probe
      security: []
      responses:
        "200":
          description: OK
  /ready:
    get:
      tags: [health]
      summary: Readiness probe
      security: []
      responses:
        "200":
          description: Ready
        "503":
          description: Not ready
  /api/v1/:
    get:
      tags: [control]
      summary: API version
      security: []
      responses:
        "200":
          description: Version info
          content:
            application/json:
              schema:
                type: object
                properties:
                  service: { type: string }
                  env: { type: string }
                  version: { type: string }
  /api/v1/me:
    get:
      tags: [control]
      summary: Current user (Logto JWT) and console properties
      responses:
        "200":
          description: User subject and properties
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Me"
        "401":
          $ref: "#/components/responses/Unauthorized"
    patch:
      tags: [control]
      summary: Update console user properties
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/MePatch"
      responses:
        "200":
          description: Updated user
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Me"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
  /api/v1/org:
    get:
      tags: [control]
      summary: Current org
      responses:
        "200":
          description: Org
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Org"
        "401":
          $ref: "#/components/responses/Unauthorized"
    patch:
      tags: [control]
      summary: Update current org
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/OrgPatch"
      responses:
        "200":
          description: Org
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Org"
  /api/v1/apps:
    get:
      tags: [control]
      summary: List apps
      responses:
        "200":
          description: App list
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/App"
    post:
      tags: [control]
      summary: Create app
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AppCreate"
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/App"
  /api/v1/apps/{appId}:
    parameters:
      - $ref: "#/components/parameters/AppId"
    get:
      tags: [control]
      summary: Get app
      responses:
        "200":
          description: App
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/App"
        "404":
          $ref: "#/components/responses/NotFound"
    patch:
      tags: [control]
      summary: Update app
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AppCreate"
      responses:
        "200":
          description: App
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/App"
    delete:
      tags: [control]
      summary: Delete app
      responses:
        "204":
          description: Deleted
  /api/v1/apps/{appId}/keys:
    parameters:
      - $ref: "#/components/parameters/AppId"
    get:
      tags: [control]
      summary: List SDK keys
      responses:
        "200":
          description: Keys (secrets omitted after create)
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/AppKey"
    post:
      tags: [control]
      summary: Create SDK key
      responses:
        "201":
          description: Created (secret shown once)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AppKeyCreated"
  /api/v1/apps/{appId}/keys/{keyId}:
    delete:
      tags: [control]
      summary: Revoke SDK key
      parameters:
        - $ref: "#/components/parameters/AppId"
        - name: keyId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "204":
          description: Revoked
  /api/v1/apps/{appId}/web-keys:
    parameters:
      - $ref: "#/components/parameters/AppId"
    get:
      tags: [control]
      summary: List web keys
      responses:
        "200":
          description: Web keys (no secret)
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/WebKey"
    post:
      tags: [control]
      summary: Create web key
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WebKeyOrigins"
      responses:
        "201":
          description: Created. The prefix is public and there is no secret.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebKey"
        "400":
          $ref: "#/components/responses/BadRequest"
  /api/v1/apps/{appId}/web-keys/{keyId}:
    parameters:
      - $ref: "#/components/parameters/AppId"
      - $ref: "#/components/parameters/WebKeyId"
    patch:
      tags: [control]
      summary: Replace web key origins
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WebKeyOrigins"
      responses:
        "200":
          description: Updated web key
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/WebKey"
        "400":
          $ref: "#/components/responses/BadRequest"
        "404":
          $ref: "#/components/responses/NotFound"
  /api/v1/apps/{appId}/web-keys/{keyId}/revoke:
    post:
      tags: [control]
      summary: Revoke web key
      parameters:
        - $ref: "#/components/parameters/AppId"
        - $ref: "#/components/parameters/WebKeyId"
      responses:
        "204":
          description: Revoked
        "404":
          $ref: "#/components/responses/NotFound"
  /api/v1/apps/{appId}/placements:
    parameters:
      - $ref: "#/components/parameters/AppId"
    get:
      tags: [control]
      summary: List placements
      responses:
        "200":
          description: Placement list
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Placement"
    post:
      tags: [control]
      summary: Create placement
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PlacementCreate"
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Placement"
  /api/v1/apps/{appId}/placements/{placementId}:
    parameters:
      - $ref: "#/components/parameters/AppId"
      - $ref: "#/components/parameters/PlacementId"
    get:
      tags: [control]
      summary: Get placement
      responses:
        "200":
          description: Placement
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Placement"
        "404":
          $ref: "#/components/responses/NotFound"
    patch:
      tags: [control]
      summary: Update placement
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/PlacementCreate"
      responses:
        "200":
          description: Placement
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Placement"
    delete:
      tags: [control]
      summary: Delete placement
      responses:
        "204":
          description: Deleted
  /api/v1/apps/{appId}/profiles/{userKey}:
    delete:
      tags: [control]
      summary: GDPR / publisher delete of a user profile (Postgres and Redis)
      parameters:
        - $ref: "#/components/parameters/AppId"
        - name: userKey
          in: path
          required: true
          schema:
            type: string
      responses:
        "204":
          description: Deleted
  /api/v1/campaigns:
    get:
      tags: [control]
      summary: List campaigns
      parameters:
        - name: house
          in: query
          schema:
            type: boolean
        - name: active
          in: query
          schema:
            type: boolean
      responses:
        "200":
          description: Campaign list
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Campaign"
    post:
      tags: [control]
      summary: Create campaign
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CampaignCreate"
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Campaign"
  /api/v1/campaigns/{campaignId}:
    parameters:
      - $ref: "#/components/parameters/CampaignId"
    get:
      tags: [control]
      summary: Get campaign
      responses:
        "200":
          description: Campaign
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Campaign"
        "404":
          $ref: "#/components/responses/NotFound"
    patch:
      tags: [control]
      summary: Update campaign
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CampaignCreate"
      responses:
        "200":
          description: Campaign
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Campaign"
  /api/v1/campaigns/{campaignId}/creatives:
    parameters:
      - $ref: "#/components/parameters/CampaignId"
    get:
      tags: [control]
      summary: List creatives
      responses:
        "200":
          description: Creative list
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Creative"
    post:
      tags: [control]
      summary: Create creative (image or native_text)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreativeCreate"
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Creative"
  /api/v1/creatives/{creativeId}:
    parameters:
      - $ref: "#/components/parameters/CreativeId"
    get:
      tags: [control]
      summary: Get creative
      responses:
        "200":
          description: Creative
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Creative"
        "404":
          $ref: "#/components/responses/NotFound"
    patch:
      tags: [control]
      summary: Update creative (material edits requeue moderation)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreativeCreate"
      responses:
        "200":
          description: Creative
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Creative"
  /api/v1/creatives/{creativeId}/submit:
    post:
      tags: [control]
      summary: Submit creative for automated moderation
      parameters:
        - $ref: "#/components/parameters/CreativeId"
      responses:
        "202":
          description: Queued (status pending_review)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Creative"
  /api/v1/creatives/{creativeId}/asset-upload:
    post:
      tags: [control]
      summary: Signed PUT URL for R2 banner upload
      parameters:
        - $ref: "#/components/parameters/CreativeId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AssetUploadRequest"
      responses:
        "200":
          description: Signed upload
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AssetUpload"
  /api/v1/segments:
    get:
      tags: [control]
      summary: List segments
      responses:
        "200":
          description: Segment list
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Segment"
    post:
      tags: [control]
      summary: Create segment
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SegmentCreate"
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Segment"
  /api/v1/segments/{segmentId}:
    parameters:
      - $ref: "#/components/parameters/SegmentId"
    get:
      tags: [control]
      summary: Get segment
      responses:
        "200":
          description: Segment
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Segment"
        "404":
          $ref: "#/components/responses/NotFound"
    patch:
      tags: [control]
      summary: Update segment
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/SegmentCreate"
      responses:
        "200":
          description: Segment
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Segment"
    delete:
      tags: [control]
      summary: Delete segment
      responses:
        "204":
          description: Deleted
  /api/v1/credits:
    get:
      tags: [credits]
      summary: Prepaid credit balance (micro-USD)
      responses:
        "200":
          description: Account
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreditAccount"
  /api/v1/credits/ledger:
    get:
      tags: [credits]
      summary: Ledger entries
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
      responses:
        "200":
          description: Ledger
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/CreditLedgerEntry"
  /api/v1/credits/checkout:
    post:
      tags: [credits]
      summary: Start Stripe checkout for a pack or custom USD amount
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CheckoutRequest"
      responses:
        "200":
          description: Checkout session
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CheckoutSession"
  /api/v1/credits/auto-reload:
    patch:
      tags: [credits]
      summary: Optional auto-reload when balance is under $10
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AutoReloadPatch"
      responses:
        "200":
          description: Account
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CreditAccount"
  /api/v1/payouts/account:
    get:
      tags: [payouts]
      summary: Publisher payout account, held vs available
      description: |
        Held is accruals younger than 7 days (pending, not cashable).
        Available is the ledger sum excluding that hold. Cash-out requires
        available >= $25 and payouts_ready from the Connect recipient capability.
      responses:
        "200":
          description: Account summary
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PayoutAccount"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "501":
          $ref: "#/components/responses/NotImplemented"
  /api/v1/payouts/account/session:
    post:
      tags: [payouts]
      summary: Stripe Connect Account Session for embedded onboarding
      responses:
        "200":
          description: Account Session
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PayoutAccountSession"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "501":
          $ref: "#/components/responses/NotImplemented"
  /api/v1/payouts/cash-out:
    post:
      tags: [payouts]
      summary: On-demand Transfer of available payables
      description: Requires available >= $25 and payouts_ready. Unlimited on-demand rows. Does not suppress the monthly scheduled Transfer.
      responses:
        "200":
          description: Payout
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Payout"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "409":
          description: Transfer already in flight for this org
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "501":
          $ref: "#/components/responses/NotImplemented"
  /api/v1/payouts:
    get:
      tags: [payouts]
      summary: Payout history for the current org
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
      responses:
        "200":
          description: Payouts
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Payout"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "501":
          $ref: "#/components/responses/NotImplemented"
  /api/v1/payouts/ledger:
    get:
      tags: [payouts]
      summary: Publisher payable ledger
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
      responses:
        "200":
          description: Ledger
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/PublisherLedgerEntry"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "501":
          $ref: "#/components/responses/NotImplemented"
  /api/v1/reports/campaigns/{campaignId}:
    get:
      tags: [reporting]
      summary: Hourly impression/click/spend rollups
      parameters:
        - $ref: "#/components/parameters/CampaignId"
        - name: from
          in: query
          required: true
          schema:
            type: string
            format: date-time
        - name: to
          in: query
          required: true
          schema:
            type: string
            format: date-time
      responses:
        "200":
          description: Rollups
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/HourlyRollup"
            text/csv:
              schema:
                type: string
  /api/v1/tokens:
    get:
      tags: [control]
      summary: List API tokens (machine)
      responses:
        "200":
          description: Tokens
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/APIToken"
    post:
      tags: [control]
      summary: Create API token (secret shown once)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/APITokenCreate"
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/APITokenCreated"
  /api/v1/tokens/{tokenId}:
    delete:
      tags: [control]
      summary: Revoke API token
      parameters:
        - name: tokenId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        "204":
          description: Revoked
  /api/v1/admin/platform:
    get:
      tags: [admin]
      summary: Staff platform take, volume, growth, and supply snapshots
      parameters:
        - name: from
          in: query
          required: true
          schema:
            type: string
            format: date-time
        - name: to
          in: query
          required: true
          schema:
            type: string
            format: date-time
      responses:
        "200":
          description: Platform metrics
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PlatformReport"
        "400":
          description: Invalid range
        "403":
          description: Admin required
  /api/v1/admin/creatives:
    get:
      tags: [admin]
      summary: Staff creative queue (all orgs)
      parameters:
        - name: status
          in: query
          schema:
            $ref: "#/components/schemas/CreativeStatus"
          description: Defaults to pending_review
      responses:
        "200":
          description: Creatives
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Creative"
        "403":
          description: Admin required
  /api/v1/admin/creatives/{creativeId}:
    get:
      tags: [admin]
      summary: Staff get creative (any org)
      parameters:
        - $ref: "#/components/parameters/CreativeId"
      responses:
        "200":
          description: Creative
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Creative"
        "403":
          description: Admin required
        "404":
          description: Not found
  /api/v1/admin/creatives/{creativeId}/override:
    post:
      tags: [admin]
      summary: Staff moderation override (audited)
      parameters:
        - $ref: "#/components/parameters/CreativeId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ModerationOverrideCreate"
      responses:
        "200":
          description: Creative after override
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Creative"
        "403":
          description: Admin required
  /api/v1/admin/creatives/{creativeId}/moderation:
    get:
      tags: [admin]
      summary: Latest moderation decision and overrides
      parameters:
        - $ref: "#/components/parameters/CreativeId"
      responses:
        "200":
          description: Audit
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ModerationAudit"
  /api/v1/admin/house-campaigns:
    get:
      tags: [admin]
      summary: First-party house campaigns (fallback only)
      responses:
        "200":
          description: House campaigns
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Campaign"
  /api/v1/reports/inventory:
    get:
      tags: [reporting]
      summary: Publisher inventory rollups including payable_micro
      parameters:
        - name: from
          in: query
          required: true
          schema:
            type: string
            format: date-time
        - name: to
          in: query
          required: true
          schema:
            type: string
            format: date-time
      responses:
        "200":
          description: Inventory rollups
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/InventoryRow"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "501":
          $ref: "#/components/responses/NotImplemented"
  /api/v1/admin/payouts:
    get:
      tags: [admin]
      summary: Staff list of payouts
      parameters:
        - name: org_id
          in: query
          schema:
            type: string
            format: uuid
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
      responses:
        "200":
          description: Payouts
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Payout"
        "403":
          description: Admin required
        "501":
          $ref: "#/components/responses/NotImplemented"
  /api/v1/admin/payouts/{payoutId}/retry:
    post:
      tags: [admin]
      summary: Staff retry a failed Transfer
      parameters:
        - $ref: "#/components/parameters/PayoutId"
      responses:
        "200":
          description: Payout
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Payout"
        "403":
          description: Admin required
        "404":
          $ref: "#/components/responses/NotFound"
        "501":
          $ref: "#/components/responses/NotImplemented"
  /api/v1/admin/payouts/adjust:
    post:
      tags: [admin]
      summary: Staff clawback or credit on the publisher ledger
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/AdminPayoutAdjust"
      responses:
        "200":
          description: Ledger entry
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PublisherLedgerEntry"
        "400":
          $ref: "#/components/responses/BadRequest"
        "403":
          description: Admin required
        "501":
          $ref: "#/components/responses/NotImplemented"
  /v1/decision:
    post:
      tags: [serve]
      summary: Request an ad
      description: |
        One placement. Authenticate with HMAC headers or `X-Web-Key`, not both.
        Empty inventory is HTTP 204.
      servers:
        - url: https://serve.meridianads.tech
          description: Production adserver
      security:
        - hmacAuth: []
        - webKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DecisionRequest"
      responses:
        "200":
          description: Filled (paid or house)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Decision"
        "204":
          description: No fill (no paid or house creative eligible)
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          description: More than 100 decisions per second for this app_key
  /v1/decisions:
    post:
      tags: [serve]
      summary: Request ads for multiple placements
      description: |
        One result per requested placement, in order, including repeated names.
        Authenticate with HMAC headers or `X-Web-Key`, not both.
        A campaign is repeated in one response only when it is the only campaign that can still fill.
      servers:
        - url: https://serve.meridianads.tech
          description: Production adserver
      security:
        - hmacAuth: []
        - webKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/DecisionBatchRequest"
      responses:
        "200":
          description: |
            Accepted, including when every slot is empty. `fills` matches `placements` in length and order.
            An unknown placement name is null for that slot.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/DecisionBatch"
        "400":
          $ref: "#/components/responses/BadRequest"
        "401":
          $ref: "#/components/responses/Unauthorized"
        "429":
          description: |
            More than 100 decisions per second for this credential prefix.
            Each placement counts as one decision. An HMAC key and a web key on the same app
            have separate counters. Nothing is reserved when this call would pass the limit.
  /v1/events:
    post:
      tags: [serve]
      summary: Fire impression on render (not IAB viewability)
      description: |
        Authenticate with HMAC headers or `X-Web-Key`, not both.
        A body with `token` is recorded with 204. A body with `tokens` (1 to 20) and event `impression` returns 200.
        Sending both `token` and `tokens` is 400.
      servers:
        - url: https://serve.meridianads.tech
          description: Production adserver
      security:
        - hmacAuth: []
        - webKeyAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ServeEvent"
      responses:
        "200":
          description: |
            Batch results, one per token, in order. One bad token does not reject the others.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/ServeEventBatchResult"
        "204":
          description: Recorded (idempotent for the same impression token)
        "400":
          description: |
            Invalid request, or a single-token body whose impression token is invalid or expired.
            Sending both `token` and `tokens`, an empty `tokens` list, or more than 20 tokens is 400.
            A batch reports a bad token in `results` instead of failing the call.
        "401":
          $ref: "#/components/responses/Unauthorized"
        "403":
          description: |
            Single-token body: authenticating app id does not match impression token aid.
            A batch reports that mismatch in `results`.
        "429":
          description: |
            More than 100 events per second for this credential prefix.
            Each token counts. A batch that would pass the limit returns 429 and confirms nothing.
  /v1/click/{token}:
    get:
      tags: [serve]
      summary: Record click once and 302 to landing URL
      servers:
        - url: https://serve.meridianads.tech
          description: Production adserver
      security: []
      parameters:
        - name: token
          in: path
          required: true
          schema:
            type: string
          description: HMAC click token bound to a prior impression
      responses:
        "302":
          description: |
            Redirect to landing URL with {click_id}, {campaign_id}, {placement_id} expanded.
            user_key is never expanded. Click tokens are one-time; replays still 302 but do not
            double-record (Redis SETNX + unique click_facts.token_id).
          headers:
            Location:
              schema:
                type: string
                format: uri
        "400":
          description: Invalid, expired, or unbound click token
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
      description: Logto access token for the control plane.
    hmacAuth:
      type: apiKey
      in: header
      name: X-Signature
      description: |
        HMAC-SHA256 hex digest of `{unix_timestamp}.{METHOD}.{path}.{raw_body}`.
        Also send `X-App-Key` (public prefix) and `X-Timestamp` (unix seconds).
        Mutually exclusive with `X-Web-Key`. Sending a web key with any HMAC header is 401.
    webKeyAuth:
      type: apiKey
      in: header
      name: X-Web-Key
      description: |
        Public web key prefix (`mw_live_` plus 16 hex characters). There is no secret.
        Do not send `X-App-Key`, `X-Timestamp`, or `X-Signature`. Sending both is 401.
        The browser `Origin` must exactly equal one stored origin: scheme, host, and port.
        `https://example.com` does not match `https://www.example.com` or `http://example.com`.
        No wildcards. A missing or non-matching `Origin`, or an unknown or revoked prefix, is 401.
  parameters:
    AppId:
      name: appId
      in: path
      required: true
      schema:
        type: string
        format: uuid
    WebKeyId:
      name: keyId
      in: path
      required: true
      schema:
        type: string
        format: uuid
    PlacementId:
      name: placementId
      in: path
      required: true
      schema:
        type: string
        format: uuid
    CampaignId:
      name: campaignId
      in: path
      required: true
      schema:
        type: string
        format: uuid
    CreativeId:
      name: creativeId
      in: path
      required: true
      schema:
        type: string
        format: uuid
    SegmentId:
      name: segmentId
      in: path
      required: true
      schema:
        type: string
        format: uuid
    PayoutId:
      name: payoutId
      in: path
      required: true
      schema:
        type: string
        format: uuid
  responses:
    Unauthorized:
      description: Unauthorized
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    NotFound:
      description: Not found
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    BadRequest:
      description: Bad request
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
    NotImplemented:
      description: Handler not wired yet
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Error"
  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: string
    MicroUSD:
      type: integer
      format: int64
      description: Integer millionths of a US dollar. $1.00 = 1000000.
    OrgRole:
      type: string
      enum: [advertiser, publisher, both]
    IntendedFeatures:
      type: string
      enum: [advertiser, publisher, both]
      description: Console onboarding intent. UI preference only.
    ConsoleUserProperties:
      type: object
      properties:
        onboarding_completed_at:
          type: string
          format: date-time
        intended_features:
          $ref: "#/components/schemas/IntendedFeatures"
    Me:
      type: object
      required: [sub, properties]
      properties:
        sub:
          type: string
        properties:
          $ref: "#/components/schemas/ConsoleUserProperties"
    MePatch:
      type: object
      required: [properties]
      properties:
        properties:
          type: object
          required: [intended_features]
          properties:
            intended_features:
              $ref: "#/components/schemas/IntendedFeatures"
    Org:
      type: object
      required: [id, logto_org_id, name, role, house]
      properties:
        id:
          type: string
          format: uuid
        logto_org_id:
          type: string
        owner_sub:
          type: string
          description: Logto user id that owns this personal tenant. Empty on the house org.
        name:
          type: string
        role:
          $ref: "#/components/schemas/OrgRole"
        house:
          type: boolean
          description: First-party Yelle Software / Meridian Ads org
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    OrgPatch:
      type: object
      properties:
        name:
          type: string
        role:
          $ref: "#/components/schemas/OrgRole"
    App:
      type: object
      required: [id, org_id, name]
      properties:
        id:
          type: string
          format: uuid
        org_id:
          type: string
          format: uuid
        name:
          type: string
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    AppCreate:
      type: object
      required: [name]
      properties:
        name:
          type: string
    KeyEnv:
      type: string
      enum: [production]
    AppKey:
      type: object
      required: [id, app_id, prefix, env]
      properties:
        id:
          type: string
          format: uuid
        app_id:
          type: string
          format: uuid
        prefix:
          type: string
        env:
          $ref: "#/components/schemas/KeyEnv"
        created_at:
          type: string
          format: date-time
        revoked_at:
          type: string
          format: date-time
          nullable: true
    AppKeyCreated:
      allOf:
        - $ref: "#/components/schemas/AppKey"
        - type: object
          required: [secret]
          properties:
            secret:
              type: string
              description: HMAC-SHA256 secret, shown once
    WebKeyOrigins:
      type: object
      required: [origins]
      properties:
        origins:
          type: array
          minItems: 1
          items:
            type: string
          description: |
            Exact browser Origin values. Scheme, host, and port must all match.
            `https://example.com` does not match `https://www.example.com` or `http://example.com`.
            No wildcards. At least one origin is required.
    WebKey:
      type: object
      required: [id, app_id, prefix, origins, created_at, revoked_at]
      properties:
        id:
          type: string
          format: uuid
        app_id:
          type: string
          format: uuid
        prefix:
          type: string
          description: Public `mw_live_` prefix plus 16 hex characters. There is no secret.
        origins:
          type: array
          minItems: 1
          items:
            type: string
          description: |
            Exact Origin allowlist. Scheme, host, and port must all match.
            No wildcards.
        created_at:
          type: string
          format: date-time
        revoked_at:
          type: string
          format: date-time
          nullable: true
    CreativeFormat:
      type: string
      enum: [image, native_text]
    Placement:
      type: object
      required: [id, app_id, name, format]
      properties:
        id:
          type: string
          format: uuid
        app_id:
          type: string
          format: uuid
        name:
          type: string
        format:
          $ref: "#/components/schemas/CreativeFormat"
        width:
          type: integer
        height:
          type: integer
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    PlacementCreate:
      type: object
      required: [name, format]
      properties:
        name:
          type: string
        format:
          $ref: "#/components/schemas/CreativeFormat"
        width:
          type: integer
        height:
          type: integer
    PacingMode:
      type: string
      enum: [even, asap]
    Campaign:
      type: object
      required: [id, org_id, name, house, cpm, priority, weight, frequency_cap, flight_start, active]
      properties:
        id:
          type: string
          format: uuid
        org_id:
          type: string
          format: uuid
        name:
          type: string
        house:
          type: boolean
          description: Fallback-only first-party campaign; does not compete with paid or debit credits
        cpm:
          $ref: "#/components/schemas/MicroUSD"
        daily_budget:
          $ref: "#/components/schemas/MicroUSD"
        total_budget:
          $ref: "#/components/schemas/MicroUSD"
        spent_total:
          $ref: "#/components/schemas/MicroUSD"
        pacing:
          $ref: "#/components/schemas/PacingMode"
        priority:
          type: integer
        weight:
          type: integer
          minimum: 0
        segment_id:
          type: string
          format: uuid
          nullable: true
        frequency_cap:
          type: integer
          description: Per user_key per UTC day. Default 3.
          default: 3
        flight_start:
          type: string
          format: date-time
        flight_end:
          type: string
          format: date-time
          nullable: true
        active:
          type: boolean
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    CampaignCreate:
      type: object
      required: [name, cpm, flight_start]
      properties:
        name:
          type: string
        house:
          type: boolean
        cpm:
          $ref: "#/components/schemas/MicroUSD"
        daily_budget:
          $ref: "#/components/schemas/MicroUSD"
        total_budget:
          $ref: "#/components/schemas/MicroUSD"
        pacing:
          $ref: "#/components/schemas/PacingMode"
        priority:
          type: integer
        weight:
          type: integer
        segment_id:
          type: string
          format: uuid
          nullable: true
        frequency_cap:
          type: integer
          default: 3
        flight_start:
          type: string
          format: date-time
        flight_end:
          type: string
          format: date-time
          nullable: true
        active:
          type: boolean
    CreativeStatus:
      type: string
      enum: [draft, pending_review, approved, rejected]
    ImageAsset:
      type: object
      required: [object_key, alt, mime]
      properties:
        object_key:
          type: string
        cdn_url:
          type: string
          format: uri
        alt:
          type: string
        width:
          type: integer
        height:
          type: integer
        mime:
          type: string
          enum: [image/jpeg, image/png, image/webp]
        bytes:
          type: integer
          maximum: 2097152
    NativeCopy:
      type: object
      required: [headline]
      properties:
        headline:
          type: string
          maxLength: 80
        body:
          type: string
          maxLength: 140
        sponsor:
          type: string
          maxLength: 40
        cta:
          type: string
          maxLength: 20
    Creative:
      type: object
      required: [id, campaign_id, format, status, destination_url]
      properties:
        id:
          type: string
          format: uuid
        campaign_id:
          type: string
          format: uuid
        format:
          $ref: "#/components/schemas/CreativeFormat"
        status:
          $ref: "#/components/schemas/CreativeStatus"
        weight:
          type: integer
        destination_url:
          type: string
          format: uri
        image:
          $ref: "#/components/schemas/ImageAsset"
        native:
          $ref: "#/components/schemas/NativeCopy"
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    CreativeCreate:
      type: object
      required: [format, destination_url]
      properties:
        format:
          $ref: "#/components/schemas/CreativeFormat"
        weight:
          type: integer
        destination_url:
          type: string
          format: uri
        image:
          $ref: "#/components/schemas/ImageAsset"
        native:
          $ref: "#/components/schemas/NativeCopy"
    AssetUploadRequest:
      type: object
      required: [mime, bytes]
      properties:
        mime:
          type: string
          enum: [image/jpeg, image/png, image/webp]
        bytes:
          type: integer
          maximum: 2097152
        filename:
          type: string
    AssetUpload:
      type: object
      required: [method, url, object_key]
      properties:
        method:
          type: string
          enum: [PUT]
        url:
          type: string
          format: uri
        object_key:
          type: string
        headers:
          type: object
          additionalProperties:
            type: string
        cdn_url:
          type: string
          format: uri
    RuleOp:
      type: string
      enum: [eq, in, range, exists]
    BoolOp:
      type: string
      enum: [and, or]
    TraitRule:
      type: object
      required: [trait, op]
      properties:
        trait:
          type: string
        op:
          $ref: "#/components/schemas/RuleOp"
        value: {}
        values:
          type: array
          items: {}
        min:
          type: number
        max:
          type: number
    RuleNode:
      type: object
      description: Nested AND/OR trait tree. Missing traits fail closed.
      properties:
        op:
          $ref: "#/components/schemas/BoolOp"
        rule:
          $ref: "#/components/schemas/TraitRule"
        children:
          type: array
          items:
            $ref: "#/components/schemas/RuleNode"
    Segment:
      type: object
      required: [id, org_id, name, rules]
      properties:
        id:
          type: string
          format: uuid
        org_id:
          type: string
          format: uuid
        name:
          type: string
        rules:
          $ref: "#/components/schemas/RuleNode"
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    SegmentCreate:
      type: object
      required: [name, rules]
      properties:
        name:
          type: string
        rules:
          $ref: "#/components/schemas/RuleNode"
    CreditAccount:
      type: object
      required: [org_id, balance]
      properties:
        org_id:
          type: string
          format: uuid
        balance:
          $ref: "#/components/schemas/MicroUSD"
        auto_reload:
          type: boolean
        auto_reload_threshold:
          $ref: "#/components/schemas/MicroUSD"
        auto_reload_amount:
          $ref: "#/components/schemas/MicroUSD"
        updated_at:
          type: string
          format: date-time
    LedgerEntryType:
      type: string
      enum: [purchase, impression_debit, auto_reload, adjust]
    CreditLedgerEntry:
      type: object
      required: [id, org_id, type, amount, created_at]
      properties:
        id:
          type: string
          format: uuid
        org_id:
          type: string
          format: uuid
        type:
          $ref: "#/components/schemas/LedgerEntryType"
        amount:
          $ref: "#/components/schemas/MicroUSD"
        campaign_id:
          type: string
          format: uuid
          nullable: true
        impression_id:
          type: string
          format: uuid
          nullable: true
        stripe_event_id:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
    CheckoutRequest:
      type: object
      properties:
        pack_usd:
          type: integer
          enum: [25, 100, 500]
          description: Prepaid pack. Omit when using custom amount.
        amount_usd:
          type: number
          minimum: 0
          exclusiveMinimum: true
          description: Custom USD amount when pack_usd is omitted.
        return_path:
          type: string
          enum: ["/onboarding", "/credits"]
          description: Optional relative path Stripe Checkout returns to. Default is /credits.
    CheckoutSession:
      type: object
      required: [url]
      properties:
        url:
          type: string
          format: uri
    AutoReloadPatch:
      type: object
      required: [enabled]
      properties:
        enabled:
          type: boolean
        threshold_usd:
          type: number
          default: 10
        amount_usd:
          type: number
    FormatCounts:
      type: object
      required: [image, native_text]
      properties:
        image:
          type: integer
          format: int64
        native_text:
          type: integer
          format: int64
    PlatformRevenue:
      type: object
      properties:
        gmv_micro:
          $ref: "#/components/schemas/MicroUSD"
        publisher_share_micro:
          $ref: "#/components/schemas/MicroUSD"
        take_micro:
          $ref: "#/components/schemas/MicroUSD"
        lifetime_gmv_micro:
          $ref: "#/components/schemas/MicroUSD"
        lifetime_publisher_share_micro:
          $ref: "#/components/schemas/MicroUSD"
        lifetime_take_micro:
          $ref: "#/components/schemas/MicroUSD"
    PlatformDay:
      type: object
      properties:
        day_utc:
          type: string
          format: date
        impressions:
          type: integer
          format: int64
        paid_impressions:
          type: integer
          format: int64
        house_impressions:
          type: integer
          format: int64
        clicks:
          type: integer
          format: int64
        gmv_micro:
          $ref: "#/components/schemas/MicroUSD"
        publisher_share_micro:
          $ref: "#/components/schemas/MicroUSD"
        take_micro:
          $ref: "#/components/schemas/MicroUSD"
        orgs_created:
          type: integer
          format: int64
        apps_created:
          type: integer
          format: int64
    PlatformPipeline:
      type: object
      required: [pending_review, approved_not_live, rejected]
      properties:
        pending_review:
          $ref: "#/components/schemas/FormatCounts"
        approved_not_live:
          $ref: "#/components/schemas/FormatCounts"
        rejected:
          $ref: "#/components/schemas/FormatCounts"
    PlatformTotals:
      type: object
      properties:
        orgs:
          type: integer
          format: int64
        apps:
          type: integer
          format: int64
        placements:
          type: integer
          format: int64
    PlatformSupply:
      type: object
      properties:
        placements:
          $ref: "#/components/schemas/FormatCounts"
        live_creatives:
          $ref: "#/components/schemas/FormatCounts"
        apps:
          $ref: "#/components/schemas/FormatCounts"
        pipeline:
          $ref: "#/components/schemas/PlatformPipeline"
        totals:
          $ref: "#/components/schemas/PlatformTotals"
    PlatformReport:
      type: object
      properties:
        from:
          type: string
          format: date-time
        to:
          type: string
          format: date-time
        revenue:
          $ref: "#/components/schemas/PlatformRevenue"
        series:
          type: array
          items:
            $ref: "#/components/schemas/PlatformDay"
        supply:
          $ref: "#/components/schemas/PlatformSupply"
    HourlyRollup:
      type: object
      properties:
        hour_utc:
          type: string
          format: date-time
        org_id:
          type: string
          format: uuid
        campaign_id:
          type: string
          format: uuid
        creative_id:
          type: string
          format: uuid
        placement_id:
          type: string
          format: uuid
        app_id:
          type: string
          format: uuid
        impressions:
          type: integer
          format: int64
        clicks:
          type: integer
          format: int64
        spend:
          $ref: "#/components/schemas/MicroUSD"
        house:
          type: boolean
    APIToken:
      type: object
      required: [id, org_id, name, prefix]
      properties:
        id:
          type: string
          format: uuid
        org_id:
          type: string
          format: uuid
        name:
          type: string
        prefix:
          type: string
        created_at:
          type: string
          format: date-time
        last_used_at:
          type: string
          format: date-time
          nullable: true
        revoked_at:
          type: string
          format: date-time
          nullable: true
    APITokenCreate:
      type: object
      required: [name]
      properties:
        name:
          type: string
    APITokenCreated:
      allOf:
        - $ref: "#/components/schemas/APIToken"
        - type: object
          required: [secret]
          properties:
            secret:
              type: string
    ModerationOverrideCreate:
      type: object
      required: [to_status, reason]
      properties:
        to_status:
          $ref: "#/components/schemas/CreativeStatus"
        reason:
          type: string
        policy_version:
          type: string
    ModerationDecision:
      type: object
      properties:
        id:
          type: string
          format: uuid
        creative_id:
          type: string
          format: uuid
        model:
          type: string
        policy_version:
          type: string
        policy_pass:
          type: boolean
        quality_pass:
          type: boolean
        landing_match_pass:
          type: boolean
        confidence:
          type: number
        reasons:
          type: array
          items:
            type: string
        policy_codes:
          type: array
          items:
            type: string
        created_at:
          type: string
          format: date-time
    ModerationAudit:
      type: object
      properties:
        latest:
          $ref: "#/components/schemas/ModerationDecision"
        overrides:
          type: array
          items:
            $ref: "#/components/schemas/ModerationOverrideCreate"
    DecisionRequest:
      type: object
      required: [placement, user_key]
      properties:
        placement:
          type: string
          description: Placement name on the authenticated app
        user_key:
          type: string
          description: Opaque publisher identifier. Hashed at rest if it looks like email.
        context:
          type: object
          additionalProperties: true
          description: Trait map merged into the profile (last-write-wins). Max 8KB JSON.
    DecisionBatchRequest:
      type: object
      required: [placements, user_key]
      properties:
        placements:
          type: array
          minItems: 1
          maxItems: 20
          items:
            type: string
          description: |
            Placement names in order. A repeated name is a separate slot.
            Zero names, or more than 20, is 400.
        user_key:
          type: string
          minLength: 1
          description: Opaque publisher identifier. Empty is 400.
        context:
          type: object
          additionalProperties: true
          description: |
            Optional trait map merged once for this call (last-write-wins). Over 8KB is 400.
    DecisionBatch:
      type: object
      required: [fills]
      properties:
        fills:
          type: array
          items:
            $ref: "#/components/schemas/DecisionFill"
          description: |
            Same length and order as `placements`. A campaign is repeated in one response
            only when it is the only campaign that can still fill.
    DecisionFill:
      type: object
      required: [placement, fill]
      properties:
        placement:
          type: string
        fill:
          nullable: true
          allOf:
            - $ref: "#/components/schemas/Decision"
          description: Decision when the slot fills, or null when it does not.
    DecisionCreative:
      type: object
      required: [id, format, click_url, impression_token]
      properties:
        id:
          type: string
          format: uuid
        format:
          $ref: "#/components/schemas/CreativeFormat"
        image:
          $ref: "#/components/schemas/ImageAsset"
        native:
          $ref: "#/components/schemas/NativeCopy"
        click_url:
          type: string
          format: uri
          description: https://serve.meridianads.tech/v1/click/{token}
        impression_token:
          type: string
    Decision:
      type: object
      required: [campaign_id, creative, house]
      properties:
        campaign_id:
          type: string
          format: uuid
        creative:
          $ref: "#/components/schemas/DecisionCreative"
        house:
          type: boolean
    ServeEvent:
      description: |
        Send `token` or `tokens`, not both. Sending both is 400.
        A single `token` succeeds with 204. `tokens` (1 to 20) succeeds with 200.
      oneOf:
        - $ref: "#/components/schemas/ServeEventSingle"
        - $ref: "#/components/schemas/ServeEventBatch"
    ServeEventSingle:
      type: object
      additionalProperties: false
      required: [token]
      properties:
        token:
          type: string
          description: Signed impression token from the decision
        event:
          type: string
          enum: [impression]
          default: impression
    ServeEventBatch:
      type: object
      additionalProperties: false
      required: [tokens, event]
      properties:
        tokens:
          type: array
          minItems: 1
          maxItems: 20
          items:
            type: string
          description: Signed impression tokens. Empty, or more than 20, is 400.
        event:
          type: string
          enum: [impression]
    ServeEventResult:
      type: object
      required: [ok]
      properties:
        ok:
          type: boolean
        error:
          type: string
          description: |
            Set when `ok` is false. For example `invalid or expired impression token`
            or `app key does not match impression token`.
    ServeEventBatchResult:
      type: object
      required: [results]
      properties:
        results:
          type: array
          items:
            $ref: "#/components/schemas/ServeEventResult"
          description: Same order as `tokens`.
    PublisherLedgerType:
      type: string
      enum: [accrual, payout, reversal, adjust]
    PayoutKind:
      type: string
      enum: [scheduled, on_demand]
    PayoutStatus:
      type: string
      enum: [pending, transferring, paid, failed, canceled]
    PayoutAccount:
      type: object
      required: [org_id, payouts_ready, held, available, minimum]
      properties:
        org_id:
          type: string
          format: uuid
        stripe_account_id:
          type: string
        payouts_ready:
          type: boolean
          description: True when the recipient stripe_transfers capability is active.
        held:
          $ref: "#/components/schemas/MicroUSD"
          description: Accruals younger than 7 days. Pending, not cashable.
        available:
          $ref: "#/components/schemas/MicroUSD"
          description: Ledger sum excluding held accruals.
        minimum:
          $ref: "#/components/schemas/MicroUSD"
          description: Cash-out and scheduled Transfer floor ($25).
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    PayoutAccountSession:
      type: object
      required: [client_secret]
      properties:
        client_secret:
          type: string
        expires_at:
          type: string
          format: date-time
    Payout:
      type: object
      required: [id, org_id, kind, amount, status, created_at]
      properties:
        id:
          type: string
          format: uuid
        org_id:
          type: string
          format: uuid
        kind:
          $ref: "#/components/schemas/PayoutKind"
        scheduled_month:
          type: string
          format: date
          nullable: true
          description: First UTC day of the month for scheduled Transfers. Null for on-demand.
        amount:
          $ref: "#/components/schemas/MicroUSD"
        status:
          $ref: "#/components/schemas/PayoutStatus"
        stripe_transfer_id:
          type: string
          nullable: true
        failure_reason:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    PublisherLedgerEntry:
      type: object
      required: [id, org_id, type, amount, created_at]
      properties:
        id:
          type: string
          format: uuid
        org_id:
          type: string
          format: uuid
        type:
          $ref: "#/components/schemas/PublisherLedgerType"
        amount:
          $ref: "#/components/schemas/MicroUSD"
          description: Signed micro-USD. Accruals are positive; payouts are negative.
        impression_id:
          type: string
          format: uuid
          nullable: true
        payout_id:
          type: string
          format: uuid
          nullable: true
        stripe_transfer_id:
          type: string
          nullable: true
        created_at:
          type: string
          format: date-time
    InventoryRow:
      type: object
      properties:
        hour_utc:
          type: string
          format: date-time
        org_id:
          type: string
          format: uuid
        app_id:
          type: string
          format: uuid
        placement_id:
          type: string
          format: uuid
        campaign_id:
          type: string
          format: uuid
        creative_id:
          type: string
          format: uuid
        impressions:
          type: integer
          format: int64
        clicks:
          type: integer
          format: int64
        spend:
          $ref: "#/components/schemas/MicroUSD"
        payable:
          $ref: "#/components/schemas/MicroUSD"
        house:
          type: boolean
    AdminPayoutAdjust:
      type: object
      required: [org_id, amount, reason]
      properties:
        org_id:
          type: string
          format: uuid
        amount:
          $ref: "#/components/schemas/MicroUSD"
          description: Signed micro-USD clawback or credit.
        reason:
          type: string

