Segments
A segment is a saved set of rules that describes an audience — "people who last opened the app more than 30 days ago", "gold-tier users in India", "devices within 5 km of the stadium". Segments are evaluated live at send time, never frozen into a list, so a segment you built last month targets today's audience.
All examples use https://app.openpush.ai, the OpenPush API base URL.
When to use this page
Use segments whenever the audience is a rule rather than a person. For sending to one known user or
device, skip the segment and use the message's target object instead — see
sending messages.
Prerequisites
- The app's REST API key.
- Tags, languages and countries on your users. A segment can only filter on data that has actually
been reported: a
tiertag rule matches nothing until your app callsaddTag("tier", …).
What is always true
Two things are hard-wired into every segment query and are not filterable:
- Only sendable subscriptions are ever matched. A subscription must have a positive status and must not have been retired. There is deliberately no "subscription status" filter field — an unsubscribed device cannot be targeted by any segment.
- Rules are evaluated over subscriptions, joined to their user. A segment's headline count is a count of devices, and a user with three devices contributes three.
The filter vocabulary
Ten fields, seven operators, and no others. GET /v1/apps/{app_id}/segments returns this same
table as a machine-readable fields array, so you can build a rule editor from it.
| Field | Reads from | Operators | Value |
|---|---|---|---|
first_session | user | greater, less | Hours ago (number) |
last_session | user | greater, less | Hours ago (number) |
session_count | user | greater, less, is | Number |
tag | user | is, is_not, exists, not_exists, greater, less | Needs key and, except for existence checks, value |
country | user | is, is_not | Country code, e.g. IN (case-insensitive) |
language | user | is, is_not | Language code, e.g. en |
location | subscription | within | Distance in metres, plus lat and lng |
device_type | subscription | is, is_not | Android or iOS (case-insensitive) |
app_version | subscription | is, is_not | e.g. 1.97 |
test_users | test devices | is | "true" or "false" |
The user vs subscription column matters more than it looks. Country, language, tags and session history live on the person, so a rule on them selects all of that person's devices together. Platform, app version and location live on the individual device, so a rule on them can select the phone and not the tablet.
Operator semantics
first_session/last_sessiontake hours ago, not a date.greatermeans further in the past ("last seen more than N hours ago");lessmeans more recent. Both require the user to have a session on record at all — a user who has never had one matches neither.is_nottreats a missing value as "is not". A user with nocountrymatchescountry is_not FR. This is usually what you want for exclusions and occasionally surprising.device_typecompares case-insensitively against the subscription's platform.countrycompares upper-cased on both sides, soin,InandINare the same rule.tagwithgreater/lesscasts the tag value to a number. A tag whose value is a word simply fails to match rather than erroring — tag values are strings on every SDK, so this is a routine situation, not an edge case.tagkeys must be[A-Za-z0-9_.-]with no empty dot segment.locationis a great-circle distance in metres from the given latitude and longitude. Devices with no stored coordinates never match. Location capture is gated by the app's data-collection switch — if it is off, no device has coordinates.test_usersmatches devices registered as test subscriptions. A value offalse,0ornoinverts it, which is how you exclude your own handsets from a campaign's statistics.
Rule shape: AND within a group, OR between groups
rules is a flat array of row objects. Each row may carry a group label. Rows in the same group
are AND'd; groups are OR'd together. That is exactly two levels — there is no deeper nesting and
no way to express it.
Code
That reads: (tier is gold AND country is IN) OR (tier is platinum).
Rows with no group all collapse into one group, which gives you plain all-AND behaviour — the
common case, and what you get if you never think about grouping.
Empty rules match everyone who is sendable, and the segment's human-readable description reads "Default — every subscribed device".
Creating a segment
Code
Code
| Body field | Type | Required | Default |
|---|---|---|---|
name | string | yes | — |
rules | array | no | [] (everyone) |
status | Active | Paused | no | Active |
is_target_default | boolean | no | false |
The response always carries three computed extras: rules parsed back out, rule_text — a plain
sentence describing the rules, useful for confirmation screens — and subscriptions, the live
count.
Statuses
| Status | Meaning |
|---|---|
Active | Normal. Available for targeting |
Paused | Kept, with its rules intact, but parked |
Pausing preserves the configuration; it is a way to retire a segment without losing the rule you
spent an afternoon getting right. Only Active and Paused are accepted — anything else is a
400.
Default segments
Every new app is created with two:
| Name | Rules |
|---|---|
| Total Subscriptions | None — every sendable device |
| Active Subscriptions | Last session less than 720 hours ago (the past 30 days) |
Both are managed: DELETE on them returns 409 managed segments cannot be deleted. You can
still pause or rename them.
Total Subscriptions also starts as the target default — the segment pre-selected when
composing. Exactly one segment holds that flag at a time. Setting is_target_default: true on
another segment moves it; you cannot clear it, only hand it over, and trying returns
400 set another segment as default instead of clearing the current default. If you delete the
segment that holds it, it falls back to Total Subscriptions, or to the oldest segment.
Recipes
1. Lapsed — no session in 30 days
The classic win-back audience. Hours, not days: 30 × 24 = 720.
Code
Pair it with an exclusion for people you contacted last week, using a tag your backend sets.
2. High-value customers by tag
Code
Tag comparisons are exact string matches, so Gold and gold are different values. Normalise on
the way in.
3. Gold or platinum
Two groups, OR'd:
Code
There is no in or "one of" operator, so a set membership test is one group per value.
4. Country and language together
Code
No group on either row, so both are AND'd. Use this to gate a translated campaign to the country
it makes sense in, rather than to everyone who has their phone set to Hindi.
5. Near a venue
Everyone within 5 km of a stadium:
Code
value is metres and must be greater than zero; latitude must be between −90 and 90 and longitude
between −180 and 180. Only devices that reported coordinates can match, and only if the app's
location collection switch is on.
6. Engaged power users
Code
Fifty-plus sessions and seen in the past week. session_count treats a missing value as zero, so
a brand-new user is never accidentally included.
7. Numeric tag threshold
Code
Even though tag values are strings, greater and less compare them numerically. A user whose
lifetime_spend is "free trial" fails the comparison rather than breaking the query.
8. New users who have not been onboarded
Code
First seen in the last two days, and the onboarded tag has never been set. exists and
not_exists need only a key — no value.
9. One platform, one build
Code
Both fields are per-device, so this selects the specific handsets on that build, not everyone who owns one. Useful for telling a stuck cohort to update.
10. Exclude your own test handsets
Code
Worth adding to any segment you use for reporting, because test devices bypass the frequency cap and quiet hours on every send and will otherwise skew small campaigns.
Using segments in a send
Code
- Includes are OR'd. A device in either segment is in the audience.
- Excludes are subtracted — the OR of the exclude segments is removed from the result.
- An empty
include_segmentsmeans every sendable device in the app. - An unknown segment id is
400 unknown segment '<id>'.
Paused segments still resolve if you name them explicitly; pausing is an organisational state,
not an enforcement mechanism.
Counts and estimates
Counts are exact. count(*) over the matching subscriptions, computed when you ask, with no
sampling, no estimator, no cached approximation and no "estimated" label anywhere in the product.
The trade-off is honesty over speed: a complex rule over a very large audience is a real query.
Two numbers exist. The segment row's subscriptions field counts devices. Elsewhere the
product can also report distinct users for the same rule — a household with a phone and a
tablet is two subscriptions and one user. When you are budgeting a campaign, subscriptions is the
number that predicts sends; users is the number that predicts how many people you annoy.
To size an audience the way a send will actually see it — including exclusions, quiet-hours holds
and frequency caps — use POST /v1/apps/{app_id}/audience-preview instead of adding up segment
counts. It runs the same audience resolution and the same planner the send runs.
Managing segments
Code
A patch revalidates the rules, and an empty patch is 400 nothing to update.
Validation errors
Every rule problem comes back as a 400 with a specific message:
| Message | Cause |
|---|---|
unknown field '<f>' | Not one of the ten fields |
<what> needs a number, got <v> | A numeric field or distance got a non-number |
<field> supports greater|less | Wrong operator for a session-time field |
session_count supports greater|less|is | Wrong operator |
<field> supports is|is_not | Wrong operator for country, language, device type or app version |
location supports is within | Location accepts only within |
location distance must be greater than 0 meters | Zero or negative radius |
location latitude must be between -90 and 90 | Out-of-range latitude |
location longitude must be between -180 and 180 | Out-of-range longitude |
tag needs a key of [A-Za-z0-9_-.] with no empty .segment | Bad or missing tag key |
tag supports is|is_not|exists|not_exists|greater|less | Wrong operator for a tag |
unknown segment '<id>' | A send named a segment that does not exist |
Limits
- Two levels of logic only — AND within a group, OR between groups. Nested parentheses are not representable.
- No set-membership operator. No
in, noone of; use one OR group per value. - No "is in segment X" relation. Segments cannot reference other segments; combine them with
include_segmentson the send instead. - No subscription-status field. Sendability is enforced, not filtered.
- No event-based rules. Custom events do not reach segments at all — they trigger journeys. Behavioural targeting is not expressible here.
- No message-engagement rules — you cannot segment on who opened or clicked a previous campaign.
- No segment duplication route. Read one and create a copy.
- No count estimator and no "estimated" labelling — counts are exact by design.
- No scheduled or snapshot segments. Every segment is live.
FAQ
Does a segment freeze its members when a message is scheduled? No. The audience is resolved when the send fires, so a scheduled campaign targets whoever matches at that moment.
Why does my tag rule match nobody? Usually one of three things: the tag was never actually set (check a user record), the value differs in case, or the value is an empty string — which the SDKs treat as deletion, so the tag does not exist at all.
How do I target "everyone except X"?
Put X in exclude_segments on the send and leave include_segments empty. Building it as a rule
also works, but exclusion at send time is clearer and reusable.
Why did country is_not FR include people with no country?
By design — a missing value counts as "is not". If you need "has a country, and it is not France",
add a positive condition alongside it.
Can I filter on whether someone received a specific message? No. That data exists on the message report, not in the segment vocabulary.
Do paused segments still count toward anything? No quota exists to count against. Pausing simply marks a segment as not in current use.
Related
- Sending messages — includes, excludes and direct targeting
- Users and subscriptions — where tags and session data come from
- Personalization — using the same tags in copy
- Events — why events do not appear here
- Segments API reference