# Segments

An audience. A segment is a filter over your contacts, a list of contacts added by hand, or both — and it is what a broadcast is sent to.

## The filter

A filter is a tree of `all`, `any` and `not` groups over conditions. A condition is either `{ field, op, value }` or `{ topic, subscription }`. Groups nest three deep and a filter holds at most twenty conditions.

A filter:

```text
{
  "any": [
    {
      "all": [
        {
          "field": "properties.plan",
          "op": "eq",
          "value": "trial"
        },
        {
          "field": "created_at",
          "op": "gte",
          "value": "2026-09-01T00:00:00Z"
        }
      ]
    },
    {
      "topic": "b6d24b8e-af0b-4c3c-be0c-359bbd97381e",
      "subscription": "opt_in"
    }
  ]
}
```

| Field | Type | Operators |
| --- | --- | --- |
| `email` | string | `eq`, `neq`, `contains`, `in` |
| `first_name` | string | `eq`, `neq`, `contains`, `exists`, `in` |
| `last_name` | string | `eq`, `neq`, `contains`, `exists`, `in` |
| `unsubscribed` | boolean | `eq` |
| `created_at` | date | `gt`, `gte`, `lt`, `lte` |
| `properties.<key>` | the property's type | Strings as `first_name`; numbers `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `in`, `exists` |

- The filter is checked when it is saved: a property that is not declared, or an operator that does not suit its type, is `422 validation_error` with one entry in `errors[]` per condition.
- A filter can be changed with `PATCH`, and is checked the same way. The change applies from then on, scheduled broadcasts included; it is refused while a broadcast to the segment is queued or sending, so one send never reaches two audiences.

## Membership

A contact is in a segment if the filter matches it *or* it was added explicitly, unless it was removed from the segment by hand: a removed contact stays out while the filter matches it, until it is added back. Membership is evaluated when it is read and when a broadcast is sent — never stored — so a contact who starts matching is in from that moment, and one who stops matching is out, unless they were added by hand. Deleted contacts are never members.

## Endpoints

### `POST /segments`

Name an audience, optionally defined by a filter over your contacts.

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `name` (required) | string | Up to 200 characters, unique among live segments. |
| `filter` | object \| null | `{ all \| any \| not: [ … ] }` over `{ field, op, value }` and `{ topic, subscription }` conditions. Omit it for a segment with explicit membership only. |

Create a segment:

```sh
curl -X POST "https://api.rasket.com/segments" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Trial accounts",
  "filter": {
    "all": [
      {
        "field": "properties.plan",
        "op": "eq",
        "value": "trial"
      },
      {
        "field": "created_at",
        "op": "gte",
        "value": "2026-09-01T00:00:00Z"
      }
    ]
  }
}'
```

```ts
const response = await fetch("https://api.rasket.com/segments", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name: "Trial accounts",
    filter: {
      all: [
        {
          field: "properties.plan",
          op: "eq",
          value: "trial"
        },
        {
          field: "created_at",
          op: "gte",
          value: "2026-09-01T00:00:00Z"
        }
      ]
    }
  }),
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/segments",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "name": "Trial accounts",
    "filter": {
      "all": [
        {
          "field": "properties.plan",
          "op": "eq",
          "value": "trial"
        },
        {
          "field": "created_at",
          "op": "gte",
          "value": "2026-09-01T00:00:00Z"
        }
      ]
    }
  },
)

id = response.json()["id"]
```

#### Response `201`

```json
{
  "object": "segment",
  "id": "78261eea-8f8b-4381-83c6-79fa7120f1cf"
}
```

- The filter is checked against your contact properties when the segment is created: a property that does not exist yet, or an operator that does not suit its type, is `422 validation_error` with one `errors[]` entry per condition.
- Membership is evaluated when it is read and when a broadcast is sent, never stored — a contact that starts matching is in the segment from that moment.

### `GET /segments`

Every live segment, newest first.

#### Query parameters

| Field | Type | Description |
| --- | --- | --- |
| `limit` | integer | How many items to return, 1–100. Defaults to 20. |
| `after` | string | Return the page that follows this item ID. Mutually exclusive with `before`. |
| `before` | string | Return the page that precedes this item ID. Mutually exclusive with `after`. |

List segments:

```sh
curl -X GET "https://api.rasket.com/segments" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/segments", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const data = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/segments",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "id": "78261eea-8f8b-4381-83c6-79fa7120f1cf",
      "name": "Trial accounts",
      "created_at": "2026-09-08T22:22:17.595Z"
    }
  ]
}
```

- List items carry no `filter`; retrieve a segment for it.

### `GET /segments/{segment}`

One segment, with its filter.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `segment` (required) | string | The segment's ID. |

Retrieve a segment:

```sh
curl -X GET "https://api.rasket.com/segments/78261eea-8f8b-4381-83c6-79fa7120f1cf" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/segments/78261eea-8f8b-4381-83c6-79fa7120f1cf", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/segments/78261eea-8f8b-4381-83c6-79fa7120f1cf",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

id = response.json()["id"]
```

