openapi: 3.1.0
info:
  title: RELO Platform API
  version: 3.1.0
  description: |
    # RELO Integration Platform

    RELO is a **performance marketing attribution platform** that ingests billions of events, resolves cross-device identity, scores fraud in real-time, and activates audiences across DSPs.

    This documentation covers every integration endpoint: from a simple JS pixel snippet to full server-to-server postback flows.

    ---

    ## Platform Architecture

    RELO is built as a **3-tier distributed system** designed for sub-50ms latency, zero data loss, and enterprise-grade reliability.

    ### Overview

    ```
    ┌──────────────────────────────────────────────────────────────────┐
    │                        INTERNET / AD PLATFORMS                    │
    │                                                                  │
    │   User clicks ad    Browser loads page    MMP fires postback     │
    │        │                   │                     │               │
    │        ▼                   ▼                     ▼               │
    ├──────────────────────────────────────────────────────────────────┤
    │              TIER 1 — EDGE NETWORK (Global, 300+ PoPs)           │
    │                                                                  │
    │  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐           │
    │  │ Click Wrapper│  │  Web Pixel   │  │  S2S Proxy   │           │
    │  │ t.relo.mx    │  │  p.relo.mx   │  │ s2s.relo.mx  │           │
    │  │              │  │              │  │              │           │
    │  │ • ULID gen   │  │ • 143-line   │  │ • HMAC verify│           │
    │  │ • Cookie set │  │   JS loader  │  │ • TLS term   │           │
    │  │ • 302 redir  │  │ • Beacon API │  │ • DDoS shield│           │
    │  │ • <50ms P99  │  │ • Fingerprint│  │ • Rate limit │           │
    │  └──────┬───────┘  └──────┬───────┘  └──────┬───────┘           │
    │         │                 │                  │                   │
    │         └────────┬────────┴──────────────────┘                   │
    │                  │                                               │
    │    3-TIER FAILOVER: Primary → Standby VPS → Object Store Buffer │
    │                  │                                               │
    ├──────────────────┼───────────────────────────────────────────────┤
    │          TIER 2 — PROCESSING ENGINE (Dedicated Server)           │
    │                  │                                               │
    │                  ▼                                               │
    │  ┌───────────────────────────────────────────────────────────┐   │
    │  │              GO INGEST SERVICE (ingest.relo.mx)           │   │
    │  │                                                           │   │
    │  │  ┌─────────┐ ┌──────────┐ ┌─────────┐ ┌──────────────┐  │   │
    │  │  │ Event   │ │ Identity │ │ Fraud   │ │ Attribution  │  │   │
    │  │  │ Parser  │ │ Resolver │ │ Scorer  │ │ Matcher      │  │   │
    │  │  └────┬────┘ └────┬─────┘ └────┬────┘ └──────┬───────┘  │   │
    │  │       │           │            │             │           │   │
    │  │       ▼           ▼            ▼             ▼           │   │
    │  │  ┌──────────────────────────────────────────────────┐    │   │
    │  │  │            BATCH WRITER (async, non-blocking)    │    │   │
    │  │  │  Flush: every 1,000 events OR every 1 second     │    │   │
    │  │  └──────────────┬──────────────┬────────────────────┘    │   │
    │  │                 │              │                          │   │
    │  └─────────────────┼──────────────┼──────────────────────────┘  │
    │                    │              │                              │
    │    ┌───────────────┼──────────────┼────────────────────┐        │
    │    │               ▼              ▼                    │        │
    │    │  ┌──────────────┐  ┌──────────────────┐          │        │
    │    │  │  Analytics DB │  │ Identity Cache   │          │        │
    │    │  │              │  │                  │          │        │
    │    │  │ • Columnar   │  │ • Sub-ms lookups │          │        │
    │    │  │ • 8x compress│  │ • 90-day TTL     │          │        │
    │    │  │ • 24mo TTL   │  │ • Device graph   │          │        │
    │    │  │ • 35-field   │  │ • Fraud cache    │          │        │
    │    │  │   schema     │  │ • RT counters    │          │        │
    │    │  └──────┬───────┘  └──────────────────┘          │        │
    │    │         │                                        │        │
    │    │    Materialized Views                             │        │
    │    │    (auto-refresh every 5 min)                     │        │
    │    │         │                                        │        │
    │    └─────────┼────────────────────────────────────────┘        │
    │              │                                                  │
    ├──────────────┼──────────────────────────────────────────────────┤
    │      TIER 3 — PLATFORM & STORAGE                                │
    │              │                                                  │
    │              ▼                                                  │
    │  ┌──────────────┐  ┌──────────────┐  ┌──────────────┐          │
    │  │  Platform DB  │  │ Object Store │  │  DSP Export  │          │
    │  │              │  │              │  │              │          │
    │  │ • 50+ tables │  │ • Data lake  │  │ • Meta CAPI  │          │
    │  │ • Aggregates │  │ • CSV archive│  │ • Google Ads │          │
    │  │ • Configs    │  │ • Backups    │  │ • TikTok API │          │
    │  │ • Payments   │  │ • 11-9s SLA  │  │ • DV360      │          │
    │  └──────────────┘  └──────────────┘  └──────────────┘          │
    │                                                                  │
    └──────────────────────────────────────────────────────────────────┘
    ```

    ### High Availability Design

    RELO is designed for **zero data loss** even under complete server failure:

    | Layer | Primary | Failover | RTO | RPO |
    |-------|---------|----------|-----|-----|
    | **Edge Workers** | Global CDN (300+ PoPs) | Auto-HA by cloud provider | 0 | 0 |
    | **Ingest Service** | Dedicated server (128GB, NVMe RAID1) | Warm standby VPS (always-on) | <2 min | 0 |
    | **Analytics DB** | NVMe RAID1 (local) | Replay from object store | <1 hour | 0 |
    | **Identity Cache** | In-memory (dedicated) | Rebuild from analytics DB | <1 hour | ~5 min |
    | **Platform DB** | Managed cloud PostgreSQL | Cloud-native auto-HA | 0 | 0 |
    | **Object Store** | 11 nines durability | Built-in replication | 0 | 0 |

    **3-Tier Failover for event ingestion:**
    1. **Primary**: Dedicated server processes events in real-time
    2. **Standby**: If primary is unreachable (3 consecutive health checks, 30s apart), edge workers route to warm standby VPS that buffers events to object store
    3. **Recovery**: When primary returns, it replays buffered events — **zero data loss guaranteed**

    ### Data Flow Summary

    ```
    Event Sources                Processing              Storage & Activation
    ─────────────               ──────────              ────────────────────
    Ad Click ──────┐
                   │
    Browser Event ─┤──► Edge Workers ──► Go Ingest ──┬──► Analytics DB (hot)
                   │    (global CDN)    (dedicated)  │
    S2S Postback ──┤                                 ├──► Platform DB (aggregates)
                   │                                 │
    MMP Pull API ──┘                                 ├──► Object Store (cold/lake)
                                                     │
                                                     └──► DSP APIs (audiences)
    ```

    ### Event Processing Pipeline

    Every event goes through this pipeline in **<200ms end-to-end**:

    1. **Receive** — Edge worker accepts event, captures geo/device headers
    2. **Enrich** — GeoIP lookup, device fingerprinting, consent flags
    3. **Resolve** — Identity graph links device IDs, fingerprints, and cookies
    4. **Score** — ML-compiled fraud model scores quality (2μs inference)
    5. **Attribute** — Match to partner via configurable rules (exact/contains/regex)
    6. **Store** — Async batch write to analytics DB (1K events or 1s, whichever first)
    7. **Sync** — Materialized views auto-aggregate, sync to platform DB every 10 min
    8. **Activate** — Purchase events forwarded to DSP APIs for real-time optimization

    ---

    ## Integration Methods

    | Method | Best For | Complexity | Auth Required |
    |--------|----------|------------|---------------|
    | **Web Pixel** | Browser event tracking (page views, cart, purchase) | Low — paste JS snippet | None (public) |
    | **Click Wrapper** | Ad click tracking with deep links + cookie | Low — use short URL | None (public) |
    | **AppsFlyer S2S** | Mobile app install + in-app event attribution | Medium — configure postback URL in AF | HMAC signature |
    | **Direct API** | Custom integrations, batch event ingest | Medium — REST API calls | Bearer token |
    | **MMP Pull API** | Scheduled data sync from AppsFlyer/Adjust | Automatic — RELO pulls hourly | Admin config |
    | **Audience Export** | DSP activation, retargeting, suppression lists | Admin-only | Admin token |

    ---

    ## Authentication

    ### Partner Auth
    Partners authenticate with their **partner_id** and a **Bearer token** (JWT from Supabase Auth).
    ```
    Authorization: Bearer <jwt_token>
    ```

    ### Admin Auth (Backbone)
    Admin endpoints on the Go backbone use a static **Bearer token** (`ADMIN_TOKEN` secret).
    ```
    Authorization: Bearer <admin_token>
    ```

    ### HMAC Auth (S2S Postbacks)
    AppsFlyer postbacks are verified via **HMAC-SHA256** signature in the `X-AF-Signature` header.
    The signature is computed over the raw request body using a shared secret configured in the AF dashboard.

    ### No Auth (Public)
    Click wrapper, web pixel, and health check endpoints are public — designed for high-volume, low-latency access from any origin.

    ---

    ## Consent & Privacy

    RELO uses an 8-bit consent bitmask to respect user privacy preferences:

    | Bit | Flag | Description |
    |-----|------|-------------|
    | 0 | `0x01` | Analytics — basic event tracking |
    | 1 | `0x02` | Personalization — product recommendations |
    | 2 | `0x04` | DSP Export — share with ad platforms |
    | 3 | `0x08` | Cross-Client — share identity across brands |
    | 4 | `0x10` | Email marketing |
    | 5 | `0x20` | Push notifications |
    | 6 | `0x40` | Extended retention (beyond 24mo) |
    | 7 | `0x80` | Reserved |

    Default: `0xFF` (all enabled). Mexico's LFPDPPP uses an opt-out model.

    Events are filtered at query time based on consent flags — e.g., audience exports only include users where `consent & 0x04 = 0x04`.

    ---

    ## Data Security & Privacy

    Architecture overview:
    - **Encryption**: TLS 1.3 in transit, AES-256 at rest
    - **Authentication**: JWT tokens with HMAC-SHA256 signing, short-lived (1h expiry)
    - **Authorization**: Role-based access control (RBAC) with 4 roles: admin, client_manager, partner, viewer
    - **Data isolation**: Multi-tenant with strict client_id scoping — queries physically cannot cross tenant boundaries
    - **Consent management**: 8-bit bitmask per device for granular consent (analytics, personalization, DSP export, cross-client, email, push, extended retention)
    - **PII handling**: Zero PII stored in analytics engine — only hashed identifiers (SHA-256)
    - **Data residency**: Event data processed and stored in EU datacenters
    - **Retention**: Automatic 24-month TTL with configurable per-consent extended retention
    - **Audit trail**: All admin actions logged with user ID, timestamp, and IP

    ## Data Compression & Efficiency

    - **Columnar storage**: 8-10x compression ratio on event data
    - **Encoding stack**: ZSTD(3) + Delta encoding for timestamps + dictionary encoding for categorical fields
    - **Storage efficiency**: ~16 bytes per event stored (vs ~130 bytes raw)
    - **Query optimization**: Lazy materialization — only reads columns needed for WHERE clause before full scan
    - **Aggregation**: Pre-computed materialized views refresh every 5 minutes — dashboard queries hit aggregates, not raw data
    - **Tiered storage**: Hot data on NVMe SSD (3 months), cold data on object storage with 11-nines durability

    ## GDPR / LFPDPPP Compliance

    - Mexico's LFPDPPP (Federal Law for Protection of Personal Data) compliance
    - Granular consent bitmask — each data use requires explicit bit
    - Right to deletion: Device data purged within 72 hours of request
    - Data minimization: Only collect what's needed for attribution and analytics
    - Cross-client data sharing only with explicit consent (bit 3)
    - No third-party data sharing without DSP export consent (bit 2)
    - Automated data lifecycle: 24-month TTL, 90-day TTL on identity cache
    - Privacy by design: Identity graph uses opaque device IDs, never raw PII

  contact:
    name: RELO Engineering
    email: engineering@relo.mx
    url: https://portal.relo.mx
  license:
    name: Proprietary
    url: https://relo.mx

