# Segments API

> Build, update, and query subscriber segments through the Broadcast API. Filter by tags, custom fields, engagement, and behavior to target campaigns precisely.

Source: https://sendbroadcast.net/docs/api-segments

The Segments API provides full CRUD (Create, Read, Update, Delete) operations for managing subscriber segments. Segments allow you to group subscribers based on rules and conditions, which can then be used to target specific audiences for broadcasts.

## Required Permissions

All endpoints require authentication via an API token with appropriate permissions:

- **Read Permission**: Required for GET endpoints (list, show)
- **Write Permission**: Required for POST, PATCH, DELETE endpoints

## Segment Object

The segment object contains the following fields:

| Field | Type | Description |
| --- | --- | --- |
| `id` | integer | Unique identifier of the segment |
| `name` | string | Name of the segment (required) |
| `description` | string | Description of the segment |
| `subscribers_count` | integer | Cached count of subscribers matching the segment |
| `segment_groups` | array | Array of rule groups (combined with OR logic) |
| `created_at` | datetime | When the segment was created |
| `updated_at` | datetime | When the segment was last updated |

## Segment Group Object

Each segment contains one or more groups. Groups are combined with OR logic (a subscriber matches if they match ANY group).

| Field | Type | Description |
| --- | --- | --- |
| `id` | integer | Unique identifier of the group |
| `match_type` | string | `all` (AND logic) or `any` (OR logic) for rules within this group |
| `position` | integer | Order of the group |
| `segment_rules` | array | Array of rules in this group |

## Segment Rule Object

Rules define the conditions for matching subscribers.

| Field | Type | Description |
| --- | --- | --- |
| `id` | integer | Unique identifier of the rule |
| `field` | string | The subscriber field to check (see allowed fields below) |
| `operator` | string | The comparison operator (see operators below) |
| `value` | string | The value to compare against |
| `secondary_value` | string | Secondary value for engagement window rules |
| `rule_type` | string | `text`, `number`, `date`, `boolean`, or `engagement_window` |
| `value_type` | string | `string`, `integer`, `float`, `date`, `boolean`, `array`, or `relative_date` |
| `is_negative` | boolean | If true, negates the rule condition |
| `case_sensitive` | boolean | If true, text comparisons are case-sensitive |
| `position` | integer | Order of the rule within its group |
| `match_type` | string | Inherited from parent group (`all` or `any`) |

### Allowed Fields

#### Standard Subscriber Fields

- `email` - Subscriber email address
- `first_name` - Subscriber first name
- `last_name` - Subscriber last name
- `tags` - Subscriber tags
- `created_at` - When the subscriber was created
- `is_active` - Whether the subscriber is active
- `is_confirmed` - Whether the subscriber is confirmed (double opt-in)
- `sequential_id` - The subscriber's sequential ID
- `last_email_sent_at` - When the last email was sent
- `last_email_opened_at` - When the last email was opened
- `last_email_clicked_at` - When the last email was clicked
- `any_email_sent_at` - When any email was sent
- `any_email_opened_at` - When any email was opened
- `any_email_clicked_at` - When any email was clicked
- `total_emails_sent` - Total emails sent to subscriber
- `total_emails_opened` - Total emails opened by subscriber
- `total_emails_clicked` - Total emails clicked by subscriber
- `has_opened_any_email` - Whether subscriber has opened any email
- `has_clicked_any_email` - Whether subscriber has clicked any email
- `emails_opened_within_days` - Emails opened within N days
- `emails_clicked_within_days` - Emails clicked within N days

#### Custom Data Fields

In addition to the standard fields above, you can filter subscribers by any key stored in their `custom_data` JSON field. Custom data fields use the `custom_data.` prefix followed by the key name.

