# Segmentation

Source: https://molted.email/docs/segmentation

Create dynamic contact segments with filter groups, nested logic, and async computation.

Segments let you group contacts based on filters - contact fields, account data, metadata, firmographic attributes, and behavioral events. Segments are computed asynchronously and can be used to target [experiments](https://molted.email/docs/experimentation.md) (`segmentId`). Journeys don't read segments: journey trigger conditions and branch conditions only look at the trigger event's payload.

On `api.molted.email`, pass `tenantId` (in the body for writes, in the query string for reads); without it a key call fails with `401 missing_tenant_id`. `mailbox.molted.email`, the CLI and the MCP server fill it in. Writes need `manage` scope and refuse a mailbox-scoped key.

## Create a segment

**CLI**

```bash
molted segments create \
  --name "Enterprise customers in US" \
  --filters '{
    "logic": "and",
    "filters": [
      { "type": "contact_field", "field": "lifecycleStage", "operator": "eq", "value": "customer" },
      { "type": "metadata", "field": "region", "operator": "eq", "value": "us-west" },
      { "type": "firmographic", "field": "companySize", "operator": "gte", "value": 500 }
    ]
  }'
```

| Flag | Type | Required | Description |
| - | - | - | - |
| `--name` | `string` | Yes | Segment name. |
| `--filters` | `JSON` | Yes | Filter group as JSON. See [filter reference](#filter-types) below. |

**API**

```
POST https://mailbox.molted.email/v1/segments
```

```bash title="curl"
curl -X POST https://mailbox.molted.email/v1/segments \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Enterprise customers in US",
    "filterGroup": {
      "logic": "and",
      "filters": [
        { "type": "contact_field", "field": "lifecycleStage", "operator": "eq", "value": "customer" },
        { "type": "metadata", "field": "region", "operator": "eq", "value": "us-west" },
        { "type": "firmographic", "field": "companySize", "operator": "gte", "value": 500 }
      ]
    }
  }'
```

| Field | Type | Required | Description |
| - | - | - | - |
| `name` | `string` | Yes | Segment name. |
| `filterGroup` | `object` | Yes | Filter group object. See [filter reference](#filter-types) below. |

Create (and update) return the segment row `{ id, tenantId, name, filterGroup, status, createdAt, updatedAt }`. The filter group is stored in the canonical shape at write time. Two shorthands are also accepted and normalized: `{ "operator": "and", "conditions": [{ "field", "op", "value" }] }`, and a single bare filter. A filter without `type` is inferred: `email`, `name`, `ownerEmail`, `lifecycleStage` and `dealStage` become `contact_field`, anything else `metadata`.

## Get a segment

**CLI**

```bash
molted segments get seg_abc123
```

**API**

```
GET https://mailbox.molted.email/v1/segments/:id
```

```bash title="curl"
curl "https://mailbox.molted.email/v1/segments/seg_abc123" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

Get also returns archived segments, and adds `computeDurationMs`. An unknown id is `404 segment_not_found`.

## List segments

**CLI**

```bash
molted segments list
```

**API**

```
GET https://mailbox.molted.email/v1/segments
```

```bash title="curl"
curl "https://mailbox.molted.email/v1/segments" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

### Response

Returns an array of active segments. Archived segments are excluded. `contactCount` and `snapshotVersion` are `null` until the segment's first compute (creating a segment doesn't compute it).

```json title="Response"
[
  {
    "id": "seg_abc123",
    "tenantId": "tenant_abc123",
    "name": "Enterprise customers in US",
    "filterGroup": { "logic": "and", "filters": [ "..." ] },
    "status": "active",
    "contactCount": 142,
    "snapshotVersion": 3,
    "createdAt": "2026-03-01T12:00:00Z",
    "updatedAt": "2026-03-15T08:30:00Z"
  }
]
```

If you have no segments yet, the response is an empty array:

```json title="Empty response"
[]
```

> **Note:** **No segments yet?** Create your first segment with `molted segments create`. See [creating segments](#create-a-segment) above for filter syntax and examples.

## Update a segment

**CLI**

```bash
molted segments update seg_abc123 \
  --name "Enterprise US (updated)" \
  --filters '{
    "logic": "and",
    "filters": [
      { "type": "contact_field", "field": "lifecycleStage", "operator": "eq", "value": "customer" },
      { "type": "metadata", "field": "region", "operator": "in", "value": ["us-west", "us-east"] }
    ]
  }'
```

| Flag | Type | Required | Description |
| - | - | - | - |
| `--name` | `string` | No | Updated segment name. |
| `--filters` | `JSON` | No | Updated filter group as JSON. |

**API**

```
PATCH https://mailbox.molted.email/v1/segments/:id
```

```bash title="curl"
curl -X PATCH https://mailbox.molted.email/v1/segments/seg_abc123 \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Enterprise US (updated)",
    "filterGroup": {
      "logic": "and",
      "filters": [
        { "type": "contact_field", "field": "lifecycleStage", "operator": "eq", "value": "customer" },
        { "type": "metadata", "field": "region", "operator": "in", "value": ["us-west", "us-east"] }
      ]
    }
  }'
```

## Archive a segment

**CLI**

```bash
molted segments archive seg_abc123
```

**API**

```
DELETE https://mailbox.molted.email/v1/segments/:id
```

```bash title="curl"
curl -X DELETE "https://mailbox.molted.email/v1/segments/seg_abc123" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

Archiving is a soft delete: it returns `{ "archived": true }` (or `404`), and the segment stays readable with `get`.

## Compute segment membership

Trigger an async recomputation of the segment's current members. Each computation creates a point-in-time snapshot of matching contacts.

Most flows do not need to call this directly: `contacts sync` automatically enqueues a recompute for every active segment in the tenant, so `segments list` / `segments get` reflect the new contacts shortly after the sync. Use this command when you want to force a refresh without syncing contacts (e.g., after manually editing contact metadata, or to verify a freshly created segment).

Create and update already store the canonical `{ logic, filters, type, operator }` shape. Compute still rewrites a legacy `filter_group` shape (e.g. `{ operator, conditions, op }` from older API versions) to the canonical one, as a safety net for rows written before that change.

The compute call returns `{ "queued": true, "segmentId": "..." }`.

**CLI**

```bash
molted segments compute seg_abc123
```

**API**

```
POST https://mailbox.molted.email/v1/segments/:id/compute
```

```bash title="curl"
curl -X POST "https://mailbox.molted.email/v1/segments/seg_abc123/compute" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
```

## List segment members

Results are paginated. Use `limit` and `offset` to page through members. Each member is `{ id, email, name, lifecycleStage, dealStage }`, ordered by email.

**CLI**

```bash
molted segments members seg_abc123 --limit 50 --offset 0
```

| Flag | Type | Required | Description |
| - | - | - | - |
| `--limit` | `number` | No | Max results per page (default 50). |
| `--offset` | `number` | No | Pagination offset (default 0). |

**API**

```
GET https://mailbox.molted.email/v1/segments/:id/members
```

```bash title="curl"
curl "https://mailbox.molted.email/v1/segments/seg_abc123/members?limit=50&offset=0" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

## Get contact segments

List all segments a contact belongs to. This is a reverse lookup -- given a contact, it returns every segment they are a member of.

To check whether a contact is in one specific segment instead, use [`analytics segment-check`](https://molted.email/docs/analytics.md#segment-check).

Returns `[{ id, name, status }]` for active segments only, or `404 contact_not_found`.

**CLI**

```bash
molted segments contact con_abc123
```

**API**

```
GET https://mailbox.molted.email/v1/segments/contact/:contactId/segments
```

```bash title="curl"
curl "https://mailbox.molted.email/v1/segments/contact/con_abc123/segments" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

## Filter types

Each filter has a `type` that determines which fields you can query.

| Type | Description | Queryable fields |
| - | - | - |
| `contact_field` | Filter on contact properties. | `email`, `name`, `ownerEmail`, `lifecycleStage`, `dealStage` |
| `account_field` | Filter on the contact's linked account. | `name`, `domain`, plus any key in the account's `metadata` object |
| `metadata` | Filter on contact custom metadata. | Any key in the contact's `metadata` object |
| `firmographic` | Filter on account custom metadata. | Any key in the account's `metadata` object |
| `behavioral` | Filter on event counts within a time window. | Uses `behavioralWindow` - see [behavioral filters](#behavioral-filters) below |

### Standard filter structure

```json
{
  "type": "contact_field",
  "field": "lifecycleStage",
  "operator": "eq",
  "value": "customer"
}
```

| Field | Type | Required | Description |
| - | - | - | - |
| `type` | `string` | Yes | One of: `contact_field`, `account_field`, `metadata`, `firmographic`, `behavioral`. |
| `field` | `string` | Yes | The field to filter on. Valid fields depend on the `type`. |
| `operator` | `string` | Yes | Comparison operator. See [operators](#operators) below. |
| `value` | `any` | Varies | The value to compare against. Not required for `exists`/`not_exists`. |

### Behavioral filters

Behavioral filters count how many times an event occurred within a time window. They use a `behavioralWindow` object instead of a simple `value`.

```json
{
  "type": "behavioral",
  "field": "event_count",
  "operator": "gte",
  "value": 0,
  "behavioralWindow": {
    "eventName": "email_opened",
    "windowDays": 30,
    "countOperator": "gte",
    "countValue": 3
  }
}
```

| Field | Type | Required | Description |
| - | - | - | - |
| `behavioralWindow.eventName` | `string` | Yes | The ingested event name to count (e.g. `email_opened`). |
| `behavioralWindow.windowDays` | `number` | Yes | Lookback window in days. Stored but not applied yet: counts always cover the last 90 days. |
| `behavioralWindow.countOperator` | `string` | Yes | Operator to apply to the event count (e.g. `gte`, `eq`, `lt`). |
| `behavioralWindow.countValue` | `number` | Yes | The threshold count to compare against. |

This example matches contacts with 3 or more `email_opened` events in the window.

> **Warning:** Behavioral counts only see events you ingest yourself with [`POST /v1/agent/events/ingest`](https://molted.email/docs/journeys.md#trigger-a-journey-via-event-ingestion) (`molted journeys trigger`). Sends, opens and clicks are not recorded there automatically, so `email_opened`-style names only match if you ingest events with those names. Two more limits today: `windowDays` is ignored and counts always cover the last 90 days, and a behavioral filter's own `field`, `operator` and `value` are ignored (only `behavioralWindow` is evaluated).

## Operators

| Operator | Value type | Description | Example `value` |
| - | - | - | - |
| `eq` | any | Equals. | `"pro"`, `500`, `true` |
| `neq` | any | Not equals. | `"free"` |
| `gt` | number | Greater than. | `100` |
| `gte` | number | Greater than or equal. | `500` |
| `lt` | number | Less than. | `50` |
| `lte` | number | Less than or equal. | `1000` |
| `contains` | string | String contains substring. | `"example.com"` |
| `not_contains` | string | String does not contain substring. | `"test"` |
| `in` | array | Value is in the provided list. | `["us-west", "us-east"]` |
| `not_in` | array | Value is not in the provided list. | `["spam", "bounced"]` |
| `exists` | none | Field exists and is not null. No `value` needed. | - |
| `not_exists` | none | Field does not exist or is null. No `value` needed. | - |
| `between` | array | Value is between two bounds (inclusive). | `[100, 500]` |

**Type constraints:** `gt`, `gte`, `lt`, `lte`, and `between` only work with numeric values. `contains` and `not_contains` only work with strings. Using an operator with the wrong value type returns no matches.

## Filter group structure

A filter group wraps one or more filters with a logic operator.

```json
{
  "logic": "and",
  "filters": [
    { "type": "contact_field", "field": "lifecycleStage", "operator": "eq", "value": "customer" },
    { "type": "metadata", "field": "region", "operator": "eq", "value": "us-west" }
  ]
}
```

| Field | Type | Required | Description |
| - | - | - | - |
| `logic` | `string` | Yes | `and` or `or`. Determines how filters are combined. |
| `filters` | `array` | Yes | Array of filters or nested filter groups. |

### Nested logic

Filter groups can contain other filter groups for complex AND/OR logic:

```json title="Nested filter group"
{
  "logic": "or",
  "filters": [
    {
      "logic": "and",
      "filters": [
        { "type": "contact_field", "field": "lifecycleStage", "operator": "eq", "value": "lead" },
        {
          "type": "behavioral",
          "field": "event_count",
          "operator": "gte",
          "value": 0,
          "behavioralWindow": {
            "eventName": "email_opened",
            "windowDays": 30,
            "countOperator": "gte",
            "countValue": 3
          }
        }
      ]
    },
    {
      "logic": "and",
      "filters": [
        { "type": "contact_field", "field": "lifecycleStage", "operator": "eq", "value": "customer" },
        { "type": "metadata", "field": "plan", "operator": "eq", "value": "enterprise" }
      ]
    }
  ]
}
```

This matches contacts who are either (leads with 3+ email opens in the last 30 days) OR (enterprise customers). Nesting depth is unlimited.

## Related

- [Contacts](https://molted.email/docs/contacts.md) - manage the contacts that segments filter on
- [Analytics](https://molted.email/docs/analytics.md) - check segment membership with `segment-check`
- [Experimentation](https://molted.email/docs/experimentation.md) - target an experiment at a segment with `segmentId`

## MCP tools

The segment tools are in the opt-in `journeys` toolset (see [MCP server](https://molted.email/docs/mcp-server.md)):

| Tool | Parameters |
| - | - |
| `create_segment` | `name`, `filter_group` (`logic`, flat `filters[]`; no nesting and no `behavioral_window`, use `update_segment` for those) |
| `get_segment` | `segment_id` |
| `update_segment` | `segment_id`, `name`, `filter_group` (supports `behavioral_window`: `event_name`, `window_days`, `count_operator`, `count_value`) |
| `archive_segment` | `segment_id` |
| `compute_segment` | `segment_id` |
| `check_membership` | `segment_id`, `contact_email` |
| `segment_members` | `segment_id`, `limit` (1-500), `offset` |
| `contact_segments` | `contact_id` |

There is no list tool; read the `molted://segments` resource instead.
