> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Segments API: build and export contact audiences

> Build and export audiences from contact filters — saved segment CRUD, ad-hoc CSV export, AI auto-suggested segments, and link-click cohorts.

# Segments API

A segment is a saved audience definition — a filter over your contacts (attributes, custom fields, engagement history, loyalty traits) that campaigns, flows, and personalization can target. The saved-segment CRUD surface lives under the Contacts API; this page covers the top-level segment endpoints that operate on audiences without requiring you to save one first, plus AI-assisted segment discovery.

**Base path:** `/api/v1/segments` (ad-hoc + AI endpoints below) and `/api/v1/contacts/segments` (saved-segment CRUD, membership, and its own `GET /{id}/export.csv` — see the [Contacts API](/api-reference/endpoints/contacts)).

**Authentication:** API key (`X-API-Key`) or session JWT.

| Method | Path                            | Scope            | Purpose                                                     |
| ------ | ------------------------------- | ---------------- | ----------------------------------------------------------- |
| `POST` | `/api/v1/segments/auto-suggest` | `contacts:write` | AI-proposed high-value segments from your org's own data    |
| `POST` | `/api/v1/segments/export.csv`   | `contacts:write` | Export an ad-hoc (unsaved) audience as CSV                  |
| `POST` | `/api/v1/segments/from-clicks`  | `contacts:write` | Turn short-link clickers into a saved, retargetable segment |

## AI auto-suggest

Unlike the description-driven segment builder (`POST /contacts/segments/autopilot`, which turns a sentence like "everyone who clicked the pricing page but didn't book a demo" into a filter), auto-suggest takes **no input** — it reads your org's own CDP event volume (top event names, contact tags, engagement histogram) and proposes up to 5 segments you probably haven't created yet, each with a name, a one-line pitch, a rationale, and a ready-to-use filter. Nothing is persisted — the operator picks a suggestion to pre-fill the create-segment dialog.

## Ad-hoc CSV export

`GET /contacts/segments/{id}/export.csv` already streams a **saved** segment's membership as CSV. `POST /segments/export.csv` covers the case where you haven't saved a segment yet — you have a filter (the same filter shape the segment builder and auto-suggest use) and just want the matching contacts as a one-off download, without persisting a segment or waiting for membership to materialize.

Hard-capped at 50,000 rows per call (the optional `limit` in the body can set a lower cap). When the result set hits the cap, the response carries an `X-Export-Truncated: true` header so you know the file is partial — save the audience as a segment and use the paginated saved-segment export for a larger or recurring pull.

## Clicked-link cohorts

`POST /segments/from-clicks` closes the loop from link engagement to audience: given a tracked short link (`link_id`) or every link in a campaign (`campaign_id`), it resolves the distinct contacts who clicked and materializes them as a static segment — no filter rules, just a point-in-time snapshot of "people who clicked this." Useful for a targeted re-engagement send to exactly the people who showed interest.

Because click activity is a point-in-time event rather than a live trait, the resulting segment does not auto-refresh — generate a fresh cohort to capture a later window. The request body must contain at least one of `link_id` or `campaign_id`, and accepts an optional `window_days` (1–365), `name`, and `description`.

## Examples

### POST /segments/auto-suggest

