---
openapi: 3.0.3
info:
  title: ScaleForge API
  version: v1
  description: |
    Public REST API for ScaleForge — a Rails 8 SaaS platform that automates
    creation, management, and optimization of Meta Ads campaigns at scale.

    ## Authentication

    All endpoints require a Bearer token in the `Authorization` header,
    except `POST /auth/firebase` which exchanges a Firebase ID token for
    a ScaleForge API token.

    ```
    Authorization: Bearer <your-api-token>
    ```

    Manage tokens at `/api_keys` in the dashboard or via
    `GET|POST|DELETE /api/v1/tokens`.

    ## Rate limits

    Per-plan limits (requests per hour / per minute):

    - Free: 100 / 10
    - Starter: 500 / 30
    - Pro: 1000 / 60
    - Agency: 5000 / 200

    Exceeding the limit returns `429 Too Many Requests`.
  contact:
    name: ScaleForge Support
    email: michael@getscaleforge.com
    url: https://getscaleforge.com
  license:
    name: Proprietary
servers:
- url: https://getscaleforge.com
  description: Production
components:
  securitySchemes:
    bearer_auth:
      type: http
      scheme: bearer
      bearerFormat: API token
  schemas:
    Error:
      type: object
      properties:
        error:
          type: string
          example: Not found
        code:
          type: string
          example: not_found
      required:
      - error
    Pagination:
      type: object
      properties:
        page:
          type: integer
          example: 1
        per_page:
          type: integer
          example: 25
        total_count:
          type: integer
          example: 137
        total_pages:
          type: integer
          example: 6
security:
- bearer_auth: []
tags:
- name: Auth
  description: Firebase token exchange and API token management
- name: Ad Accounts
  description: Meta Ad Accounts owned by the authenticated user
- name: Creatives
  description: Video and image creatives (multi-text supported)
- name: Templates
  description: 'Reusable campaign templates: targeting, bidding, placements'
- name: Uploads
  description: Bulk upload sessions — create dozens of campaigns from one template
- name: Campaigns
  description: Campaign read, activate, pause, bid updates
- name: Analytics
  description: Combined Meta + RedTrack analytics
- name: Rule Sets
  description: AutoRules for bid management, pause/activate
- name: Posts
  description: Meta Page posts (AI-generated)
- name: Webhooks
  description: Chrome extension webhook subscriptions
- name: Hunted Creatives
  description: Hunter Mode — scraped and uniqueified creatives
- name: Watchlist
  description: Competitor ads tracking via Ad Library scans
