---
title: "Get a flow"
method: GET
path: "/flows/{id}"
tags: ["Flows"]
---

# Get a flow

`GET /flows/{id}`

## Request

- Base URL: `https://api.privy.com/v1`
- URL: `https://api.privy.com/v1/flows/{id}`
- Auth: HTTP bearer

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`

## Path parameters

- `id` string, required

## Headers

- `Privy-Account-Id` string

## Response `200`

The flow.

- object
  - `data` FlowDetail
    - `id` string: Stable, unique identifier for the flow. Use this value in `/v1/flows/{id}`.
    - `title` string, nullable
    - `status` 'draft' | 'active' | 'inactive'
    - `trigger_type` string, nullable: 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.
    - `trigger_label` string, nullable: Human-readable form of `trigger_type`.
    - `nodes` object[]: The flow's steps from its current version, in tree-traversal order, with the branch graph and each step's configuration.
      - `uuid` string
      - `type` 'email' | 'sms' | 'delay' | 'split' | 'ab_test' | 'tag' | 'update_contact'
      - `name` string
      - `parent_uuid` string, nullable: `uuid` of the node's parent; `null` on the flow's first node.
      - `next_uuid` string, nullable: `uuid` of the next node. Present on every node type except `split` and `ab_test`; `null` at the end of a branch.
      - `yes_next_uuid` string, nullable: `split` nodes only — `uuid` of the first node on the branch taken when the conditions match.
      - `no_next_uuid` string, nullable: `split` nodes only — `uuid` of the first node on the branch taken when the conditions do not match.
      - `a_next_uuid` string, nullable: `ab_test` nodes only — `uuid` of the first node on path A.
      - `b_next_uuid` string, nullable: `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.
      - `data` object: 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`.
        - `conditions` ConditionTree: 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. The one row whose `type` follows the published value is an activity filter's `campaign_id`, which reads `"string"` here whether it holds a public id or `"any"`, since that is the form `POST /v1/segments` takes back.
          - `operation` 'AND' | 'OR': How this group's conditions combine — `AND` requires every condition to match, `OR` any one of them.
          - `conditions` object[]: 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.
            - `property` string: 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.
            - `operator` string: How `value` is compared against the property (e.g. `equals`, `contains`, `greater_than`).
            - `type` string: 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.
            - `value` unknown
    - `sync_coupons` boolean: 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.
    - `last_transition_at` string, date-time, nullable: When the flow last changed status.
    - `reentry` object: 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.
      - `mode` 'immediate' | 'never' | 'after_delay'
      - `frequency` integer: Present only when `mode` is `after_delay`.
      - `unit` 'minutes' | 'hours' | 'days': Present only when `mode` is `after_delay`.
    - `flow_filters` ConditionTree: 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. The one row whose `type` follows the published value is an activity filter's `campaign_id`, which reads `"string"` here whether it holds a public id or `"any"`, since that is the form `POST /v1/segments` takes back.
      - `operation` 'AND' | 'OR': How this group's conditions combine — `AND` requires every condition to match, `OR` any one of them.
      - `conditions` object[]: 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.
        - `property` string: 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.
        - `operator` string: How `value` is compared against the property (e.g. `equals`, `contains`, `greater_than`).
        - `type` string: 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.
        - `value` unknown
    - `exit_behavior` object, nullable: 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.
      - `exit_events` string[]: 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.
      - `applies_to` 'active_journeys_only': Exits only end journeys already in progress — they never block entry.
      - `enrollment_event_excluded` true: The event that enrolled the contact never triggers the exit.
      - `summary` string: Server-generated restatement of this exit behavior.
    - `behavior_summary` string: 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.

## Other responses

- `401`: Missing or invalid bearer token.
- `403`: `insufficient_scope` when the token lacks the required scope for this endpoint.
- `404`: The requested resource does not exist or does not belong to your account.
- `429`: Rate limit exceeded. Retry after the specified time.

## Changes

- **2026-11-05** `cf0885e90d5a`: 8 warning
  - added the new `forbidden` enum value to the `error/code` response property for the response status `401`
  - added the new `forbidden` enum value to the `error/code` response property for the response status `403`
  - added the new `forbidden` enum value to the `error/code` response property for the response status `404`
  - added the new `forbidden` enum value to the `error/code` response property for the response status `429`
  - …4 more
- **2026-11-03** `fe500ab59b3b`: 1 info
  - endpoint added

[Operation history](https://skmtc.dev/privy/apis/privy-api/changes/flows/:id/get.md)

---

[API](https://skmtc.dev/privy/apis/privy-api.md) - [All operations](https://skmtc.dev/privy/apis/privy-api/llms.txt) - [OpenAPI document](https://skmtc.dev/privy/apis/privy-api/revisions/bc29b48dcdd2?raw)