**Format**: `custom_data.` for top-level keys, `custom_data..` for nested keys. **Examples of valid custom data field names:** - `custom_data.plan` - A top-level key called "plan" - `custom_data.company` - A top-level key called "company" - `custom_data.preferences.language` - A nested key "language" inside "preferences" - `custom_data.address.geo.lat` - A deeply nested key **Key name rules:** - Key names may only contain letters, numbers, and underscores (`a-z`, `A-Z`, `0-9`, `_`) - Key names must start with a letter or number - Use dots (`.`) to access nested keys - Spaces and special characters are not allowed in key names **Choosing a rule type for custom data fields:** Since custom data values do not have a fixed schema, you must specify the `rule_type` to tell the system how to interpret the value. The rule type determines which operators are available and how comparisons are performed. - Use `text` for string values (e.g., plan names, cities, languages) - Use `number` for numeric values (e.g., age, order count, score): values are cast to numbers for comparison - Use `date` for date values (e.g., trial end dates, signup dates): values are cast to dates for comparison - Use `boolean` for true/false values (e.g., VIP status, opt-in flags): values are cast to booleans for comparison If a subscriber's custom data does not contain the specified key, it is treated as `NULL` (empty). See [Important: Data Type Casting](#important-data-type-casting) below for details on how this affects each rule type. #### Topic Field (Requires Broadcast 2.48 or later) Use the field `topic` to filter by one of the channel's [Subscriber Topics](https://sendbroadcast.net/docs/subscriber-topics). The `value` is the topic ID (see the [Topics API](https://sendbroadcast.net/docs/api-topics)), and it must be a topic of the same channel. `rule_type` is set to `topic` for you.
```json
{
  "field": "topic",
  "operator": "receives",
  "value": "12"
}
```

 - `receives` matches the subscribers an email sent to the topic reaches, including subscribers with no value when the topic is set to receive them. - `does_not_receive` matches everyone else. - `not_set` matches subscribers with no value for the topic. It is only valid for custom data topics: a tag topic has no "not set" value. A topic ID from another channel, or `not_set` on a tag topic, is refused with `422`. (Requires Broadcast 2.49 or later) While a rule uses the topic, the topic cannot be deleted, and its `storage`, `custom_data_key` and `tag_name` cannot change. Remove the rule first. See [Topics API](https://sendbroadcast.net/docs/api-topics). ### Operators by Rule Type **Text rules**: `equals`, `not_equals`, `contains`, `not_contains`, `starts_with`, `ends_with`, `is_empty`, `is_not_empty` **Number rules**: `equals`, `not_equals`, `greater_than`, `less_than`, `greater_than_or_equal`, `less_than_or_equal` **Date rules (standard fields)**: `equals`, `not_equals`, `before`\*, `after`\*, `on_or_before`\*, `on_or_after`\*, `is_empty`, `is_not_empty`, `never`, `within_last_days`, `not_within_last_days` **Date rules (custom data fields)**: `equals`, `not_equals`, `before`\*, `after`\*, `on_or_before`\*, `on_or_after`\*, `within_last_days`, `not_within_last_days`, `is_empty`, `is_not_empty` \* These operators support [relative dates](#relative-dates) (`value_type: "relative_date"`). > The `never` operator is not available for custom data date fields: use `is_empty` instead, which behaves identically (matches when the key is missing or empty). **Boolean rules**: `is_true`, `is_false` **Engagement window rules**: `at_least_within_days`, `fewer_than_within_days` **Topic rules** (2.48.0 or later): `receives`, `does_not_receive`, `not_set` > **Note:** Engagement window rules are not applicable to custom data fields: they only work with built-in email activity fields. ### Relative Dates The date comparison operators `before`, `after`, `on_or_before`, and `on_or_after` support **relative dates** in addition to specific dates. A relative date dynamically calculates as "N days ago from today" each time the segment is evaluated, so the segment stays useful over time without manual updates. To use a relative date, set `value_type` to `"relative_date"` and `value` to the number of days ago:
```json
{
  "field": "created_at",
  "operator": "on_or_before",
  "value": "80",
  "rule_type": "date",
  "value_type": "relative_date"
}
```

 This rule matches subscribers who signed up 80 or more days ago. Tomorrow it will mean 80 days from tomorrow, and so on. **Supported operators:** `before`, `after`, `on_or_before`, `on_or_after` only. The `equals`, `not_equals`, `within_last_days`, `not_within_last_days`, and `never` operators do not support `relative_date`. **Works with custom data date fields too.** Set `rule_type: "date"` and `value_type: "relative_date"` on any `custom_data.*` field. ### Important: Data Type Casting When using `number` or `date` rule types with custom data fields, values are cast from their stored JSON representation to the appropriate type for comparison. Be aware of the following: - **Number fields**: The stored value must be a valid number (e.g., `32`, `"32"`, `3.14`). If a subscriber's value cannot be cast to a number (e.g., `"not-a-number"`), that subscriber will be excluded from results: it will not match the rule and no error will be raised. - **Date fields**: The stored value must be in a format PostgreSQL can parse. **ISO 8601 format is strongly recommended** (e.g., `"2026-01-15"`, `"2026-01-15T10:30:00Z"`). Ambiguous formats like `"01/02/2026"` may be interpreted differently than expected. If a value cannot be parsed as a date, the subscriber will be silently excluded. - **Boolean fields**: The stored value must be `true`, `false`, `"true"`, or `"false"`. Other values will cause the subscriber to be excluded. - **Missing keys**: If the key does not exist in a subscriber's custom data, the value is `NULL`. This means the subscriber will match `is_empty` rules and will **not** match any other operator (including numeric and date comparisons). ## List Segments
```
GET /api/v1/segments
```

 Returns a paginated list of segments that belong to a channel. ### Query Parameters - `page`: Page number (default: 1) ### Request
```bash
curl -X GET \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  http://your-domain.com/api/v1/segments.json
```

 ### Response
```json
{
  "segments": [
    {
      "id": 1,
      "name": "Active Subscribers",
      "description": "Subscribers who have opened at least one email in the last 30 days.",
      "created_at": "2024-06-01T12:34:56Z"
    }
  ],
  "pagination": {
    "total": 1,
    "count": 1,
    "from": 1,
    "to": 1,
    "current": 1,
    "total_pages": 1
  }
}
```

 ## Get Segment
```
GET /api/v1/segments/:id
```

 Returns details of a specific segment along with a paginated list of matching subscribers. ### Query Parameters - `page`: Page number for subscribers list (default: 1) ### Request
```bash
curl -X GET \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  http://your-domain.com/api/v1/segments/1.json
```

 ### Response
```json
{
  "segment": {
    "id": 1,
    "name": "Active Subscribers",
    "description": "Subscribers who have opened at least one email in the last 30 days.",
    "created_at": "2024-06-01T12:34:56Z"
  },
  "subscribers": [
    {
      "id": "123",
      "email": "user@example.com",
      "first_name": "John",
      "last_name": "Doe",
      "ip_address": "192.168.1.1",
      "is_active": true,
      "source": "api_subscription",
      "subscribed_at": "2024-03-20T10:00:00Z",
      "unsubscribed_at": null,
      "created_at": "2024-03-20T10:00:00Z",
      "custom_data": { "plan": "premium" },
      "tags": ["newsletter", "product-updates"]
    }
  ],
  "pagination": {
    "total": 150,
    "count": 250,
    "from": 1,
    "to": 150,
    "current": 1,
    "total_pages": 1
  }
}
```

 ## Create Segment
```
POST /api/v1/segments
```

 Create a new segment with rules. ### Parameters - `name` (required): Name of the segment - `description` (optional): Description of the segment - `segment_groups_attributes` (required): Array of rule groups, each containing: - `match_type`: `all` or `any` - `segment_rules_attributes`: Array of rules ### Request
```bash
curl -X POST \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "segment": {
      "name": "Gmail Users",
      "description": "Subscribers with Gmail addresses",
      "segment_groups_attributes": [
        {
          "match_type": "all",
          "segment_rules_attributes": [
            {
              "field": "email",
              "operator": "contains",
              "value": "gmail.com",
              "rule_type": "text",
              "value_type": "string"
            }
          ]
        }
      ]
    }
  }' \
  http://your-domain.com/api/v1/segments
```

 ### Response
```json
{
  "id": 5,
  "name": "Gmail Users",
  "description": "Subscribers with Gmail addresses",
  "subscribers_count": 0,
  "segment_groups": [
    {
      "id": 10,
      "match_type": "all",
      "position": 1,
      "segment_rules": [
        {
          "id": 20,
          "field": "email",
          "operator": "contains",
          "value": "gmail.com",
          "secondary_value": null,
          "rule_type": "text",
          "value_type": "string",
          "position": 1,
          "is_negative": false,
          "match_type": "all",
          "case_sensitive": false
        }
      ]
    }
  ],
  "created_at": "2024-06-15T10:00:00Z",
  "updated_at": "2024-06-15T10:00:00Z"
}
```

 ### Complex Segment Example Create a segment with multiple groups and rules:
```bash
curl -X POST \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "segment": {
      "name": "Engaged Premium Users",
      "description": "Active subscribers who opened emails recently OR have VIP tag",
      "segment_groups_attributes": [
        {
          "match_type": "all",
          "segment_rules_attributes": [
            {
              "field": "is_active",
              "operator": "is_true",
              "value": "true",
              "rule_type": "boolean",
              "value_type": "boolean"
            },
            {
              "field": "last_email_opened_at",
              "operator": "within_last_days",
              "value": "30",
              "rule_type": "date",
              "value_type": "string"
            }
          ]
        },
        {
          "match_type": "all",
          "segment_rules_attributes": [
            {
              "field": "tags",
              "operator": "contains",
              "value": "vip",
              "rule_type": "text",
              "value_type": "string"
            }
          ]
        }
      ]
    }
  }' \
  http://your-domain.com/api/v1/segments
```

 This creates a segment matching subscribers who: - (Are active AND opened an email in the last 30 days) OR - (Have the "vip" tag) ## Update Segment
```
PATCH /api/v1/segments/:id
```

 Update an existing segment. ### Parameters All parameters are optional. Only include the fields you want to update: - `name`: Name of the segment - `description`: Description of the segment - `segment_groups_attributes`: Array of rule groups (include `id` for existing groups, or omit to create new) - Include `_destroy: true` to delete a group ### Request - Simple Update
```bash
curl -X PATCH \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "segment": {
      "name": "Updated Segment Name",
      "description": "Updated description"
    }
  }' \
  http://your-domain.com/api/v1/segments/5
```

 ### Request - Update Rules
```bash
curl -X PATCH \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "segment": {
      "segment_groups_attributes": [
        {
          "id": 10,
          "match_type": "any",
          "segment_rules_attributes": [
            {
              "field": "email",
              "operator": "contains",
              "value": "company.com",
              "rule_type": "text",
              "value_type": "string"
            }
          ]
        }
      ]
    }
  }' \
  http://your-domain.com/api/v1/segments/5
```

 ### Response
```json
{
  "id": 5,
  "name": "Updated Segment Name",
  "description": "Updated description",
  "subscribers_count": 0,
  "segment_groups": [
    {
      "id": 10,
      "match_type": "any",
      "position": 1,
      "segment_rules": [
        {
          "id": 21,
          "field": "email",
          "operator": "contains",
          "value": "company.com",
          "secondary_value": null,
          "rule_type": "text",
          "value_type": "string",
          "position": 1,
          "is_negative": false,
          "match_type": "all",
          "case_sensitive": false
        }
      ]
    }
  ],
  "created_at": "2024-06-15T10:00:00Z",
  "updated_at": "2024-06-15T11:00:00Z"
}
```

 ## Delete Segment
```
DELETE /api/v1/segments/:id
```

 Delete a segment. ### Request
```bash
curl -X DELETE \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  http://your-domain.com/api/v1/segments/5
```

 ### Response
```json
{
  "message": "Segment deleted successfully"
}
```

 A segment that a broadcast (not yet sent), a sequence or an autopilot excludes cannot be deleted. The request fails with `422` and names them:
```json
{
  "error": "This segment is excluded by \"Spring newsletter\". Remove it from their excluded segments first."
}
```

 Remove the segment from their `excluded_segment_ids` first. Deleting it would otherwise drop the exclusion and widen their audience without a word. ## Error Responses ### 401 Unauthorized
```json
{
  "error": "Unauthorized"
}
```

 This error occurs when: - No authorization header is provided - The token is invalid or expired - The token doesn't have the required permissions ### 404 Not Found
```json
{
  "error": "Segment not found"
}
```

 This error occurs when: - The segment ID doesn't exist - The segment belongs to a different broadcast channel ### 422 Unprocessable Entity
```json
{
  "error": "Name can't be blank, must have at least one rule"
}
```

 This error occurs when: - Required fields are missing - The segment has no rules - Validation fails for any other reason ## Usage Examples ### Create a Segment for Recent Subscribers
```bash
curl -X POST \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "segment": {
      "name": "New Subscribers (Last 7 Days)",
      "description": "Subscribers who joined in the last week",
      "segment_groups_attributes": [
        {
          "match_type": "all",
          "segment_rules_attributes": [
            {
              "field": "created_at",
              "operator": "within_last_days",
              "value": "7",
              "rule_type": "date",
              "value_type": "string"
            }
          ]
        }
      ]
    }
  }' \
  http://your-domain.com/api/v1/segments
```

 ### Create a Segment for Inactive Subscribers
```bash
curl -X POST \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "segment": {
      "name": "Inactive Subscribers",
      "description": "Subscribers who have not opened any email in 90 days",
      "segment_groups_attributes": [
        {
          "match_type": "all",
          "segment_rules_attributes": [
            {
              "field": "last_email_opened_at",
              "operator": "not_within_last_days",
              "value": "90",
              "rule_type": "date",
              "value_type": "string"
            }
          ]
        }
      ]
    }
  }' \
  http://your-domain.com/api/v1/segments
```

 ### Create a Segment for Long-Time Subscribers (Relative Date) Find subscribers who signed up more than 80 days ago. This segment automatically recalculates each day: no need to update it manually.
```bash
curl -X POST \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "segment": {
      "name": "Long-Time Subscribers",
      "description": "Subscribers who signed up more than 80 days ago",
      "segment_groups_attributes": [
        {
          "match_type": "all",
          "segment_rules_attributes": [
            {
              "field": "created_at",
              "operator": "on_or_before",
              "value": "80",
              "rule_type": "date",
              "value_type": "relative_date"
            }
          ]
        }
      ]
    }
  }' \
  http://your-domain.com/api/v1/segments
```

 ### Create a Segment for Highly Engaged Subscribers
```bash
curl -X POST \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "segment": {
      "name": "Highly Engaged",
      "description": "Subscribers who opened at least 3 emails in the last 30 days",
      "segment_groups_attributes": [
        {
          "match_type": "all",
          "segment_rules_attributes": [
            {
              "field": "emails_opened_within_days",
              "operator": "at_least_within_days",
              "value": "30",
              "secondary_value": "3",
              "rule_type": "engagement_window",
              "value_type": "string"
            }
          ]
        }
      ]
    }
  }' \
  http://your-domain.com/api/v1/segments
```

 ## Custom Data Segment Examples The following examples show how to create segments that filter subscribers based on their `custom_data` fields. For these examples, assume your subscribers have custom data like this (set via the API, CSV import, or opt-in forms):
```json
{
  "plan": "premium",
  "company": "Acme Corp",
  "age": 32,
  "vip": true,
  "preferences": {
    "language": "en",
    "frequency": "weekly"
  }
}
```

 ### Filter by a Text Custom Data Field Find all subscribers on the "premium" plan:
```bash
curl -X POST \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "segment": {
      "name": "Premium Plan Users",
      "description": "Subscribers whose plan is premium",
      "segment_groups_attributes": [
        {
          "match_type": "all",
          "segment_rules_attributes": [
            {
              "field": "custom_data.plan",
              "operator": "equals",
              "value": "premium",
              "rule_type": "text",
              "value_type": "string"
            }
          ]
        }
      ]
    }
  }' \
  http://your-domain.com/api/v1/segments
```

 Text comparisons are case-insensitive by default. To match exact case, set `"case_sensitive": true`. ### Filter by a Nested Custom Data Field Find subscribers whose preferred language is English:
```bash
curl -X POST \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "segment": {
      "name": "English Speakers",
      "description": "Subscribers who prefer English",
      "segment_groups_attributes": [
        {
          "match_type": "all",
          "segment_rules_attributes": [
            {
              "field": "custom_data.preferences.language",
              "operator": "equals",
              "value": "en",
              "rule_type": "text",
              "value_type": "string"
            }
          ]
        }
      ]
    }
  }' \
  http://your-domain.com/api/v1/segments
```

 Use dot notation to traverse nested objects. `custom_data.preferences.language` accesses the `language` key inside the `preferences` object. ### Filter by a Numeric Custom Data Field Find subscribers older than 30:
```bash
curl -X POST \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "segment": {
      "name": "Over 30",
      "description": "Subscribers older than 30",
      "segment_groups_attributes": [
        {
          "match_type": "all",
          "segment_rules_attributes": [
            {
              "field": "custom_data.age",
              "operator": "greater_than",
              "value": "30",
              "rule_type": "number",
              "value_type": "integer"
            }
          ]
        }
      ]
    }
  }' \
  http://your-domain.com/api/v1/segments
```

 When using `rule_type: "number"`, the custom data value is cast to a number before comparison. Subscribers whose custom data does not contain the key (or whose value is not a valid number) will not match. ### Filter by a Boolean Custom Data Field Find all VIP subscribers:
```bash
curl -X POST \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "segment": {
      "name": "VIP Subscribers",
      "description": "Subscribers flagged as VIP",
      "segment_groups_attributes": [
        {
          "match_type": "all",
          "segment_rules_attributes": [
            {
              "field": "custom_data.vip",
              "operator": "is_true",
              "rule_type": "boolean",
              "value_type": "boolean"
            }
          ]
        }
      ]
    }
  }' \
  http://your-domain.com/api/v1/segments
```

 Boolean rules do not require a `value` field: the operator (`is_true` or `is_false`) defines the condition. ### Filter by Missing Custom Data (is_empty) Find subscribers who do not have a company set:
```bash
curl -X POST \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "segment": {
      "name": "No Company",
      "description": "Subscribers without a company in their custom data",
      "segment_groups_attributes": [
        {
          "match_type": "all",
          "segment_rules_attributes": [
            {
              "field": "custom_data.company",
              "operator": "is_empty",
              "rule_type": "text",
              "value_type": "string"
            }
          ]
        }
      ]
    }
  }' \
  http://your-domain.com/api/v1/segments
```

 The `is_empty` operator matches subscribers where the key does not exist in their custom data, or where the value is an empty string. Conversely, `is_not_empty` matches subscribers who have a non-empty value for the key. ### Filter by a Date Custom Data Field (within last N days) Find subscribers who made a purchase in the last 30 days:
```bash
curl -X POST \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "segment": {
      "name": "Recent Purchasers",
      "description": "Subscribers who purchased in the last 30 days",
      "segment_groups_attributes": [
        {
          "match_type": "all",
          "segment_rules_attributes": [
            {
              "field": "custom_data.last_purchase_at",
              "operator": "within_last_days",
              "value": "30",
              "rule_type": "date",
              "value_type": "integer"
            }
          ]
        }
      ]
    }
  }' \
  http://your-domain.com/api/v1/segments
```

 The `within_last_days` operator checks if the date value falls within the last N days from today. Subscribers whose key is missing or whose value is not a valid date will not match. The `not_within_last_days` operator is the inverse: it matches subscribers whose date is older than N days **or** whose key is missing. > **Important:** Date values in custom data must be stored in a format PostgreSQL can parse. ISO 8601 format is strongly recommended (e.g., `"2026-01-15"` or `"2026-01-15T10:30:00Z"`). You can also use relative dates with custom data date fields. For example, find subscribers whose trial ended more than 14 days ago:
```bash
curl -X POST \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "segment": {
      "name": "Trial Expired (14+ days)",
      "description": "Subscribers whose trial ended more than 14 days ago",
      "segment_groups_attributes": [
        {
          "match_type": "all",
          "segment_rules_attributes": [
            {
              "field": "custom_data.trial_ends_at",
              "operator": "on_or_before",
              "value": "14",
              "rule_type": "date",
              "value_type": "relative_date"
            }
          ]
        }
      ]
    }
  }' \
  http://your-domain.com/api/v1/segments
```

 Set `value_type` to `"relative_date"` and `value` to the number of days. See [Relative Dates](#relative-dates) for details. ### Combine Custom Data with Standard Fields Find premium subscribers who have opened an email in the last 30 days:
```bash
curl -X POST \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "segment": {
      "name": "Engaged Premium Users",
      "description": "Premium subscribers who opened an email recently",
      "segment_groups_attributes": [
        {
          "match_type": "all",
          "segment_rules_attributes": [
            {
              "field": "custom_data.plan",
              "operator": "equals",
              "value": "premium",
              "rule_type": "text",
              "value_type": "string"
            },
            {
              "field": "last_email_opened_at",
              "operator": "within_last_days",
              "value": "30",
              "rule_type": "date",
              "value_type": "string"
            }
          ]
        }
      ]
    }
  }' \
  http://your-domain.com/api/v1/segments
```

 Custom data rules can be freely combined with standard subscriber fields in the same group. Rules within a group are joined with AND logic (when `match_type` is `"all"`) or OR logic (when `match_type` is `"any"`). ### Negate a Custom Data Rule Find subscribers who are NOT on the "free" plan:
```bash
curl -X POST \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "segment": {
      "name": "Paid Users",
      "description": "Subscribers who are not on the free plan",
      "segment_groups_attributes": [
        {
          "match_type": "all",
          "segment_rules_attributes": [
            {
              "field": "custom_data.plan",
              "operator": "equals",
              "value": "free",
              "rule_type": "text",
              "value_type": "string",
              "is_negative": true
            }
          ]
        }
      ]
    }
  }' \
  http://your-domain.com/api/v1/segments
```

 Setting `"is_negative": true` inverts the rule. In this example, the rule becomes "plan does NOT equal free". Note that subscribers who do not have a `plan` key at all will also match this rule, since their value is `NULL` which is not equal to "free".