paths:
  "/api/v1/ad_accounts":
    get:
      summary: List Meta Ad Accounts owned by the authenticated user
      tags:
      - Ad Accounts
      security:
      - bearer_auth: []
      description: Returns only visible accounts, ordered by `position` then `name`.
        Requires scope `accounts:read`.
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: integer
                          example: 7
                        name:
                          type: string
                          example: Account-13
                        meta_account_id:
                          type: string
                          example: act_123456789
                        business_id:
                          type: string
                          nullable: true
                          example: '987654321'
                        page_id:
                          type: string
                          nullable: true
                          example: '100012345678901'
                        page_name:
                          type: string
                          nullable: true
                          example: Consumer Insights Daily
                        pixel_id:
                          type: string
                          nullable: true
                          example: '2468013579'
                        pixel_name:
                          type: string
                          nullable: true
                          example: Main Pixel
                        connected:
                          type: boolean
                          example: true
                        position:
                          type: integer
                          nullable: true
                          example: 1
                        rate_limit_usage:
                          type: number
                          nullable: true
                          example: 42.5
                        last_used_at:
                          type: string
                          format: date-time
                          nullable: true
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
        '403':
          description: Missing accounts:read scope
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
  "/api/v1/ad_accounts/{id}":
    parameters:
    - name: id
      in: path
      required: true
      description: Ad Account ID
      schema:
        type: integer
    get:
      summary: Get a single Ad Account
      tags:
      - Ad Accounts
      security:
      - bearer_auth: []
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: integer
                        example: 7
                      name:
                        type: string
                        example: Account-13
                      meta_account_id:
                        type: string
                        example: act_123456789
                      business_id:
                        type: string
                        nullable: true
                        example: '987654321'
                      page_id:
                        type: string
                        nullable: true
                        example: '100012345678901'
                      page_name:
                        type: string
                        nullable: true
                        example: Consumer Insights Daily
                      pixel_id:
                        type: string
                        nullable: true
                        example: '2468013579'
                      pixel_name:
                        type: string
                        nullable: true
                        example: Main Pixel
                      connected:
                        type: boolean
                        example: true
                      position:
                        type: integer
                        nullable: true
                        example: 1
                      rate_limit_usage:
                        type: number
                        nullable: true
                        example: 42.5
                      last_used_at:
                        type: string
                        format: date-time
                        nullable: true
        '404':
          description: Account not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
  "/api/v1/ad_accounts/{id}/ads_volume":
    parameters:
    - name: id
      in: path
      required: true
      description: Ad Account ID
      schema:
        type: integer
    get:
      summary: Get Meta Page ads volume (running ads, limit, remaining)
      tags:
      - Ad Accounts
      security:
      - bearer_auth: []
      description: |
        Meta enforces a per-Page ads limit (default **250** running/in-review).
        This endpoint returns the cached counters from `MetaPage` for the
        account's associated Page so callers can pre-check capacity before
        uploading a new batch of creatives.
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      account_id:
                        type: integer
                        example: 7
                      page_id:
                        type: string
                        nullable: true
                        example: '100012345678901'
                      page_name:
                        type: string
                        nullable: true
                        example: Consumer Insights Daily
                      active_ads_count:
                        type: integer
                        example: 71
                      ads_limit:
                        type: integer
                        example: 250
                      remaining:
                        type: integer
                        example: 179
        '404':
          description: Account not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
  "/api/v1/analytics":
    get:
      summary: List combined daily stats (Meta + RedTrack)
      tags:
      - Analytics
      security:
      - bearer_auth: []
      description: |
        Returns `CombinedDailyStat` rows scoped to the user's ad accounts.
        Defaults to last 7 days. Requires scope `analytics:read`.
      parameters:
      - name: account_id
        in: query
        required: false
        schema:
          type: string
        description: Filter by Meta account ID (e.g. `act_123`)
      - name: campaign_id
        in: query
        required: false
        schema:
          type: string
      - name: date_from
        in: query
        required: false
        schema:
          type: string
          format: date
        description: ISO date; defaults to 7 days ago
      - name: date_to
        in: query
        required: false
        schema:
          type: string
          format: date
        description: ISO date; defaults to today
      - name: page
        in: query
        required: false
        schema:
          type: integer
          default: 1
      - name: per_page
        in: query
        required: false
        schema:
          type: integer
          default: 25
          maximum: 100
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: integer
                          example: 10234
                        stat_date:
                          type: string
                          format: date
                          example: '2026-04-15'
                        meta_account_id:
                          type: string
                          example: act_123456789
                        campaign_id:
                          type: string
                          nullable: true
                          example: '23851234567890123'
                        campaign_name:
                          type: string
                          nullable: true
                        spend:
                          type: number
                          example: 42.17
                        revenue:
                          type: number
                          example: 78.0
                        impressions:
                          type: integer
                          example: 5421
                        clicks:
                          type: integer
                          example: 312
                        link_clicks:
                          type: integer
                          example: 204
                        conversions:
                          type: integer
                          example: 6
                        cpl:
                          type: number
                          nullable: true
                          example: 7.03
                          description: Cost-per-lead
                        roi:
                          type: number
                          example: 84.9
                          description: Percent
                  meta:
                    "$ref": "#/components/schemas/Pagination"
        '400':
          description: Invalid date format
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
        '403':
          description: Missing analytics:read scope
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
  "/api/v1/analytics/summary":
    get:
      summary: Summary totals per ad account for a date range
      tags:
      - Analytics
      security:
      - bearer_auth: []
      description: Aggregates spend / revenue / conversions per `meta_account_id`.
        Defaults to last 7 days.
      parameters:
      - name: date_from
        in: query
        required: false
        schema:
          type: string
          format: date
      - name: date_to
        in: query
        required: false
        schema:
          type: string
          format: date
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        ad_account_id:
                          type: string
                          example: act_123456789
                        account_name:
                          type: string
                          nullable: true
                          example: Account-13
                        total_spend:
                          type: number
                          example: 1203.48
                        total_revenue:
                          type: number
                          example: 1842.0
                        total_conversions:
                          type: integer
                          example: 82
                        total_impressions:
                          type: integer
                          example: 132551
                        total_clicks:
                          type: integer
                          example: 4420
                        roi:
                          type: number
                          example: 53.0
                          description: Percent
        '400':
          description: Invalid date format
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
  "/api/v1/auth/firebase":
    post:
      summary: Exchange Firebase ID token for a ScaleForge API token
      tags:
      - Auth
      security: []
      description: |
        Verifies the Firebase ID token, resolves the matching ScaleForge user
        by email or previously linked `firebase_uid`, and issues a new API
        token scoped for Chrome extension usage (`hunted_creatives:read` +
        `hunted_creatives:write`).

        This is the only endpoint that does NOT require a Bearer token.
      parameters: []
      responses:
        '201':
          description: API token issued
          content:
            application/json:
              schema:
                type: object
                properties:
                  api_token:
                    type: string
                    example: sf_abc123xyz...
                  user:
                    type: object
                    properties:
                      id:
                        type: integer
                        example: 42
                      email:
                        type: string
                        example: user@example.com
                      name:
                        type: string
                        nullable: true
                        example: Jane Doe
        '400':
          description: firebase_token missing
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
        '401':
          description: Invalid Firebase token
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
        '404':
          description: No ScaleForge account for this email
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
              - firebase_token
              properties:
                firebase_token:
                  type: string
                  description: Firebase ID token obtained from Firebase Auth SDK
        required: true
  "/api/v1/campaigns":
    get:
      summary: List campaigns owned by the user
      tags:
      - Campaigns
      security:
      - bearer_auth: []
      description: Paginated. Joins through uploads. Requires scope `campaigns:read`.
      parameters:
      - name: status
        in: query
        required: false
        schema:
          type: string
        description: Filter by campaign-job status
      - name: page
        in: query
        required: false
        schema:
          type: integer
          default: 1
      - name: per_page
        in: query
        required: false
        schema:
          type: integer
          default: 25
          maximum: 100
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: integer
                          example: 5501
                        campaign_name:
                          type: string
                          nullable: true
                          example: SweepstakesOffer_US_BD4.40
                        adset_name:
                          type: string
                          nullable: true
                          example: Adset_01
                        ad_name:
                          type: string
                          nullable: true
                          example: Ad_creative_301
                        bid_amount:
                          type: number
                          nullable: true
                          example: 4.4
                        status:
                          type: string
                          example: completed
                        meta_status:
                          type: string
                          nullable: true
                          enum:
                          - ACTIVE
                          - PAUSED
                          - DELETED
                          - ARCHIVED
                          example: ACTIVE
                        meta_campaign_id:
                          type: string
                          nullable: true
                          example: '23851234567890123'
                        meta_adset_id:
                          type: string
                          nullable: true
                          example: '23851234567890124'
                        meta_ad_id:
                          type: string
                          nullable: true
                          example: '23851234567890125'
                        error_message:
                          type: string
                          nullable: true
                        is_excluded:
                          type: boolean
                          example: false
                        bid_strategy:
                          type: string
                          nullable: true
                          example: LOWEST_COST_WITH_BID_CAP
                        daily_budget:
                          type: number
                          nullable: true
                          example: 100.0
                        created_at:
                          type: string
                          format: date-time
                  meta:
                    "$ref": "#/components/schemas/Pagination"
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
  "/api/v1/campaigns/{id}":
    parameters:
    - name: id
      in: path
      required: true
      schema:
        type: integer
    get:
      summary: Get a single campaign
      tags:
      - Campaigns
      security:
      - bearer_auth: []
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: integer
                        example: 5501
                      campaign_name:
                        type: string
                        nullable: true
                        example: SweepstakesOffer_US_BD4.40
                      adset_name:
                        type: string
                        nullable: true
                        example: Adset_01
                      ad_name:
                        type: string
                        nullable: true
                        example: Ad_creative_301
                      bid_amount:
                        type: number
                        nullable: true
                        example: 4.4
                      status:
                        type: string
                        example: completed
                      meta_status:
                        type: string
                        nullable: true
                        enum:
                        - ACTIVE
                        - PAUSED
                        - DELETED
                        - ARCHIVED
                        example: ACTIVE
                      meta_campaign_id:
                        type: string
                        nullable: true
                        example: '23851234567890123'
                      meta_adset_id:
                        type: string
                        nullable: true
                        example: '23851234567890124'
                      meta_ad_id:
                        type: string
                        nullable: true
                        example: '23851234567890125'
                      error_message:
                        type: string
                        nullable: true
                      is_excluded:
                        type: boolean
                        example: false
                      bid_strategy:
                        type: string
                        nullable: true
                        example: LOWEST_COST_WITH_BID_CAP
                      daily_budget:
                        type: number
                        nullable: true
                        example: 100.0
                      created_at:
                        type: string
                        format: date-time
        '404':
          description: Campaign not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
  "/api/v1/campaigns/activate":
    post:
      summary: Activate campaigns on Meta (enqueues batch job)
      tags:
      - Campaigns
      security:
      - bearer_auth: []
      description: |
        Enqueues `ActivateCampaignsJob` which uses Meta **Batch API** to flip
        campaigns and adsets to `ACTIVE`. Requires scope `campaigns:write`.
      parameters: []
      responses:
        '200':
          description: Queued
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      queued:
                        type: integer
                        example: 2
                      account_id:
                        type: integer
                        example: 7
        '404':
          description: Account not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
              - account_id
              - campaign_ids
              properties:
                account_id:
                  type: integer
                  example: 7
                  description: ScaleForge AdAccount ID
                campaign_ids:
                  type: array
                  items:
                    type: integer
                  example:
                  - 5501
                  - 5502
        required: true
  "/api/v1/campaigns/pause":
    post:
      summary: Pause campaigns on Meta (enqueues batch job)
      tags:
      - Campaigns
      security:
      - bearer_auth: []
      parameters: []
      responses:
        '200':
          description: Queued
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      queued:
                        type: integer
                        example: 2
                      account_id:
                        type: integer
                        example: 7
        '404':
          description: Account not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
              - account_id
              - campaign_ids
              properties:
                account_id:
                  type: integer
                  example: 7
                campaign_ids:
                  type: array
                  items:
                    type: integer
                  example:
                  - 5501
                  - 5502
        required: true
  "/api/v1/campaigns/update_bids":
    post:
      summary: Submit bid changes for adsets
      tags:
      - Campaigns
      security:
      - bearer_auth: []
      description: |
        Queues a set of bid-change requests against the given ad account.
        Each entry produces a `BidChange` record with `source: "api"`.
        Requires scope `campaigns:write`.
      parameters: []
      responses:
        '200':
          description: Queued
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      queued:
                        type: integer
                        example: 3
                      account_id:
                        type: integer
                        example: 7
        '404':
          description: Account not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
              - account_id
              - bid_changes
              properties:
                account_id:
                  type: integer
                  example: 7
                bid_changes:
                  type: array
                  items:
                    type: object
                    required:
                    - adset_id
                    - new_bid
                    properties:
                      adset_id:
                        type: string
                        example: '23851234567890124'
                      new_bid:
                        type: number
                        example: 4.55
                        description: New bid in dollars
        required: true
  "/api/v1/campaigns/active_last_30_days":
    get:
      summary: Campaigns with spend > 0 in the last 30 days (aggregated)
      tags:
      - Campaigns
      security:
      - bearer_auth: []
      description: Aggregated from `CombinedDailyStat`. Ordered by total spend descending.
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        campaign_id:
                          type: string
                          example: '23851234567890123'
                        campaign_name:
                          type: string
                          example: SweepstakesOffer_US_BD4.40
                        meta_account_id:
                          type: string
                          example: act_123456789
                        total_spend:
                          type: number
                          example: 534.12
                        total_conversions:
                          type: integer
                          example: 27
                        total_revenue:
                          type: number
                          example: 912.5
                        roi:
                          type: number
                          example: 70.8
                          description: Percent
                        first_active:
                          type: string
                          format: date
                        last_active:
                          type: string
                          format: date
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
  "/api/v1/creatives":
    get:
      summary: List creatives
      tags:
      - Creatives
      security:
      - bearer_auth: []
      description: Returns paginated creatives. Requires scope `creatives:read`.
      parameters:
      - name: file_type
        in: query
        required: false
        schema:
          type: string
          enum:
          - video
          - image
        description: Filter by file type
      - name: status
        in: query
        required: false
        schema:
          type: string
        description: Filter by processing status
      - name: page
        in: query
        required: false
        schema:
          type: integer
          default: 1
      - name: per_page
        in: query
        required: false
        schema:
          type: integer
          default: 25
          maximum: 100
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: integer
                          example: 301
                        name:
                          type: string
                          example: winter_hook_v3
                        file_type:
                          type: string
                          enum:
                          - video
                          - image
                          example: video
                        file_size:
                          type: integer
                          nullable: true
                          example: 15482112
                        status:
                          type: string
                          example: ready
                        meta_id:
                          type: string
                          nullable: true
                          description: Meta video ID or image hash
                          example: '12345678901234567'
                        ad_text:
                          type: string
                          nullable: true
                          example: Tired of ads? Try this one trick…
                        headline:
                          type: string
                          nullable: true
                          example: Learn the secret
                        description:
                          type: string
                          nullable: true
                          example: Limited time offer
                        cta:
                          type: string
                          nullable: true
                          example: LEARN_MORE
                        ad_texts:
                          type: array
                          items:
                            type: string
                          description: For multi-text creatives
                        headlines:
                          type: array
                          items:
                            type: string
                        thumbnail_url:
                          type: string
                          nullable: true
                          format: uri
                        created_at:
                          type: string
                          format: date-time
                  meta:
                    "$ref": "#/components/schemas/Pagination"
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
        '403':
          description: Missing creatives:read scope
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
    post:
      summary: Upload a new creative
      tags:
      - Creatives
      security:
      - bearer_auth: []
      description: Create a creative record and optionally attach a video/image file.
        Requires scope `creatives:write`.
      parameters: []
      responses:
        '201':
          description: Creative created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: integer
                        example: 301
                      name:
                        type: string
                        example: winter_hook_v3
                      file_type:
                        type: string
                        enum:
                        - video
                        - image
                        example: video
                      file_size:
                        type: integer
                        nullable: true
                        example: 15482112
                      status:
                        type: string
                        example: ready
                      meta_id:
                        type: string
                        nullable: true
                        description: Meta video ID or image hash
                        example: '12345678901234567'
                      ad_text:
                        type: string
                        nullable: true
                        example: Tired of ads? Try this one trick…
                      headline:
                        type: string
                        nullable: true
                        example: Learn the secret
                      description:
                        type: string
                        nullable: true
                        example: Limited time offer
                      cta:
                        type: string
                        nullable: true
                        example: LEARN_MORE
                      ad_texts:
                        type: array
                        items:
                          type: string
                        description: For multi-text creatives
                      headlines:
                        type: array
                        items:
                          type: string
                      thumbnail_url:
                        type: string
                        nullable: true
                        format: uri
                      created_at:
                        type: string
                        format: date-time
        '422':
          description: Validation failed
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
      requestBody:
        content:
          multipart/form-data:
            schema:
              type: string
        required: true
        description: Creative display name
  "/api/v1/creatives/{id}":
    parameters:
    - name: id
      in: path
      required: true
      schema:
        type: integer
    get:
      summary: Get a single creative
      tags:
      - Creatives
      security:
      - bearer_auth: []
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: integer
                        example: 301
                      name:
                        type: string
                        example: winter_hook_v3
                      file_type:
                        type: string
                        enum:
                        - video
                        - image
                        example: video
                      file_size:
                        type: integer
                        nullable: true
                        example: 15482112
                      status:
                        type: string
                        example: ready
                      meta_id:
                        type: string
                        nullable: true
                        description: Meta video ID or image hash
                        example: '12345678901234567'
                      ad_text:
                        type: string
                        nullable: true
                        example: Tired of ads? Try this one trick…
                      headline:
                        type: string
                        nullable: true
                        example: Learn the secret
                      description:
                        type: string
                        nullable: true
                        example: Limited time offer
                      cta:
                        type: string
                        nullable: true
                        example: LEARN_MORE
                      ad_texts:
                        type: array
                        items:
                          type: string
                        description: For multi-text creatives
                      headlines:
                        type: array
                        items:
                          type: string
                      thumbnail_url:
                        type: string
                        nullable: true
                        format: uri
                      created_at:
                        type: string
                        format: date-time
        '404':
          description: Creative not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
    delete:
      summary: Delete a creative
      tags:
      - Creatives
      security:
      - bearer_auth: []
      description: Permanently deletes the creative record and its attached file.
        Requires scope `creatives:write`.
      responses:
        '200':
          description: Deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: integer
                        example: 301
                      deleted:
                        type: boolean
                        example: true
        '404':
          description: Creative not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
  "/api/v1/hunted_creatives":
    get:
      summary: List hunted creatives (scraped competitor ads)
      tags:
      - Hunted Creatives
      security:
      - bearer_auth: []
      description: |
        Returns the user's hunted creatives. Used primarily by the ScaleForge
        Chrome extension and the Hunter Mode dashboard. Requires scope
        `hunted_creatives:read`.

        NOTE: this endpoint predates the standard envelope — response keys
        are `creatives` + `meta` (not `data`).
      parameters:
      - name: status
        in: query
        required: false
        schema:
          type: string
        description: Filter by status (e.g. `new`, `processed`)
      - name: page
        in: query
        required: false
        schema:
          type: integer
          default: 1
      - name: per_page
        in: query
        required: false
        schema:
          type: integer
          default: 25
          maximum: 100
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  creatives:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: integer
                          example: 551
                        library_id:
                          type: string
                          example: '1234567890123456'
                          description: Meta Ad Library ID (natural key)
                        page_title:
                          type: string
                          nullable: true
                          example: Competitor Page
                        page_avatar:
                          type: string
                          nullable: true
                          format: uri
                        ad_text:
                          type: string
                          nullable: true
                        call_to_action:
                          type: string
                          nullable: true
                          example: LEARN_MORE
                        link_url:
                          type: string
                          nullable: true
                          format: uri
                        link_headline:
                          type: string
                          nullable: true
                        link_description:
                          type: string
                          nullable: true
                        start_date:
                          type: string
                          nullable: true
                          format: date
                        days_running:
                          type: integer
                          nullable: true
                          example: 14
                        platforms:
                          type: array
                          items:
                            type: string
                          example:
                          - Facebook
                          - Instagram
                        image_url:
                          type: string
                          nullable: true
                          format: uri
                        video_url:
                          type: string
                          nullable: true
                          format: uri
                        video_poster_url:
                          type: string
                          nullable: true
                          format: uri
                        status:
                          type: string
                          nullable: true
                          example: new
                        created_at:
                          type: string
                          format: date-time
                  meta:
                    "$ref": "#/components/schemas/Pagination"
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
        '403':
          description: Missing hunted_creatives:read scope
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
    post:
      summary: Bulk upsert hunted creatives (Chrome extension endpoint)
      tags:
      - Hunted Creatives
      security:
      - bearer_auth: []
      description: |
        Accepts a list of creatives scraped from Meta Ad Library. Records are
        matched/upserted by `library_id`. Requires scope `hunted_creatives:write`.
      parameters: []
      responses:
        '201':
          description: Upserted
          content:
            application/json:
              schema:
                type: object
                properties:
                  created:
                    type: integer
                    example: 12
                  updated:
                    type: integer
                    example: 3
                  total:
                    type: integer
                    example: 15
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
              - creatives
              properties:
                creatives:
                  type: array
                  items:
                    type: object
                    required:
                    - library_id
                    properties:
                      library_id:
                        type: string
                        example: '1234567890123456'
                      page_title:
                        type: string
                      page_avatar:
                        type: string
                        format: uri
                      ad_text:
                        type: string
                      call_to_action:
                        type: string
                      link_url:
                        type: string
                        format: uri
                      link_headline:
                        type: string
                      link_description:
                        type: string
                      start_date:
                        type: string
                        format: date
                      days_running:
                        type: integer
                      platforms:
                        type: array
                        items:
                          type: string
                      image_url:
                        type: string
                        format: uri
                      video_url:
                        type: string
                        format: uri
                      video_poster_url:
                        type: string
                        format: uri
        required: true
  "/api/v1/hunted_creatives/{id}":
    parameters:
    - name: id
      in: path
      required: true
      description: Ad Library ID (natural key)
      schema:
        type: string
    delete:
      summary: Delete a hunted creative by Ad Library ID
      tags:
      - Hunted Creatives
      security:
      - bearer_auth: []
      description: 'NOTE: `id` is the Meta Ad Library ID (`library_id`), not the DB
        primary key.'
      responses:
        '200':
          description: Deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  deleted:
                    type: boolean
                    example: true
        '404':
          description: Creative not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    example: Creative not found
  "/api/v1/posts":
    get:
      summary: List Meta Page posts
      tags:
      - Posts
      security:
      - bearer_auth: []
      description: Paginated. Requires scope `posts:read`.
      parameters:
      - name: meta_page_id
        in: query
        required: false
        schema:
          type: string
        description: Filter by page
      - name: page
        in: query
        required: false
        schema:
          type: integer
          default: 1
      - name: per_page
        in: query
        required: false
        schema:
          type: integer
          default: 25
          maximum: 100
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: integer
                          example: 88
                        meta_page_id:
                          type: string
                          example: '100012345678901'
                        page_name:
                          type: string
                          nullable: true
                          example: Consumer Insights Daily
                        message:
                          type: string
                          example: Check out these amazing travel deals!
                        content_type:
                          type: string
                          enum:
                          - text
                          - image
                          - link
                          - video
                          example: link
                        status:
                          type: string
                          enum:
                          - draft
                          - queued
                          - published
                          - failed
                          example: draft
                        meta_post_id:
                          type: string
                          nullable: true
                          example: '100012345678901_9876543210'
                        published_at:
                          type: string
                          format: date-time
                          nullable: true
                        created_at:
                          type: string
                          format: date-time
                  meta:
                    "$ref": "#/components/schemas/Pagination"
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
    post:
      summary: Create a draft post
      tags:
      - Posts
      security:
      - bearer_auth: []
      description: Requires scope `posts:write`.
      parameters: []
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: integer
                        example: 88
                      meta_page_id:
                        type: string
                        example: '100012345678901'
                      page_name:
                        type: string
                        nullable: true
                        example: Consumer Insights Daily
                      message:
                        type: string
                        example: Check out these amazing travel deals!
                      content_type:
                        type: string
                        enum:
                        - text
                        - image
                        - link
                        - video
                        example: link
                      status:
                        type: string
                        enum:
                        - draft
                        - queued
                        - published
                        - failed
                        example: draft
                      meta_post_id:
                        type: string
                        nullable: true
                        example: '100012345678901_9876543210'
                      published_at:
                        type: string
                        format: date-time
                        nullable: true
                      created_at:
                        type: string
                        format: date-time
        '422':
          description: Validation failed
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
              - meta_page_id
              - message
              properties:
                meta_page_id:
                  type: string
                message:
                  type: string
                content_type:
                  type: string
                  enum:
                  - text
                  - image
                  - link
                  - video
                link_url:
                  type: string
                  format: uri
                  nullable: true
        required: true
  "/api/v1/posts/{id}":
    parameters:
    - name: id
      in: path
      required: true
      schema:
        type: integer
    get:
      summary: Get a single post
      tags:
      - Posts
      security:
      - bearer_auth: []
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: integer
                        example: 88
                      meta_page_id:
                        type: string
                        example: '100012345678901'
                      page_name:
                        type: string
                        nullable: true
                        example: Consumer Insights Daily
                      message:
                        type: string
                        example: Check out these amazing travel deals!
                      content_type:
                        type: string
                        enum:
                        - text
                        - image
                        - link
                        - video
                        example: link
                      status:
                        type: string
                        enum:
                        - draft
                        - queued
                        - published
                        - failed
                        example: draft
                      meta_post_id:
                        type: string
                        nullable: true
                        example: '100012345678901_9876543210'
                      published_at:
                        type: string
                        format: date-time
                        nullable: true
                      created_at:
                        type: string
                        format: date-time
        '404':
          description: Post not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
    patch:
      summary: Update a post
      tags:
      - Posts
      security:
      - bearer_auth: []
      parameters: []
      responses:
        '200':
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: integer
                        example: 88
                      meta_page_id:
                        type: string
                        example: '100012345678901'
                      page_name:
                        type: string
                        nullable: true
                        example: Consumer Insights Daily
                      message:
                        type: string
                        example: Check out these amazing travel deals!
                      content_type:
                        type: string
                        enum:
                        - text
                        - image
                        - link
                        - video
                        example: link
                      status:
                        type: string
                        enum:
                        - draft
                        - queued
                        - published
                        - failed
                        example: draft
                      meta_post_id:
                        type: string
                        nullable: true
                        example: '100012345678901_9876543210'
                      published_at:
                        type: string
                        format: date-time
                        nullable: true
                      created_at:
                        type: string
                        format: date-time
        '422':
          description: Validation failed
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
        '404':
          description: Post not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
              - meta_page_id
              - message
              properties:
                meta_page_id:
                  type: string
                message:
                  type: string
                content_type:
                  type: string
                  enum:
                  - text
                  - image
                  - link
                  - video
                link_url:
                  type: string
                  format: uri
                  nullable: true
        required: true
    put:
      summary: Replace a post
      tags:
      - Posts
      security:
      - bearer_auth: []
      parameters: []
      responses:
        '200':
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: integer
                        example: 88
                      meta_page_id:
                        type: string
                        example: '100012345678901'
                      page_name:
                        type: string
                        nullable: true
                        example: Consumer Insights Daily
                      message:
                        type: string
                        example: Check out these amazing travel deals!
                      content_type:
                        type: string
                        enum:
                        - text
                        - image
                        - link
                        - video
                        example: link
                      status:
                        type: string
                        enum:
                        - draft
                        - queued
                        - published
                        - failed
                        example: draft
                      meta_post_id:
                        type: string
                        nullable: true
                        example: '100012345678901_9876543210'
                      published_at:
                        type: string
                        format: date-time
                        nullable: true
                      created_at:
                        type: string
                        format: date-time
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
              - meta_page_id
              - message
              properties:
                meta_page_id:
                  type: string
                message:
                  type: string
                content_type:
                  type: string
                  enum:
                  - text
                  - image
                  - link
                  - video
                link_url:
                  type: string
                  format: uri
                  nullable: true
        required: true
    delete:
      summary: Delete a post
      tags:
      - Posts
      security:
      - bearer_auth: []
      responses:
        '200':
          description: Deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: integer
                        example: 88
                      deleted:
                        type: boolean
                        example: true
        '404':
          description: Post not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
  "/api/v1/posts/{id}/publish":
    parameters:
    - name: id
      in: path
      required: true
      schema:
        type: integer
    post:
      summary: Publish a post to Meta
      tags:
      - Posts
      security:
      - bearer_auth: []
      description: Enqueues `PublishFanPagePostJob` and sets status to `queued`. Requires
        scope `posts:write`.
      responses:
        '200':
          description: Queued
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: integer
                        example: 88
                      status:
                        type: string
                        example: queued
        '404':
          description: Post not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
  "/api/v1/rule_sets":
    get:
      summary: List rule sets
      tags:
      - Rule Sets
      security:
      - bearer_auth: []
      description: Paginated. Requires scope `rules:read`.
      parameters:
      - name: page
        in: query
        required: false
        schema:
          type: integer
          default: 1
      - name: per_page
        in: query
        required: false
        schema:
          type: integer
          default: 25
          maximum: 100
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: integer
                          example: 55
                        name:
                          type: string
                          example: Pause losers
                        action:
                          type: string
                          enum:
                          - pause
                          - activate
                          - bid_up
                          - bid_down
                          example: pause
                        bid_step:
                          type: number
                          nullable: true
                          example: 0.05
                        bid_min:
                          type: number
                          nullable: true
                          example: 2.0
                        bid_max:
                          type: number
                          nullable: true
                          example: 8.0
                        schedule_type:
                          type: string
                          enum:
                          - interval
                          - hourly
                          - daily
                          example: interval
                        schedule_value:
                          type: integer
                          nullable: true
                          example: 30
                          description: Minutes for interval type
                        data_period:
                          type: string
                          example: today
                        enabled:
                          type: boolean
                          example: true
                        cooldown_minutes:
                          type: integer
                          example: 60
                        dry_run:
                          type: boolean
                          example: false
                        max_daily_actions:
                          type: integer
                          nullable: true
                          example: 50
                        target_level:
                          type: string
                          enum:
                          - adset
                          - campaign
                          example: adset
                        next_run_at:
                          type: string
                          format: date-time
                          nullable: true
                        created_at:
                          type: string
                          format: date-time
                  meta:
                    "$ref": "#/components/schemas/Pagination"
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
    post:
      summary: Create a rule set with nested rules
      tags:
      - Rule Sets
      security:
      - bearer_auth: []
      description: Creates a `RuleSet` and any nested conditions in `rules[]`. Requires
        scope `rules:write`.
      parameters: []
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    allOf:
                    - type: object
                      properties:
                        id:
                          type: integer
                          example: 55
                        name:
                          type: string
                          example: Pause losers
                        action:
                          type: string
                          enum:
                          - pause
                          - activate
                          - bid_up
                          - bid_down
                          example: pause
                        bid_step:
                          type: number
                          nullable: true
                          example: 0.05
                        bid_min:
                          type: number
                          nullable: true
                          example: 2.0
                        bid_max:
                          type: number
                          nullable: true
                          example: 8.0
                        schedule_type:
                          type: string
                          enum:
                          - interval
                          - hourly
                          - daily
                          example: interval
                        schedule_value:
                          type: integer
                          nullable: true
                          example: 30
                          description: Minutes for interval type
                        data_period:
                          type: string
                          example: today
                        enabled:
                          type: boolean
                          example: true
                        cooldown_minutes:
                          type: integer
                          example: 60
                        dry_run:
                          type: boolean
                          example: false
                        max_daily_actions:
                          type: integer
                          nullable: true
                          example: 50
                        target_level:
                          type: string
                          enum:
                          - adset
                          - campaign
                          example: adset
                        next_run_at:
                          type: string
                          format: date-time
                          nullable: true
                        created_at:
                          type: string
                          format: date-time
                    - type: object
                      properties:
                        rules:
                          type: array
                          items:
                            type: object
                            properties:
                              id:
                                type: integer
                                example: 101
                              metric:
                                type: string
                                example: cpl
                              operator:
                                type: string
                                enum:
                                - gt
                                - gte
                                - lt
                                - lte
                                - eq
                                example: gt
                              value:
                                type: number
                                example: 5.0
        '422':
          description: Validation failed
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                rule_set:
                  type: object
                  required:
                  - name
                  - action
                  properties:
                    name:
                      type: string
                      example: Pause losers
                    action:
                      type: string
                      enum:
                      - pause
                      - activate
                      - bid_up
                      - bid_down
                      example: pause
                    bid_step:
                      type: number
                      example: 0.05
                    bid_min:
                      type: number
                      example: 2.0
                    bid_max:
                      type: number
                      example: 8.0
                    schedule_type:
                      type: string
                      example: interval
                    schedule_value:
                      type: integer
                      example: 30
                    schedule_start_hour:
                      type: integer
                      example: 9
                    schedule_end_hour:
                      type: integer
                      example: 23
                    data_period:
                      type: string
                      example: today
                    enabled:
                      type: boolean
                      default: true
                    cooldown_minutes:
                      type: integer
                      example: 60
                    dry_run:
                      type: boolean
                      default: false
                    max_daily_actions:
                      type: integer
                      example: 50
                    target_level:
                      type: string
                      example: adset
                rules:
                  type: array
                  items:
                    type: object
                    required:
                    - metric
                    - operator
                    - value
                    properties:
                      metric:
                        type: string
                        example: cpl
                      operator:
                        type: string
                        example: gt
                      value:
                        type: number
                        example: 5.0
        required: true
  "/api/v1/rule_sets/{id}":
    parameters:
    - name: id
      in: path
      required: true
      schema:
        type: integer
    get:
      summary: Get a rule set with its rules
      tags:
      - Rule Sets
      security:
      - bearer_auth: []
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    allOf:
                    - type: object
                      properties:
                        id:
                          type: integer
                          example: 55
                        name:
                          type: string
                          example: Pause losers
                        action:
                          type: string
                          enum:
                          - pause
                          - activate
                          - bid_up
                          - bid_down
                          example: pause
                        bid_step:
                          type: number
                          nullable: true
                          example: 0.05
                        bid_min:
                          type: number
                          nullable: true
                          example: 2.0
                        bid_max:
                          type: number
                          nullable: true
                          example: 8.0
                        schedule_type:
                          type: string
                          enum:
                          - interval
                          - hourly
                          - daily
                          example: interval
                        schedule_value:
                          type: integer
                          nullable: true
                          example: 30
                          description: Minutes for interval type
                        data_period:
                          type: string
                          example: today
                        enabled:
                          type: boolean
                          example: true
                        cooldown_minutes:
                          type: integer
                          example: 60
                        dry_run:
                          type: boolean
                          example: false
                        max_daily_actions:
                          type: integer
                          nullable: true
                          example: 50
                        target_level:
                          type: string
                          enum:
                          - adset
                          - campaign
                          example: adset
                        next_run_at:
                          type: string
                          format: date-time
                          nullable: true
                        created_at:
                          type: string
                          format: date-time
                    - type: object
                      properties:
                        rules:
                          type: array
                          items:
                            type: object
                            properties:
                              id:
                                type: integer
                                example: 101
                              metric:
                                type: string
                                example: cpl
                              operator:
                                type: string
                                enum:
                                - gt
                                - gte
                                - lt
                                - lte
                                - eq
                                example: gt
                              value:
                                type: number
                                example: 5.0
        '404':
          description: Rule set not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
    patch:
      summary: Update a rule set
      tags:
      - Rule Sets
      security:
      - bearer_auth: []
      parameters: []
      responses:
        '200':
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: integer
                        example: 55
                      name:
                        type: string
                        example: Pause losers
                      action:
                        type: string
                        enum:
                        - pause
                        - activate
                        - bid_up
                        - bid_down
                        example: pause
                      bid_step:
                        type: number
                        nullable: true
                        example: 0.05
                      bid_min:
                        type: number
                        nullable: true
                        example: 2.0
                      bid_max:
                        type: number
                        nullable: true
                        example: 8.0
                      schedule_type:
                        type: string
                        enum:
                        - interval
                        - hourly
                        - daily
                        example: interval
                      schedule_value:
                        type: integer
                        nullable: true
                        example: 30
                        description: Minutes for interval type
                      data_period:
                        type: string
                        example: today
                      enabled:
                        type: boolean
                        example: true
                      cooldown_minutes:
                        type: integer
                        example: 60
                      dry_run:
                        type: boolean
                        example: false
                      max_daily_actions:
                        type: integer
                        nullable: true
                        example: 50
                      target_level:
                        type: string
                        enum:
                        - adset
                        - campaign
                        example: adset
                      next_run_at:
                        type: string
                        format: date-time
                        nullable: true
                      created_at:
                        type: string
                        format: date-time
        '422':
          description: Validation failed
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
        '404':
          description: Rule set not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                rule_set:
                  type: object
                  required:
                  - name
                  - action
                  properties:
                    name:
                      type: string
                      example: Pause losers
                    action:
                      type: string
                      enum:
                      - pause
                      - activate
                      - bid_up
                      - bid_down
                      example: pause
                    bid_step:
                      type: number
                      example: 0.05
                    bid_min:
                      type: number
                      example: 2.0
                    bid_max:
                      type: number
                      example: 8.0
                    schedule_type:
                      type: string
                      example: interval
                    schedule_value:
                      type: integer
                      example: 30
                    schedule_start_hour:
                      type: integer
                      example: 9
                    schedule_end_hour:
                      type: integer
                      example: 23
                    data_period:
                      type: string
                      example: today
                    enabled:
                      type: boolean
                      default: true
                    cooldown_minutes:
                      type: integer
                      example: 60
                    dry_run:
                      type: boolean
                      default: false
                    max_daily_actions:
                      type: integer
                      example: 50
                    target_level:
                      type: string
                      example: adset
                rules:
                  type: array
                  items:
                    type: object
                    required:
                    - metric
                    - operator
                    - value
                    properties:
                      metric:
                        type: string
                        example: cpl
                      operator:
                        type: string
                        example: gt
                      value:
                        type: number
                        example: 5.0
        required: true
    put:
      summary: Replace a rule set
      tags:
      - Rule Sets
      security:
      - bearer_auth: []
      parameters: []
      responses:
        '200':
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: integer
                        example: 55
                      name:
                        type: string
                        example: Pause losers
                      action:
                        type: string
                        enum:
                        - pause
                        - activate
                        - bid_up
                        - bid_down
                        example: pause
                      bid_step:
                        type: number
                        nullable: true
                        example: 0.05
                      bid_min:
                        type: number
                        nullable: true
                        example: 2.0
                      bid_max:
                        type: number
                        nullable: true
                        example: 8.0
                      schedule_type:
                        type: string
                        enum:
                        - interval
                        - hourly
                        - daily
                        example: interval
                      schedule_value:
                        type: integer
                        nullable: true
                        example: 30
                        description: Minutes for interval type
                      data_period:
                        type: string
                        example: today
                      enabled:
                        type: boolean
                        example: true
                      cooldown_minutes:
                        type: integer
                        example: 60
                      dry_run:
                        type: boolean
                        example: false
                      max_daily_actions:
                        type: integer
                        nullable: true
                        example: 50
                      target_level:
                        type: string
                        enum:
                        - adset
                        - campaign
                        example: adset
                      next_run_at:
                        type: string
                        format: date-time
                        nullable: true
                      created_at:
                        type: string
                        format: date-time
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                rule_set:
                  type: object
                  required:
                  - name
                  - action
                  properties:
                    name:
                      type: string
                      example: Pause losers
                    action:
                      type: string
                      enum:
                      - pause
                      - activate
                      - bid_up
                      - bid_down
                      example: pause
                    bid_step:
                      type: number
                      example: 0.05
                    bid_min:
                      type: number
                      example: 2.0
                    bid_max:
                      type: number
                      example: 8.0
                    schedule_type:
                      type: string
                      example: interval
                    schedule_value:
                      type: integer
                      example: 30
                    schedule_start_hour:
                      type: integer
                      example: 9
                    schedule_end_hour:
                      type: integer
                      example: 23
                    data_period:
                      type: string
                      example: today
                    enabled:
                      type: boolean
                      default: true
                    cooldown_minutes:
                      type: integer
                      example: 60
                    dry_run:
                      type: boolean
                      default: false
                    max_daily_actions:
                      type: integer
                      example: 50
                    target_level:
                      type: string
                      example: adset
                rules:
                  type: array
                  items:
                    type: object
                    required:
                    - metric
                    - operator
                    - value
                    properties:
                      metric:
                        type: string
                        example: cpl
                      operator:
                        type: string
                        example: gt
                      value:
                        type: number
                        example: 5.0
        required: true
    delete:
      summary: Delete a rule set
      tags:
      - Rule Sets
      security:
      - bearer_auth: []
      responses:
        '200':
          description: Deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: integer
                        example: 55
                      deleted:
                        type: boolean
                        example: true
        '404':
          description: Rule set not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
  "/api/v1/rule_sets/{id}/execute":
    parameters:
    - name: id
      in: path
      required: true
      schema:
        type: integer
    post:
      summary: Trigger immediate execution of a rule set
      tags:
      - Rule Sets
      security:
      - bearer_auth: []
      description: Enqueues `RuleSchedulerJob` to run the rule set right now, bypassing
        its schedule. Requires scope `rules:write`.
      responses:
        '200':
          description: Queued
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: integer
                        example: 55
                      status:
                        type: string
                        example: queued
        '404':
          description: Rule set not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
  "/api/v1/templates":
    get:
      summary: List campaign templates
      tags:
      - Templates
      security:
      - bearer_auth: []
      description: Paginated list of the user's reusable campaign templates. Requires
        scope `campaigns:read`.
      parameters:
      - name: page
        in: query
        required: false
        schema:
          type: integer
          default: 1
      - name: per_page
        in: query
        required: false
        schema:
          type: integer
          default: 25
          maximum: 100
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: integer
                          example: 12
                        name:
                          type: string
                          example: US-Desktop-Purchase
                        product:
                          type: string
                          nullable: true
                          example: SweepstakesOffer
                        objective:
                          type: string
                          example: OUTCOME_SALES
                        status:
                          type: string
                          example: ACTIVE
                        daily_budget:
                          type: number
                          nullable: true
                          example: 100.0
                        audience_name:
                          type: string
                          nullable: true
                          example: US 18-55 Interests
                        age_min:
                          type: integer
                          nullable: true
                          example: 18
                        age_max:
                          type: integer
                          nullable: true
                          example: 55
                        genders:
                          type: string
                          nullable: true
                          example: all
                        bid_strategy:
                          type: string
                          nullable: true
                          example: LOWEST_COST_WITH_BID_CAP
                        start_bid:
                          type: number
                          nullable: true
                          example: 4.4
                        bid_step:
                          type: number
                          nullable: true
                          example: 0.05
                        bid_count:
                          type: integer
                          nullable: true
                          example: 5
                        primary_text:
                          type: string
                          nullable: true
                        headline:
                          type: string
                          nullable: true
                        link_url:
                          type: string
                          nullable: true
                          format: uri
                        cta:
                          type: string
                          nullable: true
                          example: LEARN_MORE
                        page_id:
                          type: string
                          nullable: true
                        pixel_id:
                          type: string
                          nullable: true
                        conversion_event:
                          type: string
                          nullable: true
                          example: Purchase
                        geo_locations:
                          type: array
                          items:
                            type: string
                          example:
                          - US
                          - CA
                        placements:
                          type: array
                          items:
                            type: string
                        budget_optimization:
                          type: string
                          enum:
                          - CBO
                          - ABO
                          example: CBO
                          nullable: true
                        naming_template:
                          type: string
                          nullable: true
                          example: "{product}_{geo}_{bid}"
                        times_used:
                          type: integer
                          example: 3
                        created_at:
                          type: string
                          format: date-time
                        updated_at:
                          type: string
                          format: date-time
                  meta:
                    "$ref": "#/components/schemas/Pagination"
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
    post:
      summary: Create a template
      tags:
      - Templates
      security:
      - bearer_auth: []
      description: Requires scope `campaigns:write`.
      parameters: []
      responses:
        '201':
          description: Template created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: integer
                        example: 12
                      name:
                        type: string
                        example: US-Desktop-Purchase
                      product:
                        type: string
                        nullable: true
                        example: SweepstakesOffer
                      objective:
                        type: string
                        example: OUTCOME_SALES
                      status:
                        type: string
                        example: ACTIVE
                      daily_budget:
                        type: number
                        nullable: true
                        example: 100.0
                      audience_name:
                        type: string
                        nullable: true
                        example: US 18-55 Interests
                      age_min:
                        type: integer
                        nullable: true
                        example: 18
                      age_max:
                        type: integer
                        nullable: true
                        example: 55
                      genders:
                        type: string
                        nullable: true
                        example: all
                      bid_strategy:
                        type: string
                        nullable: true
                        example: LOWEST_COST_WITH_BID_CAP
                      start_bid:
                        type: number
                        nullable: true
                        example: 4.4
                      bid_step:
                        type: number
                        nullable: true
                        example: 0.05
                      bid_count:
                        type: integer
                        nullable: true
                        example: 5
                      primary_text:
                        type: string
                        nullable: true
                      headline:
                        type: string
                        nullable: true
                      link_url:
                        type: string
                        nullable: true
                        format: uri
                      cta:
                        type: string
                        nullable: true
                        example: LEARN_MORE
                      page_id:
                        type: string
                        nullable: true
                      pixel_id:
                        type: string
                        nullable: true
                      conversion_event:
                        type: string
                        nullable: true
                        example: Purchase
                      geo_locations:
                        type: array
                        items:
                          type: string
                        example:
                        - US
                        - CA
                      placements:
                        type: array
                        items:
                          type: string
                      budget_optimization:
                        type: string
                        enum:
                        - CBO
                        - ABO
                        example: CBO
                        nullable: true
                      naming_template:
                        type: string
                        nullable: true
                        example: "{product}_{geo}_{bid}"
                      times_used:
                        type: integer
                        example: 3
                      created_at:
                        type: string
                        format: date-time
                      updated_at:
                        type: string
                        format: date-time
        '422':
          description: Validation failed
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                product:
                  type: string
                objective:
                  type: string
                  example: OUTCOME_SALES
                status:
                  type: string
                  example: ACTIVE
                daily_budget:
                  type: number
                  example: 100.0
                audience_name:
                  type: string
                age_min:
                  type: integer
                  example: 18
                age_max:
                  type: integer
                  example: 55
                genders:
                  type: string
                  example: all
                bid_strategy:
                  type: string
                  example: LOWEST_COST_WITH_BID_CAP
                start_bid:
                  type: number
                  example: 4.4
                bid_step:
                  type: number
                  example: 0.05
                bid_count:
                  type: integer
                  example: 5
                primary_text:
                  type: string
                headline:
                  type: string
                link_url:
                  type: string
                  format: uri
                cta:
                  type: string
                  example: LEARN_MORE
                page_id:
                  type: string
                pixel_id:
                  type: string
                conversion_event:
                  type: string
                  example: Purchase
                budget_optimization:
                  type: string
                  enum:
                  - CBO
                  - ABO
                naming_template:
                  type: string
                url_tags:
                  type: string
                include_bid_in_name:
                  type: boolean
                geo_locations:
                  type: array
                  items:
                    type: string
                placements:
                  type: array
                  items:
                    type: string
                interests:
                  type: array
                  items:
                    type: object
                custom_audiences:
                  type: array
                  items:
                    type: string
                exclusions:
                  type: array
                  items:
                    type: object
                device_platforms:
                  type: array
                  items:
                    type: string
                languages:
                  type: array
                  items:
                    type: string
                publisher_platforms:
                  type: array
                  items:
                    type: string
        required: true
  "/api/v1/templates/{id}":
    parameters:
    - name: id
      in: path
      required: true
      schema:
        type: integer
    get:
      summary: Get a single template
      tags:
      - Templates
      security:
      - bearer_auth: []
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: integer
                        example: 12
                      name:
                        type: string
                        example: US-Desktop-Purchase
                      product:
                        type: string
                        nullable: true
                        example: SweepstakesOffer
                      objective:
                        type: string
                        example: OUTCOME_SALES
                      status:
                        type: string
                        example: ACTIVE
                      daily_budget:
                        type: number
                        nullable: true
                        example: 100.0
                      audience_name:
                        type: string
                        nullable: true
                        example: US 18-55 Interests
                      age_min:
                        type: integer
                        nullable: true
                        example: 18
                      age_max:
                        type: integer
                        nullable: true
                        example: 55
                      genders:
                        type: string
                        nullable: true
                        example: all
                      bid_strategy:
                        type: string
                        nullable: true
                        example: LOWEST_COST_WITH_BID_CAP
                      start_bid:
                        type: number
                        nullable: true
                        example: 4.4
                      bid_step:
                        type: number
                        nullable: true
                        example: 0.05
                      bid_count:
                        type: integer
                        nullable: true
                        example: 5
                      primary_text:
                        type: string
                        nullable: true
                      headline:
                        type: string
                        nullable: true
                      link_url:
                        type: string
                        nullable: true
                        format: uri
                      cta:
                        type: string
                        nullable: true
                        example: LEARN_MORE
                      page_id:
                        type: string
                        nullable: true
                      pixel_id:
                        type: string
                        nullable: true
                      conversion_event:
                        type: string
                        nullable: true
                        example: Purchase
                      geo_locations:
                        type: array
                        items:
                          type: string
                        example:
                        - US
                        - CA
                      placements:
                        type: array
                        items:
                          type: string
                      budget_optimization:
                        type: string
                        enum:
                        - CBO
                        - ABO
                        example: CBO
                        nullable: true
                      naming_template:
                        type: string
                        nullable: true
                        example: "{product}_{geo}_{bid}"
                      times_used:
                        type: integer
                        example: 3
                      created_at:
                        type: string
                        format: date-time
                      updated_at:
                        type: string
                        format: date-time
        '404':
          description: Template not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
    patch:
      summary: Update a template
      tags:
      - Templates
      security:
      - bearer_auth: []
      parameters: []
      responses:
        '200':
          description: Template updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: integer
                        example: 12
                      name:
                        type: string
                        example: US-Desktop-Purchase
                      product:
                        type: string
                        nullable: true
                        example: SweepstakesOffer
                      objective:
                        type: string
                        example: OUTCOME_SALES
                      status:
                        type: string
                        example: ACTIVE
                      daily_budget:
                        type: number
                        nullable: true
                        example: 100.0
                      audience_name:
                        type: string
                        nullable: true
                        example: US 18-55 Interests
                      age_min:
                        type: integer
                        nullable: true
                        example: 18
                      age_max:
                        type: integer
                        nullable: true
                        example: 55
                      genders:
                        type: string
                        nullable: true
                        example: all
                      bid_strategy:
                        type: string
                        nullable: true
                        example: LOWEST_COST_WITH_BID_CAP
                      start_bid:
                        type: number
                        nullable: true
                        example: 4.4
                      bid_step:
                        type: number
                        nullable: true
                        example: 0.05
                      bid_count:
                        type: integer
                        nullable: true
                        example: 5
                      primary_text:
                        type: string
                        nullable: true
                      headline:
                        type: string
                        nullable: true
                      link_url:
                        type: string
                        nullable: true
                        format: uri
                      cta:
                        type: string
                        nullable: true
                        example: LEARN_MORE
                      page_id:
                        type: string
                        nullable: true
                      pixel_id:
                        type: string
                        nullable: true
                      conversion_event:
                        type: string
                        nullable: true
                        example: Purchase
                      geo_locations:
                        type: array
                        items:
                          type: string
                        example:
                        - US
                        - CA
                      placements:
                        type: array
                        items:
                          type: string
                      budget_optimization:
                        type: string
                        enum:
                        - CBO
                        - ABO
                        example: CBO
                        nullable: true
                      naming_template:
                        type: string
                        nullable: true
                        example: "{product}_{geo}_{bid}"
                      times_used:
                        type: integer
                        example: 3
                      created_at:
                        type: string
                        format: date-time
                      updated_at:
                        type: string
                        format: date-time
        '422':
          description: Validation failed
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
        '404':
          description: Template not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                product:
                  type: string
                objective:
                  type: string
                  example: OUTCOME_SALES
                status:
                  type: string
                  example: ACTIVE
                daily_budget:
                  type: number
                  example: 100.0
                audience_name:
                  type: string
                age_min:
                  type: integer
                  example: 18
                age_max:
                  type: integer
                  example: 55
                genders:
                  type: string
                  example: all
                bid_strategy:
                  type: string
                  example: LOWEST_COST_WITH_BID_CAP
                start_bid:
                  type: number
                  example: 4.4
                bid_step:
                  type: number
                  example: 0.05
                bid_count:
                  type: integer
                  example: 5
                primary_text:
                  type: string
                headline:
                  type: string
                link_url:
                  type: string
                  format: uri
                cta:
                  type: string
                  example: LEARN_MORE
                page_id:
                  type: string
                pixel_id:
                  type: string
                conversion_event:
                  type: string
                  example: Purchase
                budget_optimization:
                  type: string
                  enum:
                  - CBO
                  - ABO
                naming_template:
                  type: string
                url_tags:
                  type: string
                include_bid_in_name:
                  type: boolean
                geo_locations:
                  type: array
                  items:
                    type: string
                placements:
                  type: array
                  items:
                    type: string
                interests:
                  type: array
                  items:
                    type: object
                custom_audiences:
                  type: array
                  items:
                    type: string
                exclusions:
                  type: array
                  items:
                    type: object
                device_platforms:
                  type: array
                  items:
                    type: string
                languages:
                  type: array
                  items:
                    type: string
                publisher_platforms:
                  type: array
                  items:
                    type: string
        required: true
    put:
      summary: Replace a template
      tags:
      - Templates
      security:
      - bearer_auth: []
      parameters: []
      responses:
        '200':
          description: Template updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: integer
                        example: 12
                      name:
                        type: string
                        example: US-Desktop-Purchase
                      product:
                        type: string
                        nullable: true
                        example: SweepstakesOffer
                      objective:
                        type: string
                        example: OUTCOME_SALES
                      status:
                        type: string
                        example: ACTIVE
                      daily_budget:
                        type: number
                        nullable: true
                        example: 100.0
                      audience_name:
                        type: string
                        nullable: true
                        example: US 18-55 Interests
                      age_min:
                        type: integer
                        nullable: true
                        example: 18
                      age_max:
                        type: integer
                        nullable: true
                        example: 55
                      genders:
                        type: string
                        nullable: true
                        example: all
                      bid_strategy:
                        type: string
                        nullable: true
                        example: LOWEST_COST_WITH_BID_CAP
                      start_bid:
                        type: number
                        nullable: true
                        example: 4.4
                      bid_step:
                        type: number
                        nullable: true
                        example: 0.05
                      bid_count:
                        type: integer
                        nullable: true
                        example: 5
                      primary_text:
                        type: string
                        nullable: true
                      headline:
                        type: string
                        nullable: true
                      link_url:
                        type: string
                        nullable: true
                        format: uri
                      cta:
                        type: string
                        nullable: true
                        example: LEARN_MORE
                      page_id:
                        type: string
                        nullable: true
                      pixel_id:
                        type: string
                        nullable: true
                      conversion_event:
                        type: string
                        nullable: true
                        example: Purchase
                      geo_locations:
                        type: array
                        items:
                          type: string
                        example:
                        - US
                        - CA
                      placements:
                        type: array
                        items:
                          type: string
                      budget_optimization:
                        type: string
                        enum:
                        - CBO
                        - ABO
                        example: CBO
                        nullable: true
                      naming_template:
                        type: string
                        nullable: true
                        example: "{product}_{geo}_{bid}"
                      times_used:
                        type: integer
                        example: 3
                      created_at:
                        type: string
                        format: date-time
                      updated_at:
                        type: string
                        format: date-time
        '422':
          description: Validation failed
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                product:
                  type: string
                objective:
                  type: string
                  example: OUTCOME_SALES
                status:
                  type: string
                  example: ACTIVE
                daily_budget:
                  type: number
                  example: 100.0
                audience_name:
                  type: string
                age_min:
                  type: integer
                  example: 18
                age_max:
                  type: integer
                  example: 55
                genders:
                  type: string
                  example: all
                bid_strategy:
                  type: string
                  example: LOWEST_COST_WITH_BID_CAP
                start_bid:
                  type: number
                  example: 4.4
                bid_step:
                  type: number
                  example: 0.05
                bid_count:
                  type: integer
                  example: 5
                primary_text:
                  type: string
                headline:
                  type: string
                link_url:
                  type: string
                  format: uri
                cta:
                  type: string
                  example: LEARN_MORE
                page_id:
                  type: string
                pixel_id:
                  type: string
                conversion_event:
                  type: string
                  example: Purchase
                budget_optimization:
                  type: string
                  enum:
                  - CBO
                  - ABO
                naming_template:
                  type: string
                url_tags:
                  type: string
                include_bid_in_name:
                  type: boolean
                geo_locations:
                  type: array
                  items:
                    type: string
                placements:
                  type: array
                  items:
                    type: string
                interests:
                  type: array
                  items:
                    type: object
                custom_audiences:
                  type: array
                  items:
                    type: string
                exclusions:
                  type: array
                  items:
                    type: object
                device_platforms:
                  type: array
                  items:
                    type: string
                languages:
                  type: array
                  items:
                    type: string
                publisher_platforms:
                  type: array
                  items:
                    type: string
        required: true
    delete:
      summary: Delete a template
      tags:
      - Templates
      security:
      - bearer_auth: []
      responses:
        '200':
          description: Deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: integer
                        example: 12
                      deleted:
                        type: boolean
                        example: true
        '404':
          description: Template not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
  "/api/v1/tokens":
    get:
      summary: List API tokens for the authenticated user
      tags:
      - Auth
      security:
      - bearer_auth: []
      description: Returns all API tokens belonging to the authenticated user, ordered
        by creation date (newest first).
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: integer
                          example: 42
                        name:
                          type: string
                          example: Chrome Extension
                        prefix:
                          type: string
                          example: sf_abcd
                        scopes:
                          type: array
                          items:
                            type: string
                          example:
                          - creatives:read
                          - campaigns:write
                        active:
                          type: boolean
                          example: true
                        last_used_at:
                          type: string
                          format: date-time
                          nullable: true
                        created_at:
                          type: string
                          format: date-time
        '401':
          description: Unauthorized — missing or invalid Bearer token
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
    post:
      summary: Create a new API token (email + password)
      tags:
      - Auth
      security: []
      description: |
        Authenticates the user with Devise email/password and returns a freshly
        issued raw API token. The raw token is returned ONCE; only its prefix
        and metadata are retrievable afterwards.
      parameters: []
      responses:
        '201':
          description: Token issued
          content:
            application/json:
              schema:
                type: object
                properties:
                  token:
                    type: string
                    example: sf_abc123xyz...
                    description: Raw token, shown only once
                  id:
                    type: integer
                    example: 42
                  name:
                    type: string
                    example: My CLI Token
                  prefix:
                    type: string
                    example: sf_abcd
                  scopes:
                    type: array
                    items:
                      type: string
                  created_at:
                    type: string
                    format: date-time
        '401':
          description: Invalid email or password
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
              - email
              - password
              properties:
                email:
                  type: string
                  format: email
                  example: user@example.com
                password:
                  type: string
                  format: password
                  example: s3cret!
                name:
                  type: string
                  example: My CLI Token
                scopes:
                  type: array
                  items:
                    type: string
                  example:
                  - creatives:read
                  - campaigns:read
                  - analytics:read
                  description: Subset of UserApiToken::VALID_SCOPES
        required: true
  "/api/v1/tokens/{id}":
    parameters:
    - name: id
      in: path
      required: true
      description: Token ID
      schema:
        type: integer
    delete:
      summary: Revoke an API token
      tags:
      - Auth
      security:
      - bearer_auth: []
      description: Deactivates (soft-deletes) the token. The raw token can no longer
        authenticate after this call.
      responses:
        '200':
          description: Token revoked
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: integer
                        example: 42
                      active:
                        type: boolean
                        example: false
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
        '404':
          description: Token not found or not owned by current user
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
  "/api/v1/uploads":
    get:
      summary: List bulk upload sessions
      tags:
      - Uploads
      security:
      - bearer_auth: []
      description: Paginated list. Requires scope `uploads:read`.
      parameters:
      - name: status
        in: query
        required: false
        schema:
          type: string
      - name: page
        in: query
        required: false
        schema:
          type: integer
          default: 1
      - name: per_page
        in: query
        required: false
        schema:
          type: integer
          default: 25
          maximum: 100
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: integer
                          example: 91
                        name:
                          type: string
                          example: Dec promo batch
                        status:
                          type: string
                          enum:
                          - queued
                          - processing
                          - paused
                          - cancelled
                          - completed
                          - failed
                          example: processing
                        total_campaigns:
                          type: integer
                          example: 25
                        completed_campaigns:
                          type: integer
                          example: 18
                        failed_campaigns:
                          type: integer
                          example: 1
                        multi_account:
                          type: boolean
                          example: false
                        started_at:
                          type: string
                          format: date-time
                          nullable: true
                        completed_at:
                          type: string
                          format: date-time
                          nullable: true
                        created_at:
                          type: string
                          format: date-time
                        updated_at:
                          type: string
                          format: date-time
                  meta:
                    "$ref": "#/components/schemas/Pagination"
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
    post:
      summary: Create a new bulk upload session
      tags:
      - Uploads
      security:
      - bearer_auth: []
      description: Creates an Upload record. Requires scope `uploads:write`.
      parameters: []
      responses:
        '201':
          description: Upload created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: integer
                        example: 91
                      name:
                        type: string
                        example: Dec promo batch
                      status:
                        type: string
                        enum:
                        - queued
                        - processing
                        - paused
                        - cancelled
                        - completed
                        - failed
                        example: processing
                      total_campaigns:
                        type: integer
                        example: 25
                      completed_campaigns:
                        type: integer
                        example: 18
                      failed_campaigns:
                        type: integer
                        example: 1
                      multi_account:
                        type: boolean
                        example: false
                      started_at:
                        type: string
                        format: date-time
                        nullable: true
                      completed_at:
                        type: string
                        format: date-time
                        nullable: true
                      created_at:
                        type: string
                        format: date-time
                      updated_at:
                        type: string
                        format: date-time
        '422':
          description: Validation failed
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
              - name
              - template_id
              properties:
                name:
                  type: string
                  example: Dec promo batch
                template_id:
                  type: integer
                  example: 12
                meta_account_id:
                  type: integer
                  example: 7
                  description: Ad Account ID for single-account uploads
                multi_account:
                  type: boolean
                  default: false
                  description: Fan-out across all user ad accounts
                creative_ids:
                  type: array
                  items:
                    type: integer
                  example:
                  - 301
                  - 302
                  - 303
        required: true
  "/api/v1/uploads/{id}":
    parameters:
    - name: id
      in: path
      required: true
      schema:
        type: integer
    get:
      summary: Get a single upload with its campaigns
      tags:
      - Uploads
      security:
      - bearer_auth: []
      description: Response includes nested `campaigns` array for every child campaign
        job.
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    allOf:
                    - type: object
                      properties:
                        id:
                          type: integer
                          example: 91
                        name:
                          type: string
                          example: Dec promo batch
                        status:
                          type: string
                          enum:
                          - queued
                          - processing
                          - paused
                          - cancelled
                          - completed
                          - failed
                          example: processing
                        total_campaigns:
                          type: integer
                          example: 25
                        completed_campaigns:
                          type: integer
                          example: 18
                        failed_campaigns:
                          type: integer
                          example: 1
                        multi_account:
                          type: boolean
                          example: false
                        started_at:
                          type: string
                          format: date-time
                          nullable: true
                        completed_at:
                          type: string
                          format: date-time
                          nullable: true
                        created_at:
                          type: string
                          format: date-time
                        updated_at:
                          type: string
                          format: date-time
                    - type: object
                      properties:
                        campaigns:
                          type: array
                          items:
                            type: object
                            properties:
                              id:
                                type: integer
                              campaign_name:
                                type: string
                                nullable: true
                              adset_name:
                                type: string
                                nullable: true
                              ad_name:
                                type: string
                                nullable: true
                              bid_amount:
                                type: number
                                nullable: true
                              status:
                                type: string
                                example: pending
                              meta_status:
                                type: string
                                nullable: true
                                example: ACTIVE
                              meta_campaign_id:
                                type: string
                                nullable: true
                              error_message:
                                type: string
                                nullable: true
                              is_excluded:
                                type: boolean
                                example: false
        '404':
          description: Upload not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
  "/api/v1/uploads/{id}/pause":
    parameters:
    - name: id
      in: path
      required: true
      schema:
        type: integer
    post:
      summary: Pause an in-flight upload
      tags:
      - Uploads
      security:
      - bearer_auth: []
      description: Sets the upload status to `paused`. Requires scope `uploads:write`.
      responses:
        '200':
          description: Paused
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: integer
                        example: 91
                      name:
                        type: string
                        example: Dec promo batch
                      status:
                        type: string
                        enum:
                        - queued
                        - processing
                        - paused
                        - cancelled
                        - completed
                        - failed
                        example: processing
                      total_campaigns:
                        type: integer
                        example: 25
                      completed_campaigns:
                        type: integer
                        example: 18
                      failed_campaigns:
                        type: integer
                        example: 1
                      multi_account:
                        type: boolean
                        example: false
                      started_at:
                        type: string
                        format: date-time
                        nullable: true
                      completed_at:
                        type: string
                        format: date-time
                        nullable: true
                      created_at:
                        type: string
                        format: date-time
                      updated_at:
                        type: string
                        format: date-time
        '404':
          description: Upload not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
  "/api/v1/uploads/{id}/resume":
    parameters:
    - name: id
      in: path
      required: true
      schema:
        type: integer
    post:
      summary: Resume a paused upload
      tags:
      - Uploads
      security:
      - bearer_auth: []
      description: Re-enqueues the upload for processing via BulkUploadJob. Requires
        scope `uploads:write`.
      responses:
        '200':
          description: Queued
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: integer
                        example: 91
                      name:
                        type: string
                        example: Dec promo batch
                      status:
                        type: string
                        enum:
                        - queued
                        - processing
                        - paused
                        - cancelled
                        - completed
                        - failed
                        example: processing
                      total_campaigns:
                        type: integer
                        example: 25
                      completed_campaigns:
                        type: integer
                        example: 18
                      failed_campaigns:
                        type: integer
                        example: 1
                      multi_account:
                        type: boolean
                        example: false
                      started_at:
                        type: string
                        format: date-time
                        nullable: true
                      completed_at:
                        type: string
                        format: date-time
                        nullable: true
                      created_at:
                        type: string
                        format: date-time
                      updated_at:
                        type: string
                        format: date-time
        '404':
          description: Upload not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
  "/api/v1/uploads/{id}/cancel":
    parameters:
    - name: id
      in: path
      required: true
      schema:
        type: integer
    post:
      summary: Cancel an upload permanently
      tags:
      - Uploads
      security:
      - bearer_auth: []
      description: Sets status to `cancelled`. Remaining campaigns will not be created.
        Requires scope `uploads:write`.
      responses:
        '200':
          description: Cancelled
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: integer
                        example: 91
                      name:
                        type: string
                        example: Dec promo batch
                      status:
                        type: string
                        enum:
                        - queued
                        - processing
                        - paused
                        - cancelled
                        - completed
                        - failed
                        example: processing
                      total_campaigns:
                        type: integer
                        example: 25
                      completed_campaigns:
                        type: integer
                        example: 18
                      failed_campaigns:
                        type: integer
                        example: 1
                      multi_account:
                        type: boolean
                        example: false
                      started_at:
                        type: string
                        format: date-time
                        nullable: true
                      completed_at:
                        type: string
                        format: date-time
                        nullable: true
                      created_at:
                        type: string
                        format: date-time
                      updated_at:
                        type: string
                        format: date-time
        '404':
          description: Upload not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
  "/api/v1/watchlist":
    get:
      summary: List watchlist items with their latest scan
      tags:
      - Watchlist
      security:
      - bearer_auth: []
      description: |
        Returns watchlist items plus their single most-recent scan. Requires
        scope `watchlist:read`. NOTE: response envelope is `{ watchlist: [...] }`,
        not the standard `{ data }`.
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  watchlist:
                    type: array
                    items:
                      allOf:
                      - type: object
                        properties:
                          id:
                            type: integer
                            example: 17
                          page_url:
                            type: string
                            format: uri
                            example: https://www.facebook.com/ads/library/?active_status=active&ad_type=all&view_all_page_id=100012345
                          page_name:
                            type: string
                            nullable: true
                            example: Competitor Page
                          page_avatar:
                            type: string
                            nullable: true
                            format: uri
                          page_id:
                            type: string
                            nullable: true
                            example: '100012345'
                          scan_frequency:
                            type: string
                            enum:
                            - hourly
                            - daily
                            - weekly
                            example: daily
                          active:
                            type: boolean
                            example: true
                          total_ads:
                            type: integer
                            nullable: true
                            example: 42
                          active_ads:
                            type: integer
                            nullable: true
                            example: 42
                          last_scanned_at:
                            type: string
                            format: date-time
                            nullable: true
                          created_at:
                            type: string
                            format: date-time
                      - type: object
                        properties:
                          watchlist_scans:
                            type: array
                            items:
                              type: object
                              properties:
                                id:
                                  type: integer
                                  example: 991
                                total_ads:
                                  type: integer
                                  example: 42
                                new_ads:
                                  type: integer
                                  example: 3
                                removed_ads:
                                  type: integer
                                  example: 1
                                created_at:
                                  type: string
                                  format: date-time
                            description: Most recent scan (max 1)
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
    post:
      summary: Add or update a watchlist item by page_url
      tags:
      - Watchlist
      security:
      - bearer_auth: []
      description: |
        Upsert behaviour: if a watchlist item already exists for the given
        `page_url` it is updated (status `200 OK`), otherwise created
        (status `201 Created`). Requires scope `watchlist:write`.
      parameters: []
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  watchlist_item:
                    type: object
                    properties:
                      id:
                        type: integer
                        example: 17
                      page_url:
                        type: string
                        format: uri
                        example: https://www.facebook.com/ads/library/?active_status=active&ad_type=all&view_all_page_id=100012345
                      page_name:
                        type: string
                        nullable: true
                        example: Competitor Page
                      page_avatar:
                        type: string
                        nullable: true
                        format: uri
                      page_id:
                        type: string
                        nullable: true
                        example: '100012345'
                      scan_frequency:
                        type: string
                        enum:
                        - hourly
                        - daily
                        - weekly
                        example: daily
                      active:
                        type: boolean
                        example: true
                      total_ads:
                        type: integer
                        nullable: true
                        example: 42
                      active_ads:
                        type: integer
                        nullable: true
                        example: 42
                      last_scanned_at:
                        type: string
                        format: date-time
                        nullable: true
                      created_at:
                        type: string
                        format: date-time
                  created:
                    type: boolean
                    example: true
        '200':
          description: Existing item updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  watchlist_item:
                    type: object
                    properties:
                      id:
                        type: integer
                        example: 17
                      page_url:
                        type: string
                        format: uri
                        example: https://www.facebook.com/ads/library/?active_status=active&ad_type=all&view_all_page_id=100012345
                      page_name:
                        type: string
                        nullable: true
                        example: Competitor Page
                      page_avatar:
                        type: string
                        nullable: true
                        format: uri
                      page_id:
                        type: string
                        nullable: true
                        example: '100012345'
                      scan_frequency:
                        type: string
                        enum:
                        - hourly
                        - daily
                        - weekly
                        example: daily
                      active:
                        type: boolean
                        example: true
                      total_ads:
                        type: integer
                        nullable: true
                        example: 42
                      active_ads:
                        type: integer
                        nullable: true
                        example: 42
                      last_scanned_at:
                        type: string
                        format: date-time
                        nullable: true
                      created_at:
                        type: string
                        format: date-time
                  updated:
                    type: boolean
                    example: true
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
              - watchlist_item
              properties:
                watchlist_item:
                  type: object
                  required:
                  - page_url
                  properties:
                    page_url:
                      type: string
                      format: uri
                    page_name:
                      type: string
                    page_avatar:
                      type: string
                      format: uri
                    page_id:
                      type: string
                    scan_frequency:
                      type: string
                      enum:
                      - hourly
                      - daily
                      - weekly
                    active:
                      type: boolean
        required: true
  "/api/v1/watchlist/{id}":
    parameters:
    - name: id
      in: path
      required: true
      schema:
        type: integer
    delete:
      summary: Remove a watchlist item
      tags:
      - Watchlist
      security:
      - bearer_auth: []
      responses:
        '200':
          description: Deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  deleted:
                    type: boolean
                    example: true
        '404':
          description: Watchlist item not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    example: Watchlist item not found
  "/api/v1/watchlist/{id}/scans":
    parameters:
    - name: id
      in: path
      required: true
      schema:
        type: integer
    get:
      summary: List scans for a watchlist item (most recent first)
      tags:
      - Watchlist
      security:
      - bearer_auth: []
      parameters:
      - name: limit
        in: query
        required: false
        schema:
          type: integer
          default: 20
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  scans:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: integer
                          example: 991
                        total_ads:
                          type: integer
                          example: 42
                        new_ads:
                          type: integer
                          example: 3
                        removed_ads:
                          type: integer
                          example: 1
                        created_at:
                          type: string
                          format: date-time
        '404':
          description: Watchlist item not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
    post:
      summary: Record a new scan for a watchlist item
      tags:
      - Watchlist
      security:
      - bearer_auth: []
      description: Typically called by the Chrome extension after re-scraping a page.
        Updates the item's counters.
      parameters: []
      responses:
        '201':
          description: Scan recorded
          content:
            application/json:
              schema:
                type: object
                properties:
                  scan:
                    type: object
                    properties:
                      id:
                        type: integer
                        example: 991
                      total_ads:
                        type: integer
                        example: 42
                      new_ads:
                        type: integer
                        example: 3
                      removed_ads:
                        type: integer
                        example: 1
                      created_at:
                        type: string
                        format: date-time
        '404':
          description: Watchlist item not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                total_ads:
                  type: integer
                  example: 42
                new_ads:
                  type: integer
                  example: 3
                removed_ads:
                  type: integer
                  example: 1
                summary:
                  type: object
                  description: Arbitrary JSON summary
        required: true
  "/api/v1/watchlist/{id}/ads":
    parameters:
    - name: id
      in: path
      required: true
      schema:
        type: integer
    get:
      summary: List ads seen for a watchlist item
      tags:
      - Watchlist
      security:
      - bearer_auth: []
      parameters:
      - name: new_only
        in: query
        required: false
        schema:
          type: string
          enum:
          - 'true'
          - 'false'
        description: If `true`, return only ads flagged `is_new`
      - name: page
        in: query
        required: false
        schema:
          type: integer
          default: 1
      - name: per_page
        in: query
        required: false
        schema:
          type: integer
          default: 25
          maximum: 100
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  ads:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: integer
                          example: 7001
                        library_id:
                          type: string
                          example: '1234567890123456'
                        page_title:
                          type: string
                          nullable: true
                        ad_text:
                          type: string
                          nullable: true
                        call_to_action:
                          type: string
                          nullable: true
                        link_url:
                          type: string
                          nullable: true
                          format: uri
                        link_headline:
                          type: string
                          nullable: true
                        start_date:
                          type: string
                          nullable: true
                          format: date
                        days_running:
                          type: integer
                          nullable: true
                        platforms:
                          type: array
                          items:
                            type: string
                        image_url:
                          type: string
                          nullable: true
                          format: uri
                        video_url:
                          type: string
                          nullable: true
                          format: uri
                        video_poster_url:
                          type: string
                          nullable: true
                          format: uri
                        is_new:
                          type: boolean
                          example: true
                        created_at:
                          type: string
                          format: date-time
                  meta:
                    "$ref": "#/components/schemas/Pagination"
        '404':
          description: Watchlist item not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
    post:
      summary: Bulk upsert ads for a watchlist item
      tags:
      - Watchlist
      security:
      - bearer_auth: []
      description: Upserts ads by `library_id`. Requires scope `watchlist:write`.
      parameters: []
      responses:
        '201':
          description: Upserted
          content:
            application/json:
              schema:
                type: object
                properties:
                  created:
                    type: integer
                    example: 3
                  updated:
                    type: integer
                    example: 39
                  total:
                    type: integer
                    example: 42
        '404':
          description: Watchlist item not found
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
              - ads
              properties:
                ads:
                  type: array
                  items:
                    type: object
                    required:
                    - library_id
                    properties:
                      library_id:
                        type: string
                      page_title:
                        type: string
                      ad_text:
                        type: string
                      call_to_action:
                        type: string
                      link_url:
                        type: string
                        format: uri
                      link_headline:
                        type: string
                      start_date:
                        type: string
                        format: date
                      days_running:
                        type: integer
                      platforms:
                        type: array
                        items:
                          type: string
                      image_url:
                        type: string
                        format: uri
                      video_url:
                        type: string
                        format: uri
                      video_poster_url:
                        type: string
                        format: uri
                      is_new:
                        type: boolean
        required: true
  "/api/v1/webhook_subscriptions":
    get:
      summary: List active webhook subscriptions
      tags:
      - Webhooks
      security:
      - bearer_auth: []
      description: Returns only active subscriptions. Requires scope `webhooks:read`.
      responses:
        '200':
          description: Success
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: integer
                          example: 3
                        url:
                          type: string
                          format: uri
                          example: https://example.com/webhooks/scaleforge
                        events:
                          type: array
                          items:
                            type: string
                          description: Event names the subscription receives
                          example:
                          - upload.completed
                          - campaign.activated
                          - bid_change.applied
                        active:
                          type: boolean
                          example: true
                        created_at:
                          type: string
                          format: date-time
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
    post:
      summary: Create a webhook subscription
      tags:
      - Webhooks
      security:
      - bearer_auth: []
      description: |
        Subscribes an HTTPS URL to receive signed JSON payloads for selected
        ScaleForge events. The response contains a one-time `secret` used
        to verify HMAC-SHA256 signatures — store it securely, it is not
        retrievable later. Requires scope `webhooks:write`.
      parameters: []
      responses:
        '201':
          description: Subscription created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    allOf:
                    - type: object
                      properties:
                        id:
                          type: integer
                          example: 3
                        url:
                          type: string
                          format: uri
                          example: https://example.com/webhooks/scaleforge
                        events:
                          type: array
                          items:
                            type: string
                          description: Event names the subscription receives
                          example:
                          - upload.completed
                          - campaign.activated
                          - bid_change.applied
                        active:
                          type: boolean
                          example: true
                        created_at:
                          type: string
                          format: date-time
                    - type: object
                      properties:
                        secret:
                          type: string
                          example: whsec_abc123...
                          description: HMAC-SHA256 signing secret — shown once
        '422':
          description: Validation failed
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
      requestBody:
        content:
          application/json:
            schema:
              type: object
              required:
              - url
              - events
              properties:
                url:
                  type: string
                  format: uri
                  example: https://example.com/webhooks/scaleforge
                events:
                  type: array
                  items:
                    type: string
                  example:
                  - upload.completed
                  - campaign.activated
        required: true
  "/api/v1/webhook_subscriptions/{id}":
    parameters:
    - name: id
      in: path
      required: true
      schema:
        type: integer
    delete:
      summary: Deactivate a webhook subscription
      tags:
      - Webhooks
      security:
      - bearer_auth: []
      description: 'Soft delete — sets `active: false`. Requires scope `webhooks:write`.'
      responses:
        '200':
          description: Deactivated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      id:
                        type: integer
                        example: 3
                      active:
                        type: boolean
                        example: false
        '404':
          description: Subscription not found
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/Error"