#### Response `200`

```json
{
  "object": "segment",
  "id": "78261eea-8f8b-4381-83c6-79fa7120f1cf",
  "name": "Trial accounts",
  "filter": {
    "all": [
      {
        "field": "properties.plan",
        "op": "eq",
        "value": "trial"
      },
      {
        "field": "created_at",
        "op": "gte",
        "value": "2026-09-01T00:00:00Z"
      }
    ]
  },
  "created_at": "2026-09-08T22:22:17.595Z"
}
```

- `filter` is `null` for a segment with explicit membership only.

### `PATCH /segments/{segment}`

Change the name, and the filter when one is sent.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `segment` (required) | string | The segment's ID. |

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `name` (required) | string | The name: the new one, or the current one to keep it. |
| `filter` | object \| null | A new filter, checked as on create. `null` leaves only the people added by hand. Leave it out to keep the filter. |

Update a segment:

```sh
curl -X PATCH "https://api.rasket.com/segments/78261eea-8f8b-4381-83c6-79fa7120f1cf" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Trials, September",
  "filter": {
    "all": [
      {
        "field": "properties.plan",
        "op": "eq",
        "value": "trial"
      },
      {
        "field": "created_at",
        "op": "gte",
        "value": "2026-09-01T00:00:00Z"
      }
    ]
  }
}'
```

```ts
const response = await fetch("https://api.rasket.com/segments/78261eea-8f8b-4381-83c6-79fa7120f1cf", {
  method: "PATCH",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name: "Trials, September",
    filter: {
      all: [
        {
          field: "properties.plan",
          op: "eq",
          value: "trial"
        },
        {
          field: "created_at",
          op: "gte",
          value: "2026-09-01T00:00:00Z"
        }
      ]
    }
  }),
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.patch(
    "https://api.rasket.com/segments/78261eea-8f8b-4381-83c6-79fa7120f1cf",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "name": "Trials, September",
    "filter": {
      "all": [
        {
          "field": "properties.plan",
          "op": "eq",
          "value": "trial"
        },
        {
          "field": "created_at",
          "op": "gte",
          "value": "2026-09-01T00:00:00Z"
        }
      ]
    }
  },
)

id = response.json()["id"]
```

#### Response `200`

```json
{
  "object": "segment",
  "id": "78261eea-8f8b-4381-83c6-79fa7120f1cf"
}
```

- People added to the segment or removed from it by hand stay that way when the filter changes.
- A filter change is `422 validation_error` while a broadcast to the segment is queued or sending, so one send never reaches two audiences. A scheduled broadcast goes to the new filter. Sending the current filter with a new name is a rename.
- A sent broadcast's recipients are recorded when it is sent, so its report still lists who it went to after the filter changes.

### `DELETE /segments/{segment}`

Retire the segment and free its name.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `segment` (required) | string | The segment's ID. |

Delete a segment:

```sh
curl -X DELETE "https://api.rasket.com/segments/78261eea-8f8b-4381-83c6-79fa7120f1cf" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/segments/78261eea-8f8b-4381-83c6-79fa7120f1cf", {
  method: "DELETE",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.delete(
    "https://api.rasket.com/segments/78261eea-8f8b-4381-83c6-79fa7120f1cf",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

id = response.json()["id"]
```

#### Response `200`

```json
{
  "object": "segment",
  "id": "78261eea-8f8b-4381-83c6-79fa7120f1cf",
  "deleted": true
}
```

- The segment is `404` from that moment. Past broadcasts that went to it keep their recipients; a draft that names it cannot be sent until it names another.

### `GET /segments/{segment}/contacts`

The members, evaluated now: the filter's matches, less contacts removed by hand, plus explicit additions.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `segment` (required) | string | The segment's ID. |

#### Query parameters

| Field | Type | Description |
| --- | --- | --- |
| `limit` | integer | How many items to return, 1–100. Defaults to 20. |
| `after` | string | Return the page that follows this item ID. Mutually exclusive with `before`. |
| `before` | string | Return the page that precedes this item ID. Mutually exclusive with `after`. |

List a segment's contacts:

```sh
curl -X GET "https://api.rasket.com/segments/78261eea-8f8b-4381-83c6-79fa7120f1cf/contacts" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/segments/78261eea-8f8b-4381-83c6-79fa7120f1cf/contacts", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const data = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/segments/78261eea-8f8b-4381-83c6-79fa7120f1cf/contacts",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "id": "e169aa45-1ecf-4183-9955-b1499d5701d3",
      "email": "ronald.williams@example.com",
      "first_name": "Ronald",
      "last_name": "Williams",
      "created_at": "2026-09-08T22:22:17.595Z",
      "unsubscribed": false
    }
  ]
}
```

- This is the audience a broadcast to the segment would resolve, before subscription, suppression and unsubscribe filtering. Deleted contacts are never listed.

