# Customer segments

Behavioral audiences computed **live** from your ticket history — who buys
repeatedly, who shows up, who doesn't. Segments are computed on every request
and nothing is stored: membership can change between calls, and the platform
never contacts anyone — the audience is yours to message through your own
channels.

| Segment | Who's in it |
| --- | --- |
| `repeat_buyers` | Customers with at least `min_purchases` non-refunded tickets (default 2). Revoked tickets still count as purchases. |
| `attendees` | Customers checked in at at least `min_events` distinct events (default 1). |
| `no_shows` | Customers holding at least one never-checked-in, non-refunded, non-revoked ticket for a **past** event. |

"Past" is decided by the event's own date — events with no date recorded never
produce no-shows. A refunded ticket never counts anywhere.

---

## GET /v1/customers/segments/{kind}

List one segment, newest customer first, cursor-paginated.

**Scope:** `customers:write`

### Path params

| Param | Type | Required | Description |
| --- | --- | --- | --- |
| `kind` | string | Yes | `repeat_buyers`, `attendees`, or `no_shows`. Anything else returns `400 parameter_invalid`. |

### Query params

| Param | Type | Required | Description |
| --- | --- | --- | --- |
| `min_purchases` | integer | No | `repeat_buyers` only: minimum non-refunded tickets, `>= 1` (default `2`). Silently ignored on other kinds. |
| `min_events` | integer | No | `attendees` only: minimum distinct attended events, `>= 1` (default `1`). Silently ignored on other kinds. |
| `event_id` | string | No | Scope the segment to one event. An unknown `event_id` isn't an error — it yields an empty list. |
| `limit` | integer | No | Page size, `1`–`100` (default `20`). |
| `starting_after` | string | No | Cursor — pass the last `customer_id` you saw on the previous page. |

### Example request

```bash
curl "https://api.ticketconnect.example/v1/customers/segments/repeat_buyers?min_purchases=3&limit=2" \
  -H "Authorization: Bearer sk_test_your_key_here"
```

### Example response

```json
{
  "object": "list",
  "data": [
    {
      "customer_id": "cus_ext_42",
      "email": "dana@example.com",
      "purchase_count": 5,
      "last_activity_at": "2026-07-02T21:14:00.000Z"
    },
    {
      "customer_id": "cus_ext_17",
      "email": "sam@example.com",
      "purchase_count": 3,
      "last_activity_at": "2026-06-28T19:40:00.000Z"
    }
  ],
  "has_more": true
}
```

Each row carries exactly: `customer_id`, `email`, the one kind-relevant count
(`purchase_count` | `attended_event_count` | `no_show_count`), and
`last_activity_at` (ISO datetime or `null`). Customers with no qualifying
tickets simply never appear — there is no zero-count row.

> Errors: `400 parameter_invalid` — unknown `kind`, a non-positive threshold,
> or an unknown `starting_after` cursor (`param` names the offender).

---

## Saved segments