Request (all fields are optional — `limit` defaults to 5 and is clamped to 8):

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/segments/auto-suggest \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "limit": 5, "focus_hint": "focus on retention" }'
```

Each returned suggestion carries `name`, `description` (a one-line, operator-facing pitch), `rationale` (why it is high-value for your data), and `filters` (a filter tree you can persist as-is). Every suggestion is validated against your contacts before the response — suggestions that would match nothing are dropped, and `llm_dropped_count` tells you how many were filtered out.

```json theme={null}
{
  "data": {
    "suggestions": [
      {
        "name": "Recently Engaged Buyers",
        "description": "Customers who interacted with any channel in the last 30 days.",
        "rationale": "Your top event volume is purchase-heavy, and contacts with recent message activity convert better on re-engagement sends.",
        "filters": {
          "op": "AND",
          "conditions": [
            { "field": "lifecycle_stage", "op": "equals", "value": "customer" },
            { "field": "last_message_at", "op": "in_last", "value": "30d" }
          ]
        },
        "preview_match_count": 4321
      },
      {
        "name": "Lapsed Email Openers",
        "description": "Contacts who have opened an email before, but not in the last 30 days.",
        "rationale": "A win-back audience: they historically engaged over email, so a re-open campaign on the same channel has a fair shot.",
        "filters": {
          "op": "AND",
          "conditions": [
            { "field": "last_opened_email", "op": "is_set" },
            { "field": "last_opened_email", "op": "not_in_last", "value": "30d" }
          ]
        },
        "preview_match_count": 1187
      }
    ],
    "org_summary": {
      "total_events_30d": 128402,
      "top_event_names": [
        { "event_name": "page_view", "count": 42211 },
        { "event_name": "purchase_completed", "count": 9104 }
      ],
      "top_segment_labels": [
        { "segment_label": "champion", "count": 214 },
        { "segment_label": "engaged", "count": 1860 }
      ],
      "top_tags": [
        { "tag": "vip", "count": 402 },
        { "tag": "trial", "count": 1337 }
      ],
      "total_contacts": 15640
    },
    "llm_output_preview": "{\"suggestions\":[{\"name\":\"Recently Engaged Buyers\",...",
    "llm_dropped_count": 0
  },
  "meta": {
    "request_id": "req_9f8e7d6c5b4a3210",
    "timestamp": "2026-08-25T09:41:12.083Z"
  }
}
```

If the org has no usable CDP signal yet (zero contacts or zero events in the last 30 days), the endpoint skips the AI round-trip and responds with an empty list plus a hint:

```json theme={null}
{
  "data": {
    "suggestions": [],
    "org_summary": { "total_events_30d": 0, "top_event_names": [], "top_segment_labels": [], "top_tags": [], "total_contacts": 0 },
    "llm_output_preview": "",
    "llm_dropped_count": 0,
    "note": "Not enough CDP signal yet — emit some events via the /events/track or /cdp/v1/:ingest_id/track endpoints to seed suggestions."
  },
  "meta": { "request_id": "req_9f8e7d6c5b4a3211", "timestamp": "2026-08-25T09:41:12.083Z" }
}
```

Other failure modes are explicit error envelopes — `422 AI_PARSE_FAILED` when the model output is not parseable, `502 AI_SUGGEST_FAILED` when the suggestion call itself fails, and `503 SERVICE_UNAVAILABLE` when auto-suggest is not enabled on the deployment. Retry on 502; on 422 and 503, fall back to the manual segment builder.

### POST /segments/export.csv

Request — any filter tree the segment builder accepts, plus an optional per-call row cap and download filename:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/segments/export.csv \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "filters": {
      "op": "AND",
      "conditions": [
        { "field": "lifecycle_stage", "op": "equals", "value": "customer" },
        { "field": "email_opt_in", "op": "equals", "value": true },
        { "field": "country_code", "op": "in", "value": ["US", "CA"] }
      ]
    },
    "limit": 5000,
    "filename": "na-customers-opted-in"
  }'
```

A 200 response is the CSV itself (`Content-Type: text/csv`, `Content-Disposition: attachment; filename="na-customers-opted-in-2026-08-25.csv"`). Phone and email columns honour the caller's role and reveal-context exactly as the dashboard does — a caller without reveal permission receives masked values.

```csv theme={null}
contact_id,first_name,last_name,email,phone,country_code,lifecycle_stage,tags,created_at
cnt_8f3a2b1c4d5e6f708192a3b4c5d6e7f8,Ada,Lovelace,ada@example.com,+14155550123,US,customer,"vip,newsletter",2026-03-14T08:22:10.411Z
cnt_1a2b3c4d5e6f708192a3b4c5d6e7f809,Grace,Hopper,grace@example.com,+14155550124,CA,customer,"newsletter",2026-04-02T17:03:44.902Z
cnt_5e6f708192a3b4c5d6e7f8091a2b3c4d,Alan,Turing,alan@example.com,,US,customer,,2026-05-21T11:47:01.265Z
```

An invalid filter (a non-allowlisted field, a mismatched operator, or a body that fails shape validation) returns a 400 envelope:

```json theme={null}
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Unknown segmentation field \"lifecycle\". Use one of the allowlisted fields or a 'custom_attributes.<key>' path.",
    "status": 400
  },
  "meta": { "request_id": "req_9f8e7d6c5b4a3212", "timestamp": "2026-08-25T09:41:12.083Z" }
}
```