### `POST /segments/{segment}/contacts/batch/add`

Put up to 100 contacts in the segment at once.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `segment` (required) | string | The segment's ID. |

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `contact_ids` (required) | string[] | 1 to 100 contact IDs. |

Add contacts to a segment:

```sh
curl -X POST "https://api.rasket.com/segments/78261eea-8f8b-4381-83c6-79fa7120f1cf/contacts/batch/add" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "contact_ids": ["e169aa45-1ecf-4183-9955-b1499d5701d3"]
}'
```

```ts
const response = await fetch("https://api.rasket.com/segments/78261eea-8f8b-4381-83c6-79fa7120f1cf/contacts/batch/add", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    contact_ids: ["e169aa45-1ecf-4183-9955-b1499d5701d3"]
  }),
});

const data = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/segments/78261eea-8f8b-4381-83c6-79fa7120f1cf/contacts/batch/add",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "contact_ids": ["e169aa45-1ecf-4183-9955-b1499d5701d3"]
  },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "segment_contacts",
  "segment_id": "78261eea-8f8b-4381-83c6-79fa7120f1cf",
  "added": 1
}
```

- A contact that was removed from the segment before is back in, even if the filter does not match it. Contacts already in the segment are counted and left as they are.
- All-or-nothing: an ID that names no contact is `422 invalid_parameter` at its index, and nothing is written.

### `POST /segments/{segment}/contacts/batch/remove`

Take up to 100 contacts out of the segment at once.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `segment` (required) | string | The segment's ID. |

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `contact_ids` (required) | string[] | 1 to 100 contact IDs. |

Remove contacts from a segment:

```sh
curl -X POST "https://api.rasket.com/segments/78261eea-8f8b-4381-83c6-79fa7120f1cf/contacts/batch/remove" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "contact_ids": ["e169aa45-1ecf-4183-9955-b1499d5701d3"]
}'
```

```ts
const response = await fetch("https://api.rasket.com/segments/78261eea-8f8b-4381-83c6-79fa7120f1cf/contacts/batch/remove", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    contact_ids: ["e169aa45-1ecf-4183-9955-b1499d5701d3"]
  }),
});

const data = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/segments/78261eea-8f8b-4381-83c6-79fa7120f1cf/contacts/batch/remove",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "contact_ids": ["e169aa45-1ecf-4183-9955-b1499d5701d3"]
  },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "segment_contacts",
  "segment_id": "78261eea-8f8b-4381-83c6-79fa7120f1cf",
  "removed": 1
}
```

- A removed contact stays out even while the segment's filter matches it, until it is added back. The contacts and their consent are untouched.
- All-or-nothing: an ID that names no contact is `422 invalid_parameter` at its index, and nothing is written.

### `GET /segments/{segment}/metrics`

How many contacts the segment resolves to right now.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `segment` (required) | string | The segment's ID. |

Retrieve a segment's size:

```sh
curl -X GET "https://api.rasket.com/segments/78261eea-8f8b-4381-83c6-79fa7120f1cf/metrics" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/segments/78261eea-8f8b-4381-83c6-79fa7120f1cf/metrics", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const data = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/segments/78261eea-8f8b-4381-83c6-79fa7120f1cf/metrics",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "segment_metrics",
  "segment_id": "78261eea-8f8b-4381-83c6-79fa7120f1cf",
  "contacts": 1240,
  "subscribed": 1197,
  "unsubscribed": 43
}
```

- One pass over the same membership `/contacts` lists, split on the global unsubscribe.

### `POST /segments/preview`

How many contacts a filter matches right now, without creating a segment.

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `filter` (required) | object | A filter exactly as `POST /segments` takes it. |
| `segment_id` | string | A saved segment whose filter this one would replace: the count is then what that segment would hold, with the people added and removed by hand. |

Preview a segment:

```sh
curl -X POST "https://api.rasket.com/segments/preview" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "filter": {
    "all": [
      {
        "field": "properties.plan",
        "op": "eq",
        "value": "trial"
      },
      {
        "field": "created_at",
        "op": "gte",
        "value": "2026-09-01T00:00:00Z"
      }
    ]
  }
}'
```

```ts
const response = await fetch("https://api.rasket.com/segments/preview", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    filter: {
      all: [
        {
          field: "properties.plan",
          op: "eq",
          value: "trial"
        },
        {
          field: "created_at",
          op: "gte",
          value: "2026-09-01T00:00:00Z"
        }
      ]
    }
  }),
});

const data = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/segments/preview",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "filter": {
      "all": [
        {
          "field": "properties.plan",
          "op": "eq",
          "value": "trial"
        },
        {
          "field": "created_at",
          "op": "gte",
          "value": "2026-09-01T00:00:00Z"
        }
      ]
    }
  },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "segment_preview",
  "contacts": 1284
}
```

- Nothing is stored. A filter `POST /segments` would refuse is `422 validation_error` here, with the same `errors[]` paths.
