Segments
Saved, named filters over one audience, by tags, engagement, custom properties and topic subscription, that campaigns target and you can count before sending.
A segment is a saved filter over one audience: "customers tagged vip who opened something in the last 90 days", for example. A campaign created with a segment_id mails only the people the segment matches, and the same definition can be reused across campaigns and counted before anything is sent. See Campaigns.
A segment stores its rules, not its members. Every time it is used, whether by counting it, listing its contacts or sending a campaign, it is evaluated again against the contacts as they are at that moment. Engagement windows are measured back from the moment of evaluation, so the same segment matches different people on different days. A campaign freezes its recipient list when it starts sending, so a running campaign is not affected by later changes.
How a segment matches
A contact matches when every one of these holds:
- They are on the segment's audience (
audience_idis in theiraudience_ids). - Their
unsubscribedflag isfalse. - Every rule the segment sets is satisfied. Rules left unset do not filter. Rules combine with AND; there is no OR between rules.
| Rule | Matches contacts who |
|---|---|
tags | carry all of these tags. |
exclude_tags | carry none of these tags. An exclusion wins over an inclusion: someone tagged both customer and churned is left out of a segment with tags: ["customer"] and exclude_tags: ["churned"]. |
opened_within_days | have last_opened_at within the last N days (N × 24 hours before evaluation). Someone who has never opened does not match. |
clicked_within_days | have last_clicked_at within the last N days. Someone who has never clicked does not match. |
exclude_unengaged_days | opened or clicked within the last N days, or have never been mailed (no last_sent_at), or were added within the last N days. last_sent_at is recorded each time a message to the address is accepted for delivery, so this leaves out people who have been on the list for the whole window, have been mailed, and have neither opened nor clicked. |
engaged_within_days | opened, clicked, or were active in your product (last_active_at) within the last N days. Unlike exclude_unengaged_days there is no allowance for contacts never mailed or recently added, so on a list imported this month it keeps only people with evidence of interest rather than everyone. Set last_active_at on your contacts for it to see product activity. |
properties | satisfy every property filter in the array. See Property filters. |
topic_key | are subscribed to this topic: their own preference when they have set one, otherwise the topic's default. The topic must exist when the segment is saved. If it is deleted later, the segment matches nobody. |
Opens and clicks are only recorded for mail sent from a domain whose click tracking hostname (link.) is active; see domains. Contacts mailed only from a domain without it never get last_opened_at or last_clicked_at, so opened_within_days and clicked_within_days never match them, exclude_unengaged_days leaves them out once they have been mailed and on the list for longer than the window, and engaged_within_days matches them only through last_active_at. Check the count with segment metrics before using an engagement rule on such a list.
Tags in tags and exclude_tags are normalised when the segment is saved, the same way contact tags are: trimmed, lower-cased, runs of whitespace turned into a hyphen and cut to 60 characters.
Suppressions are not consulted when a segment is evaluated, only the unsubscribed flag. They are enforced when mail is sent, so a count can include an address that will be skipped at send time.
topic_key is checked when you save: a key that names no topic in the workspace is refused with 422 unknown_topic. The property in a filter is not checked against what contacts carry. A typo there is stored as written and quietly matches nobody (or, for not_equals and not_exists, everybody). Count the segment after creating it. A property that no contact can carry, because it is empty, contains . or a null character, or starts with $, is refused with 422.
Property filters
Each entry in properties tests one value in a contact's attributes.
| Field | Type | Required | Description |
|---|---|---|---|
property | string | yes | The key in the contact's attributes. It does not have to be declared as a contact property, though declaring one is what gives the values a type. Cannot be empty, contain . or a null character, or start with $. |
operator | "equals" | "not_equals" | "contains" | "gt" | "lt" | "exists" | "not_exists" | yes | How to compare. |
value | string | number | boolean | no | What to compare against. Leave it out only for exists and not_exists. Left out of another operator, it compares against null: equals then matches contacts without the attribute, not_equals matches contacts with it, gt and lt match nobody, and contains matches every string value. |
| Operator | Matches contacts whose attribute |
|---|---|
equals | is exactly value. The comparison is type-sensitive: 42 does not equal "42", and true does not equal "true". |
not_equals | is anything other than value, including contacts who do not have the attribute at all. |
contains | is a string containing value as text, ignoring case. value is matched literally, not as a pattern. Numbers and booleans never match. |
gt | is greater than value. |
lt | is less than value. |
exists | is present and not null. value is ignored. |
not_exists | is missing or null. value is ignored. |
gt and lt only compare values of the same kind: a number with a number, a string with a string. Strings compare character by character, so "9" is greater than "42". This is why a property's type matters: declare numeric properties as number so imported values are stored as numbers, and store dates as ISO 8601 strings so they sort in time order.
The segment object
Every field is always present. A rule that is not set is null, or [] for the list rules, so creating, retrieving, listing and updating a segment all return exactly the same fields.
| Field | Type | Description |
|---|---|---|
id | string | The segment's id, a UUID. Pass it as segment_id when creating a campaign. |
key | string | The key given at creation. |
name | string | Display name. |
audience_id | string | The audience the segment filters. |
opened_within_days | integer | null | The opened_within_days rule. |
clicked_within_days | integer | null | The clicked_within_days rule. |
exclude_unengaged_days | integer | null | The exclude_unengaged_days rule. |
engaged_within_days | integer | null | The engaged_within_days rule. |
properties | array of object | The property filters, each with property, operator and value. value is null when none was given, as for exists and not_exists. [] when there are none. |
tags | array of string | The tags rule, normalised. [] when not set. |
exclude_tags | array of string | The exclude_tags rule, normalised. [] when not set. |
topic_key | string | null | The topic_key rule. |
created_at | string (ISO 8601) | When the segment was created. |
updated_at | string (ISO 8601) | null | When the segment was last updated. null until the first update. |
List segments
GET /v1/segments · requires contacts:read
Every segment in the workspace, sorted by name, up to 500. There is no paging: next_cursor is always null, and has_more is true when 500 segments came back, which means there may be more than this endpoint can return.
Example
Response
200 OK
Create a segment
POST /v1/segments · requires contacts:write
Saves a segment over one audience. Nothing is evaluated at this point; count it afterwards to check it matches who you expect.
name does not have to be unique, but key does, within the workspace: saving a second segment with a key already in use answers 409 conflict. Keep the returned id, which is what campaigns and every other segment call take.
Body
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
audience_id | string | yes | The id of the audience to filter, from GET /v1/audiences. Must be an audience in this workspace. Surrounding whitespace is ignored. | |
key | string | yes | A short identifier, unique in the workspace: lower-case letters, digits and hyphens only, at least one character. | |
name | string | yes | Display name, 1 to 120 characters. | |
tags | array of string | no | Match only contacts carrying all of these. Up to 20, each at least 1 character. | |
exclude_tags | array of string | no | Leave out contacts carrying any of these. Up to 20, each at least 1 character. | |
opened_within_days | integer | no | Only contacts who opened within this many days. 1 to 3650. | |
clicked_within_days | integer | no | Only contacts who clicked within this many days. 1 to 3650. | |
exclude_unengaged_days | integer | no | Leave out contacts who have been mailed but neither opened nor clicked within this many days. Contacts added within the window, and contacts never mailed, are kept. 1 to 3650. | |
engaged_within_days | integer | no | Only contacts who opened, clicked, or were active in your product (last_active_at) within this many days. 1 to 3650. | |
properties | array of object | no | Up to 20 property filters, all of which must hold. | |
topic_key | string | no | Only contacts subscribed to the topic with this key. Must name an existing topic. Surrounding whitespace is trimmed, and "" means no topic rule. |
Example
Response
201 Created
A segment object. Rules you did not set are null, or [] for the list rules.
Errors
| Status | Type | When |
|---|---|---|
| 422 | invalid_request | A field fails validation (message Validation failed, with details). |
| 422 | invalid_request | audience_id is not an audience in this workspace. The message starts audience_id:, and when the value matches an audience's name it gives that audience's id. |
| 422 | unknown_topic | topic_key names no topic in this workspace. The message starts topic_key:. |
| 409 | conflict | Another segment in the workspace already uses this key. |
Retrieve a segment
GET /v1/segments/{id} · requires contacts:read
Returns one segment's definition. Passing a segment's name or key instead of its id answers 404, and when it matches a segment in the workspace (case-insensitively) the message gives its id.
Path parameters
| Parameter | Description |
|---|---|
id | The segment's id. |
Example
Response
200 OK
Errors
| Status | Type | When |
|---|---|---|
| 404 | not_found | No segment with that id in this workspace. |
Update a segment
PATCH /v1/segments/{id} · requires contacts:write
Changes any of a segment's fields. Send only what should change; everything else stays as it was. Each field you send replaces the stored value whole: a properties array replaces every existing filter, and tags replaces the whole tag list.
What an update can and cannot do:
- Clear a list rule by sending an empty array:
"tags": [],"exclude_tags": []or"properties": []. - Clear the topic rule by sending
"topic_key": "". An empty key is treated as no topic, and reads back asnull. Any othertopic_keymust name an existing topic. - Clear an engagement window by sending
null:"opened_within_days": null,"clicked_within_days": null,"exclude_unengaged_days": nullor"engaged_within_days": null. It reads back asnull. keycannot be changed. A body that containskey, even with the current value, is refused with422 invalid_request(key: a segment's key cannot be changed; create a new segment) and nothing is changed. To use a different key, create a new segment.- Moving the segment to another audience with
audience_idis allowed. A campaign already using the segment is not re-checked, so its segment can then filter a different audience from the campaign's own.
A campaign that has already started sending has frozen its recipients and is not affected by an update.
Path parameters
| Parameter | Description |
|---|---|
id | The segment's id. |
Body
All fields are optional. Each has the same type and limits as on create.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
audience_id | string | no | Move the segment to this audience. Must be an audience in this workspace. Surrounding whitespace is ignored. | |
name | string | no | 1 to 120 characters. | |
tags | array of string | no | Replaces the tags rule. Up to 20. | |
exclude_tags | array of string | no | Replaces the exclude_tags rule. Up to 20. | |
opened_within_days | integer | null | no | 1 to 3650, or null to remove the rule. | |
clicked_within_days | integer | null | no | 1 to 3650, or null to remove the rule. | |
exclude_unengaged_days | integer | null | no | 1 to 3650, or null to remove the rule. | |
engaged_within_days | integer | null | no | 1 to 3650, or null to remove the rule. | |
properties | array of object | no | Replaces every property filter. Up to 20. | |
topic_key | string | no | Replaces the topic rule with an existing topic's key; "" removes it. |
Example
Response
200 OK
The updated segment object, now with updated_at set.
Errors
| Status | Type | When |
|---|---|---|
| 404 | not_found | No segment with that id in this workspace. |
| 422 | invalid_request | The body contains key. The message is key: a segment's key cannot be changed; create a new segment. Nothing is changed. |
| 422 | invalid_request | A field fails validation (message Validation failed, with details). |
| 422 | invalid_request | audience_id is not an audience in this workspace. The message starts audience_id:. |
| 422 | unknown_topic | topic_key names no topic in this workspace. The message starts topic_key:. Nothing is changed. |
Delete a segment
DELETE /v1/segments/{id} · requires contacts:write
Deletes the segment's definition. No contacts are touched.
A campaign that still refers to a deleted segment through segment_id resolves to nobody when it sends. Delete a segment only once no draft or scheduled campaign uses it.
Path parameters
| Parameter | Description |
|---|---|
id | The segment's id. |
Example
Response
200 OK
Errors
| Status | Type | When |
|---|---|---|
| 404 | not_found | No segment with that id in this workspace. |
Get segment metrics
GET /v1/segments/{id}/metrics · requires contacts:read
How many contacts the segment matches right now. Run it before building a campaign around a segment: a filter that matches nobody, or everybody, is much easier to spot here than after a send. The segment is evaluated in full on every call, so on a large audience this is not a cheap request.
Path parameters
| Parameter | Description |
|---|---|
id | The segment's id. |
Example
Response
200 OK
| Field | Type | Description |
|---|---|---|
id | string | The segment's id. |
contact_count | integer | Contacts the segment matches at the time of the request. |
Errors
| Status | Type | When |
|---|---|---|
| 404 | not_found | No segment with that id in this workspace. |
List the contacts in a segment
GET /v1/segments/{id}/contacts · requires contacts:read
The email addresses the segment matches right now, one page at a time. data is an array of address strings, not contact objects; look a person up with GET /v1/contacts?email= for the rest.
The list is cursor-paged like every other (pagination): newest contact first, by when the contact was created, the same order as GET /v1/contacts. Pass next_cursor back as cursor while has_more is true. Membership is worked out on each call, so a contact who stops matching (or starts) between two pages drops out of (or joins) the later pages. For the number of matches, use metrics; this list carries no count.
Path parameters
| Parameter | Description |
|---|---|
id | The segment's id. |
Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | integer | 200 | Page size, 1 to 1000. A larger value returns 1000, a fraction is rounded down, and a missing, zero, negative or non-numeric value uses 200. This list's range is larger than the other lists' because an address is small. |
cursor | string | none | The next_cursor from the previous page, passed back unchanged. An unreadable cursor returns the first page. |
Example
Response
200 OK
Errors
| Status | Type | When |
|---|---|---|
| 404 | not_found | No segment with that id in this workspace. |