> ## Documentation Index
> Fetch the complete documentation index at: https://help.privy.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Get campaign performance

> One message campaign's engagement funnel — sends, deliveries, opens,
clicks, attribution, and the derived rates — plus its most-clicked
links. Resolves newsletters and flow campaigns alike (flow campaigns
carry no `sent_date`, and instead name the flow that sends them in
`flow`). SMS campaigns return the SMS stat set (`opted_out`,
`failed`) and no links.

Without period parameters the funnel covers the campaign's **whole
life**. Pass `period` (or `period=custom` with `start_date`/`end_date`)
to scope it to a reporting window — the same window vocabulary as
every other report read, so its numbers line up with
`/v1/reports/campaigns` rows for the same period. The response echoes
the window in `period`; `null` means lifetime. `top_links` is always
lifetime — link clicks are not stored by day.

The campaign's configuration is `/v1/campaigns/{id}`.

**Required scope:** `reports_read`




## OpenAPI

````yaml openapi/privy-api.yaml GET /reports/campaigns/{campaign_id}
openapi: 3.1.0
info:
  title: Privy API
  version: '1.0'
  description: >
    The Privy API lets you programmatically manage your contact list and read
    what you have set up in Privy.

    Create, update, unsubscribe, and remove contacts; report orders;

    and read your campaigns, flows, segments, orders, store products, coupon
    pools,

    performance reports, and account setup.
servers:
  - url: https://api.privy.com/v1
    description: Production
security:
  - bearerAuth: []
tags:
  - name: Account
    description: Retrieve information about the authenticated Privy account.
  - name: Contacts
    description: Manage your contact list.
  - name: Imports
    description: >-
      Read your contact import history — CSV uploads, connected-provider syncs,
      and SMS consent imports.
  - name: Campaigns
    description: >-
      Read your message campaigns — email and SMS newsletters, plus the messages
      your flows send.
  - name: Displays
    description: >-
      Read your on-site displays (popups, flyouts, banners, embedded forms,
      landing pages).
  - name: Coupons
    description: Read your coupon pools, their code inventory, and individual codes.
  - name: Reports
    description: >-
      Read performance reporting — signups, views, conversion rates, and
      attributed revenue — for your business, displays, and flows.
  - name: Flows
    description: Read your automation flows and their configuration.
  - name: Segments
    description: Read your customer segments and their membership.
  - name: Orders
    description: Report orders placed in your store and read your order history.
  - name: Products
    description: Read the product catalog and collections of your connected store.
  - name: Events
    description: Ingest custom events to trigger Flows and other automations.
