Segments API
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 addressfirst_name- Subscriber first namelast_name- Subscriber last nametags- Subscriber tagscreated_at- When the subscriber was createdis_active- Whether the subscriber is activeis_confirmed- Whether the subscriber is confirmed (double opt-in)sequential_id- The subscriber’s sequential IDlast_email_sent_at- When the last email was sentlast_email_opened_at- When the last email was openedlast_email_clicked_at- When the last email was clickedany_email_sent_at- When any email was sentany_email_opened_at- When any email was openedany_email_clicked_at- When any email was clickedtotal_emails_sent- Total emails sent to subscribertotal_emails_opened- Total emails opened by subscribertotal_emails_clicked- Total emails clicked by subscriberhas_opened_any_email- Whether subscriber has opened any emailhas_clicked_any_email- Whether subscriber has clicked any emailemails_opened_within_days- Emails opened within N daysemails_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.<key> for top-level keys, custom_data.<key>.<nested_key> 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
textfor string values (e.g., plan names, cities, languages) - Use
numberfor numeric values (e.g., age, order count, score) — values are cast to numbers for comparison - Use
datefor date values (e.g., trial end dates, signup dates) — values are cast to dates for comparison - Use
booleanfor 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 below for details on how this affects each rule type.
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 (value_type: "relative_date").
The
neveroperator is not available for custom data date fields — useis_emptyinstead, 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
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:
{ "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 matchis_emptyrules 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
curl -X GET \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ http://your-domain.com/api/v1/segments.json
Response
{ "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
curl -X GET \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ http://your-domain.com/api/v1/segments/1.json
Response
{ "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": "[email protected]", "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 segmentdescription(optional): Description of the segmentsegment_groups_attributes(required): Array of rule groups, each containing:match_type:alloranysegment_rules_attributes: Array of rules
Request
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
{ "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:
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 segmentdescription: Description of the segmentsegment_groups_attributes: Array of rule groups (includeidfor existing groups, or omit to create new)- Include
_destroy: trueto delete a group
- Include
Request - Simple Update
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
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
{ "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
curl -X DELETE \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ http://your-domain.com/api/v1/segments/5
Response
{ "message": "Segment deleted successfully" }
Error Responses
401 Unauthorized
{ "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
{ "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
{ "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
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
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.
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
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):
{ "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:
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:
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:
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:
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:
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:
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:
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 for details.
Combine Custom Data with Standard Fields
Find premium subscribers who have opened an email in the last 30 days:
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:
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”.