servers:
  - url: https://ingest.relo.mx
    description: Ingest Service — event processing, identity resolution, analytics
  - url: https://t.relo.mx
    description: Click Wrapper — ad click tracking with ULID + cookie + 302 redirect
  - url: https://p.relo.mx
    description: Web Pixel — browser-side event collection (JS SDK)
  - url: https://s2s.relo.mx
    description: S2S Proxy — server-to-server postback receiver (AppsFlyer, Branch)
  - url: https://relo-api.quomx.workers.dev
    description: Platform API — partner management, payments, dashboards, config

tags:
  - name: Health & Monitoring
    description: Health checks, metrics, infrastructure stats
  - name: Event Ingestion
    description: Core event collection — clicks, pixel events, postbacks
  - name: Click Wrapper
    description: Ad click tracking with ULID generation and cookie setting
  - name: Web Pixel
    description: Browser-side event collection via JavaScript
  - name: S2S Postbacks
    description: Server-to-server attribution postbacks (AppsFlyer, Branch)
  - name: AppsFlyer Pull API
    description: Scheduled data pull from AppsFlyer reports
  - name: Aggregation & Sync
    description: ClickHouse → Supabase data synchronization
  - name: Audiences
    description: Audience segment building and export for DSP activation
  - name: Configuration
    description: Runtime pipeline configuration
  - name: Platform API
    description: Main platform CRUD (partners, clients, payments, dashboards)

security: []