paths:
  /reports/campaigns/{campaign_id}:
    get:
      tags:
        - Reports
      summary: Get campaign performance
      description: |
        One message campaign's engagement funnel — sends, deliveries, opens,
        clicks, attribution, and the derived rates — plus its most-clicked
        links. Resolves newsletters and flow campaigns alike (flow campaigns
        carry no `sent_date`, and instead name the flow that sends them in
        `flow`). SMS campaigns return the SMS stat set (`opted_out`,
        `failed`) and no links.

        Without period parameters the funnel covers the campaign's **whole
        life**. Pass `period` (or `period=custom` with `start_date`/`end_date`)
        to scope it to a reporting window — the same window vocabulary as
        every other report read, so its numbers line up with
        `/v1/reports/campaigns` rows for the same period. The response echoes
        the window in `period`; `null` means lifetime. `top_links` is always
        lifetime — link clicks are not stored by day.

        The campaign's configuration is `/v1/campaigns/{id}`.

        **Required scope:** `reports_read`
      operationId: getCampaignReport
      parameters:
        - $ref: '#/components/parameters/AccountIdParam'
        - name: campaign_id
          in: path
          required: true
          description: >-
            The campaign's unique identifier (returned as `campaign_id` in
            report responses and `id` in campaign responses). Ids are opaque
            public strings — a numeric id from a Privy dashboard URL is not a
            valid API id and will 404; list campaigns and match by title
            instead.
          schema:
            type: string
          example: kD4qN8wZr1
        - name: period
          in: query
          description: >
            Scope the funnel to a date range, resolved in the account's time
            zone. Pass `custom` together with `start_date` and `end_date`. Omit
            entirely for the campaign's lifetime funnel (the default).
          schema:
            type: string
            enum:
              - last_7_days
              - last_30_days
              - last_90_days
              - this_month
              - last_month
              - custom
          example: last_30_days
        - $ref: '#/components/parameters/ReportStartDateParam'
        - $ref: '#/components/parameters/ReportEndDateParam'
      responses:
        '200':
          description: >-
            The campaign's engagement funnel — lifetime, or scoped to the
            requested period.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/CampaignReportDetail'
              example:
                data:
                  campaign_id: kD4qN8wZr1
                  type: newsletter
                  title: June Restock Announcement
                  channel: email
                  sent_date: '2026-06-18'
                  period: null
                  stats:
                    sent:
                      value: 8200
                      unit: count
                    delivered:
                      value: 8020
                      unit: count
                    delivery_rate:
                      value: 97.8
                      unit: percent
                    opened_unique:
                      value: 3450
                      unit: count
                    open_rate:
                      value: 43
                      unit: percent
                    clicked_unique:
                      value: 610
                      unit: count
                    click_rate:
                      value: 7.6
                      unit: percent
                    bounced:
                      value: 74
                      unit: count
                    bounce_rate:
                      value: 0.9
                      unit: percent
                    spam_reports:
                      value: 3
                      unit: count
                    unsubscribed:
                      value: 21
                      unit: count
                    unsubscribe_rate:
                      value: 0.3
                      unit: percent
                    attributed_orders:
                      value: 45
                      unit: count
                    order_rate:
                      value: 0.6
                      unit: percent
                    attributed_revenue:
                      value: 2810
                      unit: usd
                    average_order_value:
                      value: 62.44
                      unit: usd
                  ab_test:
                    state: sent
                    winning_metric: click_rate
                    results_source: snapshot
                    measured_at: '2026-06-19T15:20:00Z'
                    decision_due_at: '2026-06-19T15:20:00Z'
                    winner_selected_at: '2026-06-19T15:24:00Z'
                    completed_at: '2026-06-19T17:50:00Z'
                    winner_version_id: cpv_YJGOjpJ
                    selection_reason: highest_metric
                    audience:
                      eligible_count: 8200
                      test_count: 2050
                      remainder_count: 6150
                    versions:
                      - id: cpv_dkyYZmk
                        name: Version A
                        is_control: true
                        is_winner: false
                        audience_share: 0.125
                        subject: The restock you asked for is here
                        decision:
                          metric_value:
                            value: 4.8
                            unit: percent
                          numerator:
                            value: 48
                            unit: count
                          denominator:
                            value: 1003
                            unit: count
                        stats:
                          assigned:
                            value: 1025
                            unit: count
                          sent:
                            value: 1025
                            unit: count
                          delivered:
                            value: 1003
                            unit: count
                          delivery_rate:
                            value: 97.9
                            unit: percent
                          opened_unique:
                            value: 431
                            unit: count
                          open_rate:
                            value: 43
                            unit: percent
                          clicked_unique:
                            value: 50
                            unit: count
                          click_rate:
                            value: 5
                            unit: percent
                          bounced:
                            value: 9
                            unit: count
                          bounce_rate:
                            value: 0.9
                            unit: percent
                          spam_reports:
                            value: 0
                            unit: count
                          unsubscribed:
                            value: 3
                            unit: count
                          unsubscribe_rate:
                            value: 0.3
                            unit: percent
                          attributed_orders:
                            value: 4
                            unit: count
                          order_rate:
                            value: 0.4
                            unit: percent
                          attributed_revenue:
                            value: 238
                            unit: usd
                          average_order_value:
                            value: 59.5
                            unit: usd
                        top_links:
                          - url: https://shop.example.com/collections/restock
                            name: Shop the restock
                            clicks: 44
                      - id: cpv_YJGOjpJ
                        name: Version B
                        is_control: false
                        is_winner: true
                        audience_share: 0.125
                        subject: Your size is back, but not for long
                        decision:
                          metric_value:
                            value: 8
                            unit: percent
                          numerator:
                            value: 80
                            unit: count
                          denominator:
                            value: 1004
                            unit: count
                        stats:
                          assigned:
                            value: 1025
                            unit: count
                          sent:
                            value: 7175
                            unit: count
                          delivered:
                            value: 7017
                            unit: count
                          delivery_rate:
                            value: 97.8
                            unit: percent
                          opened_unique:
                            value: 3019
                            unit: count
                          open_rate:
                            value: 43
                            unit: percent
                          clicked_unique:
                            value: 560
                            unit: count
                          click_rate:
                            value: 8
                            unit: percent
                          bounced:
                            value: 65
                            unit: count
                          bounce_rate:
                            value: 0.9
                            unit: percent
                          spam_reports:
                            value: 3
                            unit: count
                          unsubscribed:
                            value: 18
                            unit: count
                          unsubscribe_rate:
                            value: 0.3
                            unit: percent
                          attributed_orders:
                            value: 41
                            unit: count
                          order_rate:
                            value: 0.6
                            unit: percent
                          attributed_revenue:
                            value: 2572
                            unit: usd
                          average_order_value:
                            value: 62.73
                            unit: usd
                        top_links:
                          - url: https://shop.example.com/collections/restock
                            name: Shop the restock
                            clicks: 466
                  top_links:
                    - url: https://shop.example.com/collections/restock
                      name: Shop the restock
                      clicks: 44
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/InsufficientScope'
        '404':
          $ref: '#/components/responses/NotFound'
        '422':
          $ref: '#/components/responses/ValidationFailed'
        '429':
          $ref: '#/components/responses/RateLimited'
      security:
        - bearerAuth: []