Two rate limits apply to this endpoint: a heavy export is capped at 5 requests per minute per tenant, and a generic per-key limit sits on top of that. On 429, wait for the window and retry with backoff; for pulls that routinely exceed the 50,000-row hard cap or the rate budget, persist the audience as a segment and page through `GET /contacts/segments/{id}/export.csv` instead.

```json theme={null}
{
  "error": {
    "code": "RATE_LIMITED",
    "message": "Too many requests, retry later.",
    "status": 429
  },
  "meta": { "request_id": "req_9f8e7d6c5b4a3213", "timestamp": "2026-08-25T09:41:12.083Z" }
}
```

### POST /segments/from-clicks

Request — scope the cohort by `link_id` or `campaign_id`, optionally narrow to a recency window and name the segment:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/segments/from-clicks \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "campaign_id": "cmp_a1b2c3d4e5f6a7b8", "window_days": 30, "name": "Clicked Q3 promo" }'
```

On success the response is a 201 with the persisted segment and its materialized membership count:

```json theme={null}
{
  "data": {
    "segment": {
      "id": "seg_9b8a7f6e5d4c3b2a01928374a5b6c7d8",
      "name": "Clicked Q3 promo",
      "description": null,
      "auto_refresh": false
    },
    "member_count": 1842
  },
  "meta": { "request_id": "req_9f8e7d6c5b4a3214", "timestamp": "2026-08-25T09:41:12.083Z" }
}
```

Because the segment row now exists, every saved-segment surface applies to it — for example, to pull the cohort as CSV:

```bash theme={null}
curl -X GET "https://api.orbit.devotel.io/api/v1/contacts/segments/seg_9b8a7f6e5d4c3b2a01928374a5b6c7d8/export.csv?limit=500" \
  -H "X-API-Key: dv_live_sk_your_key_here"
```

If no contacts have clicked the link(s) yet — or clicks exist but none are message-attributed back to a recipient contact — the endpoint returns a 422 instead of creating an empty segment:

```json theme={null}
{
  "error": {
    "code": "EMPTY_COHORT",
    "message": "No contacts have clicked the requested link(s) yet, so there is no cohort to materialise. Clicks must be message-attributed (the link was sent in a message bound to a contact) to resolve back to a recipient.",
    "status": 422
  },
  "meta": { "request_id": "req_9f8e7d6c5b4a3215", "timestamp": "2026-08-25T09:41:12.083Z" }
}
```

## Recipe: auto-suggest → saved segment → campaign target

The full loop, end to end:

1. **Get suggestions.** `POST /segments/auto-suggest` and read `data.suggestions`. Each suggestion's `filters` is a ready-to-persist filter definition.
2. **Pick one and persist it.** Pass the suggestion's `filters` to the saved-segment create endpoint with a name — membership materializes immediately on create, so the segment is usable without waiting for the refresh cycle:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/contacts/segments \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Recently Engaged Buyers",
    "description": "Customers active in the last 30 days.",
    "filters": {
      "op": "AND",
      "conditions": [
        { "field": "lifecycle_stage", "op": "equals", "value": "customer" },
        { "field": "last_message_at", "op": "in_last", "value": "30d" }
      ]
    }
  }'
```

3. **Use the returned segment id.** Target it from a campaign audience, a flow trigger, or the personalization rule engine — anywhere a `segment_id` is accepted. A saved segment auto-refreshes (unless you created it from a click cohort, which is a fixed snapshot), so step 2 is done once and steps 1–3 can be repeated whenever you want fresh suggestions.

To sanity-check membership before committing the segment to a campaign, post the same `filters` body to `POST /segments/export.csv` and download the matching rows — that one-off export never persists anything.

## SDK note

The Node SDK covers saved-segment CRUD (`orbit.contacts.segments.list() / .create() / .get()` over `/contacts/segments`). The three ad-hoc endpoints on this page — auto-suggest, ad-hoc export, and click cohorts — are invoked directly over HTTP as shown above; parse the CSV responses yourself or save the audience as a segment and use the SDK's saved-segment surface.

## See also

* [Contacts API](/api-reference/endpoints/contacts) — saved-segment CRUD, membership, and paginated export
* [Loyalty API](/api-reference/loyalty) — loyalty traits usable as segment filter conditions
* [Links API](/api-reference/links) — the short links `from-clicks` resolves against