paths:
  # ============================================================
  # HEALTH & MONITORING
  # ============================================================
  /health:
    get:
      tags: [Health & Monitoring]
      summary: Service health check
      description: |
        Returns service status, component health, buffer stats, and last pull/sync timestamps.
        **No authentication required.** Use this for uptime monitoring.
      operationId: getHealth
      servers:
        - url: https://ingest.relo.mx
      responses:
        '200':
          description: Service health status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/HealthResponse'
              example:
                status: ok
                service: relo-ingest
                version: "3.1"
                uptime_sec: 86400
                buffer_events: 234
                buffer_clicks: 12
                supabase: true
                identity: true
                fraud: true
                metacapi: true
                audience: true
                metacapi_sent: 1500000
                metacapi_errors: 42
                recovery:
                  kv_recoveries: 5
                  r2_recoveries: 2
                  ingest_errors: 0
                  kv_errors: 0
                  r2_errors: 0
                  clicks_ingested: 50000
                last_pull_at: "2026-03-11T15:00:00Z"
                last_pull_stats:
                  rows: 1523
                  purchases: 145
                  products: 89
                  errors: 0
                last_sync_at: "2026-03-11T14:50:00Z"
                last_sync_rows: 432

  /metrics:
    get:
      tags: [Health & Monitoring]
      summary: Prometheus metrics
      description: |
        Returns Prometheus-format metrics for monitoring integration.
        **Requires admin Bearer token.**
      operationId: getMetrics
      servers:
        - url: https://ingest.relo.mx
      security:
        - adminBearer: []
      responses:
        '200':
          description: Prometheus metrics
          content:
            text/plain:
              schema:
                type: string
              example: |
                # HELP relo_ingest_uptime_seconds Service uptime
                # TYPE relo_ingest_uptime_seconds gauge
                relo_ingest_uptime_seconds 86400
                # HELP relo_ingest_buffer_events Events in buffer
                # TYPE relo_ingest_buffer_events gauge
                relo_ingest_buffer_events 1234
        '401':
          $ref: '#/components/responses/Unauthorized'

  /system/infrastructure:
    get:
      tags: [Health & Monitoring]
      summary: Infrastructure stats
      description: |
        Returns Go runtime stats, disk usage, ClickHouse table sizes with compression ratios,
        and warm standby status.
      operationId: getInfrastructure
      servers:
        - url: https://ingest.relo.mx
      security:
        - adminBearer: []
      responses:
        '200':
          description: Infrastructure data
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/InfraResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'

  /activity:
    get:
      tags: [Health & Monitoring]
      summary: Pipeline activity log
      description: |
        Returns recent pipeline activity entries from the ring buffer.
        Useful for real-time monitoring of ingestion, pulls, and errors.
      operationId: getActivity
      servers:
        - url: https://ingest.relo.mx
      security:
        - adminBearer: []
      parameters:
        - name: limit
          in: query
          description: Max entries to return (default 50, max 200)
          schema:
            type: integer
            default: 50
            maximum: 200
      responses:
        '200':
          description: Activity entries
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ActivityResponse'
              example:
                entries:
                  - time: "2026-03-11T15:29:45Z"
                    level: info
                    source: batch
                    message: "Ingested 1,234 events from CF Pipelines"
                  - time: "2026-03-11T15:28:30Z"
                    level: info
                    source: pull
                    message: "AF Pull completed: 150 new orders synced"
                count: 2
        '401':
          $ref: '#/components/responses/Unauthorized'

  # ============================================================
  # CLICK WRAPPER
  # ============================================================
  /c/{code}:
    get:
      tags: [Click Wrapper]
      summary: Track ad click & redirect
      description: |
        The click wrapper is the entry point for all ad clicks. It:

        1. Looks up the short code in KV to find the target URL
        2. Generates a **ULID click_id** (sortable, unique, includes timestamp)
        3. Captures Cloudflare headers (IP, country, city, device type)
        4. Sends click event to Go backbone (async, non-blocking)
        5. Sets first-party cookie `_relo_cid` (30 days) for web-to-app attribution
        6. **302 redirects** to target URL with `&relo_click_id={click_id}` appended

        ### 3-Tier Failover
        If the Go backbone is down:
        1. **Primary**: POST to Go service (3s timeout)
        2. **Standby**: POST to warm standby VPS (5s timeout)
        3. **KV Buffer**: Store in KV for later replay (24h TTL)

        The redirect always happens — click data is fire-and-forget.

        ### Example
        ```
        GET https://t.relo.mx/c/AbC123
        → 302 https://samsungshop.onelink.me/6zKq?pid=...&relo_click_id=01ARZ3NDEKTSV4RRFFQ69G5FAV
        → Set-Cookie: _relo_cid=01ARZ3NDEKTSV4RRFFQ69G5FAV; Domain=.relo.mx; Max-Age=2592000
        ```
      operationId: trackClick
      servers:
        - url: https://t.relo.mx
      parameters:
        - name: code
          in: path
          required: true
          description: Short URL code (e.g., `AbC123`)
          schema:
            type: string
            minLength: 3
            maxLength: 20
      responses:
        '302':
          description: Redirect to target URL with click_id appended
          headers:
            Location:
              description: Target URL with `&relo_click_id={ulid}` appended
              schema:
                type: string
                format: uri
            Set-Cookie:
              description: First-party cookie for web-to-app attribution
              schema:
                type: string
              example: "_relo_cid=01ARZ3NDEKTSV4RRFFQ69G5FAV; Domain=.relo.mx; Max-Age=2592000; SameSite=Lax; Secure; HttpOnly"
        '404':
          description: Short code not found — redirects to relo.mx/404

  # ============================================================
  # WEB PIXEL
  # ============================================================
  /r.js:
    get:
      tags: [Web Pixel]
      summary: Pixel JavaScript loader
      description: |
        Returns the RELO pixel JavaScript (~143 lines). Cache at edge for 1 hour.

        ### Installation
        ```html
        <script>
        (function(r,e,l,o){r.relo=r.relo||function(){(r.relo.q=r.relo.q||[]).push(arguments)};
        o=e.createElement('script');o.async=1;o.src=l;e.head.appendChild(o);
        })(window,document,'https://p.relo.mx/r.js');

        relo('init', {client_id: 3, consent: 0xFF});
        relo('page');
        relo('event', 'view_product', {product_id: 'SKU123', price: 24999});
        </script>
        ```

        ### Event Types
        | Type | Trigger | Properties |
        |------|---------|------------|
        | `page_view` | Every page load | url, referrer |
        | `view_product` | Product detail page | product_id, price |
        | `add_to_cart` | Cart addition | product_id, price, quantity |
        | `begin_checkout` | Checkout start | cart_value |
        | `purchase` | Conversion | order_id, product_id, revenue, quantity |
        | Custom | Any | Any key-value pairs |
      operationId: getPixelJs
      servers:
        - url: https://p.relo.mx
      responses:
        '200':
          description: Pixel JavaScript code
          content:
            application/javascript:
              schema:
                type: string

  /e:
    post:
      tags: [Web Pixel]
      summary: Receive browser events (Beacon API)
      description: |
        Receives browser events sent by the pixel JS via `navigator.sendBeacon()`.
        Supports single event or array of up to 20 events per request.

        ### 3-Tier Failover
        Same as click wrapper — primary → standby → R2 DLQ.

        ### Consent Bitmask
        ```
        Bit 0 (0x01): Analytics
        Bit 1 (0x02): Personalization
        Bit 2 (0x04): DSP Export
        Bit 3 (0x08): Cross-Client Identity
        Bit 4 (0x10): Email Marketing
        Bit 5 (0x20): Push Notifications
        Bit 6 (0x40): Extended Retention
        Bit 7 (0x80): Reserved
        Default: 0xFF (all enabled — Mexico LFPDPPP is opt-out)
        ```
      operationId: receivePixelEvents
      servers:
        - url: https://p.relo.mx
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - $ref: '#/components/schemas/PixelEvent'
                - type: array
                  items:
                    $ref: '#/components/schemas/PixelEvent'
                  maxItems: 20
            examples:
              pageView:
                summary: Page view event
                value:
                  t: page_view
                  ts: 1709654400000
                  cid: 3
                  did: fp_abc123xyz
                  url: "https://samsung.com.mx/products/galaxy-s24"
                  ref: "https://google.com"
                  consent: 255
              purchase:
                summary: Purchase event
                value:
                  t: purchase
                  ts: 1709654500000
                  cid: 3
                  did: fp_abc123xyz
                  props:
                    order_id: MX260311-001234
                    product_id: SKU123
                    product_name: Galaxy S24
                    currency: MXN
                    revenue: 24999.99
                    quantity: 1
                  consent: 255
              batch:
                summary: Batch of events
                value:
                  - t: page_view
                    ts: 1709654400000
                    cid: 3
                    did: fp_abc123xyz
                    url: "https://samsung.com.mx/products"
                    consent: 255
                  - t: view_product
                    ts: 1709654450000
                    cid: 3
                    did: fp_abc123xyz
                    props:
                      product_id: SKU123
                      price: 24999
                    consent: 255
      responses:
        '204':
          description: Events accepted (no response body)

    get:
      tags: [Web Pixel]
      summary: Image pixel fallback (1x1 GIF)
      description: |
        Fallback for environments where `navigator.sendBeacon()` isn't available.
        Pass event data as query parameters, receive a 1x1 transparent GIF.
      operationId: pixelGifFallback
      servers:
        - url: https://p.relo.mx
      parameters:
        - name: t
          in: query
          description: Event type
          schema:
            type: string
            default: page_view
        - name: cid
          in: query
          required: true
          description: Client ID
          schema:
            type: integer
        - name: did
          in: query
          description: Device fingerprint ID
          schema:
            type: string
        - name: url
          in: query
          description: Current page URL
          schema:
            type: string
            format: uri
        - name: ref
          in: query
          description: Referrer URL
          schema:
            type: string
            format: uri
      responses:
        '200':
          description: 1x1 transparent GIF
          content:
            image/gif:
              schema:
                type: string
                format: binary

  # ============================================================
  # S2S POSTBACKS
  # ============================================================
  /postback/appsflyer:
    post:
      tags: [S2S Postbacks]
      summary: AppsFlyer S2S postback
      description: |
        Receives AppsFlyer server-to-server postback events. Verifies HMAC-SHA256 signature.

        ### Processing Pipeline
        1. Verify HMAC signature (`X-AF-Signature` header)
        2. Parse event JSON (install, purchase, custom)
        3. Look up `click_id` in Redis for CTIT fraud detection
        4. Resolve device identity (GAID + IDFA + AF ID → unified Relo device_id)
        5. Run fraud scorer (score 0-255)
           - ≥240: **block** (don't write to Supabase)
           - ≥200: **flag** for review
        6. Write to ClickHouse (always, including blocked events)
        7. Dual-write to Supabase (purchases only, if `is_primary_attribution = true`)
        8. Send to Meta CAPI (purchases + engagement events)

        ### Setup in AppsFlyer
        Configure S2S postback URL in your AppsFlyer dashboard:
        ```
        https://s2s.relo.mx/postback/appsflyer?client_id={your_client_id}
        ```

        Or send via the Go backbone directly:
        ```
        https://ingest.relo.mx/postback/appsflyer?client_id={your_client_id}
        ```
      operationId: afPostback
      servers:
        - url: https://s2s.relo.mx
        - url: https://ingest.relo.mx
      security:
        - hmacSignature: []
      parameters:
        - name: client_id
          in: query
          description: RELO client ID (alternative to X-Client-ID header)
          schema:
            type: integer
          example: 3
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AppsFlyerPostback'
            example:
              event_name: af_purchase
              event_time: "2026-03-11T15:30:00"
              appsflyer_id: "1234567890-abcdef"
              advertising_id: "goog-abc123xyz"
              media_source: facebook
              campaign: "mx_samsung_mobupps_always-on_q1"
              ip: "203.0.113.45"
              country_code: MX
              city: Mexico City
              device_model: SM-G991B
              platform: android
              os_version: "13"
              app_version: "2.1.0"
              click_id: "1234567890abcdef"
              af_order_id: MX260311-001234
              event_revenue: 24999.99
              event_revenue_currency: MXN
              af_content_id: SKU123
              af_content: Galaxy S24
              af_content_type: Mobile
              af_quantity: 1
              is_primary_attribution: "true"
      responses:
        '200':
          description: Event processed
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [ok, blocked]
                  reason:
                    type: string
              examples:
                accepted:
                  value:
                    status: ok
                blocked:
                  value:
                    status: blocked
                    reason: fraud
        '400':
          description: Missing client_id
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: "client_id is required (query param, X-Client-ID header, or path segment)"

  /postback/branch:
    post:
      tags: [S2S Postbacks]
      summary: Branch.io S2S postback
      description: |
        Receives Branch.io server-to-server postback events.
        Same processing pipeline as AppsFlyer postbacks.

        ### Setup in Branch
        Configure postback URL in Branch dashboard:
        ```
        https://s2s.relo.mx/postback/branch?client_id={your_client_id}
        ```
      operationId: branchPostback
      servers:
        - url: https://s2s.relo.mx
        - url: https://ingest.relo.mx
      parameters:
        - name: client_id
          in: query
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              description: Branch.io event payload (flexible schema)
      responses:
        '200':
          description: Event processed
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
              example:
                status: ok

  /postback/generic:
    post:
      tags: [S2S Postbacks]
      summary: Generic webhook receiver
      description: |
        Accepts any JSON webhook payload. Use this for custom integrations
        (Shopify, WooCommerce, custom backends).

        ### Example: Shopify Webhook
        ```json
        {
          "event_name": "order_confirmed",
          "order_id": "ORD-12345",
          "total_price": 24999.99,
          "currency": "MXN",
          "customer_email_hash": "sha256_abc..."
        }
        ```
      operationId: genericPostback
      servers:
        - url: https://s2s.relo.mx
        - url: https://ingest.relo.mx
      parameters:
        - name: client_id
          in: query
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              additionalProperties: true
            example:
              event_name: order_confirmed
              order_id: ORD-12345
              total_price: 24999.99
              currency: MXN
      responses:
        '200':
          description: Event accepted
          content:
            application/json:
              example:
                status: ok

  # ============================================================
  # EVENT INGESTION (Internal)
  # ============================================================
  /ingest/batch:
    post:
      tags: [Event Ingestion]
      summary: Batch event ingest
      description: |
        Receives batched web pixel events from CF Pipelines or direct integrations.
        Max **10,000 events** per batch, **10MB** max payload.

        Events are written to ClickHouse. Purchase events are dual-written to Supabase
        (when `dual_write.enabled = true` in runtime config).

        ### Identity Resolution
        For each event, the system attempts to link:
        - `device_id` (pixel fingerprint)
        - `fingerprint` (device fingerprint hash)
        - `click_id` (from `_relo_cid` cookie)

        Into a unified Relo device ID via the DragonflyDB identity graph.
      operationId: batchIngest
      servers:
        - url: https://ingest.relo.mx
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BatchPayload'
            example:
              events:
                - t: page_view
                  ts: 1709654400000
                  cid: 3
                  did: fp_abc123xyz
                  url: "https://samsung.com.mx/products/galaxy-s24"
                  ref: "https://google.com"
                  consent: 255
                - t: purchase
                  ts: 1709654500000
                  cid: 3
                  did: fp_abc123xyz
                  props:
                    order_id: MX260311-001234
                    product_id: SKU123
                    product_name: Galaxy S24
                    product_line: MX
                    currency: MXN
                    revenue: 24999.99
                    quantity: 1
                  consent: 255
      responses:
        '200':
          description: Events ingested
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                  ingested:
                    type: integer
              example:
                status: ok
                ingested: 2
        '400':
          description: Invalid JSON or too many events
        '413':
          description: Payload too large (>10MB)

  /ingest/click:
    post:
      tags: [Event Ingestion]
      summary: Click event ingest
      description: |
        Receives individual click events from the Click Wrapper worker.
        Writes to ClickHouse `clicks` table and caches in Redis for CTIT matching.
      operationId: clickIngest
      servers:
        - url: https://ingest.relo.mx
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ClickPayload'
            example:
              click_id: 01ARZ3NDEKTSV4RRFFQ69G5FAV
              timestamp: 1709654400000
              ip: "203.0.113.45"
              user_agent: "Mozilla/5.0 (Linux; Android 13; SM-G991B)"
              country: MX
              city: Mexico City
              device: mobile
              short_code: AbC123
              target_url: "https://samsungshop.onelink.me/6zKq?pid=..."
              partner_id: 9
              client_id: 3
              campaign_id: 4
      responses:
        '200':
          description: Click recorded
          content:
            application/json:
              example:
                status: ok
                click_id: 01ARZ3NDEKTSV4RRFFQ69G5FAV

  # ============================================================
  # APPSFLYER PULL API
  # ============================================================
  /pull/trigger:
    post:
      tags: [AppsFlyer Pull API]
      summary: Trigger AF Pull API manually
      description: |
        Manually triggers the AppsFlyer Pull API fetch cycle. Normally runs automatically
        every 60 minutes via systemd timer.

        ### Report Types Pulled
        | Report | Endpoint | Interval |
        |--------|----------|----------|
        | In-App Events | `in_app_events_report/v5` | Every cycle (60 min) |
        | Installs | `installs_report/v5` | Every cycle (60 min) |
        | Uninstalls | `uninstall_events_report/v5` | Every 2 hours |
        | Fraud (P360) | `fraud-post-inapps/v5` | Every 2 hours |

        ### Rate Limits
        Each report type has an independent quota of **24 requests/day** per app.
        Max **200,000 rows** per export. Date ranges ≤2 days = 1 request/minute.
      operationId: triggerPull
      servers:
        - url: https://ingest.relo.mx
      security:
        - adminBearer: []
      responses:
        '200':
          description: Pull completed
          content:
            application/json:
              example:
                ok: true
                sources: 12
                stats:
                  total_orders: 1523
                  new_orders: 145
                  updated_orders: 38
                  attributed_orders: 89
                duration: "45.234s"
        '400':
          description: AF puller not configured
        '401':
          $ref: '#/components/responses/Unauthorized'

  # ============================================================
  # AGGREGATION & SYNC
  # ============================================================
  /sync/trigger:
    post:
      tags: [Aggregation & Sync]
      summary: Trigger CH → Supabase sync
      description: |
        Manually triggers the ClickHouse → Supabase daily aggregates synchronization.
        Normally runs automatically every 10 minutes with a 7-day lookback window.

        Aggregates are written to `partner_daily_aggregates` in Supabase, which powers
        the admin dashboard and partner portal.
      operationId: triggerSync
      servers:
        - url: https://ingest.relo.mx
      security:
        - adminBearer: []
      parameters:
        - name: days
          in: query
          description: Number of days to sync (default 7, max 30)
          schema:
            type: integer
            default: 7
            maximum: 30
      responses:
        '200':
          description: Sync completed
          content:
            application/json:
              example:
                status: ok
                days: 7
                start_date: "2026-03-04"
                end_date: "2026-03-11"
                rows_synced: 432
                duration: "5.123s"
        '401':
          $ref: '#/components/responses/Unauthorized'

  /sync/freeze:
    post:
      tags: [Aggregation & Sync]
      summary: Freeze month aggregates
      description: |
        Marks a month as final in Supabase (`is_final = true`).
        After freezing, aggregates for that month won't be updated by the sync job.
        Used during month-end close before generating payments.
      operationId: freezeMonth
      servers:
        - url: https://ingest.relo.mx
      security:
        - adminBearer: []
      parameters:
        - name: year
          in: query
          required: true
          schema:
            type: integer
          example: 2026
        - name: month
          in: query
          required: true
          schema:
            type: integer
            minimum: 1
            maximum: 12
          example: 3
      responses:
        '200':
          description: Month frozen
          content:
            application/json:
              example:
                status: frozen
                year: 2026
                month: 3
                frozen_at: "2026-03-11T15:30:00Z"
        '401':
          $ref: '#/components/responses/Unauthorized'

  # ============================================================
  # AUDIENCES
  # ============================================================
  /api/audiences:
    get:
      tags: [Audiences]
      summary: List saved audience segments
      description: |
        Returns all saved audience segments for a client. Audiences are built from
        analytics engine queries and can be exported for DSP activation (Meta, Google, TikTok).

        Results are ordered by `last_built` descending (most recently built first).
      operationId: listAudiences
      servers:
        - url: https://ingest.relo.mx
      security:
        - adminBearer: []
      parameters:
        - name: client_id
          in: query
          required: true
          description: RELO client ID
          schema:
            type: integer
          example: 3
        - name: audience_type
          in: query
          description: Filter by audience type
          schema:
            type: string
            enum: [retargeting, suppression, winback, custom]
        - name: is_active
          in: query
          description: Filter by active status (default true)
          schema:
            type: boolean
            default: true
      responses:
        '200':
          description: Audience list
          content:
            application/json:
              schema:
                type: object
                properties:
                  audiences:
                    type: array
                    items:
                      $ref: '#/components/schemas/AudienceSegment'
              example:
                audiences:
                  - audience_id: 01HXYZ123456789ABCDEFGHIJ
                    client_id: 3
                    name: "Winback - MX High-Value"
                    audience_type: winback
                    device_count: 45000
                    last_built: "2026-03-11T15:00:00Z"
                    created_at: "2026-03-01T10:00:00Z"
                    is_active: true
                  - audience_id: 01HXYZ987654321ZYXWVUTSRQ
                    client_id: 3
                    name: "Suppression - Recent Buyers"
                    audience_type: suppression
                    device_count: 12300
                    last_built: "2026-03-11T14:00:00Z"
                    created_at: "2026-02-15T08:00:00Z"
                    is_active: true
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/audiences/build:
    post:
      tags: [Audiences]
      summary: Build and save a new audience
      description: |
        Build a new audience segment from analytics engine event data. The segment
        is saved and can be re-built or exported later.

        ### Audience Types
        | Type | Description | Default Lookback |
        |------|-------------|------------------|
        | `retargeting` | Cart abandoners, product viewers | 7 days |
        | `suppression` | Recent purchasers (exclude from targeting) | 30 days |
        | `winback` | Lapsed high-value buyers | 30+ days inactive |
        | `custom` | User-defined filters with full control | Configurable |

        ### Consent Enforcement
        Only devices with DSP export consent (`consent & 0x04 = 0x04`) are included
        in the audience. Set `filter.require_consent` to enforce additional consent bits.

        ### Filter Logic
        All filter conditions are combined with AND logic. Omitted fields are not applied.
      operationId: buildAudience
      servers:
        - url: https://ingest.relo.mx
      security:
        - adminBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AudienceBuildRequest'
            examples:
              winback:
                summary: Winback audience — lapsed high-value MX buyers
                value:
                  client_id: 3
                  name: "Winback - MX High-Value"
                  audience_type: winback
                  within_days: 180
                  inactive_days: 30
                  min_revenue: 15000
                  filter:
                    product_line: MX
                    require_consent: 4
              suppression:
                summary: Suppression list — recent purchasers
                value:
                  client_id: 3
                  name: "Suppression - Recent Buyers 30d"
                  audience_type: suppression
                  within_days: 30
                  filter:
                    min_purchases: 1
                    require_consent: 4
              custom:
                summary: Custom audience — high-value VD buyers in Mexico
                value:
                  client_id: 3
                  name: "Custom - VD Mexico High Value"
                  audience_type: custom
                  within_days: 90
                  filter:
                    min_revenue: 20000
                    product_line: VD
                    country_code: MX
                    require_consent: 4
      responses:
        '200':
          description: Audience built and saved
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AudienceSegment'
              example:
                audience_id: 01HXYZ123456789ABCDEFGHIJ
                client_id: 3
                name: "Winback - MX High-Value"
                audience_type: winback
                device_count: 45000
                last_built: "2026-03-11T15:30:00Z"
                created_at: "2026-03-11T15:30:00Z"
                is_active: true
        '400':
          description: Invalid request — missing required fields or invalid filter
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: "client_id is required"
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/audiences/export:
    get:
      tags: [Audiences]
      summary: Export device IDs from an audience
      description: |
        Export device IDs from a saved audience segment for DSP upload.
        Returns unified Relo device IDs, GAIDs, and IDFAs where available.

        Only devices with DSP export consent (`consent & 0x04`) are included.

        ### Export Formats
        The default JSON response returns an array of device IDs. For large audiences,
        use `limit` and `offset` for pagination.
      operationId: exportAudience
      servers:
        - url: https://ingest.relo.mx
      security:
        - adminBearer: []
      parameters:
        - name: audience_id
          in: query
          required: true
          description: Audience segment ID (ULID)
          schema:
            type: string
          example: 01HXYZ123456789ABCDEFGHIJ
        - name: limit
          in: query
          description: Max device IDs to return (default 10000, max 100000)
          schema:
            type: integer
            default: 10000
            maximum: 100000
        - name: offset
          in: query
          description: Offset for pagination (default 0)
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: Exported device IDs
          content:
            application/json:
              schema:
                type: object
                properties:
                  audience_id:
                    type: string
                  audience_name:
                    type: string
                  device_count:
                    type: integer
                    description: Total devices in the audience
                  export_count:
                    type: integer
                    description: Number of device IDs in this response
                  device_ids:
                    type: array
                    items:
                      type: string
                  exported_at:
                    type: string
                    format: date-time
              example:
                audience_id: 01HXYZ123456789ABCDEFGHIJ
                audience_name: "Winback - MX High-Value"
                device_count: 45000
                export_count: 10000
                device_ids:
                  - R_001
                  - R_002
                  - goog-abc123
                exported_at: "2026-03-11T15:30:00Z"
        '404':
          description: Audience not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: "audience not found"
        '401':
          $ref: '#/components/responses/Unauthorized'

  # ============================================================
  # CONFIGURATION
  # ============================================================
  /config:
    get:
      tags: [Configuration]
      summary: Get runtime config
      description: |
        Returns current runtime pipeline configuration. Hot-reloadable — changes
        take effect without service restart.
      operationId: getConfig
      servers:
        - url: https://ingest.relo.mx
      security:
        - adminBearer: []
      responses:
        '200':
          description: Current config
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/PipelineConfig'
              example:
                af_pull:
                  enabled: true
                  interval_minutes: 60
                ingest:
                  buffer_flush_size: 1000
                  buffer_flush_interval_sec: 1
                supabase_sync:
                  enabled: true
                  interval_minutes: 10
                  lookback_days: 7
                recovery:
                  kv_enabled: true
                  kv_poll_sec: 30
                  r2_enabled: true
                  r2_poll_sec: 60
                dual_write:
                  enabled: true
        '401':
          $ref: '#/components/responses/Unauthorized'

    post:
      tags: [Configuration]
      summary: Update runtime config
      description: |
        Update pipeline configuration. Changes are applied immediately and persisted
        to disk for survival across restarts.

        ### Example: Disable dual-write
        ```json
        { "dual_write": { "enabled": false } }
        ```

        ### Example: Change AF Pull interval
        ```json
        { "af_pull": { "interval_minutes": 30 } }
        ```
      operationId: updateConfig
      servers:
        - url: https://ingest.relo.mx
      security:
        - adminBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PipelineConfig'
      responses:
        '200':
          description: Config updated
          content:
            application/json:
              example:
                ok: true
        '401':
          $ref: '#/components/responses/Unauthorized'

  # ============================================================
  # PIXEL CONFIG (Public)
  # ============================================================
  /pixel/{client_id}/config:
    get:
      tags: [Web Pixel]
      summary: Get pixel configuration for client
      description: |
        Returns the pixel configuration for a specific client. Used by `r.js` to
        initialize event tracking with the correct parameters.
      operationId: getPixelConfig
      servers:
        - url: https://ingest.relo.mx
      parameters:
        - name: client_id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Pixel config
          content:
            application/json:
              example:
                client_id: 3
                param_partner: partner_id
                param_campaign: campaign_id
                auto_pageview: true
                consent_required: false
                event_batching:
                  batch_size: 50
                  batch_timeout_ms: 5000

  # ============================================================
  # PLATFORM API (Selected Key Endpoints)
  # ============================================================
  /api/health:
    get:
      tags: [Platform API]
      summary: Platform API health check
      operationId: platformHealth
      servers:
        - url: https://relo-api.quomx.workers.dev
      responses:
        '200':
          description: API healthy
          content:
            application/json:
              example:
                status: ok
                service: relo-api

  /api/partners/{id}/leads:
    get:
      tags: [Platform API]
      summary: Partner leads (pixel lead-gen)
      description: |
        Returns the list of `lead_submit` events attributed to this partner in the period.
        Pixel counterpart of `/api/partners/{id}/orders` — used by the portal LeadsTab
        when the active campaign's `tracking_type` is `pixel`.

        Hashed email/phone are exposed as a 12-char prefix only (enough for dedup,
        not enough for a dictionary attack). Full hashes never leave the server.

        **Auth**: Partner (own data only) or Admin.
      operationId: getPartnerLeads
      servers:
        - url: https://relo-api.quomx.workers.dev
      security:
        - partnerBearer: []
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: integer }
        - name: client_id
          in: query
          required: true
          schema: { type: integer }
        - name: year
          in: query
          schema: { type: integer }
        - name: month
          in: query
          schema: { type: integer }
        - name: limit
          in: query
          schema: { type: integer, default: 100, maximum: 500 }
        - name: offset
          in: query
          schema: { type: integer, default: 0 }
        - name: search
          in: query
          description: Case-insensitive substring match on product / form_id / city
          schema: { type: string }
        - name: stage
          in: query
          description: Exact match on stage label (raw, qualified, rejected, …)
          schema: { type: string }
      responses:
        '200':
          description: Paginated lead list + summary KPIs + daily trend + top products
          content:
            application/json:
              schema:
                type: object
                properties:
                  summary:
                    type: object
                    properties:
                      total_leads: { type: integer }
                      unique_devices: { type: integer }
                      total_value: { type: number }
                  leads:
                    type: array
                    items:
                      type: object
                      properties:
                        event_time: { type: string }
                        product: { type: string }
                        stage: { type: string }
                        lead_value: { type: number }
                        email_hash_prefix: { type: string, description: '12-char prefix of SHA-256(email)' }
                        phone_hash_prefix: { type: string }
                        city: { type: string }
                  by_product:
                    type: array
                    items:
                      type: object
                      properties:
                        product: { type: string }
                        count: { type: integer }
                        total_value: { type: number }
                  daily_trend:
                    type: array
                    items:
                      type: object
                      properties:
                        date: { type: string }
                        leads: { type: integer }
                        value: { type: number }
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/partners/{id}/pixel-dashboard:
    get:
      tags: [Platform API]
      summary: Partner pixel dashboard KPIs
      description: |
        Sessions / conversions / conv rate / revenue / engagement for pixel campaigns.
        Pixel counterpart of `/api/partners/{id}/dashboard` (MMP).

        **Auth**: Partner or Admin.
      operationId: getPartnerPixelDashboard
      servers:
        - url: https://relo-api.quomx.workers.dev
      security:
        - partnerBearer: []
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
        - { name: client_id, in: query, required: true, schema: { type: integer } }
        - { name: year, in: query, schema: { type: integer } }
        - { name: month, in: query, schema: { type: integer } }
      responses:
        '200':
          description: Sessions/conversions KPIs + daily/top_pages
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/partners/{id}/pixel-analytics:
    get:
      tags: [Platform API]
      summary: Partner pixel analytics (full)
      description: |
        Returns the payload backing the partner portal's PixelAnalyticsTab:
        performance (hours/days/daily trend), traffic_sources (referrers + UTM),
        landing_pages, engagement (active_sec, scroll_max), funnel, geographic,
        devices. Single fetch, 7 subtab datasets.
      operationId: getPartnerPixelAnalytics
      servers:
        - url: https://relo-api.quomx.workers.dev
      security:
        - partnerBearer: []
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
        - { name: client_id, in: query, required: true, schema: { type: integer } }
        - { name: year, in: query, schema: { type: integer } }
        - { name: month, in: query, schema: { type: integer } }
      responses:
        '200':
          description: Full analytics payload
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/partners/{id}/pixel-links:
    get:
      tags: [Platform API]
      summary: Partner click-wrapper link stats
      description: |
        For pixel clients: per-code clicks / unique devices / conversions / conv rate
        driven by this partner through `t.relo.mx/c/:code` short URLs.
      operationId: getPartnerPixelLinks
      servers:
        - url: https://relo-api.quomx.workers.dev
      security:
        - partnerBearer: []
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
        - { name: client_id, in: query, required: true, schema: { type: integer } }
      responses:
        '200':
          description: Click-wrapper stats
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/clients/{id}/pixel/replays:
    get:
      tags: [Platform API]
      summary: List rrweb session replays (admin)
      description: |
        Lists session replays recorded for a client. Requires
        `client_pixel_config.replay_enabled = true` + consent bit 4 set on the
        visitor's pixel to produce data.

        **Auth**: Admin only.
      operationId: getClientReplays
      servers:
        - url: https://relo-api.quomx.workers.dev
      security:
        - adminBearer: []
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
        - { name: year, in: query, schema: { type: integer } }
        - { name: month, in: query, schema: { type: integer } }
        - { name: partner_id, in: query, schema: { type: integer } }
        - { name: only_conversions, in: query, schema: { type: string, enum: [true, false] } }
        - { name: limit, in: query, schema: { type: integer, default: 100 } }
      responses:
        '200':
          description: Paginated list of replay_sessions rows
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/partners/{id}/dashboard:
    get:
      tags: [Platform API]
      summary: Partner dashboard data
      description: |
        Returns complete dashboard data for a partner: KPIs, daily sales chart,
        segment breakdown, media sources, insights, and UA/RT breakdown.

        **Auth**: Partner (own data only) or Admin.
      operationId: getPartnerDashboard
      servers:
        - url: https://relo-api.quomx.workers.dev
      security:
        - partnerBearer: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
        - name: year
          in: query
          schema:
            type: integer
          example: 2026
        - name: month
          in: query
          schema:
            type: integer
          example: 3
        - name: client_id
          in: query
          schema:
            type: integer
          example: 3
      responses:
        '200':
          description: Dashboard data
          content:
            application/json:
              schema:
                type: object
                properties:
                  total_sales:
                    type: integer
                  total_commission:
                    type: number
                  orders_count:
                    type: integer
                  today_sales:
                    type: integer
                  daily_sales:
                    type: array
                    items:
                      type: object
                  segments:
                    type: array
                    items:
                      type: object
                  media_sources:
                    type: array
                    items:
                      type: object
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: Access denied — not your partner data

  /api/partners/{id}/analytics:
    get:
      tags: [Platform API]
      summary: Partner analytics
      description: |
        Returns detailed analytics: best hours, best days, daily trend,
        media source breakdown, week-over-week comparison, and conversion funnel.
      operationId: getPartnerAnalytics
      servers:
        - url: https://relo-api.quomx.workers.dev
      security:
        - partnerBearer: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
        - name: year
          in: query
          schema:
            type: integer
        - name: month
          in: query
          schema:
            type: integer
      responses:
        '200':
          description: Analytics data
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/payments/generate:
    post:
      tags: [Platform API]
      summary: Generate monthly payments
      description: |
        Generates payment records for all partners of a client for a given month.
        Commission rates are **frozen** at generation time — future rate changes
        don't affect existing payments.

        Uses PostgreSQL function `calculate_partner_commission()` as single source of truth.
      operationId: generatePayments
      servers:
        - url: https://relo-api.quomx.workers.dev
      security:
        - partnerBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [client_id, year, month]
              properties:
                client_id:
                  type: integer
                year:
                  type: integer
                month:
                  type: integer
            example:
              client_id: 3
              year: 2026
              month: 3
      responses:
        '200':
          description: Payments generated

  # ============================================================
  # SYSTEM ENDPOINTS (Go Backbone)
  # ============================================================
  /system/health:
    get:
      tags: [System]
      summary: Full system health with probe details
      description: |
        Returns comprehensive health status including individual probe results for
        ClickHouse, DragonflyDB, Supabase, and all subsystems.

        Unlike `/health` which returns a simple ok/not-ok, this endpoint provides
        granular probe information for monitoring dashboards.
      operationId: systemHealth
      servers:
        - url: https://ingest.relo.mx
      security:
        - adminBearer: []
      responses:
        '200':
          description: Full health status
          content:
            application/json:
              example:
                status: ok
                version: "7.1.0"
                uptime_sec: 864000
                probes:
                  clickhouse:
                    status: ok
                    latency_ms: 2
                    events_today: 45230
                    tables: 9
                  dragonfly:
                    status: ok
                    latency_ms: 1
                    keys: 125000
                    memory_mb: 1240
                  supabase:
                    status: ok
                    latency_ms: 45
                    dual_write: true
                  metacapi:
                    status: ok
                    events_sent_today: 3200
                  fraud:
                    status: ok
                    blocked_today: 12
                    flagged_today: 45
                  identity:
                    status: ok
                    devices_resolved: 89000
                go:
                  goroutines: 42
                  alloc_mb: 128
                  heap_mb: 256
                  gc_runs: 1500
                  num_cpu: 16
                disk:
                  total_gb: 953
                  used_gb: 145
                  free_gb: 808
                  used_percent: 15.2
                  alert: ok
        '401':
          $ref: '#/components/responses/Unauthorized'

  /system/data-freshness:
    get:
      tags: [System]
      summary: Per-client data freshness status
      description: |
        Returns data freshness information for each configured client. Shows when
        data was last pulled from AppsFlyer, how many minutes ago, and a status
        indicator (fresh/stale/critical).

        ### Status Thresholds
        | Status | Minutes Since Last Pull |
        |--------|------------------------|
        | `fresh` | < 90 minutes |
        | `stale` | 90-180 minutes |
        | `critical` | > 180 minutes |

        The admin dashboard displays a warning banner when any client is stale or critical.
      operationId: dataFreshness
      servers:
        - url: https://ingest.relo.mx
      security:
        - adminBearer: []
      responses:
        '200':
          description: Data freshness per client
          content:
            application/json:
              example:
                clients:
                  - client_id: 3
                    client_name: Samsung
                    last_pull: "2026-03-18T15:00:00Z"
                    minutes_ago: 32
                    status: fresh
                    orders_today: 145
                    orders_yesterday: 312
                  - client_id: 4
                    client_name: Optimal
                    last_pull: "2026-03-18T14:30:00Z"
                    minutes_ago: 62
                    status: fresh
                    orders_today: 23
                    orders_yesterday: 41
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/admin/system-alert:
    post:
      tags: [System]
      summary: Send a system alert notification
      description: |
        Sends an alert notification to configured recipients (email, webhook, or in-app).
        Used for manual alerts or triggered by monitoring automation.
      operationId: sendSystemAlert
      servers:
        - url: https://relo-api.quomx.workers.dev
      security:
        - adminBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [title, message, severity]
              properties:
                title:
                  type: string
                  description: Alert title
                message:
                  type: string
                  description: Alert body text
                severity:
                  type: string
                  enum: [info, warning, critical]
                client_id:
                  type: integer
                  description: Optional client scope
                channels:
                  type: array
                  items:
                    type: string
                    enum: [email, webhook, in_app]
                  description: Delivery channels (defaults to all configured)
            example:
              title: "Data Pipeline Stale"
              message: "Samsung data has not been refreshed in over 3 hours. Last pull at 12:00 PM."
              severity: warning
              client_id: 3
              channels: ["email", "webhook"]
      responses:
        '200':
          description: Alert sent
          content:
            application/json:
              example:
                ok: true
                sent_to: ["email:admin@relo.mx", "webhook:slack"]
        '401':
          $ref: '#/components/responses/Unauthorized'

  # ============================================================
  # ANALYTICS ENDPOINTS (Go Backbone)
  # ============================================================
  /analytics/orders:
    get:
      tags: [Analytics]
      summary: Paginated orders from ClickHouse
      description: |
        Returns purchase orders from ClickHouse with pagination. Supports filtering
        by client, partner, segment, date range, and campaign.

        Orders are sorted by `event_time` descending (most recent first).
      operationId: analyticsOrders
      servers:
        - url: https://ingest.relo.mx
      security:
        - adminBearer: []
      parameters:
        - name: client_id
          in: query
          required: true
          schema:
            type: integer
          example: 3
        - name: partner_id
          in: query
          description: Filter by partner
          schema:
            type: integer
        - name: segment
          in: query
          description: Filter by product segment (MX, VD, DA, etc.)
          schema:
            type: string
        - name: date_from
          in: query
          description: Start date (YYYY-MM-DD), defaults to start of current month
          schema:
            type: string
            format: date
        - name: date_to
          in: query
          description: End date (YYYY-MM-DD), defaults to today
          schema:
            type: string
            format: date
        - name: campaign
          in: query
          description: Filter by campaign name (contains match)
          schema:
            type: string
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
            maximum: 500
        - name: offset
          in: query
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: Paginated orders
          content:
            application/json:
              example:
                orders:
                  - order_id: "MX260318-12345678"
                    event_time: "2026-03-18T15:30:00Z"
                    partner_id: 9
                    partner_name: Mobupps
                    product_name: "Galaxy S24 Ultra 256GB"
                    product_line: MX
                    quantity: 1
                    revenue: 24999.00
                    campaign: "mx_pd_affiliate_relo_none_always-on_sub1_banner_none_conversion"
                    media_source: appsflyer
                    city: "Ciudad de Mexico"
                    is_retargeting: false
                total: 1523
                limit: 50
                offset: 0
        '401':
          $ref: '#/components/responses/Unauthorized'

  # ============================================================
  # CAMPAIGN ENDPOINTS (Platform API)
  # ============================================================
  /api/campaigns:
    get:
      tags: [Campaigns]
      summary: List campaigns for a client
      description: |
        Returns all campaigns for a client with basic stats. Campaigns represent
        marketing initiatives that group partners, budgets, and tracking.
      operationId: listCampaigns
      servers:
        - url: https://relo-api.quomx.workers.dev
      security:
        - adminBearer: []
      parameters:
        - name: client_id
          in: query
          required: true
          schema:
            type: integer
          example: 3
        - name: status
          in: query
          description: Filter by campaign status
          schema:
            type: string
            enum: [active, paused, completed]
      responses:
        '200':
          description: Campaign list
          content:
            application/json:
              example:
                campaigns:
                  - id: 1
                    name: "Always-On Q1 2026"
                    status: active
                    start_date: "2026-01-01"
                    end_date: "2026-03-31"
                    budget: 2000000
                    partner_count: 8
                  - id: 4
                    name: "Tablets ATB"
                    status: active
                    start_date: "2026-03-18"
                    end_date: "2026-03-30"
                    budget: 304353.88
                    partner_count: 4
        '401':
          $ref: '#/components/responses/Unauthorized'

    post:
      tags: [Campaigns]
      summary: Create a new campaign
      description: |
        Creates a new campaign for a client. After creation, use the Launch endpoint
        to configure partners, commissions, budgets, and attribution in one call.
      operationId: createCampaign
      servers:
        - url: https://relo-api.quomx.workers.dev
      security:
        - adminBearer: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [client_id, name]
              properties:
                client_id:
                  type: integer
                name:
                  type: string
                description:
                  type: string
                status:
                  type: string
                  enum: [active, paused, completed]
                  default: active
                start_date:
                  type: string
                  format: date
                end_date:
                  type: string
                  format: date
                budget:
                  type: number
                  description: Total campaign budget in MXN
            example:
              client_id: 3
              name: "Galaxy S25 Launch"
              description: "Launch campaign for Galaxy S25 series"
              start_date: "2026-04-01"
              end_date: "2026-04-30"
              budget: 500000
      responses:
        '200':
          description: Campaign created
          content:
            application/json:
              example:
                id: 5
                name: "Galaxy S25 Launch"
                status: active
                created_at: "2026-03-18T15:30:00Z"
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/campaigns/{id}/stats:
    get:
      tags: [Campaigns]
      summary: Campaign stats by partner
      description: |
        Returns campaign performance stats aggregated by partner. Includes revenue,
        units, orders, and commission breakdown per partner enrolled in the campaign.
      operationId: campaignStats
      servers:
        - url: https://relo-api.quomx.workers.dev
      security:
        - adminBearer: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
        - name: year
          in: query
          schema:
            type: integer
          example: 2026
        - name: month
          in: query
          schema:
            type: integer
          example: 3
      responses:
        '200':
          description: Campaign stats
          content:
            application/json:
              example:
                campaign_id: 4
                campaign_name: "Tablets ATB"
                total_revenue: 1250000
                total_units: 42
                total_orders: 38
                partners:
                  - partner_id: 9
                    partner_name: Mobupps
                    units: 15
                    revenue: 450000
                    orders: 14
                    commission: 3000
                  - partner_id: 13
                    partner_name: BEdigitech
                    units: 12
                    revenue: 360000
                    orders: 11
                    commission: 1800
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/campaigns/{id}/orders:
    get:
      tags: [Campaigns]
      summary: Campaign orders
      description: |
        Returns individual orders attributed to a campaign. Orders are matched
        by campaign attribution patterns configured during launch.
      operationId: campaignOrders
      servers:
        - url: https://relo-api.quomx.workers.dev
      security:
        - adminBearer: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
        - name: year
          in: query
          schema:
            type: integer
        - name: month
          in: query
          schema:
            type: integer
        - name: limit
          in: query
          schema:
            type: integer
            default: 50
            maximum: 200
        - name: offset
          in: query
          schema:
            type: integer
            default: 0
      responses:
        '200':
          description: Campaign orders
          content:
            application/json:
              example:
                orders:
                  - order_id: "MX260320-98765432"
                    event_time: "2026-03-20T10:15:00Z"
                    partner_name: Mobupps
                    product_name: "Galaxy Tab S11 Ultra 512GB"
                    product_line: TAB
                    quantity: 1
                    revenue: 27999.00
                total: 42
                limit: 50
                offset: 0
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/campaigns/{id}/partners:
    get:
      tags: [Campaigns]
      summary: Partners enrolled in a campaign
      description: |
        Returns the list of partners enrolled in a campaign along with their
        campaign-specific commission rates and budget allocations.
      operationId: campaignPartners
      servers:
        - url: https://relo-api.quomx.workers.dev
      security:
        - adminBearer: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        '200':
          description: Enrolled partners
          content:
            application/json:
              example:
                campaign_id: 4
                partners:
                  - partner_id: 9
                    partner_name: Mobupps
                    cid: sub1
                    commission_value: 200
                    commission_is_percentage: false
                    budget_amount: 76088.47
                  - partner_id: 13
                    partner_name: BEdigitech
                    cid: sub6
                    commission_value: 200
                    commission_is_percentage: false
                    budget_amount: 76088.47
        '401':
          $ref: '#/components/responses/Unauthorized'

  /api/campaigns/{id}/launch:
    post:
      tags: [Campaigns]
      summary: Launch campaign with full configuration
      description: |
        One-call endpoint to fully configure and launch a campaign. This sets up:

        1. **Campaign patterns** — attribution patterns added to campaign record
        2. **Partner associations** — enroll partners in the campaign
        3. **Segment commissions** — set commission rates per partner for the campaign segment
        4. **Segment budgets** — allocate budget per partner

        ### CID Pattern Generated
        ```
        mx_pd_affiliate_relo_none_{campaign-slug}_{partner-sub}_banner_none_conversion
        ```

        After launch, partners can be notified via the
        `POST /api/admin/send-campaign-notification` endpoint.
      operationId: launchCampaign
      servers:
        - url: https://relo-api.quomx.workers.dev
      security:
        - adminBearer: []
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [client_id, segment_code, partners]
              properties:
                client_id:
                  type: integer
                segment_code:
                  type: string
                  description: Product segment for this campaign (MX, VD, DA, TAB, etc.)
                campaign_slug:
                  type: string
                  description: URL-friendly slug for CID generation
                onelink_url:
                  type: string
                  description: OneLink/Branch base URL for the campaign
                partners:
                  type: array
                  items:
                    type: object
                    required: [partner_id, commission_value]
                    properties:
                      partner_id:
                        type: integer
                      commission_value:
                        type: number
                      commission_is_percentage:
                        type: boolean
                        default: false
                      budget_amount:
                        type: number
            example:
              client_id: 3
              segment_code: TAB
              campaign_slug: atb-tabs
              onelink_url: "https://samsungshop.onelink.me/6zKq/vcke1sef"
              partners:
                - partner_id: 9
                  commission_value: 200
                  commission_is_percentage: false
                  budget_amount: 76088.47
                - partner_id: 13
                  commission_value: 200
                  commission_is_percentage: false
                  budget_amount: 76088.47
      responses:
        '200':
          description: Campaign launched
          content:
            application/json:
              example:
                ok: true
                campaign_id: 4
                partners_enrolled: 4
                patterns_added: ["atb-tabs"]
                cids_generated:
                  - partner: Mobupps
                    cid: "mx_pd_affiliate_relo_none_atb-tabs_sub1_banner_none_conversion"
                  - partner: BEdigitech
                    cid: "mx_pd_affiliate_relo_none_atb-tabs_sub6_banner_none_conversion"
        '400':
          description: Invalid request
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              example:
                error: "partners array is required"
        '401':
          $ref: '#/components/responses/Unauthorized'