components:
  parameters:
    AccountIdParam:
      name: Privy-Account-Id
      in: header
      required: false
      description: >
        Target a subaccount instead of the main connected account.

        Pass a subaccount `id` from get_account's `subaccounts` list. Only
        direct subaccounts

        of the connected account are reachable — omit this for the main account
        itself.
      schema:
        type: string
      example: S1U2B3A4C5C6T7
    ReportStartDateParam:
      name: start_date
      in: query
      description: >-
        Inclusive start of a `custom` period (ISO 8601 date). Required when
        `period=custom`, ignored otherwise.
      schema:
        type: string
        format: date
      example: '2026-06-01'
    ReportEndDateParam:
      name: end_date
      in: query
      description: >-
        Inclusive end of a `custom` period (ISO 8601 date). Required when
        `period=custom`, ignored otherwise. Custom periods are capped at 366
        days.
      schema:
        type: string
        format: date
      example: '2026-06-30'
  schemas:
    CampaignReportDetail:
      type: object
      properties:
        campaign_id:
          type: string
          description: >
            The campaign's unique identifier. It matches `id` in `/v1/campaigns`
            responses.
          example: kD4qN8wZr1
        type:
          type: string
          description: >
            Discriminates the two campaign families — `newsletter` (email and
            SMS one-time sends) and `flow` (the messages automation flows send).
          enum:
            - newsletter
            - flow
          example: newsletter
        title:
          type:
            - string
            - 'null'
          example: June Restock Announcement
        channel:
          type: string
          enum:
            - email
            - sms
          example: email
        ab_test:
          oneOf:
            - $ref: '#/components/schemas/CampaignReportAbTest'
            - type: 'null'
          description: >
            Per-version A/B results when this newsletter is a test, `null`
            otherwise (flow campaigns never are). Test-scoped: the period
            parameters do not apply to this block.
        sent_date:
          type:
            - string
            - 'null'
          format: date
          description: >
            Newsletters only — flow campaigns send continuously, so their detail
            omits this field.
          example: '2026-06-18'
        flow:
          oneOf:
            - $ref: '#/components/schemas/CampaignFlowRef'
            - type: 'null'
          description: >
            Flow campaigns only — the flow that sends this message. Newsletter
            details omit the field entirely; a flow campaign whose node has
            since been deleted carries `null`.
        period:
          oneOf:
            - $ref: '#/components/schemas/ReportPeriod'
            - type: 'null'
          description: >
            The window `stats` covers. `null` means the campaign's whole life —
            the default when no period parameters were passed.
        stats:
          $ref: '#/components/schemas/CampaignReportFunnelStats'
        top_links:
          type: array
          description: >
            The campaign's most-clicked links, best first, deduped by URL — at
            most 100 links are reported. For an A/B test these are the control
            version's links; every version's own are on
            `ab_test.versions[].top_links`. Email campaigns only; absent for SMS
            campaigns.
          items:
            $ref: '#/components/schemas/CampaignReportLink'
    CampaignReportAbTest:
      type: object
      description: |
        Results of the newsletter's A/B test: the winner and each version's
        figures. `null` when the campaign is not a test.

        Each version carries two blocks. `decision` is the winning metric over
        the decision window, the figures the winner was chosen on. `stats` is
        the version's lifetime performance in the campaign-level shape,
        including revenue and deliverability.

        `results_source` and `measured_at` describe `decision` only. A decided
        test serves the snapshot frozen at `decision_due_at`, so it does not
        move as late opens arrive. Any other test counts `decision` within
        the last 10 minutes, bounded by `decision_due_at` once it is set. A
        test decided before the current snapshot format has no readable
        snapshot, so it reports `live` figures recounted within its decision
        window even though a winner is set; those can differ slightly from the
        figures the winner was actually chosen on.
      properties:
        state:
          type: string
          enum:
            - draft
            - scheduled
            - preparing
            - awaiting_winner
            - sending
            - sent
            - partially_sent
            - canceled
            - unsent
          description: >
            The campaign's send lifecycle, the same value as `status` on
            `/v1/campaigns`. `preparing`, `awaiting_winner` and `sending` mean
            the test is still running.
          example: sent
        winning_metric:
          type:
            - string
            - 'null'
          enum:
            - open_rate
            - click_rate
            - order_rate
            - null
          description: >-
            The metric the winner is chosen on, reported in each version's
            `decision`.
          example: click_rate
        results_source:
          type: string
          enum:
            - snapshot
            - live
          description: >
            `snapshot` — frozen figures the winner was decided on. `live` —
            counted within the last 10 minutes; the numbers can still move. A
            decided test with no readable snapshot also reports `live`, bounded
            by `decision_due_at`.
          example: snapshot
        measured_at:
          type: string
          format: date-time
          description: >
            When the `decision` figures were taken: the snapshot's cutoff, or
            for live figures `decision_due_at` once it has passed, and otherwise
            the time they were counted, up to 10 minutes before this request.
          example: '2026-06-19T09:00:00Z'
        decision_due_at:
          type:
            - string
            - 'null'
          format: date-time
          description: >
            When the decision window closes. `null` until the test audience has
            finished sending, including while an `awaiting_winner` test is still
            sending to it, and for a test that ended before then.
          example: '2026-06-19T09:00:00Z'
        winner_selected_at:
          type:
            - string
            - 'null'
          format: date-time
          description: When a winner was committed. `null` until then.
          example: '2026-06-19T09:04:00Z'
        completed_at:
          type:
            - string
            - 'null'
          format: date-time
          description: >
            When the winning version finished sending to the remainder. `null`
            until then, and for a canceled or failed test.
          example: '2026-06-19T11:30:00Z'
        winner_version_id:
          type:
            - string
            - 'null'
          description: The `id` of the winning version. `null` until a winner is committed.
          example: cpv_YJGOjpJ
        selection_reason:
          type:
            - string
            - 'null'
          enum:
            - highest_metric
            - tie_control
            - tie_random
            - no_data_control
            - null
          description: >
            Why the winning version was chosen. `null` until a winner is

            committed.


            - `highest_metric` — one version led on the metric.

            - `tie_control` — versions tied and the control was among them, so
              the control was kept.
            - `tie_random` — versions tied with no control among them, so one
            was
              picked at random.
            - `no_data_control` — nothing was delivered for any version, so the
              control was kept by default.
          example: highest_metric
        audience:
          type: object
          description: >
            The audience split, populated when generation runs — every count is
            `null` on a test that has not launched.
          properties:
            eligible_count:
              type:
                - integer
                - 'null'
              description: Contacts matching the campaign's audience.
              example: 12000
            test_count:
              type:
                - integer
                - 'null'
              description: Contacts in the test cohort, across all versions.
              example: 3000
            remainder_count:
              type:
                - integer
                - 'null'
              description: >
                Contacts held back to receive the winning version after the
                decision. Zero when the versions' shares consume the whole
                audience — a legitimate setup that leaves nobody for the winner
                to be sent to.
              example: 9000
        versions:
          type: array
          description: The versions in labelled order (A, B, C…); the control is first.
          items:
            $ref: '#/components/schemas/CampaignReportAbTestVersion'
    CampaignFlowRef:
      type: object
      description: >
        The automation flow that sends this campaign. Pass `id` to `GET
        /v1/flows/{id}` for the flow's trigger, filters, and node graph.
      properties:
        id:
          type: string
          description: The flow's identifier, as returned by `GET /v1/flows`.
          example: 0Rqw3Z8
        title:
          type:
            - string
            - 'null'
          example: Welcome Series
    ReportPeriod:
      type: object
      description: |
        The resolved date range the report covers, inclusive, in the account's
        time zone. Every label except `last_month` ends today, so the last day
        of the range is still in progress: `last_7_days` is the six days before
        today plus today, `this_month` runs from the 1st through today, and
        `last_month` is the whole previous calendar month. Custom ranges cannot
        exceed 366 days.
      properties:
        label:
          type: string
          enum:
            - last_7_days
            - last_30_days
            - last_90_days
            - this_month
            - last_month
            - custom
          example: last_30_days
        start_date:
          type: string
          format: date
          example: '2026-06-21'
        end_date:
          type: string
          format: date
          example: '2026-07-20'
    CampaignReportFunnelStats:
      type: object
      description: |
        The campaign's engagement funnel over the window the response covers —
        the resolved report period on list rows and period-scoped detail reads,
        or the campaign's whole life on the default (no-period) detail read.
        Email campaigns carry `delivery_rate`, `opened_unique`, `open_rate`,
        `bounced`, `bounce_rate`, `spam_reports`, `unsubscribed`,
        `unsubscribe_rate`, and `average_order_value`; SMS campaigns carry
        `opted_out` and `failed`; both carry the rest.

        `attributed_orders` and `attributed_revenue` credit the campaign when
        one of its messages was the contact's most recent engagement — email
        open, email click, or SMS click — in the 7 days before the order was
        placed. `order_rate` is those orders over the messages the campaign
        got out: email deliveries for an email campaign, sends for an SMS one —
        so an SMS campaign's `order_rate` is not directly comparable with an
        email campaign's (undelivered messages still sit in the denominator).

        The rates do not share one denominator. `delivery_rate` and
        `bounce_rate` are over `sent`; `open_rate`, `click_rate` and
        `unsubscribe_rate` are over `delivered`. `unsubscribed`, `bounced` and
        `spam_reports` count every event, while `opened_unique` and
        `clicked_unique` de-duplicate — but only against the previous month, so
        on a campaign that has been sending for longer, which includes any
        long-running flow campaign, a contact who re-engages after that gap is
        counted again and these counters drift above the true number of
        distinct contacts.
        `average_order_value` is `attributed_revenue / attributed_orders`,
        reported as 0 when there are no attributed orders. SMS stat blocks
        carry no `average_order_value` — divide `attributed_revenue` by
        `attributed_orders` yourself.
      properties:
        sent:
          $ref: '#/components/schemas/ReportMetric'
        delivered:
          $ref: '#/components/schemas/ReportMetric'
        delivery_rate:
          $ref: '#/components/schemas/ReportMetric'
        opened_unique:
          $ref: '#/components/schemas/ReportMetric'
        open_rate:
          $ref: '#/components/schemas/ReportMetric'
        clicked_unique:
          $ref: '#/components/schemas/ReportMetric'
        click_rate:
          $ref: '#/components/schemas/ReportMetric'
        bounced:
          $ref: '#/components/schemas/ReportMetric'
        bounce_rate:
          $ref: '#/components/schemas/ReportMetric'
        spam_reports:
          $ref: '#/components/schemas/ReportMetric'
        unsubscribed:
          $ref: '#/components/schemas/ReportMetric'
        unsubscribe_rate:
          $ref: '#/components/schemas/ReportMetric'
        opted_out:
          $ref: '#/components/schemas/ReportMetric'
        failed:
          $ref: '#/components/schemas/ReportMetric'
        attributed_orders:
          $ref: '#/components/schemas/ReportMetric'
        order_rate:
          $ref: '#/components/schemas/ReportMetric'
        attributed_revenue:
          $ref: '#/components/schemas/ReportMetric'
        average_order_value:
          $ref: '#/components/schemas/ReportMetric'
    CampaignReportLink:
      type: object
      properties:
        url:
          type: string
          example: https://shop.example.com/collections/restock
        name:
          type:
            - string
            - 'null'
          description: The link's display name from the email content, when one was set.
          example: Shop the restock
        clicks:
          type: integer
          example: 402
    ErrorResponse:
      type: object
      properties:
        error:
          type: object
          required:
            - code
            - message
          properties:
            code:
              type: string
              enum:
                - invalid_client
                - invalid_scope
                - unauthorized
                - insufficient_scope
                - paid_plan_required
                - api_access_required
                - not_found
                - no_connected_store
                - conflict
                - contacts_not_ready
                - validation_failed
                - rate_limited
                - store_unavailable
              example: validation_failed
            message:
              type: string
              example: One or more fields are invalid
            details:
              type: array
              items:
                type: object
                properties:
                  field:
                    type: string
                    example: email
                  message:
                    type: string
                    example: is required
    CampaignReportAbTestVersion:
      type: object
      description: One version's results.
      properties:
        id:
          type: string
          description: >
            Opaque, stable version identifier, prefixed `cpv_`. The same id as
            the version on `GET /v1/campaigns/{id}`.
          example: cpv_dkyYZmk
        name:
          type: string
          example: Version A
        is_control:
          type: boolean
          example: true
        is_winner:
          type: boolean
          description: >-
            Whether this version won. `false` on every version until a winner is
            committed.
          example: false
        audience_share:
          type:
            - number
            - 'null'
          description: >-
            Share of the eligible audience (0–1) this version was sent to during
            the test.
          example: 0.125
        subject:
          type:
            - string
            - 'null'
          description: >
            This version's human-readable label: its subject line, or its
            message body for SMS.
          example: The restock you asked for is here
        decision:
          oneOf:
            - $ref: '#/components/schemas/CampaignReportAbTestVersionDecision'
            - type: 'null'
          description: >-
            The figures the winner is chosen on. `null` when the test has no
            winning metric.
        stats:
          $ref: '#/components/schemas/CampaignReportAbTestVersionStats'
        top_links:
          type: array
          description: >
            This version's most-clicked links, best first, deduped by URL — at
            most 100 links are reported. Lifetime clicks, like `stats`. Email
            tests only; absent for SMS.
          items:
            $ref: '#/components/schemas/CampaignReportLink'
    ReportMetric:
      type: object
      description: |
        A single report metric with its unit. `count` values are whole
        numbers, `percent` values are 0–100 with one decimal. Money metrics
        carry the account's lowercased ISO 4217 currency code as their unit
        (e.g. `usd`, `eur`, `jpy`) and are expressed in that currency's major
        units, two decimals. A rate whose denominator was zero reports 0, not
        null — so 0 does not distinguish "no activity" from "activity that
        never converted"; read the underlying counts to tell those apart.
      properties:
        value:
          type: number
          example: 42
        unit:
          type: string
          description: '`count`, `percent`, or a lowercased ISO 4217 currency code.'
          example: count
    CampaignReportAbTestVersionDecision:
      type: object
      description: |
        The winning metric for one version over the decision window — the
        figures the winner is chosen on. Frozen once a winner is committed;
        before that, counted within the last 10 minutes and bounded by
        `decision_due_at` once it is set.
      properties:
        metric_value:
          allOf:
            - $ref: '#/components/schemas/ReportMetric'
          description: '`numerator` over `denominator`, as a percent.'
        numerator:
          allOf:
            - $ref: '#/components/schemas/ReportMetric'
          description: >
            The version's count for `winning_metric`: unique opens, unique
            clicks, or attributed orders placed by the window's close.
        denominator:
          allOf:
            - $ref: '#/components/schemas/ReportMetric'
          description: Messages delivered for this version within the window.
    CampaignReportAbTestVersionStats:
      description: |
        One version's lifetime performance, with the keys and units of the
        campaign-level `stats` block (email or SMS set), plus `assigned`.
        Counts every send of this version's message, so the winning version's
        figures include the remainder sent to it after the decision. Unlike
        `decision`, these keep moving as late engagement and orders arrive, and
        they ignore the report period.

        Differences from the campaign-level block: `opened_unique` and
        `clicked_unique` count distinct recipients exactly, with no monthly
        drift, and an SMS version's `order_rate` divides by `delivered` rather
        than `sent`. `bounced`, `spam_reports`, `unsubscribed` and `opted_out`
        count every event, as at campaign level.
      allOf:
        - type: object
          properties:
            assigned:
              allOf:
                - $ref: '#/components/schemas/ReportMetric'
              description: >
                Contacts allocated to this version's test cohort when the
                audience was frozen. The winner's other figures include the
                remainder send, so its `sent` can exceed `assigned`.
        - $ref: '#/components/schemas/CampaignReportFunnelStats'
  responses:
    Unauthorized:
      description: Missing or invalid bearer token.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              code: unauthorized
              message: Bearer token is missing or invalid
    InsufficientScope:
      description: Token lacks the required scope for this endpoint.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              code: insufficient_scope
              message: Token does not have the required scope
    NotFound:
      description: >-
        The requested resource does not exist or does not belong to your
        account.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              code: not_found
              message: Not found
    ValidationFailed:
      description: One or more fields failed validation.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              code: validation_failed
              message: One or more fields are invalid
              details:
                - field: email
                  message: is required
    RateLimited:
      description: Rate limit exceeded. Retry after the specified time.
      headers:
        Retry-After:
          description: Seconds to wait before retrying.
          schema:
            type: integer
          example: 42
        X-RateLimit-Limit-Minute:
          description: Maximum requests allowed per minute.
          schema:
            type: integer
          example: 60
        X-RateLimit-Remaining-Minute:
          description: Requests remaining in the current minute window.
          schema:
            type: integer
          example: 0
        X-RateLimit-Reset-Minute:
          description: Unix timestamp when the minute window resets.
          schema:
            type: integer
          example: 1711929600
        X-RateLimit-Limit-Day:
          description: Maximum requests allowed per day.
          schema:
            type: integer
          example: 10000
        X-RateLimit-Remaining-Day:
          description: Requests remaining in the current day window.
          schema:
            type: integer
          example: 9500
        X-RateLimit-Reset-Day:
          description: Unix timestamp when the day window resets.
          schema:
            type: integer
          example: 1712016000
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
          example:
            error:
              code: rate_limited
              message: Rate limit exceeded
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: API token or OAuth access token
      description: >
        Send either an API token or an OAuth access token as

        `Authorization: Bearer <token>`. See the Authentication page for
        details.

````