# GENERATED by bin/generate-api-specs — DO NOT EDIT.
# Source of truth: openapi/privy-api.yaml (public slice).
---
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.
components:
  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.
  schemas:
    Account:
      type: object
      properties:
        id:
          type: string
          description: Stable, unique identifier for the Privy account.
          example: A1B2C3D4E5F6G7H8I9J0K1L2
        name:
          type:
          - string
          - 'null'
          description: Display name of the Privy account. `null` if the account has
            not configured a name.
          example: My Store
        url:
          type:
          - string
          - 'null'
          description: Primary website URL for the account. `null` if the account
            has not configured an URL.
          example: https://mystore.com
        subaccounts:
          type: array
          description: |
            Subaccounts of this account that the presented credential can access.
            Each `id` is the value to send in the `Privy-Account-Id` header to make  requests on that subaccount's behalf.

            The list is credential-scoped: API tokens list the subaccounts covered by their subaccount-access grant;
            OAuth tokens (which have no subaccount access mechanism) and requests already targeting a subaccount get an empty list.
          items:
            "$ref": "#/components/schemas/Subaccount"
    Subaccount:
      type: object
      properties:
        id:
          type: string
          description: The subaccount's identifier — usable as the `Privy-Account-Id`
            header value.
          example: S1U2B3A4C5C6T7
        name:
          type:
          - string
          - 'null'
          example: My Store — Canada
        url:
          type:
          - string
          - 'null'
          example: https://ca.mystore.com
    AccountSetup:
      type: object
      properties:
        steps:
          type: array
          description: |
            The account's onboarding steps and their statuses. Steps are plan-dependent —
            only steps the account qualifies for are returned. Accounts created before guided onboarding return an empty list.
          items:
            "$ref": "#/components/schemas/SetupStep"
        started_at:
          type:
          - string
          - 'null'
          format: date-time
          description: When the account first viewed the setup guide. `null` if never
            viewed.
          example: '2026-05-01T09:00:00Z'
        completed_at:
          type:
          - string
          - 'null'
          format: date-time
          description: When every required step was completed. `null` while setup
            is in progress.
          example:
        summary:
          type: object
          description: What the account actually has live right now, derived from
            current configuration.
          properties:
            live_display_types:
              type: array
              description: Display types of campaigns currently live on the merchant's
                site.
              items:
                type: string
              example:
              - flyout
              - popup
            flow_triggers_in_use:
              type: array
              description: Normalized trigger names of currently active flows.
              items:
                type: string
              example:
              - order_placed
              - customer_subscribed_to_marketing
            connected_integrations:
              type: array
              description: Providers of the account's connected integrations.
              items:
                type: string
              example:
              - shopify
              - bigcommerce
    SetupStep:
      type: object
      properties:
        key:
          type: string
          description: Stable identifier of the onboarding step.
          example: connect_store
        status:
          type: string
          enum:
          - not_started
          - in_progress
          - pending
          - completed
          - skipped
          example: completed
        required:
          type: boolean
          description: Whether the step is required for setup to be considered complete.
          example: false
    Campaign:
      type: object
      description: |
        A message campaign — configuration only; send statistics belong to the
        reporting surface. `type` discriminates the two families: `newsletter`
        (email + SMS broadcasts) and `flow` (the email/SMS messages a flow sends).
        Newsletter-only fields (`status`, `send_date`, `audience`) are `null`
        for flow campaigns — the flow's trigger decides who receives them and when.
      properties:
        id:
          type: string
          description: Stable, unique identifier for the campaign. Use this value
            in `/v1/campaigns/{id}` and `/v1/reports/campaigns/{campaign_id}`.
          example: Nw8K2rT5xY
        type:
          type: string
          enum:
          - newsletter
          - flow
          description: "`newsletter` — a broadcast send; `flow` — a message sent by
            an automation flow."
          example: newsletter
        title:
          type:
          - string
          - 'null'
          example: June Restock Announcement
        channel:
          type: string
          enum:
          - email
          - sms
          example: email
        status:
          type:
          - string
          - 'null'
          enum:
          - draft
          - scheduled
          - preparing
          - awaiting_winner
          - sending
          - sent
          - partially_sent
          - unsent
          - canceled
          -
          description: |
            The newsletter's send lifecycle. `null` for flow campaigns.

            Two kinds of send reach beyond the plain draft → scheduled → sending →
            sent path, and they share some states.

            A newsletter scheduled to land at a recipient-local wall-clock time
            ("send in recipient's timezone") rolls delivery out over roughly 24
            hours as each timezone reaches that time. `preparing` is audience
            generation, and it is the last point at which the send can be
            unscheduled; after that a merchant can only cancel the remaining
            delivery windows, leaving `partially_sent` when some recipients were
            already sent to and `canceled` when none were.

            An A/B test runs
            `preparing` → `awaiting_winner` → `sending` → `sent`: it generates its
            audience, sends to the test cohort, then holds at `awaiting_winner`
            while the decision window runs, and only sends the winning version to
            the remaining audience once a winner is committed. A test cancelled
            mid-flight also reports `canceled`. `awaiting_winner` occurs on A/B
            tests only.
          example: sent
        subject:
          type:
          - string
          - 'null'
          description: Email subject line. `null` for SMS campaigns.
          example: The restock you asked for is here
        send_date:
          type:
          - string
          - 'null'
          format: date-time
          description: When the newsletter is scheduled to send (or sent). `null`
            for flow campaigns.
          example: '2026-06-18T15:00:00Z'
        flow:
          oneOf:
          - "$ref": "#/components/schemas/CampaignFlowRef"
          - type: 'null'
          description: 'Flow campaigns only — the flow that sends this message, so
            naming it needs no follow-up request. Newsletters omit the field entirely;
            a flow campaign whose node has since been deleted carries `null`.

            '
        audience:
          type:
          - object
          - 'null'
          description: |
            Audience segment references. `null` for flow campaigns — the flow's
            trigger decides the audience. Each entry is `{id, name, prebuilt}`;
            saved segments carry an id you can look up via `GET /v1/segments/{id}`
            for counts and members. `prebuilt: true` marks Privy's
            built-in smart segments (Potential Customers, Engaged Contacts, …) —
            when one hasn't been saved to the account, its `id` is the stable
            slug rather than a segments-API id.
          properties:
            included_segments:
              type: array
              items:
                "$ref": "#/components/schemas/AudienceSegmentRef"
            excluded_segments:
              type: array
              items:
                "$ref": "#/components/schemas/AudienceSegmentRef"
        coupon:
          oneOf:
          - "$ref": "#/components/schemas/CouponRef"
          - type: 'null'
          description: 'The coupon pool attached to this campaign, `null` when none.
            A `{{ coupon_code }}` tag in the message body only renders when a pool
            is attached — a body using the tag with `coupon: null` will not render
            a code.

            '
        ab_test:
          oneOf:
          - "$ref": "#/components/schemas/CampaignAbTest"
          - type: 'null'
          description: 'The A/B test''s configuration when this newsletter is a test,
            `null` otherwise (flow campaigns never are). Per-version results live
            on `GET /v1/reports/campaigns/{campaign_id}`, joined by version `id`.

            '
        created_at:
          type: string
          format: date-time
          example: '2026-06-10T09:00:00Z'
        updated_at:
          type: string
          format: date-time
          example: '2026-06-18T15:05:00Z'
    CampaignAbTest:
      type: object
      description: How the newsletter's A/B test is set up.
      properties:
        winning_metric:
          type:
          - string
          - 'null'
          enum:
          - open_rate
          - click_rate
          - order_rate
          -
          description: 'The metric the winner is chosen on. `open_rate` only appears
            on older email tests and never on SMS.

            '
          example: click_rate
        test_duration_seconds:
          type:
          - integer
          - 'null'
          description: How long the test cohort runs before a winner is chosen.
          example: 86400
        versions:
          type: array
          description: The versions in labelled order (A, B, C…); the control is first.
          items:
            "$ref": "#/components/schemas/CampaignAbTestVersion"
    CampaignAbTestVersion:
      type: object
      properties:
        id:
          type: string
          description: 'Opaque, stable version identifier, prefixed `cpv_`. Matches
            the version''s results on `GET /v1/reports/campaigns/{campaign_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 is sent
            to during the test. `null` until set on a draft. Shares sum to the test
            size; the remainder goes to the winner.

            '
          example: 0.125
        subject:
          type:
          - string
          - 'null'
          description: This version's subject line, or its message body for SMS.
          example: The restock you asked for is here
        preheader:
          type:
          - string
          - 'null'
          description: This version's preheader. `null` for SMS or when none was set.
          example: Back in stock and going fast
        coupon:
          oneOf:
          - "$ref": "#/components/schemas/CouponRef"
          - type: 'null'
          description: The coupon pool this version sends with, `null` when none.
    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
    AudienceSegmentRef:
      type: object
      properties:
        id:
          type: string
          description: |
            For saved segments, the segment id — pass it to `GET /v1/segments/{id}`
            (counts) or `GET /v1/segments/{id}/contacts` (members).
            A smart segment that has not been saved to the account has no
            segment record; its `id` is the built-in segment's stable slug
            (e.g. `potential_customers`), with no segments-API detail read.
          example: aBc1234
        name:
          type: string
          description: Display name of the segment.
          example: Engaged 90d
        prebuilt:
          type: boolean
          description: |
            Whether this is one of Privy's built-in smart segments (Potential
            Customers, Engaged Contacts, …) — the same flag the segments API
            returns, so refs match `GET /v1/segments` rows. `false` for segments
            you created.
          example: false
    Display:
      type: object
      properties:
        id:
          type: string
          description: Stable, unique identifier for the display. Use this value in
            `/v1/displays/{id}`.
          example: gk2P8Zw1Lq
        title:
          type:
          - string
          - 'null'
          example: Summer Sale Popup
        display_type:
          type:
          - string
          - 'null'
          enum:
          - popup
          - flyout
          - banner
          - bar
          - spin_to_win
          - embedded_form
          - landing_page
          - thank_you_page
          - multi_display
          -
          description: The display's on-site shape. `null` when the display's shape
            cannot be derived.
          example: popup
        goal:
          type:
          - string
          - 'null'
          description: What the display is optimized for.
          example: signup
        status:
          type: string
          enum:
          - draft
          - scheduled
          - live
          - paused
          - ended
          - archived
          description: |
            - `draft` — creation not completed.
            - `scheduled` — completed, scheduled to start later.
            - `live` — running and enabled on the site.
            - `paused` — running window but disabled.
            - `ended` — schedule window has passed.
            - `archived` — archived by the merchant (excluded from the list unless `status=archived`).
          example: live
        starts_at:
          type:
          - string
          - 'null'
          format: date-time
          example: '2026-06-01T00:00:00Z'
        ends_at:
          type:
          - string
          - 'null'
          format: date-time
          example:
        ab_test:
          type: object
          properties:
            present:
              type: boolean
              example: false
            state:
              type:
              - string
              - 'null'
              description: State of the display's A/B test, when one exists.
              example:
        coupon:
          oneOf:
          - "$ref": "#/components/schemas/CouponRef"
          - type: 'null'
          description: The coupon pool this display hands out codes from, `null` when
            none.
        created_at:
          type: string
          format: date-time
          example: '2026-05-20T10:00:00Z'
    DisplayDetail:
      allOf:
      - "$ref": "#/components/schemas/Display"
      - type: object
        properties:
          triggers:
            type: object
            description: How the display decides to show. Legacy displays may return
              all-null values.
            properties:
              exit_intent:
                type: boolean
                example: false
              auto_show_after_seconds:
                type:
                - integer
                - 'null'
                example: 5
              scroll_threshold_pct:
                type:
                - integer
                - 'null'
                example:
              teaser_tab:
                type: boolean
                example: false
          audience_summary:
            type:
            - string
            - 'null'
            description: Human-readable summary of the display's audience targeting.
              Currently always `null`.
            example:
          after_signup_email:
            type: object
            description: 'The display''s autoresponder: the one email sent immediately
              when a visitor signs up. Scheduled follow-up emails sent days after
              signup are a separate sequence and are not reported here. Its coupon
              code comes from the display''s own `coupon` — so `enabled: true` on
              a display whose `coupon` is `null` means the email goes out without
              a code, even if its copy promises one.'
            properties:
              enabled:
                type: boolean
                description: 'Whether the after-signup email is published. A published
                  email sends for every signup the display takes, so read it alongside
                  `status`: an archived or paused display takes no signups.'
                example: true
              subject:
                type:
                - string
                - 'null'
                description: The email's subject line as stored, which may include
                  merge tags such as `{{ business.name }}`. `null` when the display
                  has no after-signup email configured.
                example: Take 10% Off
          updated_at:
            type: string
            format: date-time
            example: '2026-06-10T15:30:00Z'
    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
    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'
    ReportSummaryMetric:
      type: object
      description: |
        A report metric with previous-period comparison fields.
        `previous_value`, `change` and `change_type` are null unless the
        request passed `compare=previous_period`. `change_type` says how to
        read `change`, and the four values are not interchangeable:
        `relative` is the percent change (one decimal, e.g. 26.5 means
        +26.5%) and is used for count and money metrics whenever the previous
        period was non-zero; `percent_point` is the absolute percentage-point
        delta and is used for every rate metric (never a relative percent, so
        a small rate move cannot read as a huge swing); `absolute` is the raw
        delta in the metric's unit, used when the previous period was zero and
        this one was not; and `not_comparable` means no delta could be
        computed, with `change` null. Units follow the `ReportMetric`
        conventions. A null `value` on a rate metric means the period had no
        volume to divide by, which is how "no data" is distinguished from a
        real 0% — though not every rate nulls, so a 0 can still mean either.
      properties:
        value:
          type:
          - number
          - 'null'
          example: 1240
        previous_value:
          type:
          - number
          - 'null'
          example: 980
        change:
          type:
          - number
          - 'null'
          example: 26.5
        change_type:
          type:
          - string
          - 'null'
          enum:
          - relative
          - percent_point
          - absolute
          - not_comparable
          -
          example: relative
        unit:
          type: string
          description: "`count`, `percent`, or a lowercased ISO 4217 currency code."
          example: count
    ReportChannelBreakdown:
      type:
      - object
      - 'null'
      description: |
        Current-period split by channel, in the parent metric's unit. Null when
        the period reaches before the account's reporting boundary — the
        historical source cannot credit an order to a single message channel;
        never treat null as zero. Never
        carries comparison fields, even when the request asked for one — only
        the parent metric does. On attributed orders and revenue the two legs
        never double count, because each order is credited to a single message
        and so to a single channel. On signups they can: the split is built
        from what each form captured, so a form that took both an email address
        and a phone number counts in both legs, and the legs need not add up to
        the signup total.
      properties:
        email:
          type: number
          example: 1050
        sms:
          type: number
          example: 190
    ReportTypeBreakdown:
      type:
      - object
      - 'null'
      description: |
        Current-period split by message type, in the parent metric's unit. Null
        when the period reaches before the account's reporting boundary — the
        historical source does not carry the split; never treat null as zero.
        Never carries comparison fields, even when the request asked for one —
        only the parent metric does. `campaign` counts newsletters, the one-time
        email and SMS sends. `flow` is broader than the flows in `/v1/flows`:
        it also covers the account's older automations and the follow-up email
        or SMS a display sends after a signup. The two legs cover every
        attributed message, so they add up to the parent metric's value.
      properties:
        campaign:
          type: number
          example: 190
        flow:
          type: number
          example: 122
    DisplayReportStats:
      type: object
      description: |
        Performance metrics for one display over the report period. `views`
        counts impressions — one per time the display loaded on a visitor's
        screen, so a visitor who saw it on three pages counts three times — and
        `signups` counts form submissions, not distinct contacts. `signup_rate`
        is `signups` over `views`. `attributed_orders` and `attributed_revenue`
        credit the display under the same last-touch rule as the rest of
        Privy's reporting: an order counts when the contact's most recent
        engagement in the 7 days before it was an open or click on a message
        this display sends, such as its follow-up email or SMS. They are not a
        count of visitors who signed up here and later bought, which is the
        figure Privy's own display reporting shows — expect the two to differ,
        and expect a display that sends no follow-up message to report zero.
      properties:
        views:
          "$ref": "#/components/schemas/ReportMetric"
        signups:
          "$ref": "#/components/schemas/ReportMetric"
        signup_rate:
          "$ref": "#/components/schemas/ReportMetric"
        attributed_orders:
          "$ref": "#/components/schemas/ReportMetric"
        attributed_revenue:
          "$ref": "#/components/schemas/ReportMetric"
    DisplayReportRow:
      type: object
      description: One display's entry in the ranked leaderboard. `GET /v1/reports/displays/{display_id}`
        returns the richer `DisplayReportDetail` shape for a single display.
      properties:
        display_id:
          type: string
          description: The display's unique identifier. Use this value in `/v1/reports/displays/{display_id}`
            and `/v1/displays/{id}`.
          example: gk2P8Zw1Lq
        title:
          type:
          - string
          - 'null'
          description: The display's name. `null` when the merchant never named it.
          example: Summer Sale Popup
        display_type:
          type:
          - string
          - 'null'
          description: The display's on-site shape (e.g. `popup`, `flyout`, `banner`,
            `bar`, `embedded_form`, `landing_page`).
          example: popup
        status:
          type: string
          description: The display's lifecycle status (e.g. `draft`, `scheduled`,
            `live`, `paused`, `ended`, `archived`).
          example: live
        published_date:
          type:
          - string
          - 'null'
          format: date
          description: The date the display started showing, in the account's time
            zone. `null` when it has never been published.
          example: '2026-06-01'
        stats:
          "$ref": "#/components/schemas/DisplayReportStats"
    DisplayReportAbTestVariation:
      type: object
      description: One arm of the display's A/B test — the control or a challenger
        — with its own traffic share and results. Figures cover the test's whole life
        and ignore the report period, like the enclosing `ab_test` block.
      properties:
        id:
          type: string
          description: Opaque, stable identifier for this variation row, prefixed
            `abv_`. Use it to key the `variations` array. No `/v1` resource resolves
            it — the prefix marks it as a report-local handle, not a lookup id. Unlike
            `display_id` it is always present, including for legacy variations with
            no display attached.
          example: abv_Jq8T2vY
        display_id:
          type:
          - string
          - 'null'
          description: Public id of the display this variation renders, resolvable
            at `GET /v1/displays/{id}`. For the control variation this is the display
            the report is about; each challenger is its own display. `null` only for
            legacy variations with no display attached — use `id` when a value is
            always required.
          example: gk2P8Zw1Lq
        name:
          type:
          - string
          - 'null'
          description: Merchant-assigned label for this variation. `null` when the
            variation was never named.
          example: Challenger
        is_control:
          type: boolean
          description: Whether this variation is the original (control) version.
          example: false
        traffic_ratio:
          type: number
          description: Fraction of traffic (0–1) assigned to this variation.
          example: 0.5
        participants:
          type: integer
          description: Distinct visitors assigned to this variation over the test's
            life.
          example: 4700
        conversions:
          type: integer
          description: 'Goal events recorded for this variation — every occurrence,
            not distinct converters, so one visitor converting twice counts twice.

            '
          example: 129
        conversion_rate:
          allOf:
          - "$ref": "#/components/schemas/ReportMetric"
          description: "`conversions` over `participants`, 0 when the variation has
            no participants. It can exceed 100 because the numerator counts events
            and the denominator counts visitors.\n"
    DisplayReportAbTest:
      type: object
      description: |
        Results of the display's A/B test. `null` when the display has never
        had one; otherwise the display's currently running test, or its most
        recently created test once none is running. Figures cover the test's
        whole life and ignore the report period, so they will not line up with
        the surrounding `stats` block.
      properties:
        name:
          type: string
          description: The merchant's name for the test.
          example: Headline test
        state:
          type:
          - string
          - 'null'
          description: The test's lifecycle state (e.g. `draft`, `active`, `finished`).
          example: finished
        goal:
          type:
          - string
          - 'null'
          description: The event the test optimizes for.
          example: signup
        total_participants:
          type: integer
          description: Visitors assigned across every variation — the sum of the variations'
            `participants`.
          example: 9400
        winner_variation_id:
          type:
          - string
          - 'null'
          description: Currently always `null` — a merged test winner is not persisted.
            When populated it matches a variation's `id` in `variations`.
          example:
        variations:
          type: array
          description: One entry per arm of the test. The order is not significant
            — use `is_control` to find the original and `id` to key rows.
          items:
            "$ref": "#/components/schemas/DisplayReportAbTestVariation"
    DisplayReportDetail:
      type: object
      description: 'One display''s report: its identity, the period-scoped `stats`
        block, and A/B test results when the display has ever had a test.'
      properties:
        display_id:
          type: string
          description: The display's unique identifier. Use this value in `/v1/displays/{id}`.
          example: gk2P8Zw1Lq
        title:
          type:
          - string
          - 'null'
          description: The display's name. `null` when the merchant never named it.
          example: Summer Sale Popup
        display_type:
          type:
          - string
          - 'null'
          description: The display's on-site shape (e.g. `popup`, `flyout`, `banner`,
            `bar`, `embedded_form`, `landing_page`).
          example: popup
        status:
          type: string
          description: The display's lifecycle status (e.g. `draft`, `scheduled`,
            `live`, `paused`, `ended`, `archived`).
          example: live
        period:
          "$ref": "#/components/schemas/ReportPeriod"
        stats:
          description: The list shape's metrics plus `average_order_value`, which
            only this endpoint reports.
          allOf:
          - "$ref": "#/components/schemas/DisplayReportStats"
          - type: object
            properties:
              average_order_value:
                "$ref": "#/components/schemas/ReportMetric"
        ab_test:
          description: The display's A/B test results, or `null` when it has never
            had one. Figures cover the test's whole life, so they will not line up
            with the surrounding `stats` block.
          oneOf:
          - "$ref": "#/components/schemas/DisplayReportAbTest"
          - type: 'null'
    FlowReportStats:
      type: object
      description: |
        Combined email + SMS performance for one flow over the report period,
        summed across the flow's message nodes. `sent` counts send attempts and
        `delivered` counts provider-confirmed deliveries — separate counters,
        neither derived from the other. `opened` and `clicked` are unique counts
        (contacts who opened or clicked at least once within the report period),
        not totals of every event.
        `open_rate` is `opened` over email deliveries only, since opens do not
        exist for SMS; `click_rate` and `order_rate` are over email and SMS
        deliveries combined. `attributed_orders` and `attributed_revenue` credit
        the flow 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: last-touch attribution, where that single most
        recent engagement takes the whole order.
      properties:
        sent:
          "$ref": "#/components/schemas/ReportMetric"
        delivered:
          "$ref": "#/components/schemas/ReportMetric"
        opened:
          "$ref": "#/components/schemas/ReportMetric"
        clicked:
          "$ref": "#/components/schemas/ReportMetric"
        open_rate:
          "$ref": "#/components/schemas/ReportMetric"
        click_rate:
          "$ref": "#/components/schemas/ReportMetric"
        attributed_orders:
          "$ref": "#/components/schemas/ReportMetric"
        attributed_revenue:
          "$ref": "#/components/schemas/ReportMetric"
        order_rate:
          "$ref": "#/components/schemas/ReportMetric"
        average_order_value:
          "$ref": "#/components/schemas/ReportMetric"
        unsubscribed:
          "$ref": "#/components/schemas/ReportMetric"
        bounced:
          "$ref": "#/components/schemas/ReportMetric"
        sms_opted_out:
          "$ref": "#/components/schemas/ReportMetric"
        sms_failed:
          "$ref": "#/components/schemas/ReportMetric"
    FlowReportRow:
      type: object
      properties:
        flow_id:
          type: string
          description: The flow's unique identifier. Use this value in `/v1/reports/flows/{flow_id}`
            and `/v1/flows/{id}`.
          example: Vq7L3mN9xR
        title:
          type:
          - string
          - 'null'
          example: Abandoned Cart Recovery
        status:
          type: string
          enum:
          - draft
          - active
          - inactive
          example: active
        trigger_type:
          type:
          - string
          - 'null'
          description: Normalized name of the event that starts the flow (e.g. `order_placed`,
            `cart_abandoned`, `customer_subscribed_to_marketing`) — the same vocabulary
            `GET /v1/flows` returns.
          example: cart_abandoned
        trigger_label:
          type:
          - string
          - 'null'
          description: Human-readable form of `trigger_type`.
          example: Cart abandoned
        channels:
          type:
          - string
          - 'null'
          enum:
          - email
          - sms
          - multi
          -
          description: Message channels the flow sends on. `null` when the flow has
            no message nodes.
          example: multi
        current_enrollment:
          allOf:
          - "$ref": "#/components/schemas/ReportMetric"
          description: |
            Contacts with a journey in progress right now — a point-in-time count,
            not scoped to the report period, and at most one journey per contact.
        stats:
          "$ref": "#/components/schemas/FlowReportStats"
    FlowReportNodeStats:
      type: object
      description: |
        Per-node deliverability and attribution metrics for one message step over
        the report period. Email nodes carry `opened_unique`, `bounced`, and
        `unsubscribed`; `sms` nodes carry `opted_out` and `failed`; both carry the
        rest. `opened_unique` and `clicked_unique` count contacts who opened or
        clicked at least once, not total opens or clicks. `attributed_orders` and
        `attributed_revenue` credit this node's message under the same last-touch,
        7-day rule as the flow total.
      properties:
        sent:
          "$ref": "#/components/schemas/ReportMetric"
        delivered:
          "$ref": "#/components/schemas/ReportMetric"
        opened_unique:
          "$ref": "#/components/schemas/ReportMetric"
        clicked_unique:
          "$ref": "#/components/schemas/ReportMetric"
        bounced:
          "$ref": "#/components/schemas/ReportMetric"
        unsubscribed:
          "$ref": "#/components/schemas/ReportMetric"
        opted_out:
          "$ref": "#/components/schemas/ReportMetric"
        failed:
          "$ref": "#/components/schemas/ReportMetric"
        attributed_orders:
          "$ref": "#/components/schemas/ReportMetric"
        attributed_revenue:
          "$ref": "#/components/schemas/ReportMetric"
    FlowReportNode:
      type: object
      properties:
        uuid:
          type: string
          example: 7a2f9e1b-5c3d-4d2e-8b4f-3a6c8d9e0f1a
        type:
          type: string
          enum:
          - email
          - sms
          example: email
        name:
          type:
          - string
          - 'null'
          example: Cart reminder email
        subject:
          type:
          - string
          - 'null'
          description: The email's subject line. Present on email nodes only.
          example: You left something behind
        stats:
          "$ref": "#/components/schemas/FlowReportNodeStats"
    FlowReportDetail:
      allOf:
      - "$ref": "#/components/schemas/FlowReportRow"
      - type: object
        properties:
          period:
            "$ref": "#/components/schemas/ReportPeriod"
          nodes:
            type: array
            description: Per-node breakdown of the flow's email and SMS message steps.
            items:
              "$ref": "#/components/schemas/FlowReportNode"
    CampaignReportAudience:
      type: object
      description: 'Saved segment names the send targeted and excluded. Smart-segment
        audiences carry no saved name and are omitted.

        '
      properties:
        included_segments:
          type: array
          items:
            type: string
          example:
          - Engaged 90d
        excluded_segments:
          type: array
          items:
            type: string
          example:
          - Recent purchasers
    CampaignReportRow:
      type: object
      properties:
        campaign_id:
          type: string
          description: 'The campaign''s unique identifier. Use this value in `/v1/reports/campaigns/{campaign_id}`;
            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
        sent_date:
          type:
          - string
          - 'null'
          format: date
          description: 'Newsletter rows only — flow campaigns send continuously, so
            their rows omit this field.

            '
          example: '2026-06-18'
        audience:
          allOf:
          - "$ref": "#/components/schemas/CampaignReportAudience"
          description: 'Newsletter rows only — flow campaigns target no segments,
            so their rows omit this field.

            '
        flow:
          oneOf:
          - "$ref": "#/components/schemas/CampaignFlowRef"
          - type: 'null'
          description: 'Flow rows only — the flow that sends this message, so ranking
            a flow''s messages needs no follow-up request. Newsletter rows omit the
            field entirely; a flow row whose node has since been deleted carries `null`.

            '
        stats:
          "$ref": "#/components/schemas/CampaignReportFunnelStats"
    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
    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"
    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"
    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
          -
          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`.\n"
          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
          -
          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"
    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"
    FlowNode:
      type: object
      description: One step in the flow. `email` and `sms` send a message — the only
        node types that carry per-node report stats; `delay` waits; `split` branches
        on conditions; `ab_test` sends each contact down path A or B by a percentage
        split; `tag` and `update_contact` change the contact.
      properties:
        uuid:
          type: string
          example: 6f1e8d0a-4b2c-4c1d-9a3e-2f5b7c8d9e0f
        type:
          type: string
          enum:
          - email
          - sms
          - delay
          - split
          - ab_test
          - tag
          - update_contact
          example: email
        name:
          type: string
          example: Welcome email
    Flow:
      type: object
      properties:
        id:
          type: string
          description: Stable, unique identifier for the flow. Use this value in `/v1/flows/{id}`.
          example: Vq7L3mN9xR
        title:
          type:
          - string
          - 'null'
          example: Abandoned Cart Recovery
        status:
          type: string
          enum:
          - draft
          - active
          - inactive
          example: active
        trigger_type:
          type:
          - string
          - 'null'
          description: |
            Normalized name of the event that starts the flow — e.g.
            `order_placed`, `order_delivered`, `cart_abandoned`,
            `checkout_abandoned`, `browse_abandoned`, `back_in_stock`,
            `date_triggered`, `customer_subscribed_to_marketing`,
            `customer_tag_added`, `customer_tag_removed`, `flow_completed`.
            `order_placed` is the one name for the per-platform order events
            (Shopify, BigCommerce, Wix, and the public API) and `order_delivered`
            renames the order-received event; every other trigger passes through as
            stored, so integration and custom API event names (`api_event:...`)
            appear here verbatim.

            Platform availability: `browse_abandoned` and `back_in_stock` currently
            fire for Shopify stores only — the underlying product-view and inventory
            signals aren't collected for other platforms, so a flow using one of
            these triggers on a non-Shopify business will never enroll a contact.
            `checkout_abandoned` and `cart_abandoned` are available on both Shopify
            and BigCommerce. Shopify checkout starts come from the Web Pixel, so the
            pixel must hold the scopes that track them; BigCommerce checkout starts
            are captured when the Privy widget loads on the standard Optimized
            One-Page Checkout, so custom or headless BigCommerce checkouts produce no
            event and never enroll a contact.
          example: cart_abandoned
        trigger_label:
          type:
          - string
          - 'null'
          description: Human-readable form of `trigger_type`.
          example: Cart abandoned
        nodes:
          type: array
          description: The flow's steps, from its current version.
          items:
            "$ref": "#/components/schemas/FlowNode"
        sync_coupons:
          type: boolean
          description: |
            Whether the flow uses one shared coupon: when true, a single coupon is
            set at the flow level and applied to every email and SMS node in every
            version of the flow, including nodes added later. When false, coupons
            are attached to individual nodes by hand and nothing is propagated.
          example: false
        last_transition_at:
          type:
          - string
          - 'null'
          format: date-time
          description: When the flow last changed status.
          example: '2026-06-01T12:00:00Z'
    ConditionTree:
      type: object
      description: |-
        A group of conditions. `operation` joins them (`AND`/`OR`); each condition tests a property (e.g. segment membership, order count, cart value) with an operator and value. The same shape carries a flow's entry filters (`flow_filters`) and a split node's `conditions` — each is evaluated at a different point, described on those fields. A flow's exit criteria are not published in this shape; see `exit_behavior`.
        Entity ids embedded in condition values are public ids. Unprefixed ids round-trip through their GET endpoints: `segment` values via `/v1/segments/{id}`, `source_flow_id` and `skip_customer_in_flows` values via `/v1/flows/{id}`, and a signup `source`'s values via `/v1/displays/{id}`.
        Ids for objects with no lookup endpoint carry a type prefix, so a prefixed id is never a valid argument to any `/v1` path: Privy tags read `tag_<id>`, Shopify tags read `stag_<id>` (both in `privy_tag`, `shopify_tag`, and the `*_tag_added`/`*_tag_removed` trigger filters), and a `date_triggered` value's `date_field_label_id` reads `dfl_<id>`. The same prefix appears inside the `date_field_label$<id>` property name. Do not strip a prefix to retry a lookup — the remainder decodes as another resource's id and will return the wrong record.
        A `segment` value may also be a smart-segment reference of the form `smart_segments/<name>` — an opaque built-in segment name, not an id. The prefix is there to mark it unresolvable, so the same segment reads differently on a campaign's audience, where it appears as the bare `<name>` with `"prebuilt": true`; the name after the prefix is what matches the two. Sentinel values (`"*"` for any display, `"any"`) are not ids. Never treat other condition values as ids — product and collection values are the connected store's own ids.
        A condition's `type` describes the value as Privy stores it, not as it is emitted here: an id stored as an integer is published as its public id, so a row reading `"type": "integer"` still carries a string. Read ids as strings regardless of the `type` beside them.
      properties:
        operation:
          type: string
          description: How this group's conditions combine — `AND` requires every
            condition to match, `OR` any one of them.
          enum:
          - AND
          - OR
          example: AND
        conditions:
          type: array
          description: The group's members. An entry is either a single test, as described
            below, or a nested group carrying its own `operation` and `conditions`
            — trees can be arbitrarily deep, so recurse rather than assuming one level.
          items:
            type: object
            properties:
              property:
                type: string
                description: The attribute being tested. Some property names embed
                  a discriminator — `custom_fields$<identifier>` names a merchant-defined
                  field and `date_field_label$<id>` a date field.
                example: segment
              operator:
                type: string
                description: How `value` is compared against the property (e.g. `equals`,
                  `contains`, `greater_than`).
                example: contains
              type:
                type: string
                description: How Privy stores the value (e.g. `integer`, `string`,
                  `array`) — not how it is emitted here. An id stored as an integer
                  is published as its public id, so a row reading `integer` still
                  carries a string. Read ids as strings regardless of this field.
                example: array
              value:
                description: The comparison value — a scalar or an array, depending
                  on the property and operator.
                example:
                - Nk2P5rW8yU
    FlowDetailNode:
      allOf:
      - "$ref": "#/components/schemas/FlowNode"
      - type: object
        description: Node shape returned by `GET /v1/flows/{id}` — adds the branch
          graph and the node's configuration. Nodes are listed in tree-traversal order
          (depth-first from the node whose `parent_uuid` is `null`, yes branch before
          no branch), so after a split the whole yes branch reads before the no branch.
        properties:
          parent_uuid:
            type:
            - string
            - 'null'
            description: "`uuid` of the node's parent; `null` on the flow's first
              node."
            example:
          next_uuid:
            type:
            - string
            - 'null'
            description: "`uuid` of the next node. Present on every node type except
              `split` and `ab_test`; `null` at the end of a branch."
            example: 7a2f9e1b-5c3d-4d2e-8b4f-3a6c8d9e0f1a
          yes_next_uuid:
            type:
            - string
            - 'null'
            description: "`split` nodes only — `uuid` of the first node on the branch
              taken when the conditions match."
            example: 7a2f9e1b-5c3d-4d2e-8b4f-3a6c8d9e0f1a
          no_next_uuid:
            type:
            - string
            - 'null'
            description: "`split` nodes only — `uuid` of the first node on the branch
              taken when the conditions do not match."
            example: 9c4d1a3e-7e5f-4f4a-8d6b-5c8e0f1a2b3c
          a_next_uuid:
            type:
            - string
            - 'null'
            description: "`ab_test` nodes only — `uuid` of the first node on path
              A."
            example: 7a2f9e1b-5c3d-4d2e-8b4f-3a6c8d9e0f1a
          b_next_uuid:
            type:
            - string
            - 'null'
            description: "`ab_test` nodes only — `uuid` of the first node on path
              B. When it is another `ab_test` node, that node splits the B traffic
              again."
            example: 9c4d1a3e-7e5f-4f4a-8d6b-5c8e0f1a2b3c
          data:
            type: object
            description: The node's configuration; fields depend on `type`. `email`
              — `campaign_id` (usable with `GET /v1/campaigns/{id}`), `subject`, `coupon`
              (the attached pool as `{id, title, kind, platform}`, `null` when none
              — a `{{ coupon_code }}` tag only renders when attached). `sms` — `campaign_id`,
              `message_body`, `coupon` (as above). `delay` — `delay_unit` (`minutes`/`hours`/`days`),
              `delay_unit_count`. `split` — `conditions` (the condition group the
              split evaluates). `ab_test` — `a_percent` (share of contacts sent down
              path A; path B gets the rest), `winner` (`a` or `b` once a winner is
              picked, otherwise `null`). `tag` — `action_type` (`add`/`remove`), `tags`
              (tag names). `update_contact` — `action_type`.
            properties:
              conditions:
                allOf:
                - "$ref": "#/components/schemas/ConditionTree"
                description: Present on `split` nodes only — the condition group the
                  split evaluates. Ids embedded in its values follow the id and prefix
                  rules described on `ConditionTree`; this is where the `tag_`, `stag_`,
                  and `dfl_` prefixed ids surface.
            example:
              delay_unit: hours
              delay_unit_count: 1
    FlowDetail:
      allOf:
      - "$ref": "#/components/schemas/Flow"
      - type: object
        properties:
          nodes:
            type: array
            description: The flow's steps from its current version, in tree-traversal
              order, with the branch graph and each step's configuration.
            items:
              "$ref": "#/components/schemas/FlowDetailNode"
          reentry:
            type: object
            description: |
              The effective re-entry behavior — whether a contact who has
              been through the flow can enter it again. `immediate`: a new
              qualifying trigger re-enrolls the contact as soon as their
              previous journey has ended. `never`: the contact enters at
              most once. `after_delay`: the contact may re-enter once
              `frequency` `unit` have elapsed since their previous journey
              ended — measured from when it ended, however it ended
              (reaching the last node, being removed by exit criteria, or
              the flow being deactivated mid-journey). In every mode a
              contact with a journey in progress is never re-enrolled — no
              setting runs two journeys at once.
            properties:
              mode:
                type: string
                enum:
                - immediate
                - never
                - after_delay
                example: after_delay
              frequency:
                type: integer
                description: Present only when `mode` is `after_delay`.
                example: 30
              unit:
                type: string
                enum:
                - minutes
                - hours
                - days
                description: Present only when `mode` is `after_delay`.
                example: days
          flow_filters:
            anyOf:
            - "$ref": "#/components/schemas/ConditionTree"
            - type: 'null'
            description: |
              Conditions a contact must meet to enter the flow, evaluated once
              when the triggering event is processed and never re-checked
              afterwards. Contact properties (segment membership, tags, order
              count, lifetime spend, country, custom fields, integration fields)
              are matched against the contact's live record; event properties (the
              triggering cart or order's value, subscription channel, source, and
              history) are matched against the triggering event's payload. `null`
              when the flow has no filters.
          exit_behavior:
            type:
            - object
            - 'null'
            description: |
              The effective early-exit behavior; `null` when nothing ends
              journeys early. When any `exit_events` event occurs after a
              contact enrolled, their in-progress journey ends. This applies
              only to journeys already in progress: the event that enrolled
              the contact never triggers the exit, and exits can never block
              entry. On a single event the exit of a previous journey runs
              before enrollment is evaluated, so a trigger and an exit
              firing on the same event type (e.g. an order-placed trigger
              with an order-placed exit) is a working setup — the standard
              winback / purchase follow-up pattern, not a misconfiguration.
              Whether that same event also re-enrolls the contact is
              `reentry.mode`. The stored exit condition's comparison
              operator and value are not evaluated by the engine and are
              deliberately not returned.
            properties:
              exit_events:
                type: array
                description: |
                  Events that end an in-progress journey. An order-count
                  exit condition listens to both `order_placed` and
                  `order_delivered`, so those two always appear together.
                items:
                  type: string
                  enum:
                  - order_placed
                  - order_delivered
                  - product_added_to_cart
                  - checkout_started
                example:
                - order_placed
                - order_delivered
              applies_to:
                type: string
                enum:
                - active_journeys_only
                description: Exits only end journeys already in progress — they never
                  block entry.
              enrollment_event_excluded:
                type: boolean
                enum:
                - true
                description: The event that enrolled the contact never triggers the
                  exit.
              summary:
                type: string
                description: Server-generated restatement of this exit behavior.
                example: After enrollment, a subsequent order placed or order delivered
                  event ends the journey early. The event that enrolled the contact
                  never triggers this exit.
          behavior_summary:
            type: string
            description: |
              Server-generated plain-language description of the flow's
              effective runtime behavior: enrollment, message timing, early
              exits, and re-entry. Deterministic — derived from the
              configuration by the server. Prefer it over reconstructing
              event ordering from the raw fields.

              When `status` is not `active` the summary opens by saying so:
              a flow that is not active enrolls no contacts and sends no
              messages, and the sentences that follow describe what it is
              configured to do once activated, not what it is doing now.
            example: A contact enrolls when the "Cart abandoned" event occurs. The
              flow has 2 message steps. A subsequent order placed or order delivered
              event ends an active journey early; the event that enrolled the contact
              never triggers this exit. A contact can re-enter 30 days after their
              previous journey ends; no setting ever runs two journeys at once.
    Segment:
      type: object
      properties:
        id:
          type: string
          description: Stable, unique identifier for the segment. Use this value in
            `/v1/segments/{id}`.
          example: Rz5T2kW8pB
        name:
          type: string
          example: VIP Customers
        prebuilt:
          type: boolean
          description: |
            Whether the segment was created from one of Privy's prebuilt smart
            segment templates. (All segments are dynamic — membership is computed
            from criteria, not a static list — so there is no `type` field.)
          example: false
        mailable_count:
          type:
          - integer
          - 'null'
          description: Number of members reachable by email, as of `counts.refreshed_at`.
            `null` if never computed.
          example: 1240
        textable_count:
          type:
          - integer
          - 'null'
          description: Number of members reachable by SMS, as of `counts.refreshed_at`.
            `null` if never computed.
          example: 315
        counts:
          type: object
          properties:
            recounting:
              type: boolean
              description: Whether a membership refresh is currently running.
              example: false
            refreshed_at:
              type:
              - string
              - 'null'
              format: date-time
              description: When membership and counts were last computed. `null` if
                never computed.
              example: '2026-06-15T08:00:00Z'
            freshness:
              type: string
              description: Note on how current the counts are.
              example: Counts are refreshed periodically and may be up to an hour
                old.
        created_at:
          type: string
          format: date-time
          example: '2026-03-01T10:00:00Z'
        updated_at:
          type: string
          format: date-time
          example: '2026-06-01T10:00:00Z'
    SegmentDetail:
      description: 'A single segment, as returned by `GET /v1/segments/{id}`.

        '
      allOf:
      - "$ref": "#/components/schemas/Segment"
      - type: object
        properties: {}
    ProductVariant:
      type: object
      properties:
        id:
          type: integer
          format: int64
          description: The store's variant id.
          example: 40012345678901
        title:
          type:
          - string
          - 'null'
          description: '`null` on BigCommerce stores for a product''s base variant
            (it has no option values); Shopify default variants carry "Default Title"
            instead.'
          example: Small
        price:
          type:
          - string
          - 'null'
          description: Variant price in the store's currency, as a decimal string.
          example: '10.00'
        compare_at_price:
          type:
          - string
          - 'null'
          example: '15.00'
        sku:
          type:
          - string
          - 'null'
          example: TSHIRT-S
        inventory_quantity:
          type:
          - integer
          - 'null'
          example: 12
        available:
          type: boolean
          description: Whether the variant can currently be purchased (untracked inventory
            counts as available).
          example: true
    CouponRef:
      type: object
      description: 'The coupon pool attached to a message or display — a `{{ coupon_code
        }}` tag in the content only renders when one is attached. Pass `id` to `GET
        /v1/coupons/{id}` for the pool''s inventory and health; `kind` and `platform`
        match that endpoint''s rows.

        '
      properties:
        id:
          type: string
          description: The pool's identifier, as returned by `GET /v1/coupons`.
          example: Xk2fW9qLpV
        title:
          type: string
          example: Welcome 10%
        kind:
          type: string
          enum:
          - master
          - unique
          description: "`master` is a single shared code; `unique` is a pool of individually
            generated codes."
        platform:
          type: string
          enum:
          - manual
          - shopify
          - bigcommerce
          - woocommerce
    Coupon:
      type: object
      properties:
        id:
          type: string
          description: Stable, unique identifier for the coupon pool.
          example: Xk2fW9qLpV
        title:
          type: string
          example: Welcome 10%
        kind:
          type: string
          enum:
          - master
          - unique
          description: "`master` is a single shared code; `unique` is a pool of individually
            generated codes."
        platform:
          type: string
          enum:
          - manual
          - shopify
          - bigcommerce
          - woocommerce
          description: Where the pool's codes live — created by hand in Privy, or
            synced from the connected store.
        master_code:
          type:
          - string
          - 'null'
          description: The pool's single shared code. Master pools only — always `null`
            for unique pools.
          example: SAVE20
        prefix_code:
          type:
          - string
          - 'null'
          description: The prefix the pool's generated codes start with. Unique pools
            only — always `null` for master pools, and `null` on unique pools that
            generate without a prefix.
          example: VIP
        coupon_codes_remaining:
          type:
          - integer
          - 'null'
          description: Unused codes still held in the pool, decreasing as codes are
            handed out. Manual unique pools only (they hold an uploaded inventory);
            `null` for store-backed unique pools (Shopify/BigCommerce mint codes on
            demand and hold no inventory) and for master coupons.
          example: 4
        coupon_codes_issued:
          type:
          - integer
          - 'null'
          description: 'Codes Privy has generated for contacts from this pool, increasing
            over time. Counting began when this field was introduced — earlier issuance
            is not included, so an older pool can read `0` despite having sent codes.
            Approximate: a bulk batch whose generation Privy could not confirm is
            not counted even if the store created its codes. Unique pools only; `null`
            for master coupons.'
          example: 4210
        healthy:
          type: boolean
          description: Whether the pool is syncing and generating codes normally.
        failure_message:
          type:
          - string
          - 'null'
          description: Human-readable failure detail. `null` when the pool is healthy.
        attached_to:
          type: object
          description: 'Every message surface currently pointing at this pool — answers
            "is this pool actually used anywhere?" without walking the other endpoints.
            Flow campaign refs cover only each flow''s current version (the active
            one, or the newest draft) — a flow''s older versions reference the pool
            through their own campaign rows and are excluded rather than listed as
            duplicate same-title refs. Ids round-trip through `GET /v1/displays/{id}`
            and `GET /v1/campaigns/{id}`.

            '
          properties:
            displays:
              type: array
              items:
                type: object
                properties:
                  id:
                    type: string
                    example: gk2P8Zw1Lq
                  title:
                    type:
                    - string
                    - 'null'
                    example: Summer Sale Popup
            campaigns:
              type: array
              items:
                type: object
                properties:
                  id:
                    type: string
                    example: Nw8K2rT5xY
                  title:
                    type:
                    - string
                    - 'null'
                    example: June Restock Announcement
                  type:
                    type: string
                    enum:
                    - newsletter
                    - flow
                  channel:
                    type: string
                    enum:
                    - email
                    - sms
    CouponDetail:
      allOf:
      - "$ref": "#/components/schemas/Coupon"
      - type: object
        properties:
          usage_count:
            type: integer
            description: The connected store's pool-wide redemption counter, read
              live. Omitted when unavailable — absent is not zero.
            example: 12
    Product:
      type: object
      properties:
        id:
          type: integer
          format: int64
          description: The store's product id. Use this value in `/v1/products/{id}`.
            Matches the product ids on order line items.
          example: 7890123456789
        title:
          type: string
          example: Classic T-Shirt
        handle:
          type:
          - string
          - 'null'
          description: The product's URL slug. `null` on BigCommerce products with
            no storefront path.
          example: classic-t-shirt
        status:
          type: string
          enum:
          - active
          - draft
          - archived
          example: active
        product_type:
          type:
          - string
          - 'null'
          description: The merchant-defined product category. Always `null` on BigCommerce
            stores.
          example: Shirts
        vendor:
          type:
          - string
          - 'null'
          description: The product's vendor/brand. Always `null` on BigCommerce stores.
          example: Acme Apparel
        url:
          type:
          - string
          - 'null'
          description: The product's storefront URL. `null` if not published to the
            online store; always `null` on BigCommerce stores.
          example: https://mystore.com/products/classic-t-shirt
        image_url:
          type:
          - string
          - 'null'
          example: https://cdn.shopify.com/s/files/1/0001/products/tshirt.jpg
        price_range:
          type:
          - object
          - 'null'
          properties:
            min:
              type: string
              example: '10.00'
            max:
              type: string
              example: '15.00'
            currency_code:
              type: string
              example: USD
        total_inventory:
          type:
          - integer
          - 'null'
          description: Total sellable inventory across variants. `null` when the store
            does not report it.
          example: 42
        tracks_inventory:
          type: boolean
          example: true
        variants_count:
          type:
          - integer
          - 'null'
          description: Total number of variants on the product (the `variants` array
            may be truncated below this).
          example: 3
        variants:
          type: array
          items:
            "$ref": "#/components/schemas/ProductVariant"
        created_at:
          type: string
          format: date-time
          example: '2026-01-10T09:00:00Z'
        updated_at:
          type: string
          format: date-time
          example: '2026-06-01T09:00:00Z'
    ProductImage:
      type: object
      properties:
        id:
          type:
          - integer
          - 'null'
          format: int64
          description: The store's image id.
          example: 30012345678901
        url:
          type: string
          example: https://cdn.shopify.com/s/files/1/0001/products/tshirt.jpg
        alt_text:
          type:
          - string
          - 'null'
          example: Front of the classic t-shirt
        width:
          type:
          - integer
          - 'null'
          example: 1600
        height:
          type:
          - integer
          - 'null'
          example: 1200
    ProductVariantDetail:
      allOf:
      - "$ref": "#/components/schemas/ProductVariant"
      - type: object
        properties:
          barcode:
            type:
            - string
            - 'null'
            example: '0123456789012'
          inventory_policy:
            type:
            - string
            - 'null'
            description: Whether the variant can be sold when out of stock. Always
              `null` on BigCommerce stores.
            enum:
            - deny
            - continue
            -
            example: deny
          position:
            type:
            - integer
            - 'null'
            description: The variant's position within the product. Always `null`
              on BigCommerce stores.
            example: 1
          product_id:
            type: integer
            format: int64
            description: The store's id of the parent product.
            example: 7890123456789
          taxable:
            type:
            - boolean
            - 'null'
            description: Always `null` on BigCommerce stores.
            example: true
          image_id:
            type:
            - integer
            - 'null'
            format: int64
            description: The store's id of the variant's image. `null` when the variant
              has no dedicated image.
            example: 30012345678901
          updated_at:
            type:
            - string
            - 'null'
            format: date-time
            description: Always `null` on BigCommerce stores.
            example: '2026-06-01T09:00:00Z'
    ProductCollection:
      type: object
      description: A collection of the connected store's products — a Shopify collection
        or a BigCommerce category.
      properties:
        id:
          type: integer
          format: int64
          description: The store's collection id. Use this value in a `purchase_history`
            segment condition's `collection` filter.
          example: 421123456789
        handle:
          type:
          - string
          - 'null'
          description: The collection's URL slug. On BigCommerce, which has no handles,
            it is the last segment of the category's storefront path, and `null` when
            the category has none.
          example: summer-collection
        title:
          type: string
          example: Summer Collection
    ProductDetail:
      description: |
        A product with the additional detail fields only returned by `GET /v1/products/{id}`
        (the list endpoint returns the leaner `Product` shape).
      allOf:
      - "$ref": "#/components/schemas/Product"
      - type: object
        properties:
          description:
            type:
            - string
            - 'null'
            description: The product description, as plain text.
            example: A timeless crew-neck tee in heavyweight cotton.
          tags:
            type: array
            items:
              type: string
            example:
            - cotton
            - summer
          options:
            type: array
            description: The product's option definitions (e.g. Size, Color).
            items:
              type: object
              properties:
                id:
                  type: integer
                  format: int64
                  example: 10012345678901
                name:
                  type: string
                  example: Size
                position:
                  type: integer
                  example: 1
                values:
                  type: array
                  items:
                    type: string
                  example:
                  - Small
                  - Large
          image:
            description: The product's featured image. `null` when the product has
              no images.
            anyOf:
            - "$ref": "#/components/schemas/ProductImage"
            - type: 'null'
          images:
            type: array
            description: All product images (up to 100).
            items:
              "$ref": "#/components/schemas/ProductImage"
          media:
            type: array
            description: All product media — images, videos, 3D models (up to 100).
            items:
              type: object
              properties:
                id:
                  type: integer
                  format: int64
                  example: 20012345678901
                media_content_type:
                  type: string
                  enum:
                  - image
                  - video
                  - external_video
                  - model_3d
                  example: image
                alt:
                  type:
                  - string
                  - 'null'
                  example: Front of the classic t-shirt
                preview_image_url:
                  type:
                  - string
                  - 'null'
                  example: https://cdn.shopify.com/s/files/1/0001/products/tshirt.jpg
          variants:
            type: array
            items:
              "$ref": "#/components/schemas/ProductVariantDetail"
    CursorPagination:
      type: object
      description: Cursor-based pagination envelope, used by the Products and Product
        Collections endpoints.
      properties:
        per_page:
          type: integer
          example: 40
        total_count:
          type:
          - integer
          - 'null'
          description: Total items across every page. `null` when the store does not
            report one.
          example: 128
        has_more:
          type: boolean
          example: true
        next_cursor:
          type:
          - string
          - 'null'
          description: Opaque cursor for the next page. Pass as `?cursor=`. `null`
            on the last page.
          example: eyJsYXN0X2lkIjo3ODkwMTIzNDU2Nzg5fQ==
    Contact:
      type: object
      description: |
        A contact in your Privy account: identity, marketing consent per channel, tags, and custom
        fields. Email and SMS consent are tracked independently — a contact who opts out of one
        keeps whatever consent they had for the other.
      properties:
        id:
          type: string
          description: Stable, unique identifier for the contact. Use this value in
            `/v1/contacts/{id}` endpoints.
          example: cus_a1b2c3d4e5f6g7h8
        first_name:
          type:
          - string
          - 'null'
          example: Jane
        last_name:
          type:
          - string
          - 'null'
          example: Doe
        email:
          type:
          - string
          - 'null'
          format: email
          example: jane@example.com
        email_consent:
          type: string
          enum:
          - subscribed
          - unsubscribed
          - never_subscribed
          - suppressed
          - compliance_suppressed
          description: |
            The contact's email marketing consent. `subscribed` is the only value that makes a
            contact mailable; Privy sends no marketing email to any of the other four.

            - `subscribed` — the contact has opted in. Covers both explicit consent (they ticked an
              email opt-in box, confirmed a double opt-in, or were subscribed through this API) and
              implicit consent (they gave you their address while buying, or arrived in an approved
              "subscribed" import). The two are not distinguished here.
            - `unsubscribed` — the contact opted out, typically through the preferences link in a
              Privy email. A later signup that provides consent resubscribes them.
            - `never_subscribed` — no opt-in and no opt-out on record; shown as "unset" in the Privy
              dashboard. This is the usual state for someone who bought without opting in, and for
              an imported contact whose import is still awaiting review.
            - `suppressed` — you suppressed the contact, from the dashboard or by writing
              `suppressed` to this field. Reversible: writing any other consent value unsuppresses
              the contact first, then applies the change you asked for.
            - `compliance_suppressed` — Privy suppressed the address to protect your sending
              reputation: a hard bounce, a spam complaint, a rejected delivery, an address that
              failed Privy's compliance check during an import, or a flow's Suppress Contact action.
              Read-only through this API — a write to this field returns `422`, and a later signup
              does not resubscribe them. Other fields on the contact remain editable.
          example: subscribed
        email_consent_at:
          type:
          - string
          - 'null'
          format: date-time
          description: 'When `email_consent` last changed. Null when consent was never
            set.

            '
          example: '2025-03-20T14:22:00Z'
        phone_number:
          type:
          - string
          - 'null'
          example: "+15551234567"
        sms_consent:
          type: string
          enum:
          - subscribed
          - unsubscribed
          - never_subscribed
          - single_opt_in
          - pending
          description: |
            The contact's SMS marketing consent. `subscribed` is the only value that makes a contact
            textable; Privy sends no marketing text to any of the other four.

            - `subscribed` — the contact's SMS opt-in is confirmed. Reached by replying `Y` to a
              double opt-in text, by signing up on a display set to Single Opt-In, by replying
              `START` or `UNSTOP`, by an opt-in recorded through this API, by a CSV import of
              SMS consent, or by a synced consent status from a connected store provider
              (e.g. Shopify). A confirmed opt-in does not record which method produced it:
              Single Opt-In and Double Opt-In displays both land on `subscribed`, so this field
              cannot tell you whether a contact double-opted in.
            - `unsubscribed` — the contact opted out, normally by replying `STOP`. Unlike email
              compliance suppression this is reversible: a new confirmed opt-in subscribes them again.
            - `never_subscribed` — no opt-in and no opt-out on record. Also the value after a
              reset: Privy clears SMS consent to this state when the phone number is removed from
              the contact, when a carrier reports the number as a landline, and when
              `never_subscribed` is written through this API. A contact who completed the SMS step
              of a multi-step display and has not yet replied `Y` also reads `never_subscribed` —
              that flow holds the opt-in off the contact record until it is confirmed — so do not
              read this value as "Privy never asked".
            - `single_opt_in` — a transitional pre-confirmation state inside the double opt-in
              flow, and the only pre-confirmation state a display signup can leave a contact in.
              The contact submitted a single-step display whose SMS opt-in method is Double Opt-In
              (Privy's default): the phone number is recorded, consent is not yet confirmed, and
              Privy is about to send the `Reply Y` confirmation text. In the healthy path the
              contact leaves this state within seconds — `pending` once the confirmation text is
              accepted by the carrier, then `subscribed` on the `Y` reply. A contact who stays
              here is one whose confirmation text was never sent (unsupported region, non-mobile
              number, SMS sending disabled on the account) or one written here directly through
              this API, which sends no confirmation text and so has no way forward. Not textable.
              Despite the name this is not the display's Single Opt-In setting — a display set to
              Single Opt-In confirms the contact outright, which reads as `subscribed`.
            - `pending` — Privy sent the double opt-in confirmation text and is waiting for the
              `Y` reply. Only reachable from `single_opt_in`, and only on the single-step display
              flow; the multi-step display flow tracks its awaiting-reply opt-ins outside this
              field. Not textable. Read-only.
          example: subscribed
        sms_consent_at:
          type:
          - string
          - 'null'
          format: date-time
          description: When `sms_consent` last changed. Null when consent was never
            set.
          example: '2025-03-20T14:22:00Z'
        source:
          type:
          - string
          - 'null'
          description: |
            How the contact RECORD was created: an import mechanism (`csv`, `shopify`,
            `mailchimp`, …), a Privy display, or the API. Free-form vocabulary — new
            integrations add values. This is not signup attribution: a store webhook or
            sync can create the record before the visitor ever signs up on a display, and
            a later display signup does not rewrite it. For which display(s) a contact
            signed up on, read `signups` on `GET /v1/contacts/{id}`.
          example: csv
        source_type:
          type:
          - string
          - 'null'
          description: |
            The kind of origin behind `source`. Common values: `import` (arrived in a contact
            import or store sync), `web` (signed up on a display), `hook` (created by a store
            webhook — e.g. `source: shopify, source_type: hook` is a contact first seen through
            a Shopify order or customer webhook). Same caveat as `source`: describes record
            creation, not later display signups.
          example: import
        imported:
          type:
          - boolean
          - 'null'
          description: Whether the contact arrived in a contact import.
          example: true
        import_ids:
          type: array
          items:
            type: string
          description: |
            The email contact imports that touched this contact, oldest first. SMS consent
            imports are tracked separately and are not referenced here.
          example:
          - im_k2R5m8Q
        tags:
          type: array
          items:
            type: string
          description: |
            The contact's Privy tags — free-form labels you define, with no fixed vocabulary. Privy
            lower-cases each name, trims surrounding whitespace, and drops duplicates on save, so tags
            always read back normalized. A name Privy has not seen for the account before is
            registered automatically and becomes available for contact filtering, segment conditions,
            and tag-triggered flows.

            Tags reach a contact from a CSV import, the contact's profile, a bulk action on the
            Contacts page, a display's signup tagging, a flow's Tag Contact action, and this API.
            Tags synced from Shopify are stored separately and are not included here.
          example:
          - vip
          - repeat-buyer
        custom_fields:
          type: object
          description: |
            The contact's custom field values. Values are usually strings, but integration-written fields
            can carry other JSON scalars (e.g. numeric loyalty point balances).
          additionalProperties: true
          example:
            loyalty_tier: gold
            referral_code: JANE2024
        created_at:
          type: string
          format: date-time
          example: '2025-01-15T10:30:00Z'
        updated_at:
          type: string
          format: date-time
          example: '2025-03-20T14:22:00Z'
    Import:
      type: object
      description: |
        A contact import that created or updated contacts in bulk. Two channels share this
        shape: `email` (CSV uploads and connected-provider syncs) and `sms` (SMS consent
        imports, a separate review pipeline). Email imports create their contacts when the
        import is processed — shortly after `created_at`, before review — and approving the
        import then applies the pending consent (e.g. `never_subscribed` to `subscribed`).

        The base row is deliberately slim — the same facts the Privy dashboard's import
        history shows (`id`, `channel`, `source`, `status`, `review_status`, `requested_by`,
        `counts`, `created_at`).
      properties:
        id:
          type: string
          description: |
            Stable prefixed identifier (`im_…` email, `sim_…` sms). Use this value in
            `/v1/imports/{id}`; contacts reference email imports in `import_ids`.
          example: im_k2R5m8Q
        channel:
          type: string
          enum:
          - email
          - sms
          description: Which import pipeline the row belongs to.
          example: email
        source:
          type:
          - string
          - 'null'
          description: |
            The import mechanism, lowercased (`csv`, `shopify`, `mailchimp`, …). SMS consent
            imports are always `csv`.
          example: csv
        status:
          type:
          - string
          - 'null'
          description: |
            Processing outcome. Email values: `pending`, `partial`, `successful`, `failed`.
            Omitted on SMS rows for merchant tokens (the dashboard's SMS import history has
            no status column).
          example: partial
        review_status:
          type: string
          description: |
            Approval state. Email values: `NA` (review not required), `exempt`, `pending`,
            `approving`, `approved`, `auto_approved`, `partially_approved`, `rejected`.
            SMS values: `pending`, `approved`, `rejected`.
          example: approved
        requested_by:
          type:
          - string
          - 'null'
          description: |
            Display name of the merchant user who requested the import (for SMS consent
            imports, the TCPA compliance signer).
          example: Rita Requester
        counts:
          type:
          - object
          - 'null'
          description: |
            Outcome counts. Email keys: `imported`, `new_contacts`. SMS keys: `imported`,
            `rejected`, `ignored` (all audiences). Null on SMS rows until processing
            has produced real counts (pending/rejected imports) — never read null as zero.
          properties:
            imported:
              type: integer
              description: Contacts processed by the import, including ones that already
                existed.
            new_contacts:
              type: integer
              description: Contacts newly created by the import (email only).
            rejected:
              type: integer
            ignored:
              type: integer
        created_at:
          type: string
          format: date-time
    ContactEvent:
      type: object
      description: One entry on a contact's activity timeline.
      properties:
        type:
          type: string
          description: The event name as stored. The `type` filter lists the common
            values; other stored names (e.g. `email-bounced`, `text-clicked`) appear
            as well and filter the same way.
          example: email-opened
        occurred_at:
          type: string
          format: date-time
          example: '2026-08-20T14:22:00Z'
        campaign:
          type:
          - object
          - 'null'
          description: 'The display or message campaign the event belongs to; `null`
            when the event carries none (e.g. a store order or a tag change). `display`
            ids round-trip through `GET /v1/displays/{id}`; `newsletter` and `flow`
            ids through `GET /v1/campaigns/{id}` — unless the campaign has since been
            deleted: it still names itself here but no longer resolves there. `workflow`
            is the legacy automation type, which that read does not serve.'
          properties:
            id:
              type: string
              example: gk2P8Zw1Lq
            title:
              type: string
              example: Welcome Popup
            type:
              type: string
              enum:
              - display
              - newsletter
              - flow
              - workflow
              example: display
        properties:
          type: object
          description: The raw event payload. Shape varies by `type`. The recipient
            `ip` and `useragent` stamped on email opens and clicks are omitted. A
            signup's `browsing_data` carries only the page, referrer, and utm attribution
            the widget recorded; the IP-derived location is omitted. SMS send events
            carry the `message_body` that was sent.
          additionalProperties: true
    PaginationEnvelope:
      type: object
      properties:
        page:
          type: integer
          example: 1
        per_page:
          type: integer
          example: 25
        total_count:
          type: integer
          example: 142
        total_pages:
          type: integer
          example: 6
    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
    CreateContactRequest:
      type: object
      description: |
        At least one of `email` or `phone_number` is required.

        Use `email_consent` and `sms_consent` to set the contact's marketing
        consent status at creation time.
      properties:
        email:
          type: string
          format: email
          description: Contact's email address.
          example: jane@example.com
        phone_number:
          type: string
          description: Contact's phone number in E.164 format.
          example: "+15551234567"
        first_name:
          type: string
          description: Contact's first name.
          example: Jane
        last_name:
          type: string
          description: Contact's last name.
          example: Doe
        tags:
          type: array
          items:
            type: string
          description: Tags to apply to the contact.
          example:
          - vip
          - repeat-buyer
        custom_fields:
          type: object
          additionalProperties:
            type: string
          description: Flat key-value pairs. Nested objects are not allowed.
          example:
            loyalty_tier: gold
        email_consent:
          type: string
          enum:
          - subscribed
          - unsubscribed
          - never_subscribed
          - suppressed
          description: |
            Email marketing consent status. If not provided, defaults to `never_subscribed`.
            `compliance_suppressed` is read-only and cannot be set on create.
          example: subscribed
        sms_consent:
          type: string
          enum:
          - subscribed
          - unsubscribed
          - never_subscribed
          - single_opt_in
          description: |
            SMS marketing consent status. `pending` is read-only and cannot be set.
            `subscribed` and `single_opt_in` require a valid `phone_number` from a
            supported country. Cannot be `unsubscribed` for new contacts (no existing
            consent to revoke).
          example: never_subscribed
        send_welcome_sms:
          type: boolean
          default: true
          description: |
            When `sms_consent` is set to `subscribed` and the contact transitions
            to a confirmed SMS opt-in state, Privy sends a TCPA-required welcome
            SMS. Set to `false` to suppress this message when you have already
            collected consent outside of Privy.
          example: false
    UpdateContactByIdRequest:
      type: object
      description: |
        Include one or more fields to update. The contact is identified
        by the `id` path parameter. Field updates and consent changes can be
        combined in a single request.

        Mutable fields: `first_name`, `last_name`, `email`, `phone_number`, `tags`, `custom_fields`.

        `email` and `phone_number` can only be set when the existing value is
        `null`. Once populated, they cannot be changed or cleared.

        Consent fields: `email_consent`, `sms_consent`. Use these to manage a
        contact's marketing consent instead of the deprecated unsubscribe endpoint.
      properties:
        first_name:
          type: string
          description: Updated first name.
          example: Janet
        last_name:
          type: string
          description: Updated last name.
          example: Smith
        email:
          type: string
          format: email
          description: |
            Sets the contact's email address. Only allowed when the contact's
            current `email` is `null`; once populated, `email` is immutable.
          example: janet@example.com
        phone_number:
          type: string
          description: |
            Sets the contact's phone number in E.164 format. Only allowed when
            the contact's current `phone_number` is `null`; once populated,
            `phone_number` is immutable.
          example: "+15551234567"
        tags:
          type: array
          items:
            type: string
          description: Replaces all existing tags.
          example:
          - vip
          - wholesale
        custom_fields:
          type: object
          additionalProperties:
            type: string
          description: Replaces all existing custom fields. Must be a flat key-value
            object.
          example:
            loyalty_tier: platinum
        email_consent:
          type: string
          enum:
          - subscribed
          - unsubscribed
          - never_subscribed
          - suppressed
          description: |
            Update the contact's email marketing consent.
            `compliance_suppressed` is read-only — any write to this field on a
            compliance-suppressed contact returns `422`. The contact's other
            fields, and its `sms_consent`, stay editable.

            For merchant-suppressed contacts, writing any consent value first
            unsuppresses, then applies the requested transition (e.g.
            `subscribed` unsuppresses then subscribes).
          example: unsubscribed
        sms_consent:
          type: string
          enum:
          - subscribed
          - unsubscribed
          - never_subscribed
          - single_opt_in
          description: |
            Update the contact's SMS marketing consent. `pending` is read-only.
            `subscribed`, `single_opt_in`, and `unsubscribed` require the contact
            to have a phone number. `single_opt_in` is idempotent when the contact
            is already in that state, and returns `422` from terminal states
            (`pending`, `subscribed`, or `unsubscribed`).
          example: unsubscribed
        send_welcome_sms:
          type: boolean
          default: true
          description: |
            When `sms_consent` is set to `subscribed` and the contact transitions
            to a confirmed SMS opt-in state, Privy sends a TCPA-required welcome
            SMS. Set to `false` to suppress this message when you have already
            collected consent outside of Privy.
          example: false
    PlaceOrderRequest:
      type: object
      description: |
        `order_id`, `total`, `currency`, and `order_date` are required on every call,
        plus at least one of `email` or `phone`. All other fields are optional;
        on an update, omitted optional fields keep their previously  stored value.
      required:
      - order_id
      - total
      - currency
      - order_date
      properties:
        order_id:
          type: integer
          format: int64
          description: |
            Your unique identifier for the order. Must be a positive integer (a digits-only string is also accepted).
            This is the idempotency key: the first call for an `order_id` creates the order, subsequent calls update it.
          example: 1001
        email:
          type: string
          format: email
          description: |
            Buyer's email. Used to find or create the associated contact.
            At least one of `email` or `phone` is required.
          example: buyer@example.com
        phone:
          type: string
          description: |
            Buyer's phone number. Loosely formatted input is accepted and normalized to E.164.
            Required when `accepts_sms_marketing` is `true`.
          example: "+12025550123"
        customer_id:
          type: string
          description: Your external identifier for the customer.
          example: ext-42
        subtotal:
          type: number
          description: Order subtotal before tax, shipping, and discounts.
          example: 90.0
        discounts:
          type: array
          description: Discounts applied to the order.
          items:
            "$ref": "#/components/schemas/Discount"
        tax_lines:
          type: array
          description: Tax lines applied to the order.
          items:
            "$ref": "#/components/schemas/TaxLine"
        total:
          type: number
          description: Order grand total. Required.
          example: 95.0
        currency:
          type: string
          description: ISO 4217 three-letter currency code. Required.
          example: USD
        total_items:
          type: integer
          description: Total number of items in the order.
          example: 2
        financial_status:
          type: string
          enum:
          - authorized
          - expired
          - paid
          - partially_paid
          - partially_refunded
          - pending
          - refunded
          - voided
          description: |
            Payment status of the order, using Shopify's order financial-status vocabulary, which Privy
            treats as canonical across every source. Privy stores the value and reports it back; no
            flow or automation is triggered by it.
          example: paid
        fulfillment_status:
          type: string
          enum:
          - fulfilled
          - in_progress
          - on_hold
          - open
          - partially_fulfilled
          - pending
          - pending_fulfillment
          - request_declined
          - restocked
          - scheduled
          - unfulfilled
          description: |
            The order's overall fulfillment state — where the order sits in your store's fulfillment
            process (mirrors Shopify's OrderDisplayFulfillmentStatus). Stored with the order and echoed
            back, but it drives nothing: delivery-based flow triggers key off `shipment_status` instead.
          example: fulfilled
        shipment_status:
          type: string
          enum:
          - attempted_delivery
          - carrier_picked_up
          - confirmed
          - delayed
          - delivered
          - failure
          - in_transit
          - label_printed
          - label_purchased
          - out_for_delivery
          - picked_up
          - ready_for_pickup
          description: |
            Carrier delivery status of the shipment — where the parcel is (mirrors Shopify's
            FulfillmentEventStatus). This is the one order status Privy acts on: when a stored order's
            `shipment_status` becomes `delivered`, any "Order Received" flows start for the contact.

            Only the change fires the flows. Sending `delivered` on the call that creates the order
            counts as a change; repeating it on a later call does not fire them again. `initial_sync`
            suppresses this along with the other real-time side effects.
          example: delivered
        line_items:
          type: array
          description: The products purchased.
          items:
            "$ref": "#/components/schemas/LineItem"
        billing_address:
          "$ref": "#/components/schemas/Address"
        shipping_address:
          "$ref": "#/components/schemas/Address"
        accepts_email_marketing:
          type: boolean
          description: |
            Whether the buyer opted into email marketing as part of this order.

            When `true`, Privy records email consent for the associated contact — including
            resubscribing a contact who had previously unsubscribed. Two limits apply: the order must
            have been placed within the last six months, so a backdated `order_date` older than that
            has no consent effect; and a suppressed contact stays suppressed.

            When `false`, nothing is revoked. It never unsubscribes an existing contact and never
            weakens consent already on record — it only leaves a contact created by this call without
            email consent. The field has no effect on an order sent with a `phone` and no `email`.
          example: true
        accepts_sms_marketing:
          type: boolean
          description: |
            Whether the buyer opted into SMS marketing as part of this order. Requires `phone`.

            Applies to newly created contacts only: the phone number becomes the contact's, and the
            opt-in is written to Privy's SMS consent log, which makes the contact textable immediately
            with no confirmation reply. An existing contact's SMS consent is never changed by this
            endpoint — use `PATCH /v1/contacts/{id}` for that.

            Because this path bypasses Privy's own opt-in flows, you are responsible for having
            collected the consent in a TCPA-compliant manner. Unlike the Contacts endpoints, this one
            sends no welcome text.
          example: true
        order_date:
          type: string
          format: date-time
          description: |
            ISO 8601 date-time the order was placed. Required. Expected to be in UTC
            (zero offset) — end the timestamp with `Z` (e.g. `2026-06-01T12:00:00Z`).
          example: '2026-06-01T12:00:00Z'
        initial_sync:
          type: boolean
          default: false
          description: |
            Set to `true` for bulk historical imports. Suppresses real-time side effects
            (Flows and other automations, and campaign-revenue attribution) while still
            recording the order, associating the contact, and updating the contact's
            `order_count`, `first_order_at`, and `last_order_at` values.
          example: false
    Discount:
      type: object
      description: A discount applied to the order. Stored and echoed back verbatim.
      additionalProperties: true
      properties:
        code:
          type:
          - string
          - 'null'
          example: SAVE10
        amount:
          type:
          - number
          - 'null'
          example: 10.0
    TaxLine:
      type: object
      description: A tax line on the order. Stored and echoed back verbatim.
      additionalProperties: true
      properties:
        title:
          type:
          - string
          - 'null'
          example: VAT
        amount:
          type:
          - number
          - 'null'
          example: 5.0
    LineItem:
      type: object
      description: A product line on the order. Stored and echoed back verbatim.
      additionalProperties: true
      properties:
        product_id:
          type:
          - string
          - number
          example: prod_305
        variant_id:
          type:
          - string
          - number
          example: var_305
        sku:
          type:
          - string
          - 'null'
          example: SKU-1
        title:
          type:
          - string
          - 'null'
          example: Sample Product
        quantity:
          type:
          - integer
          - 'null'
          example: 2
        price:
          type:
          - string
          - number
          - 'null'
          description: 'Unit price for the line, echoed back as sent.

            '
          example: 45.0
    Address:
      type: object
      description: |
        A billing or shipping address. Stored and echoed back verbatim.
        When a new contact is created, its name is taken from the address —
        shipping first, billing as fallback — using `first_name`/`last_name`,
        or a single `name` field split on whitespace.
      additionalProperties: true
      properties:
        first_name:
          type:
          - string
          - 'null'
          example: Jane
        last_name:
          type:
          - string
          - 'null'
          example: Shopper
        name:
          type:
          - string
          - 'null'
          description: Full name; used when `first_name`/`last_name` are absent.
          example: Jane Shopper
        address1:
          type:
          - string
          - 'null'
          example: 123 Main St
        address2:
          type:
          - string
          - 'null'
          example: Apt 101
        city:
          type:
          - string
          - 'null'
          example: Boston
        state_code:
          type:
          - string
          - 'null'
          example: MA
        country_code:
          type:
          - string
          - 'null'
          example: US
        postal_code:
          type:
          - string
          - 'null'
          example: '02118'
    OrderIntegration:
      type: object
      description: |
        The Custom Integration the order is tied to. Present only when the order was attributed to an integration
        (via the `X-Privy-Integration-Token` header or auto-tie).
      properties:
        store_name:
          type: string
          example: My Store
        store_url:
          type: string
          example: https://mystore.example.com
    Order:
      type: object
      description: |
        An order as persisted. Echoes back the fields you sent
        (column-backed fields normalized, the rest verbatim).
        Empty values are omitted from the response.
      properties:
        order_id:
          type: integer
          format: int64
          description: Your order identifier, echoed back. The only public identifier
            for the order.
          example: 1001
        source:
          type: string
          example: api
        email:
          type: string
          format: email
          example: buyer@example.com
        phone:
          type: string
          description: Normalized to E.164.
          example: "+12025550123"
        total_amount:
          type: string
          description: Order grand total, as a decimal string.
          example: '95.00'
        currency_code:
          type: string
          example: USD
        placed_at:
          type: string
          format: date-time
          example: '2026-06-01T12:00:00Z'
        financial_status:
          type: string
          description: |
            Payment status of the order, using Shopify's order financial-status vocabulary. Absent when
            no payment status was recorded for the order.
          example: paid
        fulfillment_status:
          type: string
          description: |
            The order's overall fulfillment state — where the order sits in the store's fulfillment
            process. Delivery-based flow triggers key off `shipment_status`, not this field.
          example: fulfilled
        shipment_status:
          type: string
          description: |
            Carrier delivery status of the shipment — where the parcel is. Privy acts on this field:
            when it becomes `delivered`, the contact's "Order Received" flows start.
          example: in_transit
        customer_id:
          type:
          - string
          - integer
          description: 'The customer identifier reported with the order, echoed back
            as sent.

            '
          example: ext-42
        subtotal:
          type: number
          example: 90.0
        discounts:
          type: array
          items:
            "$ref": "#/components/schemas/Discount"
        tax_lines:
          type: array
          items:
            "$ref": "#/components/schemas/TaxLine"
        total_items:
          type: integer
          example: 2
        line_items:
          type: array
          items:
            "$ref": "#/components/schemas/LineItem"
        billing_address:
          "$ref": "#/components/schemas/Address"
        shipping_address:
          "$ref": "#/components/schemas/Address"
        accepts_email_marketing:
          type: boolean
          description: |
            The email marketing opt-in reported with the order, echoed back as sent. It records what
            the order said, not the contact's consent now — read that from the contact's
            `email_consent`.
          example: true
        accepts_sms_marketing:
          type: boolean
          description: |
            The SMS marketing opt-in reported with the order, echoed back as sent. As with
            `accepts_email_marketing`, the contact's current consent lives on the contact
            (`sms_consent`), not here.
          example: true
        integration:
          "$ref": "#/components/schemas/OrderIntegration"
        created_at:
          type: string
          format: date-time
          example: '2026-06-01T12:00:05Z'
        updated_at:
          type: string
          format: date-time
          example: '2026-06-01T12:00:05Z'
    IngestEventRequest:
      type: object
      description: |
        An event to ingest. You must identify the contact with **exactly one** of
        `email`, `phone`, or `privy_id`, and provide an `event_type`.

        Everything else is optional. Unknown top-level keys are ignored — put your
        own data under `event_data`.
      required:
      - event_type
      properties:
        email:
          type: string
          format: email
          description: |
            The contact's email address. Provide exactly one of `email`, `phone`,
            or `privy_id` to identify the contact.
          example: shopper@example.com
        phone:
          type: string
          description: |
            The contact's phone number in E.164 format (`+` followed by country
            code and number). Provide exactly one of `email`, `phone`, or `privy_id`.
          example: "+15551234567"
        privy_id:
          type: string
          description: |
            The contact's Privy customer ID (the `id` returned by the Contacts API,
            prefixed with `cus_`). Provide exactly one of `email`, `phone`, or `privy_id`.
          example: cus_a1b2c3d4e5f6g7h8
        event_type:
          type: string
          maxLength: 128
          pattern: "^[A-Za-z0-9_-]+$"
          description: |
            A name for the event. Up to 128 characters; letters, numbers, hyphens,
            and underscores only. Choose a stable, descriptive name — you'll use it
            to trigger Flows (e.g. `purchase_completed`, `quiz_finished`).
          example: purchase_completed
        event_time:
          type: string
          format: date-time
          description: |
            ISO 8601 date-time the event occurred. Must be UTC — end the timestamp
            with `Z` (or a zero offset). Optional; defaults to the time Privy
            received the event.
          example: '2026-06-15T10:30:00Z'
        event_data:
          type: object
          additionalProperties: true
          description: |
            Arbitrary key-value data describing the event. Values may be strings,
            numbers, booleans, `null`, objects, or arrays. Objects and arrays may be
            nested up to 10 levels deep. Stored and made available to downstream
            automations verbatim.
          example:
            order_id: ORD-12345
            total: 99.99
            currency: USD
            items:
            - sku: SKU-1
              quantity: 2
        idempotency_key:
          type: string
          description: |
            Optional key to safely retry a request without creating a duplicate
            event. Prefer the `Idempotency-Key` header; if both are sent, the header
            wins. See the endpoint description for replay semantics.
          example: 550e8400-e29b-41d4-a716-446655440000
    EventAccepted:
      type: object
      description: The event was accepted (or replayed from a prior identical request).
      properties:
        event_id:
          type: string
          description: Privy's identifier for the stored event.
          example: 507f1f77bcf86cd799439011
        status:
          type: string
          enum:
          - accepted
          example: accepted
        idempotent_replay:
          type: boolean
          description: |
            Present and `true` only when this response was served from a prior
            request with the same idempotency key, rather than ingesting a new event.
          example: true
    EventRejected:
      type: object
      description: |
        The event failed shape validation. It is **not** ingested for processing
        (though Privy retains a copy for debugging). Note that this response does
        **not** use the standard `error` envelope — validation problems are
        returned in the top-level `errors` array.
      properties:
        event_id:
          type: string
          description: Privy's identifier for the stored (rejected) event.
          example: 507f1f77bcf86cd799439012
        status:
          type: string
          enum:
          - rejected
          example: rejected
        errors:
          type: array
          description: One entry per validation problem.
          items:
            type: object
            properties:
              field:
                type: string
                example: event_type
              message:
                type: string
                example: is required
  parameters:
    PageParam:
      name: page
      in: query
      description: Page number (starts at 1).
      schema:
        type: integer
        default: 1
        minimum: 1
      example: 1
    PerPageParam:
      name: per_page
      in: query
      description: Number of results per page. Values outside 10–100 are clamped to
        the nearest bound.
      schema:
        type: integer
        default: 40
        minimum: 10
        maximum: 100
      example: 40
    ReportPeriodParam:
      name: period
      in: query
      description: The date range to report over, resolved in the account's time zone.
        Pass `custom` together with `start_date` and `end_date`.
      schema:
        type: string
        enum:
        - last_7_days
        - last_30_days
        - last_90_days
        - this_month
        - last_month
        - custom
        default: last_30_days
      example: last_30_days
    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'
    ContactIdParam:
      name: id
      in: path
      required: true
      description: The contact's unique identifier (returned as `id` in contact responses).
        Ids are opaque `cus_`-prefixed strings — a numeric id from a Privy dashboard
        URL is not a valid API id and will 404; list contacts, or look the contact
        up by `?email=` or `?phone_number=`, instead.
      schema:
        type: string
      example: cus_a1b2c3d4e5f6g7h8
    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
  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
    NoConnectedStore:
      description: The account has no connected store, so there is no catalog to read.
      content:
        application/json:
          schema:
            "$ref": "#/components/schemas/ErrorResponse"
          example:
            error:
              code: no_connected_store
              message: No connected store. Product reads require a connected Shopify
                or BigCommerce store.
    StoreUnavailable:
      description: The connected store's API could not be reached or returned an error.
        Retry shortly.
      content:
        application/json:
          schema:
            "$ref": "#/components/schemas/ErrorResponse"
          example:
            error:
              code: store_unavailable
              message: The connected store could not be reached. Try again shortly.
    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
    ContactsNotReady:
      description: |
        The account's contacts have not been assigned public API ids, so contact reads
        are unavailable. This happens when contacts were saved before the account had
        API access. Enable API access for the account, or have Privy staff run the
        public_id backfill task for it, then retry.
      content:
        application/json:
          schema:
            "$ref": "#/components/schemas/ErrorResponse"
          example:
            error:
              code: contacts_not_ready
              message: This account's contacts have not been assigned public API ids,
                so contact reads are unavailable. Enable API access for the account,
                or have Privy staff run the public_id backfill task for it, then retry.
    ProductsRateLimited:
      description: Rate limit exceeded — either Privy's API rate limit (code `rate_limited`,
        with the `X-RateLimit-*` headers) or the connected store's own quota passed
        through (code `store_rate_limited`). `Retry-After` carries the wait when it
        is known — BigCommerce's reset window, or a wait derived from Shopify's throttle
        cost payload.
      headers:
        Retry-After:
          description: Seconds to wait before retrying.
          schema:
            type: integer
          example: 15
      content:
        application/json:
          schema:
            "$ref": "#/components/schemas/ErrorResponse"
          example:
            error:
              code: store_rate_limited
              message: The connected store is rate limiting requests. Try again shortly.
    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
    PayloadTooLarge:
      description: The request body exceeds the maximum allowed size.
      content:
        application/json:
          schema:
            "$ref": "#/components/schemas/ErrorResponse"
          example:
            error:
              code: payload_too_large
              message: Request payload must not exceed 256KB.
paths:
  "/account":
    get:
      operationId: getAccount
      summary: Get account
      description: |
        Retrieve information about the Privy account associated with the API token.

        The identity read: who the token belongs to, and which subaccounts it can
        reach. Each subaccount `id` is the `Privy-Account-Id` value that points
        every other endpoint at that subaccount. Nothing else is here — audit
        configuration with `/v1/account/setup` and read results from
        `/v1/reports/summary`.

        No scope is required — any valid bearer token can call this endpoint.
      tags:
      - Account
      security:
      - bearerAuth: []
      responses:
        '200':
          description: The authenticated Privy account.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    "$ref": "#/components/schemas/Account"
              example:
                data:
                  id: A1B2C3D4E5F6G7H8I9J0K1L2
                  name: My Store
                  url: https://mystore.com
                  subaccounts:
                  - id: S1U2B3A4C5C6T7
                    name: My Store — Canada
                    url: https://ca.mystore.com
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '429':
          "$ref": "#/components/responses/RateLimited"
      parameters:
      - "$ref": "#/components/parameters/AccountIdParam"
  "/account/setup":
    get:
      operationId: getAccountSetup
      summary: Get account setup
      description: |
        Audit what the account has and hasn't set up: the onboarding steps with their statuses,
        plus a summary of what is actually live right now (display types, flow triggers, connected integrations).

        The fastest answer to "is this account configured, and what is it using" —
        the summary names the kinds of display, trigger, and integration in use
        without paging `/v1/displays` and `/v1/flows`. Kinds only: no titles, no
        ids, no performance. Use those endpoints to name the individual records
        and `/v1/reports/summary` for results.

        No scope is required — any valid bearer token can call this endpoint.
      tags:
      - Account
      security:
      - bearerAuth: []
      responses:
        '200':
          description: The account's setup steps and live-configuration summary.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    "$ref": "#/components/schemas/AccountSetup"
              example:
                data:
                  steps:
                  - key: account_created
                    status: completed
                    required: false
                  - key: connect_store
                    status: completed
                    required: false
                  - key: verify_contact_email
                    status: not_started
                    required: true
                  - key: authenticate_sending_domain
                    status: pending
                    required: true
                  - key: verify_physical_address
                    status: not_started
                    required: true
                  started_at: '2026-05-01T09:00:00Z'
                  completed_at:
                  summary:
                    live_display_types:
                    - flyout
                    - popup
                    flow_triggers_in_use:
                    - order_placed
                    - customer_subscribed_to_marketing
                    connected_integrations:
                    - shopify
                    - klaviyo
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '429':
          "$ref": "#/components/responses/RateLimited"
      parameters:
      - "$ref": "#/components/parameters/AccountIdParam"
  "/campaigns":
    get:
      operationId: listCampaigns
      summary: List campaigns
      description: |
        Retrieve a paginated list of your message campaigns, newest first.

        By default the list contains your **newsletters** — email and SMS
        broadcasts. Pass `type=flows` to instead list the **flow campaigns**:
        the email and SMS messages your automation flows send, each naming its
        owning flow in `flow`.

        Configuration only — no send statistics. `/v1/reports/campaigns` lists
        every campaign sent in a period with its full stat block (revenue-ranked,
        top 100); one campaign's funnel is
        `/v1/reports/campaigns/{campaign_id}`.

        **Required scope:** `campaigns_read`
      tags:
      - Campaigns
      security:
      - bearerAuth: []
      parameters:
      - "$ref": "#/components/parameters/AccountIdParam"
      - "$ref": "#/components/parameters/PageParam"
      - "$ref": "#/components/parameters/PerPageParam"
      - name: type
        in: query
        description: Pass `flows` to list the messages your flows send instead of
          newsletters.
        schema:
          type: string
          enum:
          - flows
        example: flows
      - name: channel
        in: query
        description: Filter by message channel. Works for both newsletters and flow
          campaigns.
        schema:
          type: string
          enum:
          - email
          - sms
        example: email
      - name: status
        in: query
        description: |
          Filter newsletters by send lifecycle. Not supported with `type=flows`
          (flow campaigns have no send lifecycle of their own — their live/draft
          state belongs to the parent flow, via `GET /v1/flows` → `status`) —
          combining them returns a `422`.
        schema:
          type: string
          enum:
          - draft
          - scheduled
          - preparing
          - awaiting_winner
          - sending
          - sent
          - partially_sent
          - unsent
          - canceled
        example: sent
      responses:
        '200':
          description: A paginated list of message campaigns.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/Campaign"
                  pagination:
                    "$ref": "#/components/schemas/PaginationEnvelope"
              example:
                data:
                - id: Nw8K2rT5xY
                  type: newsletter
                  title: June Restock Announcement
                  channel: email
                  status: sent
                  subject: The restock you asked for is here
                  send_date: '2026-06-18T15:00:00Z'
                  audience:
                    included_segments:
                    - id: aBc1234
                      name: Engaged 90d
                      prebuilt: false
                    - id: potential_customers
                      name: Potential Customers
                      prebuilt: true
                    excluded_segments:
                    - id: dEf5678
                      name: Recent purchasers
                      prebuilt: false
                  coupon:
                  ab_test:
                    winning_metric: click_rate
                    test_duration_seconds: 86400
                    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
                      preheader: Back in stock and going fast
                      coupon:
                    - 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
                      preheader: We saved you a look
                      coupon:
                        id: 7k2dG6Y
                        title: RESTOCK20
                        kind: master
                        platform: manual
                  created_at: '2026-06-10T09:00:00Z'
                  updated_at: '2026-06-18T15:05:00Z'
                pagination:
                  page: 1
                  per_page: 40
                  total_count: 12
                  total_pages: 1
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '403':
          "$ref": "#/components/responses/InsufficientScope"
        '422':
          "$ref": "#/components/responses/ValidationFailed"
        '429':
          "$ref": "#/components/responses/RateLimited"
  "/campaigns/{id}":
    get:
      operationId: getCampaign
      summary: Get a campaign
      description: |
        Retrieve a single message campaign by its identifier. Resolves both
        newsletters and flow campaigns — no `type` parameter needed. A flow
        campaign names the flow that sends it in `flow`; newsletters omit
        that field.

        A `/v1/campaigns` row already carries this exact shape, so following up
        per campaign adds nothing — reach for this when the id came from
        somewhere else, such as a flow node's `data.campaign_id` or a report
        row's `campaign_id`. Performance is
        `/v1/reports/campaigns/{campaign_id}`.

        **Required scope:** `campaigns_read`
      tags:
      - Campaigns
      security:
      - bearerAuth: []
      parameters:
      - "$ref": "#/components/parameters/AccountIdParam"
      - name: id
        in: path
        required: true
        description: The campaign's unique identifier (returned as `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: Nw8K2rT5xY
      responses:
        '200':
          description: The message campaign.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    "$ref": "#/components/schemas/Campaign"
              examples:
                ab_test_newsletter:
                  summary: A decided email A/B test
                  value:
                    data:
                      id: Nw8K2rT5xY
                      type: newsletter
                      title: June Restock Announcement
                      channel: email
                      status: sent
                      subject: The restock you asked for is here
                      send_date: '2026-06-18T15:00:00Z'
                      audience:
                        included_segments:
                        - id: aBc1234
                          name: Engaged 90d
                          prebuilt: false
                        - id: potential_customers
                          name: Potential Customers
                          prebuilt: true
                        excluded_segments:
                        - id: dEf5678
                          name: Recent purchasers
                          prebuilt: false
                      coupon:
                      ab_test:
                        winning_metric: click_rate
                        test_duration_seconds: 86400
                        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
                          preheader: Back in stock and going fast
                          coupon:
                        - 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
                          preheader: We saved you a look
                          coupon:
                            id: 7k2dG6Y
                            title: RESTOCK20
                            kind: master
                            platform: manual
                      created_at: '2026-06-10T09:00:00Z'
                      updated_at: '2026-06-18T15:05:00Z'
                flow_campaign:
                  summary: A flow's email
                  value:
                    data:
                      id: Xp4R7mQ2vB
                      type: flow
                      title: Cart reminder email
                      channel: email
                      status:
                      subject: You left something behind
                      send_date:
                      audience:
                      coupon:
                      ab_test:
                      created_at: '2026-04-02T15:00:00Z'
                      updated_at: '2026-04-02T15:00:00Z'
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '403':
          "$ref": "#/components/responses/InsufficientScope"
        '404':
          "$ref": "#/components/responses/NotFound"
        '429':
          "$ref": "#/components/responses/RateLimited"
  "/displays":
    get:
      operationId: listDisplays
      summary: List displays
      description: |
        Retrieve a paginated list of your on-site displays, newest first.
        Archived displays are excluded by default — pass `status=archived` to list them.

        The display inventory — use it to enumerate or count displays, since
        `/v1/reports/displays` is a capped leaderboard that leaves out displays
        with no activity. Configuration only: views, signups, and attributed
        revenue come from `/v1/reports/displays` and
        `/v1/reports/displays/{display_id}`. Rows carry no show triggers or
        audience summary; `/v1/displays/{id}` adds those.

        **Required scope:** `displays_read`
      tags:
      - Displays
      security:
      - bearerAuth: []
      parameters:
      - "$ref": "#/components/parameters/AccountIdParam"
      - "$ref": "#/components/parameters/PageParam"
      - "$ref": "#/components/parameters/PerPageParam"
      - name: status
        in: query
        description: Filter by display status.
        schema:
          type: string
          enum:
          - draft
          - scheduled
          - live
          - paused
          - ended
          - archived
        example: live
      responses:
        '200':
          description: A paginated list of displays.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/Display"
                  pagination:
                    "$ref": "#/components/schemas/PaginationEnvelope"
              example:
                data:
                - id: gk2P8Zw1Lq
                  title: Summer Sale Popup
                  display_type: popup
                  goal: signup
                  status: live
                  starts_at: '2026-06-01T00:00:00Z'
                  ends_at:
                  ab_test:
                    present: false
                    state:
                  created_at: '2026-05-20T10:00:00Z'
                pagination:
                  page: 1
                  per_page: 40
                  total_count: 12
                  total_pages: 1
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '403':
          "$ref": "#/components/responses/InsufficientScope"
        '422':
          "$ref": "#/components/responses/ValidationFailed"
        '429':
          "$ref": "#/components/responses/RateLimited"
  "/displays/{id}":
    get:
      operationId: getDisplay
      summary: Get a display
      description: |
        Retrieve a single display by its identifier, including its show triggers and audience summary.

        Configuration only — no views, signups, attributed revenue, or A/B test
        results. Those are in `/v1/reports/displays/{display_id}`, keyed on the
        same identifier. Over a `/v1/displays` row this adds the show triggers
        and the audience summary.

        **Required scope:** `displays_read`
      tags:
      - Displays
      security:
      - bearerAuth: []
      parameters:
      - "$ref": "#/components/parameters/AccountIdParam"
      - name: id
        in: path
        required: true
        description: The display's unique identifier (returned as `id` in display
          responses). Ids are opaque public strings — a numeric id from a Privy dashboard
          URL is not a valid API id and will 404; list displays and match by title
          instead.
        schema:
          type: string
        example: gk2P8Zw1Lq
      responses:
        '200':
          description: The display.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    "$ref": "#/components/schemas/DisplayDetail"
              example:
                data:
                  id: gk2P8Zw1Lq
                  title: Summer Sale Popup
                  display_type: popup
                  goal: signup
                  status: live
                  starts_at: '2026-06-01T00:00:00Z'
                  ends_at:
                  ab_test:
                    present: false
                    state:
                  triggers:
                    exit_intent: false
                    auto_show_after_seconds: 5
                    scroll_threshold_pct:
                    teaser_tab: false
                  audience_summary:
                  after_signup_email:
                    enabled: true
                    subject: Take 10% Off
                  created_at: '2026-05-20T10:00:00Z'
                  updated_at: '2026-06-10T15:30:00Z'
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '403':
          "$ref": "#/components/responses/InsufficientScope"
        '404':
          "$ref": "#/components/responses/NotFound"
        '429':
          "$ref": "#/components/responses/RateLimited"
  "/coupons":
    get:
      operationId: listCoupons
      summary: List coupons
      description: |
        Retrieve a paginated list of your coupon pools with their code inventory
        and health status.

        This is inventory and health only. Privy does not track coupon
        redemptions — check an individual code's store-side usage counter with
        `GET /v1/coupons/{id}/codes/{code}` — and every response carries an
        in-band `note` saying so. Two code counters, each `null` where it
        doesn't apply: `coupon_codes_remaining` is a manual unique pool's
        unused inventory; `coupon_codes_issued` is how many codes any unique
        pool has generated for contacts (approximate, and counted only since
        the field was introduced).
        Master pools have a single shared code and carry neither.

        `attached_to` lists every display and message campaign currently
        pointing at each pool, so "is this pool actually wired to my
        abandoned-cart flow?" needs no dashboard spot-check.

        **Required scope:** `coupons_read`
      tags:
      - Coupons
      security:
      - bearerAuth: []
      parameters:
      - "$ref": "#/components/parameters/AccountIdParam"
      - name: kind
        in: query
        required: false
        description: Only pools of this kind.
        schema:
          type: string
          enum:
          - master
          - unique
      - name: platform
        in: query
        required: false
        description: Only pools on this platform.
        schema:
          type: string
          enum:
          - manual
          - shopify
          - bigcommerce
          - woocommerce
      - name: title
        in: query
        required: false
        description: Case-insensitive substring match on the pool title.
        schema:
          type: string
      - "$ref": "#/components/parameters/PageParam"
      - "$ref": "#/components/parameters/PerPageParam"
      responses:
        '200':
          description: A paginated list of coupon pools.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/Coupon"
                  note:
                    type: string
                    description: Fixed caveat that redemption/usage data is not tracked
                      by Privy.
                  pagination:
                    "$ref": "#/components/schemas/PaginationEnvelope"
              example:
                data:
                - id: Xk2fW9qLpV
                  title: Welcome 10%
                  kind: unique
                  platform: shopify
                  master_code:
                  prefix_code: VIP
                  coupon_codes_remaining:
                  coupon_codes_issued: 4210
                  healthy: true
                  failure_message:
                  attached_to:
                    displays:
                    - id: gk2P8Zw1Lq
                      title: Welcome Popup
                    campaigns: []
                - id: Bn7dR4mTzC
                  title: Free shipping master
                  kind: master
                  platform: manual
                  master_code: FREESHIP
                  prefix_code:
                  coupon_codes_remaining:
                  coupon_codes_issued:
                  healthy: true
                  failure_message:
                  attached_to:
                    displays: []
                    campaigns:
                    - id: Nw8K2rT5xY
                      title: Cart Reminder
                      type: flow
                      channel: sms
                note: Coupon pool inventory and health only. Privy does not track
                  coupon redemptions, so usage counts are only available when you
                  look up a single coupon by id.
                pagination:
                  page: 1
                  per_page: 40
                  total_count: 2
                  total_pages: 1
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '403':
          "$ref": "#/components/responses/InsufficientScope"
        '422':
          "$ref": "#/components/responses/ValidationFailed"
        '429':
          "$ref": "#/components/responses/RateLimited"
  "/coupons/{id}":
    get:
      operationId: getCoupon
      summary: Get a coupon
      description: |
        Retrieve a single coupon pool by its identifier — the list rows'
        inventory and health, plus `usage_count`, the connected store's
        pool-wide redemption counter read live. Privy tracks no redemption
        events, so there is no per-code breakdown here; check an individual
        code with `GET /v1/coupons/{id}/codes/{code}`.

        **Required scope:** `coupons_read`
      tags:
      - Coupons
      security:
      - bearerAuth: []
      parameters:
      - "$ref": "#/components/parameters/AccountIdParam"
      - name: id
        in: path
        required: true
        description: The coupon pool's unique identifier (returned as `id` in coupon
          responses). Ids are opaque public strings — a numeric id from a Privy dashboard
          URL is not a valid API id and will 404; list coupons and match by title
          instead.
        schema:
          type: string
        example: Xk2fW9qLpV
      responses:
        '200':
          description: The coupon pool.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    "$ref": "#/components/schemas/CouponDetail"
              example:
                data:
                  id: Xk2fW9qLpV
                  title: Welcome 10%
                  kind: unique
                  platform: shopify
                  master_code:
                  prefix_code: VIP
                  coupon_codes_remaining:
                  coupon_codes_issued: 4210
                  usage_count: 12
                  healthy: true
                  failure_message:
                  attached_to:
                    displays:
                    - id: gk2P8Zw1Lq
                      title: Welcome Popup
                    campaigns: []
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '403':
          "$ref": "#/components/responses/InsufficientScope"
        '404':
          "$ref": "#/components/responses/NotFound"
        '429':
          "$ref": "#/components/responses/RateLimited"
  "/coupons/{id}/codes/{code}":
    get:
      operationId: getCouponCode
      summary: Check a coupon code
      description: |
        Check whether an individual code from a Shopify or BigCommerce pool has
        been redeemed, read live from the connected store's per-code usage
        counter (Privy stores no redemption events).

        This is a counter, not an event log: `usage_count` says how many times
        the store has accepted the code, but redemption dates, order ids, and
        customer attribution are not available — every response repeats this in
        an in-band `note`. Manual pools have no store-side counter and return
        `422 unsupported_coupon_type`.

        A spot check for individual codes — there is no bulk sweep; asking
        "which of my thousands of codes were redeemed" is not served by this
        endpoint.

        Membership semantics: a master pool's single code is matched exactly
        (case-insensitively) before the store is asked. Unique-pool codes are
        resolved against the store directly — Privy stores no per-code linkage
        for platform pools, so a code from one of your OTHER pools would still
        resolve; the check never crosses accounts.

        **Required scope:** `coupons_read`
      tags:
      - Coupons
      security:
      - bearerAuth: []
      parameters:
      - "$ref": "#/components/parameters/AccountIdParam"
      - name: id
        in: path
        required: true
        description: The coupon pool's unique identifier (returned as `id` in coupon
          responses). Ids are opaque public strings — a numeric id from a Privy dashboard
          URL is not a valid API id and will 404; list coupons and match by title
          instead.
        schema:
          type: string
        example: Xk2fW9qLpV
      - name: code
        in: path
        required: true
        description: The coupon code to check, URL-encoded. Matched case-insensitively.
        schema:
          type: string
        example: VIP4X9K2
      responses:
        '200':
          description: The code's store-side usage status.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      code:
                        type: string
                        description: The code in the store's canonical casing.
                        example: VIP4X9K2
                      redeemed:
                        type: boolean
                        description: Whether the store's usage counter is above zero.
                        example: true
                      usage_count:
                        type: integer
                        description: How many times the store has accepted the code.
                        example: 1
                  note:
                    type: string
                    description: Fixed caveat that this is a usage counter, not an
                      event log.
              example:
                data:
                  code: VIP4X9K2
                  redeemed: true
                  usage_count: 1
                note: 'redeemed and usage_count come from the connected store''s usage
                  counter, not an event log: redemption dates, order ids, and customer
                  attribution are not available.'
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '403':
          "$ref": "#/components/responses/InsufficientScope"
        '404':
          description: The pool id is unknown, the code is not the master pool's code,
            the store no longer knows the code, or the pool's store connection is
            gone (code `no_connected_store`).
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
              example:
                error:
                  code: not_found
                  message: Code not found on the connected store
        '422':
          description: Manual pools have no store-side usage counter to check.
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
              example:
                error:
                  code: unsupported_coupon_type
                  message: Manual coupon pools have no store-side usage counter to
                    check.
        '429':
          description: Rate limit exceeded — either Privy's API rate limit (code `rate_limited`)
            or the connected store's own quota passed through (code `store_rate_limited`).
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/ErrorResponse"
              example:
                error:
                  code: store_rate_limited
                  message: The connected store is rate limiting requests. Try again
                    shortly.
        '502':
          "$ref": "#/components/responses/StoreUnavailable"
  "/reports/displays":
    get:
      operationId: listDisplayReports
      summary: List display performance
      description: |
        Rank your displays by performance over a period — signups by default,
        or any of the documented sort keys. Displays whose value for the chosen
        sort key is zero are left out entirely, and ranking stops at 50
        displays, which also caps the reported `total_count`. Use
        `/v1/reports/displays/{display_id}` for one display's full stat set,
        including average order value and A/B test results.

        **Required scope:** `reports_read`
      tags:
      - Reports
      security:
      - bearerAuth: []
      parameters:
      - "$ref": "#/components/parameters/AccountIdParam"
      - "$ref": "#/components/parameters/ReportPeriodParam"
      - "$ref": "#/components/parameters/ReportStartDateParam"
      - "$ref": "#/components/parameters/ReportEndDateParam"
      - name: sort
        in: query
        description: The metric to rank displays by, best first.
        schema:
          type: string
          enum:
          - signups
          - signup_rate
          - views
          - revenue
          - orders
          default: signups
        example: signups
      - "$ref": "#/components/parameters/PageParam"
      - "$ref": "#/components/parameters/PerPageParam"
      responses:
        '200':
          description: Displays ranked by the requested sort, with the resolved period.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/DisplayReportRow"
                  period:
                    "$ref": "#/components/schemas/ReportPeriod"
                  pagination:
                    "$ref": "#/components/schemas/PaginationEnvelope"
              example:
                data:
                - display_id: gk2P8Zw1Lq
                  title: Summer Sale Popup
                  display_type: popup
                  status: live
                  published_date: '2026-06-01'
                  stats:
                    views:
                      value: 4200
                      unit: count
                    signups:
                      value: 310
                      unit: count
                    signup_rate:
                      value: 7.4
                      unit: percent
                    attributed_orders:
                      value: 28
                      unit: count
                    attributed_revenue:
                      value: 1840.5
                      unit: usd
                period:
                  label: last_30_days
                  start_date: '2026-06-21'
                  end_date: '2026-07-20'
                pagination:
                  page: 1
                  per_page: 40
                  total_count: 1
                  total_pages: 1
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '403':
          "$ref": "#/components/responses/InsufficientScope"
        '422':
          "$ref": "#/components/responses/ValidationFailed"
        '429':
          "$ref": "#/components/responses/RateLimited"
  "/reports/displays/{display_id}":
    get:
      operationId: getDisplayReport
      summary: Get display performance
      description: |
        The full performance stat set for one display over a period, including
        average order value and, when the display has run an A/B test, per-variation
        participants, conversions, and conversion rates.

        Ask this per display rather than hunting the leaderboard: unlike
        `/v1/reports/displays` it reports zeroed stats for a quiet display instead
        of leaving it out. The display's own configuration — triggers, schedule,
        status — is `/v1/displays/{id}`, keyed on the same identifier.

        **Required scope:** `reports_read`
      tags:
      - Reports
      security:
      - bearerAuth: []
      parameters:
      - "$ref": "#/components/parameters/AccountIdParam"
      - name: display_id
        in: path
        required: true
        description: The display's unique identifier (returned as `display_id` in
          report responses and `id` in display responses). Ids are opaque public strings
          — a numeric id from a Privy dashboard URL is not a valid API id and will
          404; list displays and match by title instead.
        schema:
          type: string
        example: gk2P8Zw1Lq
      - "$ref": "#/components/parameters/ReportPeriodParam"
      - "$ref": "#/components/parameters/ReportStartDateParam"
      - "$ref": "#/components/parameters/ReportEndDateParam"
      responses:
        '200':
          description: The display's performance over the resolved period.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    "$ref": "#/components/schemas/DisplayReportDetail"
              example:
                data:
                  display_id: gk2P8Zw1Lq
                  title: Summer Sale Popup
                  display_type: popup
                  status: live
                  period:
                    label: last_30_days
                    start_date: '2026-06-21'
                    end_date: '2026-07-20'
                  stats:
                    views:
                      value: 4200
                      unit: count
                    signups:
                      value: 310
                      unit: count
                    signup_rate:
                      value: 7.4
                      unit: percent
                    attributed_orders:
                      value: 28
                      unit: count
                    attributed_revenue:
                      value: 1840.5
                      unit: usd
                    average_order_value:
                      value: 65.73
                      unit: usd
                  ab_test:
                    name: Headline test
                    state: finished
                    goal: signup
                    total_participants: 9400
                    winner_variation_id:
                    variations:
                    - id: abv_Jq8T2vY
                      display_id: gk2P8Zw1Lq
                      name: Original
                      is_control: true
                      traffic_ratio: 0.5
                      participants: 4700
                      conversions: 94
                      conversion_rate:
                        value: 2.0
                        unit: percent
                    - id: abv_Nk2P5rW
                      display_id: mP4tR9xK2b
                      name: Challenger
                      is_control: false
                      traffic_ratio: 0.5
                      participants: 4700
                      conversions: 129
                      conversion_rate:
                        value: 2.7
                        unit: percent
        '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"
  "/reports/summary":
    get:
      operationId: getReportSummary
      summary: Get business report summary
      description: |
        The business-level performance rollup for a period: signups, new
        subscribers, attributed revenue and orders, engagement rates, and
        business-wide order and revenue totals for context. Pass
        `compare=previous_period` to add previous-period values and typed
        change deltas to every metric.

        `attributed_revenue` and `attributed_orders` credit the email and SMS
        messages Privy sent, under the same last-touch rule as the rest of
        Privy's reporting: an order counts when the contact's most recent
        engagement in the 7 days before it was an email open, an email click, or
        an SMS click. Orders credited to a display signup are excluded, and
        every figure here is dated by when the crediting message was **sent**,
        not by when the order or the engagement happened. A period's attributed
        revenue is therefore the revenue driven by messages sent in that period,
        and it includes orders placed after the period closed, whenever the
        contact engaged later — so the newest days keep filling in for a while
        after the fact. The engagement counts and rates use the same send
        window: an open in this period on a message sent last month lands in
        last month.

        The rates each divide by something different. `open_rate` is opens
        counted once per contact per campaign, over email deliveries — SMS
        messages have no opens. `click_rate` counts clicks the same way but per
        channel, over email and SMS deliveries combined. `unsubscribe_rate` is
        unsubscribe and opt-out events, not distinct contacts, over those same
        combined deliveries. `order_rate` is attributed orders over delivered
        messages. `revenue_per_recipient` divides attributed revenue by
        **email** sends only. `signup_rate` is display form submissions over
        display views, so it measures on-site capture, not messaging. `signups`
        counts form submissions on your displays, while `new_subscribers` counts
        contacts becoming subscribed to marketing on either channel from any
        source, so the two answer different questions and neither is
        de-duplicated by contact.

        Periods reaching back before the account's reporting boundary
        (2026-07-01 for most accounts, earlier where more history was
        backfilled) are served whole — comparison period included — from
        Privy's historical rollups instead of the live reporting store. The
        rate formulas above still hold, but the basis differs: new
        subscribers, every by_channel/by_type split, and `totals_context` are
        null; attributed orders and revenue include display-credited orders
        and are dated by when the attribution was recorded, not by send time —
        so `order_rate`'s numerator counts orders its message denominator
        never sent, and reads a little high for accounts with display-credited
        orders; and days are bucketed in US Eastern time regardless of the
        account's time zone. Treat comparisons that straddle the boundary as
        approximate, and say so.

        **Required scope:** `reports_read`
      tags:
      - Reports
      security:
      - bearerAuth: []
      parameters:
      - "$ref": "#/components/parameters/AccountIdParam"
      - "$ref": "#/components/parameters/ReportPeriodParam"
      - "$ref": "#/components/parameters/ReportStartDateParam"
      - "$ref": "#/components/parameters/ReportEndDateParam"
      - name: compare
        in: query
        description: Pass `previous_period` to include `previous_value`, `change`
          and `change_type` on every metric and a `comparison_period` block. Any other
          value is rejected.
        schema:
          type: string
          enum:
          - previous_period
        example: previous_period
      responses:
        '200':
          description: The business's performance rollup over the resolved period.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: object
                    properties:
                      period:
                        "$ref": "#/components/schemas/ReportPeriod"
                      comparison_period:
                        type:
                        - object
                        - 'null'
                        description: The previous period of the same length, immediately
                          before the report period. Null unless `compare=previous_period`.
                        properties:
                          start_date:
                            type: string
                            format: date
                            example: '2026-05-22'
                          end_date:
                            type: string
                            format: date
                            example: '2026-06-20'
                      signups:
                        allOf:
                        - "$ref": "#/components/schemas/ReportSummaryMetric"
                        - type: object
                          properties:
                            by_channel:
                              "$ref": "#/components/schemas/ReportChannelBreakdown"
                      new_subscribers:
                        allOf:
                        - "$ref": "#/components/schemas/ReportSummaryMetric"
                        - type: object
                          properties:
                            by_source:
                              type:
                              - object
                              - 'null'
                              additionalProperties:
                                type: integer
                              description: |
                                New subscribers grouped by signup source (`display`, `import`,
                                `api`, `shopify`, …; `unknown` for unattributed). Current period
                                only. Null means unavailable — the period starts before the
                                account's reporting boundary (2026-07-01 for most accounts,
                                earlier where more history was backfilled) or the breakdown
                                could not be computed; an empty object means genuinely zero.
                                Never treat null as zero.
                            by_channel:
                              type:
                              - object
                              - 'null'
                              additionalProperties:
                                type: integer
                              description: |
                                New subscribers by channel (`email`, `sms`). Same availability
                                semantics as `by_source`: null means unavailable — never treat
                                null as zero.
                      signup_rate:
                        "$ref": "#/components/schemas/ReportSummaryMetric"
                      attributed_revenue:
                        allOf:
                        - "$ref": "#/components/schemas/ReportSummaryMetric"
                        - type: object
                          properties:
                            by_channel:
                              "$ref": "#/components/schemas/ReportChannelBreakdown"
                            by_type:
                              "$ref": "#/components/schemas/ReportTypeBreakdown"
                      attributed_orders:
                        allOf:
                        - "$ref": "#/components/schemas/ReportSummaryMetric"
                        - type: object
                          properties:
                            by_channel:
                              "$ref": "#/components/schemas/ReportChannelBreakdown"
                            by_type:
                              "$ref": "#/components/schemas/ReportTypeBreakdown"
                      order_rate:
                        "$ref": "#/components/schemas/ReportSummaryMetric"
                      average_order_value:
                        "$ref": "#/components/schemas/ReportSummaryMetric"
                      revenue_per_recipient:
                        "$ref": "#/components/schemas/ReportSummaryMetric"
                      open_rate:
                        "$ref": "#/components/schemas/ReportSummaryMetric"
                      click_rate:
                        "$ref": "#/components/schemas/ReportSummaryMetric"
                      unsubscribe_rate:
                        "$ref": "#/components/schemas/ReportSummaryMetric"
                      totals_context:
                        type:
                        - object
                        - 'null'
                        description: 'Null when the period reaches before the account''s
                          reporting boundary; never treat null as zero. Business-wide
                          (not just Privy-attributed) totals over the period, dated
                          by when each order was placed. They are on a different basis
                          from `attributed_revenue`: `total_revenue` is the order
                          total the store charged, tax included, while attributed
                          revenue excludes tax. An attributed share divided out of
                          these will therefore read a little low, and the two are
                          dated differently — treat it as approximate and say so.

                          '
                        properties:
                          total_orders:
                            type: integer
                            example: 890
                          total_revenue:
                            type: number
                            example: 52300.0
              example:
                data:
                  period:
                    label: last_30_days
                    start_date: '2026-06-21'
                    end_date: '2026-07-20'
                  comparison_period:
                    start_date: '2026-05-22'
                    end_date: '2026-06-20'
                  signups:
                    value: 1240
                    previous_value: 980
                    change: 26.5
                    change_type: relative
                    unit: count
                    by_channel:
                      email: 1050
                      sms: 190
                  new_subscribers:
                    value: 1130
                    previous_value: 950
                    change: 18.9
                    change_type: relative
                    unit: count
                    by_source:
                      display: 980
                      import: 120
                      shopify: 30
                    by_channel:
                      email: 1000
                      sms: 130
                  signup_rate:
                    value: 3.2
                    previous_value: 2.9
                    change: 0.3
                    change_type: percent_point
                    unit: percent
                  attributed_revenue:
                    value: 18450.2
                    previous_value: 15200.0
                    change: 21.4
                    change_type: relative
                    unit: usd
                    by_channel:
                      email: 15200.0
                      sms: 3250.2
                    by_type:
                      campaign: 11400.0
                      flow: 7050.2
                  attributed_orders:
                    value: 312
                    previous_value: 261
                    change: 19.5
                    change_type: relative
                    unit: count
                    by_channel:
                      email: 262
                      sms: 50
                    by_type:
                      campaign: 190
                      flow: 122
                  order_rate:
                    value: 0.6
                    previous_value: 0.5
                    change: 0.1
                    change_type: percent_point
                    unit: percent
                  average_order_value:
                    value: 59.13
                    previous_value: 58.24
                    change: 1.5
                    change_type: relative
                    unit: usd
                  revenue_per_recipient:
                    value: 0.34
                    previous_value: 0.3
                    change: 13.3
                    change_type: relative
                    unit: usd
                  open_rate:
                    value: 41.2
                    previous_value: 39.8
                    change: 1.4
                    change_type: percent_point
                    unit: percent
                  click_rate:
                    value: 3.1
                    previous_value: 2.9
                    change: 0.2
                    change_type: percent_point
                    unit: percent
                  unsubscribe_rate:
                    value: 0.2
                    previous_value: 0.2
                    change: 0.0
                    change_type: percent_point
                    unit: percent
                  totals_context:
                    total_orders: 890
                    total_revenue: 52300.0
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '403':
          "$ref": "#/components/responses/InsufficientScope"
        '422':
          "$ref": "#/components/responses/ValidationFailed"
        '429':
          "$ref": "#/components/responses/RateLimited"
  "/reports/flows":
    get:
      operationId: listFlowReports
      summary: List flow performance
      description: |
        Rank your flows by performance over a period — best performers first,
        remaining flows by recency. Each row combines email and SMS stats
        across the flow's message steps. Use `/v1/reports/flows/{flow_id}`
        for one flow's report including the per-node message breakdown.

        Every flow with the requested status is listed, quiet ones included —
        unlike the display and campaign leaderboards nothing is dropped for
        having no activity, so this doubles as a performance-annotated flow
        inventory. A flow's configuration is `/v1/flows/{id}`.

        **Required scope:** `reports_read`
      tags:
      - Reports
      security:
      - bearerAuth: []
      parameters:
      - "$ref": "#/components/parameters/AccountIdParam"
      - "$ref": "#/components/parameters/ReportPeriodParam"
      - "$ref": "#/components/parameters/ReportStartDateParam"
      - "$ref": "#/components/parameters/ReportEndDateParam"
      - name: status
        in: query
        description: Only include flows with this status. Any other value is rejected.
        schema:
          type: string
          enum:
          - draft
          - active
          - inactive
        example: active
      - "$ref": "#/components/parameters/PageParam"
      - name: per_page
        in: query
        description: Flows per page. Values above 25 are clamped to 25; omitted or
          invalid values default to 10.
        schema:
          type: integer
          default: 10
          minimum: 1
          maximum: 25
        example: 10
      responses:
        '200':
          description: Flows ranked by performance over the resolved period.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/FlowReportRow"
                  period:
                    "$ref": "#/components/schemas/ReportPeriod"
                  pagination:
                    "$ref": "#/components/schemas/PaginationEnvelope"
              example:
                data:
                - flow_id: Vq7L3mN9xR
                  title: Abandoned Cart Recovery
                  status: active
                  trigger_type: cart_abandoned
                  channels: multi
                  current_enrollment:
                    value: 86
                    unit: count
                  stats:
                    sent:
                      value: 5200
                      unit: count
                    delivered:
                      value: 5050
                      unit: count
                    opened:
                      value: 2210
                      unit: count
                    clicked:
                      value: 640
                      unit: count
                    open_rate:
                      value: 51.4
                      unit: percent
                    click_rate:
                      value: 12.7
                      unit: percent
                    attributed_orders:
                      value: 210
                      unit: count
                    attributed_revenue:
                      value: 9640.5
                      unit: usd
                    order_rate:
                      value: 4.2
                      unit: percent
                    average_order_value:
                      value: 45.91
                      unit: usd
                    unsubscribed:
                      value: 34
                      unit: count
                    bounced:
                      value: 61
                      unit: count
                    sms_opted_out:
                      value: 12
                      unit: count
                    sms_failed:
                      value: 9
                      unit: count
                period:
                  label: last_30_days
                  start_date: '2026-06-21'
                  end_date: '2026-07-20'
                pagination:
                  page: 1
                  per_page: 10
                  total_count: 4
                  total_pages: 1
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '403':
          "$ref": "#/components/responses/InsufficientScope"
        '422':
          "$ref": "#/components/responses/ValidationFailed"
        '429':
          "$ref": "#/components/responses/RateLimited"
  "/reports/flows/{flow_id}":
    get:
      operationId: getFlowReport
      summary: Get flow performance
      description: |
        One flow's performance over a period: the combined email + SMS stat
        set plus a per-node breakdown of each email and SMS message step.

        Configuration — trigger, filters, re-entry behavior, branch graph — is
        `/v1/flows/{id}`. The per-node `uuid`s match that response's node `uuid`s,
        which is how a message step is placed in the flow graph; a step the flow
        no longer contains has no counterpart there.

        **Required scope:** `reports_read`
      tags:
      - Reports
      security:
      - bearerAuth: []
      parameters:
      - "$ref": "#/components/parameters/AccountIdParam"
      - name: flow_id
        in: path
        required: true
        description: The flow's unique identifier (returned as `flow_id` in report
          responses and `id` in flow responses). Ids are opaque public strings — a
          numeric id from a Privy dashboard URL is not a valid API id and will 404;
          list flows and match by title instead.
        schema:
          type: string
        example: Vq7L3mN9xR
      - "$ref": "#/components/parameters/ReportPeriodParam"
      - "$ref": "#/components/parameters/ReportStartDateParam"
      - "$ref": "#/components/parameters/ReportEndDateParam"
      responses:
        '200':
          description: The flow's performance over the resolved period.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    "$ref": "#/components/schemas/FlowReportDetail"
              example:
                data:
                  flow_id: Vq7L3mN9xR
                  title: Abandoned Cart Recovery
                  status: active
                  trigger_type: cart_abandoned
                  channels: multi
                  current_enrollment:
                    value: 86
                    unit: count
                  period:
                    label: last_30_days
                    start_date: '2026-06-21'
                    end_date: '2026-07-20'
                  stats:
                    sent:
                      value: 5200
                      unit: count
                    delivered:
                      value: 5050
                      unit: count
                    opened:
                      value: 2210
                      unit: count
                    clicked:
                      value: 640
                      unit: count
                    open_rate:
                      value: 51.4
                      unit: percent
                    click_rate:
                      value: 12.7
                      unit: percent
                    attributed_orders:
                      value: 210
                      unit: count
                    attributed_revenue:
                      value: 9640.5
                      unit: usd
                    order_rate:
                      value: 4.2
                      unit: percent
                    average_order_value:
                      value: 45.91
                      unit: usd
                    unsubscribed:
                      value: 34
                      unit: count
                    bounced:
                      value: 61
                      unit: count
                    sms_opted_out:
                      value: 12
                      unit: count
                    sms_failed:
                      value: 9
                      unit: count
                  nodes:
                  - uuid: 7a2f9e1b-5c3d-4d2e-8b4f-3a6c8d9e0f1a
                    type: email
                    name: Cart reminder email
                    subject: You left something behind
                    stats:
                      sent:
                        value: 2600
                        unit: count
                      delivered:
                        value: 2540
                        unit: count
                      opened_unique:
                        value: 1180
                        unit: count
                      clicked_unique:
                        value: 340
                        unit: count
                      bounced:
                        value: 38
                        unit: count
                      unsubscribed:
                        value: 21
                        unit: count
                      attributed_orders:
                        value: 120
                        unit: count
                      attributed_revenue:
                        value: 5610.25
                        unit: usd
                  - uuid: 8b3a0f2c-6d4e-4e3f-9c5a-4b7d9e0f1a2b
                    type: sms
                    name: Cart reminder text
                    stats:
                      sent:
                        value: 780
                        unit: count
                      delivered:
                        value: 762
                        unit: count
                      clicked_unique:
                        value: 96
                        unit: count
                      opted_out:
                        value: 7
                        unit: count
                      failed:
                        value: 18
                        unit: count
                      attributed_orders:
                        value: 41
                        unit: count
                      attributed_revenue:
                        value: 1890.0
                        unit: usd
        '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"
  "/reports/campaigns":
    get:
      operationId: listCampaignReports
      summary: List campaign performance
      description: |
        Every message campaign **sent in the period**, ranked by attributed
        revenue, each row carrying the full channel stat block (delivery,
        engagement, unsubscribes, attribution) scoped to the same period — so
        period comparisons need no per-campaign detail calls. Zero-revenue
        sends are listed too, ranked after the revenue rows; at most 100
        campaigns are ranked, and a send with no recorded activity at all in
        the period does not appear.

        By default the list serves your **newsletters** — email and SMS
        one-time sends. Pass `type=flows` to instead rank the **flow
        campaigns**: the email and SMS messages your automation flows send
        (flow rows carry no `sent_date` or `audience`, and instead name the
        flow that sends them in `flow`, so attributing a message to its flow
        needs no follow-up request). Use
        `/v1/reports/campaigns/{campaign_id}` for one campaign's
        most-clicked links or its lifetime funnel.

        **Required scope:** `reports_read`
      tags:
      - Reports
      security:
      - bearerAuth: []
      parameters:
      - "$ref": "#/components/parameters/AccountIdParam"
      - "$ref": "#/components/parameters/ReportPeriodParam"
      - "$ref": "#/components/parameters/ReportStartDateParam"
      - "$ref": "#/components/parameters/ReportEndDateParam"
      - name: type
        in: query
        description: Pass `flows` to rank the messages your flows send instead of
          newsletters.
        schema:
          type: string
          enum:
          - flows
        example: flows
      - name: channel
        in: query
        description: Filter by message channel. Works for both newsletters and flow
          campaigns.
        schema:
          type: string
          enum:
          - email
          - sms
        example: email
      - "$ref": "#/components/parameters/PageParam"
      - name: per_page
        in: query
        description: Campaigns per page. Values above 50 are clamped to 50; omitted
          or invalid values default to 10.
        schema:
          type: integer
          default: 10
          minimum: 1
          maximum: 50
        example: 10
      responses:
        '200':
          description: Campaigns ranked by attributed revenue over the resolved period.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/CampaignReportRow"
                  period:
                    "$ref": "#/components/schemas/ReportPeriod"
                  pagination:
                    "$ref": "#/components/schemas/PaginationEnvelope"
              example:
                data:
                - campaign_id: kD4qN8wZr1
                  type: newsletter
                  title: June Restock Announcement
                  channel: email
                  sent_date: '2026-06-18'
                  audience:
                    included_segments:
                    - Engaged 90d
                    excluded_segments:
                    - Recent purchasers
                  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.0
                      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.0
                      unit: usd
                    average_order_value:
                      value: 62.44
                      unit: usd
                period:
                  label: last_30_days
                  start_date: '2026-06-21'
                  end_date: '2026-07-20'
                pagination:
                  page: 1
                  per_page: 10
                  total_count: 12
                  total_pages: 2
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '403':
          "$ref": "#/components/responses/InsufficientScope"
        '422':
          "$ref": "#/components/responses/ValidationFailed"
        '429':
          "$ref": "#/components/responses/RateLimited"
  "/reports/campaigns/{campaign_id}":
    get:
      operationId: getCampaignReport
      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`
      tags:
      - Reports
      security:
      - bearerAuth: []
      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:
                  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.0
                      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.0
                      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.0
                          unit: percent
                        clicked_unique:
                          value: 50
                          unit: count
                        click_rate:
                          value: 5.0
                          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.0
                          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.0
                          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.0
                          unit: percent
                        clicked_unique:
                          value: 560
                          unit: count
                        click_rate:
                          value: 8.0
                          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.0
                          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"
  "/reports/top_products":
    get:
      operationId: listTopProducts
      summary: List top products
      description: |
        Retrieve your store's top products ranked by views or sales over a
        30, 60, or 90 day window, or all-time where the platform supports it.

        Entries are the connected store platform's product ids (e.g. the
        numeric Shopify product id) ranked best-first. Each entry carries the
        product `title`, resolved live from the connected store; it is `null`
        when the store cannot supply it (no connected store, the product was
        deleted, or the store API was unreachable). Full product details are
        at `GET /v1/products/{id}`. Every response repeats this in an in-band
        `note`.

        Rankings are served from a daily-refreshed cache. `refreshed_at` is
        the cache timestamp; `null` means this metric/window combination has
        never been computed (as opposed to computed and empty) — the request
        itself enqueues the background refresh that fills it, so retry after
        a short wait.

        Platform support differs by metric, and for `views` by window.
        Windowed view rankings (`window_days` 30, 60, or 90) require a
        connected Shopify store: dated product views are only tracked through
        Shopify's Web Pixel, so those windows return an empty ranking on every
        other platform. All-time view rankings (`window_days=0`) come from the
        store platform's own view counter and are available for BigCommerce
        only — BigCommerce exposes a per-product view count with no date
        dimension, so it can be ranked all-time but never windowed. Shopify
        has no such counter, so `window_days=0` returns an empty ranking for a
        Shopify-only store; a store connected to both platforms is ranked from
        its BigCommerce counter.

        The all-time ranking is built by scanning the BigCommerce catalog, and
        that scan is capped at 25,000 products. On a larger catalog it covers
        the products scanned rather than the whole catalog, so treat it as
        approximate there.

        `sales` is derived from synced order data and works for any store that
        syncs orders to Privy (Shopify, BigCommerce, Wix, and orders submitted
        through the API). There is no all-time sales bucket, so
        `metric=sales` with `window_days=0` is rejected.

        **Required scope:** `reports_read`
      tags:
      - Reports
      security:
      - bearerAuth: []
      parameters:
      - "$ref": "#/components/parameters/AccountIdParam"
      - name: metric
        in: query
        required: false
        description: Ranking metric. `views` availability depends on the store's platform
          and the requested window (see the operation description); `sales` works
          for any store that syncs orders to Privy.
        schema:
          type: string
          enum:
          - views
          - sales
          default: sales
      - name: window_days
        in: query
        required: false
        description: Ranking window in days. `0` means all-time, which is supported
          for `metric=views` on BigCommerce stores only and is rejected for `metric=sales`.
        schema:
          type: integer
          enum:
          - 0
          - 30
          - 60
          - 90
          default: 30
      - "$ref": "#/components/parameters/PageParam"
      - "$ref": "#/components/parameters/PerPageParam"
      responses:
        '200':
          description: The ranked product ids for the requested metric and window.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        rank:
                          type: integer
                          description: 1-based rank, best first.
                        product_id:
                          type: string
                          description: The connected store platform's product id (e.g.
                            the numeric Shopify product id).
                        title:
                          type:
                          - string
                          - 'null'
                          description: Product title resolved live from the connected
                            store; `null` when the store cannot supply it.
                  metric:
                    type: string
                    enum:
                    - views
                    - sales
                  window_days:
                    type: integer
                    enum:
                    - 0
                    - 30
                    - 60
                    - 90
                    description: The window this ranking covers. `0` means all-time.
                  refreshed_at:
                    type:
                    - string
                    - 'null'
                    format: date-time
                    description: When this ranking was last computed. `null` if it
                      has never been computed. For the rolling windows the request
                      enqueues the refresh that fills it. The all-time views ranking
                      is computed only for BigCommerce stores that have configured
                      a most-viewed dynamic product block, so `null` there means either
                      the store's platform produces no all-time view counts or no
                      such block exists yet.
                  note:
                    type: string
                    description: Fixed caveat that entries are store product ids with
                      live-resolved titles, and full details live at GET /v1/products/{id}.
                  pagination:
                    "$ref": "#/components/schemas/PaginationEnvelope"
              example:
                data:
                - rank: 1
                  product_id: '812345'
                  title: Blue Tee
                - rank: 2
                  product_id: '819921'
                  title: Beach Towel
                metric: views
                window_days: 30
                refreshed_at: '2026-07-29T06:10:00Z'
                note: Entries are the connected store platform's product ids (e.g.
                  the numeric Shopify product id) ranked best-first. Titles are resolved
                  live from the connected store and are null when it cannot supply
                  them; full product details are at GET /v1/products/{product_id}.
                pagination:
                  page: 1
                  per_page: 40
                  total_count: 2
                  total_pages: 1
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '403':
          "$ref": "#/components/responses/InsufficientScope"
        '422':
          "$ref": "#/components/responses/ValidationFailed"
        '429':
          "$ref": "#/components/responses/RateLimited"
  "/flows":
    get:
      operationId: listFlows
      summary: List flows
      description: |
        Retrieve a paginated list of your automation flows, including each flow's trigger and steps.
        Use `GET /v1/flows/{id}` for a flow's full configuration — entry frequency,
        flow filters, exit criteria, and the node branch graph.

        Configuration only — no performance, and no membership. Flow results and
        the live enrollment count both come from `/v1/reports/flows`, which lists
        every flow, so pair the two rather than expecting stats here.

        **Required scope:** `flows_read`
      tags:
      - Flows
      security:
      - bearerAuth: []
      parameters:
      - "$ref": "#/components/parameters/AccountIdParam"
      - "$ref": "#/components/parameters/PageParam"
      - "$ref": "#/components/parameters/PerPageParam"
      - name: status
        in: query
        description: Filter by flow status.
        schema:
          type: string
          enum:
          - draft
          - active
          - inactive
        example: active
      responses:
        '200':
          description: A paginated list of flows.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/Flow"
                  pagination:
                    "$ref": "#/components/schemas/PaginationEnvelope"
              example:
                data:
                - id: Vq7L3mN9xR
                  title: Abandoned Cart Recovery
                  status: active
                  trigger_type: cart_abandoned
                  trigger_label: Cart abandoned
                  nodes:
                  - uuid: 6f1e8d0a-4b2c-4c1d-9a3e-2f5b7c8d9e0f
                    type: delay
                    name: Wait 1 hour
                  - uuid: 7a2f9e1b-5c3d-4d2e-8b4f-3a6c8d9e0f1a
                    type: email
                    name: Cart reminder email
                  sync_coupons: false
                  last_transition_at: '2026-06-01T12:00:00Z'
                pagination:
                  page: 1
                  per_page: 40
                  total_count: 4
                  total_pages: 1
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '403':
          "$ref": "#/components/responses/InsufficientScope"
        '422':
          "$ref": "#/components/responses/ValidationFailed"
        '429':
          "$ref": "#/components/responses/RateLimited"
  "/flows/{id}":
    get:
      operationId: getFlow
      summary: Get a flow
      description: |
        Retrieve a single flow by its identifier, including its full
        configuration: re-entry behavior, flow filters, effective exit
        behavior, a server-generated behavior summary, and the node tree —
        each step's configuration (`data`) plus the branch graph, with split
        conditions and the nodes on each branch.

        `exit_behavior` and `reentry` state the engine's effective semantics
        directly: exits end only journeys already in progress (the enrolling
        event never triggers them), and `reentry.mode` says explicitly
        whether and when a contact can go through again. Trust these fields
        and `behavior_summary` over inferences from the node structure.

        **Required scope:** `flows_read`
      tags:
      - Flows
      security:
      - bearerAuth: []
      parameters:
      - "$ref": "#/components/parameters/AccountIdParam"
      - name: id
        in: path
        required: true
        description: The flow's unique identifier (returned as `id` in flow responses).
          Ids are opaque public strings — a numeric id from a Privy dashboard URL
          is not a valid API id and will 404; list flows and match by title instead.
        schema:
          type: string
        example: Vq7L3mN9xR
      responses:
        '200':
          description: The flow.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    "$ref": "#/components/schemas/FlowDetail"
              example:
                data:
                  id: Vq7L3mN9xR
                  title: Abandoned Cart Recovery
                  status: active
                  trigger_type: cart_abandoned
                  trigger_label: Cart abandoned
                  reentry:
                    mode: after_delay
                    frequency: 30
                    unit: days
                  flow_filters:
                    operation: AND
                    conditions:
                    - property: channel
                      operator: equals
                      value: email
                    - property: segment
                      operator: contains
                      value:
                      - Nk2P5rW8yU
                  exit_behavior:
                    exit_events:
                    - order_placed
                    - order_delivered
                    applies_to: active_journeys_only
                    enrollment_event_excluded: true
                    summary: After enrollment, a subsequent order placed or order
                      delivered event ends the journey early. The event that enrolled
                      the contact never triggers this exit.
                  behavior_summary: A contact enrolls when the "Cart abandoned" event
                    occurs. The flow has 2 message steps. A subsequent order placed
                    or order delivered event ends an active journey early; the event
                    that enrolled the contact never triggers this exit. A contact
                    can re-enter 30 days after their previous journey ends; no setting
                    ever runs two journeys at once.
                  nodes:
                  - uuid: 6f1e8d0a-4b2c-4c1d-9a3e-2f5b7c8d9e0f
                    type: delay
                    name: Wait 1 hour
                    parent_uuid:
                    next_uuid: 8b3c0f2d-6d4e-4e3f-9c5a-4b7d9e0f1a2b
                    data:
                      delay_unit: hours
                      delay_unit_count: 1
                  - uuid: 8b3c0f2d-6d4e-4e3f-9c5a-4b7d9e0f1a2b
                    type: split
                    name: High-value cart?
                    parent_uuid: 6f1e8d0a-4b2c-4c1d-9a3e-2f5b7c8d9e0f
                    yes_next_uuid: 7a2f9e1b-5c3d-4d2e-8b4f-3a6c8d9e0f1a
                    no_next_uuid: 9c4d1a3e-7e5f-4f4a-8d6b-5c8e0f1a2b3c
                    data:
                      conditions:
                        operation: AND
                        conditions:
                        - property: abandoned_cart_value
                          operator: greater_than
                          value: 100
                  - uuid: 7a2f9e1b-5c3d-4d2e-8b4f-3a6c8d9e0f1a
                    type: email
                    name: VIP cart reminder
                    parent_uuid: 8b3c0f2d-6d4e-4e3f-9c5a-4b7d9e0f1a2b
                    next_uuid:
                    data:
                      campaign_id: Xk4M7pQ2wT
                      subject: You left something behind!
                  - uuid: 9c4d1a3e-7e5f-4f4a-8d6b-5c8e0f1a2b3c
                    type: sms
                    name: Cart reminder text
                    parent_uuid: 8b3c0f2d-6d4e-4e3f-9c5a-4b7d9e0f1a2b
                    next_uuid:
                    data:
                      campaign_id: Zt6N1rV4yU
                      message_body: Your cart is waiting — checkout now and save 10%.
                  sync_coupons: false
                  last_transition_at: '2026-06-01T12:00:00Z'
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '403':
          "$ref": "#/components/responses/InsufficientScope"
        '404':
          "$ref": "#/components/responses/NotFound"
        '429':
          "$ref": "#/components/responses/RateLimited"
  "/segments":
    get:
      operationId: listSegments
      summary: List segments
      description: |
        Retrieve a paginated list of your customer segments with their mailable/textable member counts.

        Rows carry each segment's name, prebuilt flag, and reachable-member counts
        but not its members: `/v1/segments/{id}/contacts` returns them. Nothing in the API
        reports performance per segment — there is no segment report, so revenue
        or engagement for a segment cannot be answered from here.

        **Required scope:** `segments_read`
      tags:
      - Segments
      security:
      - bearerAuth: []
      parameters:
      - "$ref": "#/components/parameters/AccountIdParam"
      - "$ref": "#/components/parameters/PageParam"
      - "$ref": "#/components/parameters/PerPageParam"
      responses:
        '200':
          description: A paginated list of segments.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/Segment"
                  pagination:
                    "$ref": "#/components/schemas/PaginationEnvelope"
              example:
                data:
                - id: Rz5T2kW8pB
                  name: VIP Customers
                  prebuilt: false
                  mailable_count: 1240
                  textable_count: 315
                  counts:
                    recounting: false
                    refreshed_at: '2026-06-15T08:00:00Z'
                    freshness: Counts are refreshed periodically and may be up to
                      an hour old.
                  created_at: '2026-03-01T10:00:00Z'
                  updated_at: '2026-06-01T10:00:00Z'
                pagination:
                  page: 1
                  per_page: 40
                  total_count: 7
                  total_pages: 1
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '403':
          "$ref": "#/components/responses/InsufficientScope"
        '429':
          "$ref": "#/components/responses/RateLimited"
  "/segments/{id}":
    get:
      operationId: getSegment
      summary: Get a segment
      description: |
        Retrieve a single segment by its identifier. The members themselves are
        `/v1/segments/{id}/contacts`. A campaign's `audience` segment ids resolve
        here when the segment was saved to the account. There is no per-segment performance report anywhere in the API.

        **Required scope:** `segments_read`
      tags:
      - Segments
      security:
      - bearerAuth: []
      parameters:
      - "$ref": "#/components/parameters/AccountIdParam"
      - name: id
        in: path
        required: true
        description: The segment's unique identifier (returned as `id` in segment
          responses). Ids are opaque public strings — a numeric id from a Privy dashboard
          URL is not a valid API id and will 404; list segments and match by name
          instead.
        schema:
          type: string
        example: Rz5T2kW8pB
      responses:
        '200':
          description: The segment.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    "$ref": "#/components/schemas/SegmentDetail"
              example:
                data:
                  id: Rz5T2kW8pB
                  name: VIP Customers
                  prebuilt: false
                  mailable_count: 1240
                  textable_count: 315
                  counts:
                    recounting: false
                    refreshed_at: '2026-06-15T08:00:00Z'
                    freshness: Counts are refreshed periodically and may be up to
                      an hour old.
                  created_at: '2026-03-01T10:00:00Z'
                  updated_at: '2026-06-01T10:00:00Z'
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '403':
          "$ref": "#/components/responses/InsufficientScope"
        '404':
          "$ref": "#/components/responses/NotFound"
        '429':
          "$ref": "#/components/responses/RateLimited"
  "/segments/{id}/contacts":
    get:
      operationId: listSegmentContacts
      summary: List a segment's contacts
      description: |
        Retrieve the segment's members as a paginated list of contacts, in the same shape as the
        Contacts API, most recently created first.

        Membership is computed periodically, so the list may be up to an hour old (see
        `counts.refreshed_at` on the segment). If the segment's membership has **never** been computed,
        this endpoint returns `202 Accepted`, starts the computation in the background, and you should
        retry shortly.

        Every member is returned whatever their consent state, so `pagination.total_count` is the
        segment's total membership. It is neither the segment's `mailable_count` nor its
        `textable_count` — those are consent-filtered counts taken at the last refresh.

        **Required scope:** `segments_read`
      tags:
      - Segments
      security:
      - bearerAuth: []
      parameters:
      - "$ref": "#/components/parameters/AccountIdParam"
      - name: id
        in: path
        required: true
        description: The segment's unique identifier (returned as `id` in segment
          responses). Ids are opaque public strings — a numeric id from a Privy dashboard
          URL is not a valid API id and will 404; list segments and match by name
          instead.
        schema:
          type: string
        example: Rz5T2kW8pB
      - "$ref": "#/components/parameters/PageParam"
      - "$ref": "#/components/parameters/PerPageParam"
      responses:
        '200':
          description: A paginated list of the segment's member contacts.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/Contact"
                  pagination:
                    "$ref": "#/components/schemas/PaginationEnvelope"
              example:
                data:
                - id: cus_a1b2c3d4e5f6g7h8
                  first_name: Jane
                  last_name: Doe
                  email: jane@example.com
                  email_consent: subscribed
                  email_consent_at: '2025-03-20T14:22:00Z'
                  phone_number: "+15551234567"
                  sms_consent: subscribed
                  sms_consent_at: '2025-03-20T14:22:00Z'
                  source: csv
                  source_type: import
                  imported: true
                  import_ids:
                  - im_k2R5m8Q
                  tags:
                  - vip
                  created_at: '2025-01-15T10:30:00Z'
                  updated_at: '2025-03-20T14:22:00Z'
                pagination:
                  page: 1
                  per_page: 40
                  total_count: 1240
                  total_pages: 31
        '202':
          description: 'The segment''s membership has never been computed. Computation
            has been started — retry in a little while.

            '
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum:
                    - processing
                    example: processing
                  message:
                    type: string
                    example: Segment membership is being computed. Check back shortly.
              example:
                status: processing
                message: Segment membership is being computed. Check back shortly.
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '403':
          "$ref": "#/components/responses/InsufficientScope"
        '404':
          "$ref": "#/components/responses/NotFound"
        '409':
          "$ref": "#/components/responses/ContactsNotReady"
        '429':
          "$ref": "#/components/responses/RateLimited"
  "/contacts":
    get:
      operationId: listContacts
      summary: List contacts
      description: |
        Retrieve a paginated list of contacts, most recently created first. Optionally filter by email,
        phone number, or consent status.

        Unfiltered, the list covers every contact in the account whatever their consent state, so
        `pagination.total_count` is the size of your contact list — not the size of your mailable or
        textable audience. Filter on `email_consent=subscribed` or `sms_consent=subscribed` for those.
        Contacts you have deleted are excluded — but deletion is soft: creating a contact with
        the same email or phone number restores the existing record rather than creating a new
        one, so a previously deleted contact can reappear here later with its original id and
        history.

        **Required scope:** `contacts_read`
      tags:
      - Contacts
      security:
      - bearerAuth: []
      parameters:
      - "$ref": "#/components/parameters/AccountIdParam"
      - "$ref": "#/components/parameters/PageParam"
      - "$ref": "#/components/parameters/PerPageParam"
      - name: email
        in: query
        description: Filter by exact email address.
        schema:
          type: string
          format: email
        example: jane@example.com
      - name: phone_number
        in: query
        description: Filter by exact phone number (E.164 format).
        schema:
          type: string
        example: "+15551234567"
      - name: email_consent
        in: query
        description: |
          Filter by email consent status. `subscribed` matches both explicit
          and implicit opt-in. `suppressed` matches merchant-suppressed contacts
          only; `compliance_suppressed` matches system/compliance-suppressed contacts.
        schema:
          type: string
          enum:
          - subscribed
          - unsubscribed
          - never_subscribed
          - suppressed
          - compliance_suppressed
        example: subscribed
      - name: sms_consent
        in: query
        description: Filter by SMS consent status.
        schema:
          type: string
          enum:
          - subscribed
          - unsubscribed
          - never_subscribed
          - single_opt_in
          - pending
        example: subscribed
      - name: created_after
        in: query
        description: Only contacts created strictly after this ISO 8601 datetime (or
          date). Zone-less datetimes are interpreted as UTC; bare dates as midnight
          US Eastern time.
        schema:
          type: string
          format: date-time
        example: '2026-08-01T00:00:00Z'
      - name: created_before
        in: query
        description: Only contacts created strictly before this ISO 8601 datetime
          (or date). Zone-less datetimes are interpreted as UTC; bare dates as midnight
          US Eastern time.
        schema:
          type: string
          format: date-time
      - name: updated_after
        in: query
        description: |
          Only contacts updated strictly after this ISO 8601 datetime (or date).
          Zone-less datetimes are interpreted as UTC; bare dates as midnight US Eastern time.
        schema:
          type: string
          format: date-time
      - name: updated_before
        in: query
        description: Only contacts updated strictly before this ISO 8601 datetime
          (or date). Zone-less datetimes are interpreted as UTC; bare dates as midnight
          US Eastern time.
        schema:
          type: string
          format: date-time
      - name: import_id
        in: query
        description: |
          Only contacts touched by this import — including contacts that already existed
          before it ran. Takes an email import id (`im_…`) from a contact's `import_ids`.
          Unknown or malformed ids return a `422`; SMS consent imports (`sim_…`) do not
          link to contacts.
        schema:
          type: string
        example: im_k2R5m8Q
      responses:
        '200':
          description: A paginated list of contacts.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/Contact"
                  pagination:
                    "$ref": "#/components/schemas/PaginationEnvelope"
              example:
                data:
                - id: cus_a1b2c3d4e5f6g7h8
                  first_name: Jane
                  last_name: Doe
                  email: jane@example.com
                  email_consent: subscribed
                  email_consent_at: '2025-03-20T14:22:00Z'
                  phone_number: "+15551234567"
                  sms_consent: subscribed
                  sms_consent_at: '2025-03-20T14:22:00Z'
                  source: csv
                  source_type: import
                  imported: true
                  import_ids:
                  - im_k2R5m8Q
                  tags:
                  - vip
                  custom_fields:
                    loyalty_tier: gold
                  created_at: '2025-01-15T10:30:00Z'
                  updated_at: '2025-03-20T14:22:00Z'
                pagination:
                  page: 1
                  per_page: 25
                  total_count: 142
                  total_pages: 6
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '403':
          "$ref": "#/components/responses/InsufficientScope"
        '409':
          "$ref": "#/components/responses/ContactsNotReady"
        '422':
          "$ref": "#/components/responses/ValidationFailed"
        '429':
          "$ref": "#/components/responses/RateLimited"
    post:
      operationId: createContact
      summary: Create a contact
      description: |
        Create a new contact. At least one of `email` or `phone_number` is required.

        If a contact with the same email or phone number already exists, a `409 Conflict`
        error is returned with the `id` of the existing contact in `error.id`. Use the **Update a contact** endpoint to modify existing contacts.

        If a previously deleted contact matches the provided email or phone number,
        the contact is restored with the new data.

        When `sms_consent` is `subscribed`, Privy automatically sends a TCPA-required
        welcome SMS to the contact. Pass `send_welcome_sms: false` to suppress this
        message if you have already collected consent outside of Privy.

        **Required scope:** `contacts_write`
      tags:
      - Contacts
      security:
      - bearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              "$ref": "#/components/schemas/CreateContactRequest"
            example:
              email: jane@example.com
              first_name: Jane
              last_name: Doe
              tags:
              - new-contact
              email_consent: subscribed
      responses:
        '201':
          description: Contact created successfully.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    "$ref": "#/components/schemas/Contact"
              example:
                data:
                  id: cus_x9y8z7w6v5u4t3s2
                  first_name: Jane
                  last_name: Doe
                  email: jane@example.com
                  email_consent: subscribed
                  phone_number:
                  sms_consent: never_subscribed
                  tags:
                  - new-contact
                  custom_fields: {}
                  created_at: '2025-04-01T12:00:00Z'
                  updated_at: '2025-04-01T12:00:00Z'
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '403':
          "$ref": "#/components/responses/InsufficientScope"
        '409':
          description: A contact with this email or phone number already exists. The
            response includes the `id` of the existing contact in `error.id`.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: object
                    properties:
                      code:
                        type: string
                      message:
                        type: string
                      id:
                        type: string
                        description: The ID of the existing contact that caused the
                          conflict.
              example:
                error:
                  code: conflict
                  message: A contact with this email already exists
                  id: cus_x9y8z7w6v5u4t3s2
        '422':
          "$ref": "#/components/responses/ValidationFailed"
        '429':
          "$ref": "#/components/responses/RateLimited"
      parameters:
      - "$ref": "#/components/parameters/AccountIdParam"
  "/contacts/{id}":
    get:
      operationId: getContact
      summary: Get a contact
      description: |
        Retrieve a single contact by its unique `id`.

        **Required scope:** `contacts_read`
      tags:
      - Contacts
      security:
      - bearerAuth: []
      parameters:
      - "$ref": "#/components/parameters/AccountIdParam"
      - "$ref": "#/components/parameters/ContactIdParam"
      responses:
        '200':
          description: The requested contact.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    allOf:
                    - "$ref": "#/components/schemas/Contact"
                    - type: object
                      properties:
                        signups:
                          type: array
                          description: |
                            The contact's display signups, newest first — the attribution
                            `source`/`source_type` do not carry, since those describe how the record
                            was created. Each entry is one form submission on a display: `display_id`
                            round-trips through `GET /v1/displays/{id}` (A/B-test signups are recorded
                            against the parent display; a display deleted since the signup still names
                            itself here but no longer resolves there), `coupon_code` is the code handed out at that signup
                            (`null` when the display had no coupon attached — the usual reason an
                            after-signup email arrives without a code). Empty when the contact never
                            signed up on a display. Only the newest 100 signups are returned.
                          items:
                            type: object
                            properties:
                              display_id:
                                type: string
                                example: gk2P8Zw1Lq
                              display_title:
                                type:
                                - string
                                - 'null'
                                example: Welcome Popup
                              coupon_code:
                                type:
                                - string
                                - 'null'
                                example: VIP4F2A9C1B
                              created_at:
                                type: string
                                format: date-time
                                example: '2025-03-20T14:22:00Z'
              example:
                data:
                  id: cus_a1b2c3d4e5f6g7h8
                  first_name: Jane
                  last_name: Doe
                  email: jane@example.com
                  email_consent: subscribed
                  email_consent_at: '2025-03-20T14:22:00Z'
                  phone_number: "+15551234567"
                  sms_consent: subscribed
                  sms_consent_at: '2025-03-20T14:22:00Z'
                  source: csv
                  source_type: import
                  imported: true
                  import_ids:
                  - im_k2R5m8Q
                  tags:
                  - vip
                  custom_fields:
                    loyalty_tier: gold
                  signups:
                  - display_id: gk2P8Zw1Lq
                    display_title: Welcome Popup
                    coupon_code: VIP4F2A9C1B
                    created_at: '2025-03-20T14:22:00Z'
                  created_at: '2025-01-15T10:30:00Z'
                  updated_at: '2025-03-20T14:22:00Z'
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '403':
          "$ref": "#/components/responses/InsufficientScope"
        '404':
          "$ref": "#/components/responses/NotFound"
        '429':
          "$ref": "#/components/responses/RateLimited"
    patch:
      operationId: updateContactById
      summary: Update a contact by ID
      description: |
        Update an existing contact identified by its unique `id`.
        Include one or more fields to change. Field updates and consent changes
        can be combined in a single request.

        Mutable fields: `first_name`, `last_name`, `email`, `phone_number`, `tags`, `custom_fields`.

        `email` and `phone_number` can only be set when the existing value is
        `null`. Once populated, they cannot be changed or cleared.

        Consent fields: `email_consent`, `sms_consent`. Use these to subscribe,
        unsubscribe, or otherwise manage a contact's marketing consent.

        When `sms_consent` is set to `subscribed` and the contact transitions to
        a confirmed opt-in state, Privy automatically sends a TCPA-required
        welcome SMS. Pass `send_welcome_sms: false` to suppress this message if
        you have already collected consent outside of Privy.

        **Required scope:** `contacts_write`
      tags:
      - Contacts
      security:
      - bearerAuth: []
      parameters:
      - "$ref": "#/components/parameters/AccountIdParam"
      - "$ref": "#/components/parameters/ContactIdParam"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              "$ref": "#/components/schemas/UpdateContactByIdRequest"
            example:
              first_name: Janet
              tags:
              - vip
              - wholesale
              email_consent: unsubscribed
      responses:
        '200':
          description: Contact updated successfully.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    "$ref": "#/components/schemas/Contact"
              example:
                data:
                  id: cus_a1b2c3d4e5f6g7h8
                  first_name: Janet
                  last_name: Doe
                  email: jane@example.com
                  email_consent: unsubscribed
                  phone_number: "+15551234567"
                  sms_consent: subscribed
                  tags:
                  - vip
                  - wholesale
                  custom_fields:
                    loyalty_tier: gold
                  created_at: '2025-01-15T10:30:00Z'
                  updated_at: '2025-04-01T12:15:00Z'
        '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"
    delete:
      operationId: deleteContactById
      summary: Delete a contact by ID
      description: |
        Soft-delete a contact identified by its unique `id`.

        Deleted contacts can be restored if a new contact is created with
        the same email or phone number via the **Create a contact** endpoint.

        **Required scope:** `contacts_write`
      tags:
      - Contacts
      security:
      - bearerAuth: []
      parameters:
      - "$ref": "#/components/parameters/AccountIdParam"
      - "$ref": "#/components/parameters/ContactIdParam"
      responses:
        '200':
          description: Contact deleted successfully.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    "$ref": "#/components/schemas/Contact"
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '403':
          "$ref": "#/components/responses/InsufficientScope"
        '404':
          "$ref": "#/components/responses/NotFound"
        '429':
          "$ref": "#/components/responses/RateLimited"
  "/contacts/{id}/events":
    get:
      operationId: listContactEvents
      summary: List a contact's activity
      description: |
        Retrieve the contact's activity timeline — signups, email and SMS sends, opens and clicks,
        orders, checkouts, tag changes — newest first. This is the same record the Privy dashboard's
        contact page shows, so "why did this contact get X?" can be answered from the API alone:
        find the send, the signup that triggered it, and the display or campaign each traces to.

        Each entry names its `type` (the event vocabulary below, as stored), when it happened, the
        display or campaign it belongs to (when the event carries one, even one since deleted), and the
        event's raw `properties`, whose shape varies by type (the recipient IP and user agent recorded
        on email opens and clicks, and the IP-derived location on signups, are not included).

        By default the timeline covers the last 24 months; narrow it with `occurred_after` /
        `occurred_before` (both bounds exclusive; a bare date means midnight at the start of that
        day) or to one event type with `type`.

        **Required scope:** `contacts_read`
      tags:
      - Contacts
      security:
      - bearerAuth: []
      parameters:
      - "$ref": "#/components/parameters/AccountIdParam"
      - "$ref": "#/components/parameters/ContactIdParam"
      - name: type
        in: query
        required: false
        description: 'Only return events of this type. Any `type` value a row carries
          is accepted (including names a merchant''s own integration sent through
          the events ingest), so a type read off the timeline always filters. Common
          values: customer-subscribed, email-sent, email-delivered, email-opened,
          email-clicked, text-message-sent, text-message-delivered, view, new-session,
          redemption, signup, sms-signup, order-created, checkout-started, customer-tag-added,
          customer-tag-removed.'
        schema:
          type: string
        example: email-opened
      - name: occurred_after
        in: query
        required: false
        description: Only events after this instant (exclusive). ISO 8601 datetime
          or date. Defaults to 24 months before `occurred_before` — 24 months ago
          when neither bound is given.
        schema:
          type: string
        example: '2026-06-01T00:00:00Z'
      - name: occurred_before
        in: query
        required: false
        description: Only events before this instant (exclusive). ISO 8601 datetime
          or date. Defaults to now.
        schema:
          type: string
        example: '2026-07-01'
      - "$ref": "#/components/parameters/PageParam"
      - "$ref": "#/components/parameters/PerPageParam"
      responses:
        '200':
          description: A paginated list of the contact's events, newest first.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/ContactEvent"
                  note:
                    type: string
                    description: Present only when the account's plan limits the timeline
                      to signup events.
                  pagination:
                    "$ref": "#/components/schemas/PaginationEnvelope"
              example:
                data:
                - type: email-opened
                  occurred_at: '2026-08-20T14:22:00Z'
                  campaign:
                    id: Nw8K2rT5xY
                    title: Welcome Series — Email 1
                    type: flow
                  properties:
                    subject: Take 10% Off
                - type: signup
                  occurred_at: '2026-08-20T14:20:31Z'
                  campaign:
                    id: gk2P8Zw1Lq
                    title: Welcome Popup
                    type: display
                  properties:
                    redemption_code: '482910375563'
                pagination:
                  page: 1
                  per_page: 40
                  total_count: 2
                  total_pages: 1
        '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"
  "/contacts/{id}/unsubscribe":
    post:
      operationId: unsubscribeContact
      summary: Unsubscribe a contact
      deprecated: true
      description: |
        **Deprecated.** Use `PATCH /v1/contacts/{id}` with `email_consent: "unsubscribed"`
        instead. This endpoint will be removed in a future release.

        Globally unsubscribe a contact from all email marketing.

        If the contact is already unsubscribed, this endpoint returns successfully
        without making changes.

        Responses include `Deprecation: true` and a `Link` header pointing to
        migration documentation.

        **Required scope:** `contacts_write`
      tags:
      - Contacts
      security:
      - bearerAuth: []
      parameters:
      - "$ref": "#/components/parameters/AccountIdParam"
      - "$ref": "#/components/parameters/ContactIdParam"
      requestBody:
        content:
          application/json:
            schema:
              type: object
              properties:
                reason:
                  type: string
                  maxLength: 500
                  description: Optional reason for unsubscribing.
                  example: Contact requested removal from mailing list
      responses:
        '200':
          description: Contact unsubscribed successfully.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    "$ref": "#/components/schemas/Contact"
              example:
                data:
                  id: cus_a1b2c3d4e5f6g7h8
                  first_name: Jane
                  last_name: Doe
                  email: jane@example.com
                  email_consent: unsubscribed
                  phone_number: "+15551234567"
                  sms_consent: subscribed
                  tags:
                  - vip
                  custom_fields:
                    loyalty_tier: gold
                  created_at: '2025-01-15T10:30:00Z'
                  updated_at: '2025-04-01T12:30:00Z'
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '403':
          "$ref": "#/components/responses/InsufficientScope"
        '404':
          "$ref": "#/components/responses/NotFound"
        '429':
          "$ref": "#/components/responses/RateLimited"
  "/imports":
    get:
      operationId: listImports
      summary: List contact imports
      description: |
        Retrieve a paginated list of your contact imports — CSV uploads and connected-provider
        syncs — most recently requested first. Each row mirrors the dashboard's import history:
        type, processing status, contact counts, who requested it, when, and its review status.

        `channel` selects which universe is listed: `email` (the default — contact imports and
        provider syncs) or `sms` (SMS consent imports, a separate review pipeline). The two are
        different record types with per-channel `status`/`review_status` vocabularies and count
        keys; rows state their `channel`.

        **Required scope:** `contacts_read`
      tags:
      - Imports
      security:
      - bearerAuth: []
      parameters:
      - "$ref": "#/components/parameters/AccountIdParam"
      - "$ref": "#/components/parameters/PageParam"
      - "$ref": "#/components/parameters/PerPageParam"
      - name: channel
        in: query
        description: Which import universe to list. Defaults to `email`.
        schema:
          type: string
          enum:
          - email
          - sms
      - name: status
        in: query
        description: |
          Filter by processing status. Email values: `pending`, `partial`, `successful`,
          `failed`. Not available on `channel=sms` for merchant tokens (`status` is
          omitted from their SMS rows) — the filter returns a `422` there.
        schema:
          type: string
      - name: review_status
        in: query
        description: |
          Filter by approval state. Email values: `NA`, `exempt`, `pending`, `approving`,
          `approved`, `auto_approved`, `partially_approved`, `rejected`. SMS values:
          `pending`, `approved`, `rejected`.
        schema:
          type: string
      responses:
        '200':
          description: A paginated list of imports.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/Import"
                  pagination:
                    "$ref": "#/components/schemas/PaginationEnvelope"
              example:
                data:
                - id: im_k2R5m8Q
                  channel: email
                  source: csv
                  status: partial
                  review_status: partially_approved
                  requested_by: Rita Requester
                  counts:
                    imported: 17506
                    new_contacts: 7615
                  created_at: '2026-08-08T17:52:00Z'
                pagination:
                  page: 1
                  per_page: 40
                  total_count: 3
                  total_pages: 1
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '403':
          "$ref": "#/components/responses/InsufficientScope"
        '422':
          "$ref": "#/components/responses/ValidationFailed"
        '429':
          "$ref": "#/components/responses/RateLimited"
  "/imports/{id}":
    get:
      operationId: getImport
      summary: Get a contact import
      description: |
        Retrieve a single contact import — the same row `/v1/imports` lists for it.

        **Required scope:** `contacts_read`
      tags:
      - Imports
      security:
      - bearerAuth: []
      parameters:
      - "$ref": "#/components/parameters/AccountIdParam"
      - name: id
        in: path
        required: true
        description: |
          The import's prefixed id (`im_…` for email imports, `sim_…` for SMS consent
          imports), as returned by `/v1/imports` or a contact's `import_ids`. The prefix
          selects the record type, so no channel parameter is needed. The part after the
          prefix is opaque — a numeric id from a Privy dashboard URL is not a valid API id
          and will 404, with or without the prefix; list imports to find the id.
        schema:
          type: string
      responses:
        '200':
          description: The import.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    allOf:
                    - "$ref": "#/components/schemas/Import"
                    - type: object
                      properties: {}
              example:
                data:
                  id: im_k2R5m8Q
                  channel: email
                  source: csv
                  status: partial
                  review_status: partially_approved
                  requested_by: Rita Requester
                  counts:
                    imported: 17506
                    new_contacts: 7615
                  created_at: '2026-08-08T17:52:00Z'
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '403':
          "$ref": "#/components/responses/InsufficientScope"
        '404':
          "$ref": "#/components/responses/NotFound"
        '429':
          "$ref": "#/components/responses/RateLimited"
  "/orders":
    get:
      operationId: listOrders
      summary: List orders
      description: |
        Retrieve a paginated list of your orders across every source (Shopify,
        BigCommerce, Weebly, Wix, and orders reported via this API), most recently created first.

        Orders imported before Privy began denormalizing order timestamps may be excluded when a date filter is applied.

        Rows carry the full order shape, so no per-order follow-up is needed, and
        the only filters are the placed-date ones — none by contact, email, or
        status.

        **Required scope:** `orders_read`
      tags:
      - Orders
      security:
      - bearerAuth: []
      parameters:
      - "$ref": "#/components/parameters/AccountIdParam"
      - "$ref": "#/components/parameters/PageParam"
      - "$ref": "#/components/parameters/PerPageParam"
      - name: placed_after
        in: query
        description: Only orders placed strictly after this ISO 8601 datetime (or
          date). Zone-less datetimes are interpreted as UTC; bare dates as midnight
          US Eastern time.
        schema:
          type: string
          format: date-time
        example: '2026-06-01T00:00:00Z'
      - name: placed_before
        in: query
        description: Only orders placed strictly before this ISO 8601 datetime (or
          date). Zone-less datetimes are interpreted as UTC; bare dates as midnight
          US Eastern time.
        schema:
          type: string
          format: date-time
        example: '2026-07-01T00:00:00Z'
      responses:
        '200':
          description: A paginated list of orders.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/Order"
                  pagination:
                    "$ref": "#/components/schemas/PaginationEnvelope"
              example:
                data:
                - order_id: 1001
                  source: api
                  email: buyer@example.com
                  phone: "+12025550123"
                  total_amount: '95.00'
                  currency_code: USD
                  placed_at: '2026-06-01T12:00:00Z'
                  financial_status: paid
                  fulfillment_status: fulfilled
                  total_items: 2
                  created_at: '2026-06-01T12:00:05Z'
                  updated_at: '2026-06-01T12:00:05Z'
                pagination:
                  page: 1
                  per_page: 40
                  total_count: 3502
                  total_pages: 88
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '403':
          "$ref": "#/components/responses/InsufficientScope"
        '422':
          "$ref": "#/components/responses/ValidationFailed"
        '429':
          "$ref": "#/components/responses/RateLimited"
  "/orders/{order_id}":
    get:
      operationId: getOrder
      summary: Get an order
      description: |
        Retrieve a single order by its order id — the identifier assigned by the order's source
        (your store platform, or the `order_id` you sent to `POST /v1/orders/placed`).

        Order ids are unique per source. In the rare case two sources share an id, the most recently created order is returned.

        A `/v1/orders` row already carries the whole order, so this is for an id
        that came from somewhere else rather than a follow-up read.

        **Required scope:** `orders_read`
      tags:
      - Orders
      security:
      - bearerAuth: []
      parameters:
      - "$ref": "#/components/parameters/AccountIdParam"
      - name: order_id
        in: path
        required: true
        description: The order's source-assigned id (returned as `order_id` in order
          responses).
        schema:
          type: integer
          format: int64
        example: 1001
      responses:
        '200':
          description: The order.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    "$ref": "#/components/schemas/Order"
              example:
                data:
                  order_id: 1001
                  source: api
                  email: buyer@example.com
                  phone: "+12025550123"
                  total_amount: '95.00'
                  currency_code: USD
                  placed_at: '2026-06-01T12:00:00Z'
                  financial_status: paid
                  fulfillment_status: fulfilled
                  subtotal: 90.0
                  total_items: 2
                  line_items:
                  - sku: SKU-1
                    quantity: 2
                    price: 45.0
                  created_at: '2026-06-01T12:00:05Z'
                  updated_at: '2026-06-01T12:00:05Z'
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '403':
          "$ref": "#/components/responses/InsufficientScope"
        '404':
          "$ref": "#/components/responses/NotFound"
        '429':
          "$ref": "#/components/responses/RateLimited"
  "/products":
    get:
      operationId: listProducts
      summary: List products
      description: |
        Retrieve the product catalog of your connected store (Shopify or BigCommerce), proxied live from the store's API.

        Unlike the other list endpoints, products use **cursor pagination**:
        pass the `next_cursor` from each response as `?cursor=` to fetch the next page,
        keeping the same filters. There are no `page` numbers.

        Requires a connected Shopify or BigCommerce store — without one, this endpoint returns `404 no_connected_store`.

        The response shape is the same on both platforms; fields BigCommerce does not
        supply (`product_type`, `vendor`, `url`) are `null`. Large `per_page` values can
        be slow on BigCommerce stores whose products carry many variants — reduce
        `per_page` if requests return `502 store_unavailable`.

        A live read of the store's own catalog — nothing here is Privy data, and no
        endpoint reports sales or performance per product. `/v1/products/{id}` adds
        description, tags, options, and images.

        **Required scope:** `products_read`
      tags:
      - Products
      security:
      - bearerAuth: []
      parameters:
      - "$ref": "#/components/parameters/AccountIdParam"
      - name: per_page
        in: query
        description: Number of results per page. Products default lower than other
          endpoints and clamp to 1–50 — these reads proxy the store live, and large
          pages on variant-heavy BigCommerce catalogs get expensive.
        schema:
          type: integer
          default: 25
          minimum: 1
          maximum: 50
        example: 25
      - name: title
        in: query
        description: 'Narrow to products whose title matches, at most 255 characters.
          Matching differs by platform. On Shopify stores, each word of the value
          must start a word of the title: a word is a run of letters and digits, apostrophes
          inside it included (`O''Neill`), and at least one is required. On BigCommerce
          stores, the title must contain the value as typed, where `%` and `_` may
          act as wildcards.'
        schema:
          type: string
        example: summer
      - name: handle
        in: query
        description: 'Narrow to the product with this exact handle (the slug in its
          storefront URL). Letters, digits, hyphens and underscores only. A Shopify-only
          filter: BigCommerce stores have no product handle and return `422 validation_failed`.'
        schema:
          type: string
        example: summer-tee
      - name: cursor
        in: query
        description: Opaque pagination cursor from a previous response's `pagination.next_cursor`.
        schema:
          type: string
        example: eyJsYXN0X2lkIjo3ODkwMTIzNDU2Nzg5fQ==
      responses:
        '200':
          description: A page of the store's product catalog.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/Product"
                  pagination:
                    "$ref": "#/components/schemas/CursorPagination"
              example:
                data:
                - id: 7890123456789
                  title: Classic T-Shirt
                  handle: classic-t-shirt
                  status: active
                  product_type: Shirts
                  vendor: Acme Apparel
                  url: https://mystore.com/products/classic-t-shirt
                  image_url: https://cdn.shopify.com/s/files/1/0001/products/tshirt.jpg
                  price_range:
                    min: '10.00'
                    max: '15.00'
                    currency_code: USD
                  total_inventory: 42
                  tracks_inventory: true
                  variants_count: 2
                  variants:
                  - id: 40012345678901
                    title: Small
                    price: '10.00'
                    compare_at_price:
                    sku: TSHIRT-S
                    inventory_quantity: 12
                    available: true
                  - id: 40012345678902
                    title: Large
                    price: '15.00'
                    compare_at_price: '20.00'
                    sku: TSHIRT-L
                    inventory_quantity: 30
                    available: true
                  created_at: '2026-01-10T09:00:00Z'
                  updated_at: '2026-06-01T09:00:00Z'
                pagination:
                  per_page: 25
                  total_count: 128
                  has_more: true
                  next_cursor: eyJsYXN0X2lkIjo3ODkwMTIzNDU2Nzg5fQ==
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '403':
          "$ref": "#/components/responses/InsufficientScope"
        '404':
          "$ref": "#/components/responses/NoConnectedStore"
        '422':
          "$ref": "#/components/responses/ValidationFailed"
        '429':
          "$ref": "#/components/responses/ProductsRateLimited"
        '502':
          "$ref": "#/components/responses/StoreUnavailable"
  "/products/{id}":
    get:
      operationId: getProduct
      summary: Get a product
      description: |
        Retrieve a single product from your connected store's catalog by its
        store product id.

        Includes detail fields not returned by the list endpoint: `description`,
        `tags`, `options`, `image`, `images`, and `media`, plus per-variant
        `barcode`, `inventory_policy`, `position`, `product_id`, `taxable`,
        `image_id`, and `updated_at`.

        Requires a connected Shopify or BigCommerce store — without one, this endpoint returns
        `404 no_connected_store`.

        The response shape is the same on both platforms; detail fields BigCommerce does
        not supply (`media`, per-variant `inventory_policy`, `position`, `taxable`,
        `image_id`, `updated_at`) are `null` or empty.

        **Required scope:** `products_read`
      tags:
      - Products
      security:
      - bearerAuth: []
      parameters:
      - "$ref": "#/components/parameters/AccountIdParam"
      - name: id
        in: path
        required: true
        description: The store's product id (returned as `id` in product responses).
        schema:
          type: integer
          format: int64
        example: 7890123456789
      responses:
        '200':
          description: The product.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    "$ref": "#/components/schemas/ProductDetail"
              example:
                data:
                  id: 7890123456789
                  title: Classic T-Shirt
                  handle: classic-t-shirt
                  status: active
                  product_type: Shirts
                  vendor: Acme Apparel
                  url: https://mystore.com/products/classic-t-shirt
                  image_url: https://cdn.shopify.com/s/files/1/0001/products/tshirt.jpg
                  description: A timeless crew-neck tee in heavyweight cotton.
                  tags:
                  - cotton
                  - summer
                  options:
                  - id: 10012345678901
                    name: Size
                    position: 1
                    values:
                    - Small
                    - Large
                  image:
                    id: 30012345678901
                    url: https://cdn.shopify.com/s/files/1/0001/products/tshirt.jpg
                    alt_text: Front of the classic t-shirt
                    width: 1600
                    height: 1200
                  images:
                  - id: 30012345678901
                    url: https://cdn.shopify.com/s/files/1/0001/products/tshirt.jpg
                    alt_text: Front of the classic t-shirt
                    width: 1600
                    height: 1200
                  media:
                  - id: 20012345678901
                    media_content_type: image
                    alt: Front of the classic t-shirt
                    preview_image_url: https://cdn.shopify.com/s/files/1/0001/products/tshirt.jpg
                  price_range:
                    min: '10.00'
                    max: '15.00'
                    currency_code: USD
                  total_inventory: 42
                  tracks_inventory: true
                  variants_count: 2
                  variants:
                  - id: 40012345678901
                    title: Small
                    price: '10.00'
                    compare_at_price:
                    sku: TSHIRT-S
                    inventory_quantity: 12
                    available: true
                    barcode: '0123456789012'
                    inventory_policy: deny
                    position: 1
                    product_id: 7890123456789
                    taxable: true
                    image_id: 30012345678901
                    updated_at: '2026-06-01T09:00:00Z'
                  created_at: '2026-01-10T09:00:00Z'
                  updated_at: '2026-06-01T09:00:00Z'
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '403':
          "$ref": "#/components/responses/InsufficientScope"
        '404':
          "$ref": "#/components/responses/NotFound"
        '429':
          "$ref": "#/components/responses/ProductsRateLimited"
        '502':
          "$ref": "#/components/responses/StoreUnavailable"
  "/product_collections":
    get:
      operationId: listProductCollections
      summary: List product collections
      description: |
        Retrieve the collections of your connected store, proxied live from the store's API:
        Shopify collections, or BigCommerce categories.

        Use a collection's `id` in a `purchase_history` segment condition's `collection` filter.
        To find the collections one product belongs to, use `/v1/products/{id}/collections`;
        for the products in one collection, use `/v1/product_collections/{id}/products`.

        Uses the same **cursor pagination** as `/v1/products`: pass the `next_cursor` from each
        response as `?cursor=` to fetch the next page, keeping the same filters.
        `total_count` is `null` on Shopify stores with more collections than Shopify will count exactly.

        Requires a connected Shopify or BigCommerce store — without one, this endpoint returns `404 no_connected_store`.

        **Required scope:** `products_read`
      tags:
      - Products
      security:
      - bearerAuth: []
      parameters:
      - "$ref": "#/components/parameters/AccountIdParam"
      - name: per_page
        in: query
        description: Number of results per page, clamped to 1–50.
        schema:
          type: integer
          default: 50
          minimum: 1
          maximum: 50
        example: 50
      - name: title
        in: query
        description: 'Narrow to collections whose title matches, at most 255 characters.
          Matching differs by platform. On Shopify stores, each word of the value
          must start a word of the title: a word is a run of letters and digits, apostrophes
          inside it included (`O''Neill`), and at least one is required. On BigCommerce
          stores, the title must contain the value as typed, where `%` and `_` may
          act as wildcards.'
        schema:
          type: string
        example: summer
      - name: handle
        in: query
        description: 'Narrow to the collection with this exact handle (the slug in
          its storefront URL). Letters, digits, hyphens and underscores only. A Shopify-only
          filter: BigCommerce stores have no collection handle and return `422 validation_failed`.'
        schema:
          type: string
        example: summer-sale
      - name: cursor
        in: query
        description: Opaque pagination cursor from a previous response's `pagination.next_cursor`.
        schema:
          type: string
        example: eyJsYXN0X2lkIjo0MjExMjM0NTY3OH0=
      responses:
        '200':
          description: A page of the store's collections.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/ProductCollection"
                  pagination:
                    "$ref": "#/components/schemas/CursorPagination"
              example:
                data:
                - id: 421123456789
                  handle: summer-collection
                  title: Summer Collection
                - id: 421123456790
                  handle: sale
                  title: Sale
                pagination:
                  per_page: 50
                  total_count: 12
                  has_more: false
                  next_cursor:
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '403':
          "$ref": "#/components/responses/InsufficientScope"
        '404':
          "$ref": "#/components/responses/NoConnectedStore"
        '422':
          "$ref": "#/components/responses/ValidationFailed"
        '429':
          "$ref": "#/components/responses/ProductsRateLimited"
        '502':
          "$ref": "#/components/responses/StoreUnavailable"
  "/product_collections/{id}":
    get:
      operationId: getProductCollection
      summary: Get a product collection
      description: |
        Retrieve one collection (Shopify) or category (BigCommerce) of your connected store
        by its store id, the `id` a `purchase_history` segment condition's `collection` filter holds.

        Requires a connected Shopify or BigCommerce store — without one, this endpoint returns
        `404 no_connected_store`.

        **Required scope:** `products_read`
      tags:
      - Products
      security:
      - bearerAuth: []
      parameters:
      - "$ref": "#/components/parameters/AccountIdParam"
      - name: id
        in: path
        required: true
        description: The store's collection id (returned as `id` in collection responses).
        schema:
          type: integer
          format: int64
        example: 421123456789
      responses:
        '200':
          description: The collection.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    "$ref": "#/components/schemas/ProductCollection"
              example:
                data:
                  id: 421123456789
                  handle: summer-collection
                  title: Summer Collection
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '403':
          "$ref": "#/components/responses/InsufficientScope"
        '404':
          "$ref": "#/components/responses/NotFound"
        '429':
          "$ref": "#/components/responses/ProductsRateLimited"
        '502':
          "$ref": "#/components/responses/StoreUnavailable"
  "/product_collections/{id}/products":
    get:
      operationId: listProductCollectionProducts
      summary: List a collection's products
      description: |
        Retrieve the products in one collection (Shopify) or category (BigCommerce) of your
        connected store, in the same shape as `/v1/products`.

        Uses the same **cursor pagination** as `/v1/products`. `total_count` is `null` on
        Shopify stores when the collection holds more products than Shopify will count exactly.
        The `title` and `handle` filters of `/v1/products` are not available here and return
        `422 validation_failed`.

        Requires a connected Shopify or BigCommerce store — without one, this endpoint returns
        `404 no_connected_store`.

        **Required scope:** `products_read`
      tags:
      - Products
      security:
      - bearerAuth: []
      parameters:
      - "$ref": "#/components/parameters/AccountIdParam"
      - name: id
        in: path
        required: true
        description: The store's collection id (returned as `id` in collection responses).
        schema:
          type: integer
          format: int64
        example: 421123456789
      - name: per_page
        in: query
        description: Number of results per page, clamped to 1–50.
        schema:
          type: integer
          default: 25
          minimum: 1
          maximum: 50
        example: 25
      - name: cursor
        in: query
        description: Opaque pagination cursor from a previous response's `pagination.next_cursor`.
        schema:
          type: string
        example: eyJsYXN0X2lkIjo3ODkwMTIzNDU2Nzg5fQ==
      responses:
        '200':
          description: A page of the collection's products.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/Product"
                  pagination:
                    "$ref": "#/components/schemas/CursorPagination"
              example:
                data:
                - id: 7890123456789
                  title: Classic T-Shirt
                  handle: classic-t-shirt
                  status: active
                  product_type: Shirts
                  vendor: Acme Apparel
                  url: https://mystore.com/products/classic-t-shirt
                  image_url: https://cdn.shopify.com/s/files/1/0001/products/tshirt.jpg
                  price_range:
                    min: '10.00'
                    max: '15.00'
                    currency_code: USD
                  total_inventory: 42
                  tracks_inventory: true
                  variants_count: 2
                  variants:
                  - id: 40012345678901
                    title: Small
                    price: '10.00'
                    compare_at_price:
                    sku: TSHIRT-S
                    inventory_quantity: 12
                    available: true
                  - id: 40012345678902
                    title: Large
                    price: '15.00'
                    compare_at_price: '20.00'
                    sku: TSHIRT-L
                    inventory_quantity: 30
                    available: true
                  created_at: '2026-01-10T09:00:00Z'
                  updated_at: '2026-06-01T09:00:00Z'
                pagination:
                  per_page: 25
                  total_count: 12
                  has_more: false
                  next_cursor:
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '403':
          "$ref": "#/components/responses/InsufficientScope"
        '404':
          "$ref": "#/components/responses/NotFound"
        '422':
          "$ref": "#/components/responses/ValidationFailed"
        '429':
          "$ref": "#/components/responses/ProductsRateLimited"
        '502':
          "$ref": "#/components/responses/StoreUnavailable"
  "/products/{id}/collections":
    get:
      operationId: listProductCollectionsForProduct
      summary: List a product's collections
      description: |
        Retrieve the collections (Shopify) or categories (BigCommerce) one product of your
        connected store belongs to, by its store product id.

        Uses the same **cursor pagination** as `/v1/product_collections`. `total_count` is
        `null` on Shopify stores, which do not report it. The `title` and `handle` filters of
        `/v1/product_collections` are not available here and return `422 validation_failed`.

        Requires a connected Shopify or BigCommerce store — without one, this endpoint returns
        `404 no_connected_store`.

        **Required scope:** `products_read`
      tags:
      - Products
      security:
      - bearerAuth: []
      parameters:
      - "$ref": "#/components/parameters/AccountIdParam"
      - name: id
        in: path
        required: true
        description: The store's product id (returned as `id` in product responses).
        schema:
          type: integer
          format: int64
        example: 7890123456789
      - name: per_page
        in: query
        description: Number of results per page, clamped to 1–50.
        schema:
          type: integer
          default: 50
          minimum: 1
          maximum: 50
        example: 50
      - name: cursor
        in: query
        description: Opaque pagination cursor from a previous response's `pagination.next_cursor`.
        schema:
          type: string
        example: eyJsYXN0X2lkIjo0MjExMjM0NTY3OH0=
      responses:
        '200':
          description: A page of the product's collections.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      "$ref": "#/components/schemas/ProductCollection"
                  pagination:
                    "$ref": "#/components/schemas/CursorPagination"
              example:
                data:
                - id: 421123456789
                  handle: summer-collection
                  title: Summer Collection
                pagination:
                  per_page: 50
                  total_count:
                  has_more: false
                  next_cursor:
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '403':
          "$ref": "#/components/responses/InsufficientScope"
        '404':
          "$ref": "#/components/responses/NotFound"
        '422':
          "$ref": "#/components/responses/ValidationFailed"
        '429':
          "$ref": "#/components/responses/ProductsRateLimited"
        '502':
          "$ref": "#/components/responses/StoreUnavailable"
  "/orders/placed":
    post:
      operationId: placeOrder
      summary: Order placed
      description: |
        Report an order placed in your store.

        This is an **idempotent upsert** keyed on `order_id`:
        the first call for an `order_id` creates the order and returns `201 Created`;
        subsequent calls with the same `order_id` update it and return `200 OK`.
        The order carries the full current state each time —
        omitted optional fields keep their previously stored value.

        The order is associated to a contact by `email` (or, when no email is sent, by `phone`),
        creating the contact if needed.

        `accepts_email_marketing` and  `accepts_sms_marketing`  drive the contact's marketing consent —
        see the field descriptions for the exact semantics.

        Optionally send the `X-Privy-Integration-Token` header to tie the order to a specific
        Custom Integration (see the header parameter below).

        **Required scope:** `orders_write`
      tags:
      - Orders
      security:
      - bearerAuth: []
      parameters:
      - "$ref": "#/components/parameters/AccountIdParam"
      - name: X-Privy-Integration-Token
        in: header
        required: false
        description: |
          Optional. The integration token of a Custom Integration, sent **in addition to**
          the `Authorization: Bearer` token, to tie the order to that specific
          integration. Find it in your Privy dashboard under
          **Settings → Integrations Hub → Connected**, or generate a new one in the
          same **Integrations Hub** section under **Custom Integration**.

          When omitted and the business has exactly one Custom Integration, the order
          is auto-tied to it; otherwise no integration is attributed. If the value
          doesn't match an active Custom Integration for your business, the request
          returns `422`. This token only narrows attribution within the business
          already established by the bearer token — it is not a standalone credential.
        schema:
          type: string
        example: prv_a1b2c3d4e5f6g7h8
      requestBody:
        required: true
        content:
          application/json:
            schema:
              "$ref": "#/components/schemas/PlaceOrderRequest"
            example:
              order_id: 1001
              email: buyer@example.com
              phone: "+12025550123"
              customer_id: ext-42
              subtotal: 90.0
              discounts:
              - code: SAVE10
                amount: 10.0
              tax_lines:
              - title: VAT
                amount: 5.0
              total: 95.0
              currency: USD
              total_items: 2
              financial_status: paid
              fulfillment_status: fulfilled
              shipment_status: delivered
              line_items:
              - sku: SKU-1
                quantity: 2
                price: 45.0
              billing_address:
                first_name: Janet
                last_name: Biller
                city: Boston
                state_code: MA
                country_code: US
                postal_code: '02118'
              shipping_address:
                first_name: Jane
                last_name: Shopper
                city: Boston
                state_code: MA
                country_code: US
                postal_code: '02118'
              accepts_email_marketing: true
              accepts_sms_marketing: true
              order_date: '2026-06-01T12:00:00Z'
      responses:
        '200':
          description: Existing order updated.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    "$ref": "#/components/schemas/Order"
              example:
                data:
                  order_id: 1001
                  source: api
                  email: buyer@example.com
                  phone: "+12025550123"
                  total_amount: '95.00'
                  currency_code: USD
                  placed_at: '2026-06-01T12:00:00Z'
                  financial_status: paid
                  fulfillment_status: fulfilled
                  shipment_status: delivered
                  customer_id: ext-42
                  total_items: 2
                  accepts_email_marketing: true
                  accepts_sms_marketing: true
                  created_at: '2026-06-01T12:00:05Z'
                  updated_at: '2026-06-02T09:14:00Z'
        '201':
          description: Order created.
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    "$ref": "#/components/schemas/Order"
              example:
                data:
                  order_id: 1001
                  source: api
                  email: buyer@example.com
                  phone: "+12025550123"
                  total_amount: '95.00'
                  currency_code: USD
                  placed_at: '2026-06-01T12:00:00Z'
                  financial_status: paid
                  fulfillment_status: fulfilled
                  shipment_status: delivered
                  customer_id: ext-42
                  total_items: 2
                  accepts_email_marketing: true
                  accepts_sms_marketing: true
                  integration:
                    store_name: My Store
                    store_url: https://mystore.example.com
                  created_at: '2026-06-01T12:00:05Z'
                  updated_at: '2026-06-01T12:00:05Z'
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '403':
          "$ref": "#/components/responses/InsufficientScope"
        '422':
          "$ref": "#/components/responses/ValidationFailed"
        '429':
          "$ref": "#/components/responses/RateLimited"
  "/events":
    post:
      operationId: ingestEvent
      summary: Ingest an event
      description: |
        Send a custom event to Privy. Use events to trigger **Flows** and other
        automations from activity in your own systems (a completed purchase, a
        finished quiz, a loyalty milestone, etc.).

        Identify the contact with **exactly one** of `email`, `phone`, or `privy_id`.
        If the contact doesn't exist yet, Privy creates one (by email or phone);
        `privy_id` must reference an existing contact.

        ## Response status

        Unlike most endpoints, a successful ingest returns **`200 OK`** with a
        `status` field rather than `201`. A payload that fails validation returns
        **`400 Bad Request`** with a top-level `errors` array — a different shape
        from the standard `error` envelope used for auth and rate-limit errors.

        ## Idempotency

        Set the `Idempotency-Key` header (or an `idempotency_key` body field; the
        header wins if both are present) to safely retry. The first request ingests
        the event; a retry with the same key returns the original response with
        `idempotent_replay: true` instead of ingesting a duplicate. Only accepted
        events are replayed — a rejected payload re-validates on retry.

        ## Limits

        The request body must not exceed **256 KB**; larger bodies return `413`.

        **Required scope:** `events_write`
      tags:
      - Events
      security:
      - bearerAuth: []
      parameters:
      - "$ref": "#/components/parameters/AccountIdParam"
      - name: Idempotency-Key
        in: header
        required: false
        description: |
          Optional key to safely retry without creating a duplicate event. Use a
          unique value (such as a UUID) per distinct event. Takes precedence over
          an `idempotency_key` body field.
        schema:
          type: string
        example: 550e8400-e29b-41d4-a716-446655440000
      requestBody:
        required: true
        content:
          application/json:
            schema:
              "$ref": "#/components/schemas/IngestEventRequest"
            example:
              email: shopper@example.com
              event_type: purchase_completed
              event_time: '2026-06-15T10:30:00Z'
              event_data:
                order_id: ORD-12345
                total: 99.99
                currency: USD
      responses:
        '200':
          description: |
            The event was accepted, or replayed from a prior request with the same
            idempotency key.
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/EventAccepted"
              examples:
                accepted:
                  summary: Event accepted
                  value:
                    event_id: 507f1f77bcf86cd799439011
                    status: accepted
                idempotent_replay:
                  summary: Idempotent replay
                  value:
                    event_id: 507f1f77bcf86cd799439011
                    status: accepted
                    idempotent_replay: true
        '400':
          description: |
            The payload failed validation and was not ingested. Problems are listed
            in the top-level `errors` array (not the standard `error` envelope).
          content:
            application/json:
              schema:
                "$ref": "#/components/schemas/EventRejected"
              example:
                event_id: 507f1f77bcf86cd799439012
                status: rejected
                errors:
                - field: identifier
                  message: email, phone, or privy_id is required
                - field: event_type
                  message: is required
        '401':
          "$ref": "#/components/responses/Unauthorized"
        '403':
          "$ref": "#/components/responses/InsufficientScope"
        '413':
          "$ref": "#/components/responses/PayloadTooLarge"
        '429':
          "$ref": "#/components/responses/RateLimited"