The endpoint above computes an audience **on the fly** and forgets it. When you
want to name an audience, keep it, and reuse it — re-evaluate it later, or wire
it into a [discount trigger](#api-discount-triggers) — save it with
`/v1/segments`.

The distinction:

| | `GET /v1/customers/segments/{kind}` | `/v1/segments` |
| --- | --- | --- |
| What it is | A one-off computation | A persisted, named definition |
| Stored? | No — nothing is kept | Yes — `name` + `kind` + threshold + optional `event_id` |
| Reusable? | Re-pass the params each time | Reference by `id`; re-evaluate any time |
| Wires into triggers? | No | Yes — link it to a discount code |

Both run through the **same audience engine**, so a saved segment's members
always agree with the equivalent on-the-fly computation. Saved-segment
membership is still evaluated **fresh** on every read — the definition is
stored, the members are not.

All saved-segment endpoints require the `customers:write` scope.

A serialized saved segment:

```json
{
  "id": "seg_1f2e3d4c5b6a",
  "name": "Loyal fans",
  "description": "Bought 3 or more times",
  "kind": "repeat_buyers",
  "min_count": 3,
  "event_id": "evt_1a2b3c",
  "created_at": "2026-07-11T09:00:00.000Z"
}
```

`description` and `event_id` are only present when set.

### POST /v1/segments

Save a named audience definition.

**Scope:** `customers:write`

#### Request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | Yes | 1–80 characters, unique per account. A duplicate returns `409 segment_name_taken`. |
| `kind` | string | Yes | `repeat_buyers`, `attendees`, or `no_shows`. Anything else returns `400 parameter_invalid`. |
| `min_count` | integer | No | Threshold, `>= 1`. Defaults to `2` for `repeat_buyers`, `1` otherwise. |
| `event_id` | string | No | Scope the segment to a single event. |
| `description` | string | No | Free-text note for your own reference. |

#### Example request

```bash
curl https://api.ticketconnect.example/v1/segments \
  -H "Authorization: Bearer sk_test_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Loyal fans",
    "kind": "repeat_buyers",
    "min_count": 3,
    "description": "Bought 3 or more times"
  }'
```

#### Example response

`201 Created` — returns the serialized saved segment shown above.

> Errors: `400 parameter_invalid` — bad `name`, `kind`, or `min_count` (`param`
> names the offender). `409 segment_name_taken` — a segment with that `name`
> already exists for your account.

### GET /v1/segments

List your saved segments, newest first, cursor-paginated.

**Scope:** `customers:write`

#### Query params

| Param | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | integer | No | Page size, `1`–`100` (default `20`). |
| `starting_after` | string | No | Cursor — the last segment `id` you saw. |

#### Example request

```bash
curl https://api.ticketconnect.example/v1/segments \
  -H "Authorization: Bearer sk_test_your_key_here"
```

#### Example response

```json
{
  "object": "list",
  "has_more": false,
  "data": [
    { "id": "seg_1f2e3d4c5b6a", "name": "Loyal fans", "description": "Bought 3 or more times", "kind": "repeat_buyers", "min_count": 3, "created_at": "2026-07-11T09:00:00.000Z" }
  ]
}
```

### GET /v1/segments/{id}

Retrieve one saved segment definition.

**Scope:** `customers:write`

#### Path params

| Param | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | Yes | Segment id. Unknown id returns `404 resource_missing`. |

#### Example request

```bash
curl https://api.ticketconnect.example/v1/segments/seg_1f2e3d4c5b6a \
  -H "Authorization: Bearer sk_test_your_key_here"
```

### DELETE /v1/segments/{id}

Delete a saved segment definition. Any discount trigger still pointing at it
simply stops matching anyone (its `matches` list goes empty).

**Scope:** `customers:write`

#### Path params

| Param | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | Yes | Segment id. Unknown id returns `404 resource_missing`. |

#### Example request

```bash
curl https://api.ticketconnect.example/v1/segments/seg_1f2e3d4c5b6a \
  -H "Authorization: Bearer sk_test_your_key_here" \
  -X DELETE
```

#### Example response

```json
{ "id": "seg_1f2e3d4c5b6a", "deleted": true }
```

### GET /v1/segments/{id}/members

Evaluate the saved segment now and return its qualifying customers,
cursor-paginated. Membership is computed fresh — nothing is cached — so the list
always reflects your current ticket history.

**Scope:** `customers:write`

#### Path params

| Param | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | Yes | Segment id. Unknown id returns `404 resource_missing`. |

#### Query params

| Param | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | integer | No | Page size, `1`–`100` (default `20`). |
| `starting_after` | string | No | Cursor — the last `customer_id` you saw. |

#### Example request

```bash
curl "https://api.ticketconnect.example/v1/segments/seg_1f2e3d4c5b6a/members?limit=2" \
  -H "Authorization: Bearer sk_test_your_key_here"
```

#### Example response

Rows have the same shape as the on-the-fly segment above — `customer_id`,
`email`, the one kind-relevant count, and `last_activity_at`:

```json
{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "customer_id": "cus_ext_42",
      "email": "dana@example.com",
      "purchase_count": 5,
      "last_activity_at": "2026-07-02T21:14:00.000Z"
    }
  ]
}
```
