openapi: 3.0.3
info:
  title: Feedico  -  customer data API
  version: "1.0.0"
  description: |
    **Firms (merchant programmes)** and **coupons** for your account.

    Data listing is **`POST` only**: send **`Content-Type: application/json`** (body may be `{}`). **`GET`** is not supported (`405`)  -  avoids exposing filter tokens in URLs or caches.

    **Monthly API quota:** every successful **`/api/v1/me/networks`** or **`/api/v1/me/coupons`** response counts one request against your plan (UTC calendar month). Starter, Pro, and Business each have limits; when exceeded the API returns **`429`** with **`monthly_api_limit`**.

    - **`recordCount`**: total matching rows (with current filters), not just the page length.
    - **`availableProviders`**: distinct integration keys from your data (`cj_affiliate`, `awin_affiliate`, …)  -  unified affiliate tables **and** Awin when `network_service_awin_entity_snapshot` has rows for your account; use for filter UI.
    - **`page`** (1-based), **`pageSize`** (max **200**).

    Auth: **`Authorization: Bearer fdco_...`** (Account page).

servers:
  - url: https://api.feedico.io
    description: Production
  - url: http://127.0.0.1:3010
    description: Local Next (`next start` same port as systemd feedico-web)

tags:
  - name: Firms
  - name: Coupons

paths:
  /api/v1/me/networks:
    post:
      tags: [Firms]
      summary: List firms  -  paginated POST
      operationId: meNetworks
      security:
        - ApiTokenBearer: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CustomerDataListRequest"
            example:
              page: 1
              pageSize: 50
              provider: cj_affiliate
              firmName: Acme
      responses:
        "200":
          description: Page of networks
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/NetworkListResponse"
        "400":
          description: Invalid JSON body
        "401":
          description: Missing or invalid bearer token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MeError"
        "429":
          description: Monthly plan API quota exceeded (each successful call counts  -  Starter / Pro / Business each have a `/api/v1/me/*` request cap per UTC month)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MonthlyApiQuotaError"
        "503":
          description: Database or schema unavailable

  /api/v1/me/coupons:
    post:
      tags: [Coupons]
      summary: List coupons  -  paginated POST
      operationId: meCoupons
      security:
        - ApiTokenBearer: []
      requestBody:
        required: false
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CustomerDataListRequest"
      responses:
        "200":
          description: Page of coupons
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/CouponListResponse"
        "400":
          description: Invalid JSON body
        "401":
          description: Missing or invalid bearer token
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MeError"
        "429":
          description: Monthly plan API quota exceeded (each successful call counts  -  Starter / Pro / Business each have a `/api/v1/me/*` request cap per UTC month)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/MonthlyApiQuotaError"
        "503":
          description: Database or schema unavailable

components:
  securitySchemes:
    ApiTokenBearer:
      type: http
      scheme: bearer
      bearerFormat: fdco_<hex>
      description: Raw `fdco_...` token from dashboard Account.

  schemas:
    MeError:
      type: object
      properties:
        error:
          type: string
        hint:
          type: string

    MonthlyApiQuotaError:
      type: object
      properties:
        ok:
          type: boolean
          example: false
        error:
          type: string
          example: monthly_api_limit
        message:
          type: string
        used:
          type: integer
          format: int64
        limit:
          type: integer
          format: int64

    CustomerDataListRequest:
      type: object
      properties:
        page:
          type: integer
          minimum: 1
          default: 1
        pageSize:
          type: integer
          minimum: 1
          maximum: 200
          default: 50
        provider:
          type: string
          nullable: true
          description: |
            Omit, `null`, or empty string → **all** firms (**CJ/Impact**/… from unified table **plus** Awin snapshot programmes).  
            **`cj_affiliate`** or **`awin_affiliate`** → only that source (filters `availableProviders`).
        firmName:
          type: string
          nullable: true
          description: Substring filter  -  networks **`display_name`**; coupons  -  linked merchant display, coupon **title**, or **externalMerchantKey**.

    NetworkListResponse:
      type: object
      required: [ok, recordCount, page, pageSize, availableProviders, networks]
      properties:
        ok:
          type: boolean
        recordCount:
          type: integer
        page:
          type: integer
        pageSize:
          type: integer
        availableProviders:
          type: array
          items:
            type: string
        networks:
          type: array
          items:
            $ref: "#/components/schemas/NetworkRow"

    NetworkRow:
      type: object
      properties:
        id:
          type: string
          description: "CJ unified table  -  numeric id string. Awin  -  `awin_snap_` + snapshot row primary key."
        propertyId:
          type: string
        provider:
          type: string
        externalMerchantKey:
          type: string
        displayName:
          type: string
        description:
          type: string
          nullable: true
          description: Optional firm notes (panel); not overwritten by feed sync.
        merchantWebsiteUrl:
          type: string
          nullable: true
          description: Public merchant or programme website URL (HTTP(S)).
        status:
          type: string
        lastSyncedAt:
          type: string
          nullable: true
        lastSyncError:
          type: string
          nullable: true
        couponCount:
          type: integer
          minimum: 0
          description: >
            Rows in `app_user_affiliate_coupons` linked to this network (unified integrations), or for Awin
            programmes a best-effort count of `promotion` snapshot rows whose JSON references this programme id.

    CouponListResponse:
      type: object
      required: [ok, recordCount, page, pageSize, availableProviders, coupons]
      properties:
        ok:
          type: boolean
        recordCount:
          type: integer
        page:
          type: integer
        pageSize:
          type: integer
        availableProviders:
          type: array
          items:
            type: string
        coupons:
          type: array
          items:
            $ref: "#/components/schemas/CouponRow"

    CouponRow:
      type: object
      properties:
        id:
          type: string
        networkId:
          type: string
        networkName:
          type: string
          nullable: true
          description: Merchant or programme label from the affiliate network API at sync time.
        provider:
          type: string
        externalMerchantKey:
          type: string
        externalCouponId:
          type: string
        code:
          type: string
          nullable: true
        title:
          type: string
          nullable: true
        description:
          type: string
          nullable: true
        startsAt:
          type: string
          nullable: true
        endsAt:
          type: string
          nullable: true
        offerUrl:
          type: string
          nullable: true
        extra:
          nullable: true
        fetchedAt:
          type: string
          nullable: true