components:
  securitySchemes:
    adminBearer:
      type: http
      scheme: bearer
      description: |
        Admin token for Go backbone endpoints.
        ```
        Authorization: Bearer {ADMIN_TOKEN}
        ```
    partnerBearer:
      type: http
      scheme: bearer
      description: |
        Supabase JWT token for platform API endpoints.
        Partners can only access their own data.
        ```
        Authorization: Bearer {supabase_jwt}
        ```
    hmacSignature:
      type: apiKey
      in: header
      name: X-AF-Signature
      description: |
        HMAC-SHA256 signature for AppsFlyer S2S postbacks.
        Computed over the request body with the shared secret.

  responses:
    Unauthorized:
      description: Missing or invalid authentication
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: Unauthorized

  schemas:
    Error:
      type: object
      properties:
        error:
          type: string
      required: [error]

    HealthResponse:
      type: object
      properties:
        status:
          type: string
        service:
          type: string
        version:
          type: string
        uptime_sec:
          type: number
        buffer_events:
          type: integer
        buffer_clicks:
          type: integer
        supabase:
          type: boolean
        identity:
          type: boolean
        fraud:
          type: boolean
        metacapi:
          type: boolean
        audience:
          type: boolean

    InfraResponse:
      type: object
      properties:
        timestamp:
          type: string
          format: date-time
        go:
          type: object
          properties:
            goroutines:
              type: integer
            alloc_mb:
              type: number
            sys_mb:
              type: number
            heap_mb:
              type: number
            gc_runs:
              type: integer
            num_cpu:
              type: integer
        disk:
          type: object
          properties:
            total_gb:
              type: number
            used_gb:
              type: number
            free_gb:
              type: number
            used_percent:
              type: number
            alert:
              type: string
              enum: [ok, warning, critical, unknown]
        clickhouse:
          type: object
          properties:
            tables:
              type: array
              items:
                type: object
            total_compressed_mb:
              type: number
            total_compressed_gb:
              type: number
        infrastructure_status:
          type: object
          properties:
            standby_healthy:
              type: boolean
            primary_healthy:
              type: boolean

    ActivityResponse:
      type: object
      properties:
        entries:
          type: array
          items:
            type: object
            properties:
              time:
                type: string
                format: date-time
              level:
                type: string
                enum: [ok, warn, error, info]
              source:
                type: string
              message:
                type: string
        count:
          type: integer

    PixelEvent:
      type: object
      required: [t, cid]
      properties:
        t:
          type: string
          description: Event type (page_view, view_product, add_to_cart, purchase, custom)
        ts:
          type: integer
          format: int64
          description: Client timestamp (Unix ms)
        cid:
          type: integer
          description: RELO client ID
        did:
          type: string
          description: Device fingerprint ID
        url:
          type: string
          format: uri
          description: Current page URL
        ref:
          type: string
          format: uri
          description: Referrer URL
        props:
          type: object
          additionalProperties: true
          description: Custom event properties
        consent:
          type: integer
          description: Consent bitmask (default 0xFF = all enabled)
          default: 255
        ip:
          type: string
          description: Client IP (auto-filled from CF headers if omitted)
        cc:
          type: string
          description: Country code (auto-filled from CF headers if omitted)
        city:
          type: string
          description: City (auto-filled from CF headers if omitted)
        ua:
          type: string
          description: User agent (auto-filled from request headers if omitted)
        click_id:
          type: string
          description: Click ID from _relo_cid cookie
        fp:
          type: string
          description: Device fingerprint hash

    BatchPayload:
      type: object
      required: [events]
      properties:
        events:
          type: array
          maxItems: 10000
          items:
            $ref: '#/components/schemas/PixelEvent'

    ClickPayload:
      type: object
      required: [click_id, timestamp, ip, country, city, target_url, partner_id, client_id]
      properties:
        click_id:
          type: string
          description: ULID click identifier
        timestamp:
          type: integer
          format: int64
          description: Unix milliseconds
        ip:
          type: string
        user_agent:
          type: string
        country:
          type: string
        city:
          type: string
        device:
          type: string
          enum: [mobile, desktop, tablet]
        short_code:
          type: string
        target_url:
          type: string
          format: uri
        partner_id:
          type: integer
        client_id:
          type: integer
        campaign_id:
          type: integer

    AppsFlyerPostback:
      type: object
      properties:
        event_name:
          type: string
          description: "AF event name (af_purchase, install, af_add_to_cart, etc.)"
        event_time:
          type: string
          format: date-time
        appsflyer_id:
          type: string
        idfa:
          type: string
        advertising_id:
          type: string
          description: Google Advertising ID (GAID)
        media_source:
          type: string
        campaign:
          type: string
        af_adset:
          type: string
        af_ad:
          type: string
        af_channel:
          type: string
        ip:
          type: string
        country_code:
          type: string
        city:
          type: string
        device_model:
          type: string
        platform:
          type: string
          enum: [android, ios, web]
        os_version:
          type: string
        app_version:
          type: string
        click_id:
          type: string
        af_order_id:
          type: string
        event_revenue:
          type: number
        event_revenue_currency:
          type: string
        af_content_id:
          type: string
        af_content:
          type: string
        af_content_type:
          type: string
        af_quantity:
          type: integer
        is_primary_attribution:
          type: string
          enum: ["true", "false"]

    AudienceSegment:
      type: object
      properties:
        audience_id:
          type: string
          description: Unique audience identifier (ULID)
          example: 01HXYZ123456789ABCDEFGHIJ
        client_id:
          type: integer
          description: RELO client ID
          example: 3
        name:
          type: string
          description: Human-readable audience name
          example: "Winback - MX High-Value"
        audience_type:
          type: string
          enum: [retargeting, suppression, winback, custom]
          description: Audience segment type
        device_count:
          type: integer
          description: Number of unique devices in the audience
          example: 45000
        last_built:
          type: string
          format: date-time
          description: When the audience was last built/refreshed
        created_at:
          type: string
          format: date-time
          description: When the audience was first created
        is_active:
          type: boolean
          description: Whether the audience is active and available for export
          default: true

    AudienceBuildRequest:
      type: object
      required: [client_id, name, audience_type]
      properties:
        client_id:
          type: integer
          description: RELO client ID
        name:
          type: string
          description: Human-readable name for the audience segment
        audience_type:
          type: string
          enum: [retargeting, suppression, winback, custom]
          description: |
            Type of audience segment:
            - `retargeting`: Cart abandoners, product viewers (default lookback 7 days)
            - `suppression`: Recent purchasers to exclude from targeting (default 30 days)
            - `winback`: Lapsed high-value buyers (30+ days inactive)
            - `custom`: Fully user-defined filters
        within_days:
          type: integer
          description: Only include devices active within this many days (lookback window)
          example: 180
        inactive_days:
          type: integer
          description: Only include devices inactive for at least this many days (winback use case)
          example: 30
        min_revenue:
          type: number
          description: Minimum total revenue from the device to qualify
          example: 15000
        filter:
          $ref: '#/components/schemas/AudienceFilter'

    AudienceFilter:
      type: object
      description: |
        Fine-grained filter conditions for audience building. All conditions are
        combined with AND logic. Omitted fields are not applied.
      properties:
        min_purchases:
          type: integer
          description: Minimum number of purchases to qualify
          example: 1
        max_purchases:
          type: integer
          description: Maximum number of purchases (for suppression or one-time buyer targeting)
          example: 5
        min_revenue:
          type: number
          description: Minimum total revenue from the device
          example: 15000
        inactive_days:
          type: integer
          description: Minimum days since last activity
          example: 30
        active_within_days:
          type: integer
          description: Maximum days since last activity (must have been active within this window)
          example: 180
        product_line:
          type: string
          description: "Filter by product segment (MX, VD, DA, HA, IT, NW)"
          example: MX
        country_code:
          type: string
          description: "ISO 3166-1 alpha-2 country code filter"
          example: MX
        require_consent:
          type: integer
          description: |
            Consent bitmask — only include devices where `consent & require_consent = require_consent`.
            DSP export consent (bit 2 = 0x04) is always enforced regardless of this field.
          default: 4
          example: 4

    PipelineConfig:
      type: object
      properties:
        af_pull:
          type: object
          properties:
            enabled:
              type: boolean
            interval_minutes:
              type: integer
        ingest:
          type: object
          properties:
            buffer_flush_size:
              type: integer
            buffer_flush_interval_sec:
              type: integer
        supabase_sync:
          type: object
          properties:
            enabled:
              type: boolean
            interval_minutes:
              type: integer
            lookback_days:
              type: integer
        recovery:
          type: object
          properties:
            kv_enabled:
              type: boolean
            kv_poll_sec:
              type: integer
            r2_enabled:
              type: boolean
            r2_poll_sec:
              type: integer
        dual_write:
          type: object
          properties:
            enabled:
              type: boolean
