openapi: 3.0.3
info:
  title: Siyaqi Serve API
  version: 0.1.0
  description: Hot-path ad serve, impression, and click endpoints (Fly `apps/serve`).
servers:
  - url: https://siyaqi.fly.dev
    description: Staging-aligned Fly app
paths:
  /health:
    get:
      operationId: health
      summary: Liveness
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok: { type: boolean }
                  service: { type: string }
  /v1/ads/serve:
    get:
      operationId: serveAd
      summary: Serve a text ad for a placement
      parameters:
        - name: placementId
          in: query
          required: true
          schema: { type: string }
        - name: context
          in: query
          required: false
          schema: { type: string }
      responses:
        "200":
          description: Served creative
          content:
            application/json:
              schema:
                type: object
                properties:
                  ad:
                    $ref: "#/components/schemas/ServedAd"
        "400":
          description: Missing placementId
        "404":
          description: No eligible creatives
  /v1/ads/impression:
    post:
      operationId: recordImpression
      summary: Record an impression event
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ImpressionEventInput"
      responses:
        "200":
          description: Accepted or soft-rejected (fraud)
        "429":
          description: Rate limited
  /v1/ads/click:
    post:
      operationId: recordClick
      summary: Record a click event
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/ClickEventInput"
      responses:
        "200":
          description: Accepted
        "409":
          description: Duplicate click
  /v1/ads/conversion:
    post:
      operationId: recordConversion
      summary: Record a CPA conversion (attributed via trackingId)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [conversionId, trackingId]
              properties:
                conversionId: { type: string }
                trackingId: { type: string }
                campaignId: { type: string }
                valueMinor: { type: number }
      responses:
        "200":
          description: Accepted
        "404":
          description: No attributable click
        "409":
          description: Duplicate conversion
  /v1/admin/warm:
    post:
      operationId: adminWarm
      summary: Warm Redis eligibility (internal)
      parameters:
        - name: x-internal-api-key
          in: header
          required: true
          schema: { type: string }
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                placementIds:
                  type: array
                  items: { type: string }
      responses:
        "200":
          description: Warmed
        "401":
          description: Unauthorized
components:
  schemas:
    ServedAd:
      type: object
      required:
        - campaignId
        - variantId
        - text
        - cta
        - advertiser
        - placementId
        - trackingId
      properties:
        campaignId: { type: string }
        variantId: { type: string }
        text: { type: string }
        cta: { type: string }
        advertiser: { type: string }
        placementId: { type: string }
        trackingId: { type: string }
    ClickEventInput:
      type: object
      required: [trackingId, campaignId, variantId, placementId]
      properties:
        trackingId: { type: string }
        campaignId: { type: string }
        variantId: { type: string }
        placementId: { type: string }
    ImpressionEventInput:
      type: object
      required: [trackingId, campaignId, variantId, placementId]
      properties:
        trackingId: { type: string }
        campaignId: { type: string }
        variantId: { type: string }
        placementId: { type: string }
