Users and subscriptions
OpenPush separates the person from the device. A user is someone you know by an external ID; a subscription is one addressable install — a phone, a tablet, a browser — holding one push token. One user commonly owns several subscriptions, and a subscription can exist with no user attached at all.
Everything about targeting, identity, opt-out and delivery statistics rests on that split, so it is worth getting exactly right.
When to use
Read this before wiring login into your app, before writing a segment that filters on tags, and before interpreting a status number in the console or an export.
The model
| Concept | What it is | Identified by |
|---|---|---|
| Subscription | One device or browser install with one push token | Its own subscription id, plus the token |
| User | The person those installs belong to | Your external_id |
| External ID | Your own user identifier — the id your backend already uses | A string you choose |
| Alias | A second label the server can address the same user by | A label: id pair |
| Tag | A string key/value on the user, used by segments | Key name |
A subscription with no external ID is anonymous: perfectly targetable, countable, and sendable, just not tied to a person. Attaching an external ID later does not create a new subscription; it claims the existing one.
Tags
Tags are string-to-string, on every SDK, with no numeric, boolean or date variants anywhere. Two rules follow from that:
- An empty string means delete.
""is how "remove this tag" is distinguished from "leave it alone". - The whole map rides every registration. A token refresh cannot lose your tags, and a removed key keeps riding as
""for the rest of the session so a re-registration cannot resurrect it.
Segments filter on tags; see segments.md.
Aliases
An alias is a second addressable label beside the external ID — a CRM id, a loyalty number, a player id.
- The cap is 10 pairs per user.
- The label
external_idis reserved and refused. - Refusals do not fail the request. A registration that includes an over-cap or reserved label returns
200with analiases_refusednote listing the labels it declined, and the rest of the registration proceeds. - Deleting an alias, like a tag, is an empty-string value.
Registering a subscription
Devices register themselves through the SDK. The underlying call is:
Code
Code
It is an idempotent upsert keyed on the token, so re-registering the same device updates it rather than duplicating it. platform defaults to android when omitted — a real trap for web integrations, which must send "platform": "web" explicitly.
The response can also carry not_collected (categories dropped by a collection switch), aliases_refused, and identity_rejected. The complete body reference is in ../api-handbook/03-subscriptions-users.md.
Registering also has side effects worth knowing: it may count a session, it bumps the user's local-hour activity histogram (which feeds best-hour delivery), and it fires the journey session trigger.
The login flow, end to end
In the app
Code
A login issued before a push token exists is not lost: the identity is stored and applied to the next registration, including one caused by a token refresh.
On the wire
The external_id field is tri-state, and the distinction is load-bearing:
external_id in the body | Meaning |
|---|---|
| Absent | No claim — leave whatever identity the subscription already has |
| A non-empty string | Claim this subscription for that user |
Present and empty ("") | Detach — this is what logout means |
On your server
If identity verification is on, your backend computes the auth hash and hands it to the app. The app never computes it.
Signing out cleanly
logout() detaches the identity but does not delete the subscription or the token: the device stays registered and reachable, just anonymous. If your app supports account switching, retire the previous account's delivery address on your side before linking a new one — otherwise two accounts end up sharing one install's history.
Identity verification
Identity verification is an optional per-app setting. Turn it on and OpenPush stops taking a device's word for who it is.
How it works. When the setting is on, any registration that claims an external_id, tags or aliases must also send:
Code
computed on your server. The client never sees the REST key. The hash is checked against every active REST API key for the app, so multiple REST keys are interchangeable and a rotation does not invalidate hashes generated moments earlier.
The failure mode is a downgrade, not a rejection. This is the single most important behavior on this page. A registration with a missing or wrong hash is not rejected:
- The identity fields — external ID, tags, aliases — are stripped.
- The device still registers, anonymously.
- Push delivery to that device is unaffected.
- The response carries an
identity_rejectednote explaining why.
The reasoning is operational: rejecting would break every device on a released binary the moment an admin flips a switch or a backend deploys a hash bug. Silently succeeding would be worse, so the device is registered but the claim is refused and named.
Every SDK surfaces this as a distinct result — never as success. The Android and iOS SDKs report IdentityRejected(note); Unity reports Success == false with a non-null IdentityRejectedNote. Treat it as a failure in your own UI: do not show the user as identified.
Hash lifetime. The auth hash is a bearer proof of identity. No SDK persists it — the external ID survives a cold start but the hash does not, so call login again at launch with a freshly supplied hash.
Turning the setting on or off is a console action on the app's settings.
Subscription status
Status is a single integer that answers "should this subscription receive a push, and if not, why not". Positive means yes.
| Status | Name | Written by | Meaning |
|---|---|---|---|
any positive up to 511 | Subscribed | Device | Targetable. On iOS the value is an authorization bitmask |
1 | Subscribed | Device | The plain "yes" that Android and Unity report |
0 | Never Subscribed | Device | Permission not granted |
-2 | Unsubscribed | Device or provider | The user opted out in your app, or a provider rejected the token as bad |
-10 | Uninstalled | Provider only | The push provider reported the token gone — uninstalled or expired |
-18 | Never Prompted | Device | The permission prompt has not been shown |
-19 | Never Answered | Device | The prompt was shown and dismissed without an answer |
-22 | Dashboard Disabled | Console only | An operator disabled it |
-31 | Disabled via REST API | API only | Disabled through an admin call |
-99 | Never Subscribed | Legacy | A value seen in older exports |
The iOS authorization bitmask
On iOS, a positive status is a bitmask, not a rank:
| Bit | Meaning |
|---|---|
1 | Badge |
2 | Sound |
4 | Alert |
8 | CarPlay |
16 | Critical |
32 | App settings |
64 | Provisional |
128 | Announcement |
256 | Time sensitive |
Bits combine, and the maximum accepted value is 511. Provisional is bit 64, and it is a positive value — a provisionally authorized device is subscribed and fully targetable, receiving notifications quietly to Notification Center. There is no negative "provisional" code.
What a device may and may not send
A device may report any positive value up to 511, or 0, -2, -18, -19. It may not claim these, and an attempt returns 400 naming the owner of the code:
-10— the push provider writes this, not a device-22— the console writes this-31— the admin API writes this-50— not a status code at all; provisional is bit64- Anything above
511
The status field must be a genuine JSON integer. A float, a boolean or a numeric string is rejected.
Omitting status entirely leaves the stored value unchanged — which is what a registration that only updates tags should do.
Lifecycle
A subscription's life runs like this:
- Created at first registration, with whatever status the device reports. Often
-18or0before the prompt, then a positive value after a grant. - Updated on every later registration — token refresh, permission change, tag write, login, logout. Same row, keyed on the token.
- Opted out to
-2when the user turns push off in your app. The subscription and token are kept; the state is durable across process death and reversible with an opt-in. - Marked unreachable to
-10when APNs or FCM reports the token gone, or-2when APNs reports a bad device token. Only the provider writes these two, and only these two provider outcomes ever change a status. - Disabled to
-22or-31by an operator or an admin call.
Two guarantees hold through all of it:
- A configuration fault — a bad APNs key, a failed OAuth refresh — stops the fan-out and touches zero subscriptions. A broken credential can never mass-unsubscribe your audience.
- A departure date is never moved forward. Once a subscription is marked unsubscribed, a later failure does not rewrite when that happened.
Retryable provider responses (429, 5xx, transport errors) are recorded on the delivery row only and never on the subscription.
Sessions
A session is an app-foreground observation. SDKs send one automatically on cold launch and on foreground transitions, throttled to at most one request per 30 minutes and persisted across restarts.
Code
Code
counted is true only when more than the server's session gap — 30 minutes by default — has passed since the user's last session. An uncounted ping still updates the subscription's last-seen time.
A counted session increments the user's session count, sets their last-session time, and bumps their local-hour activity histogram. Together with first-ever clicks, that histogram is the training signal behind best-hour delivery. A device that never posts sessions still receives push, but contributes nothing to per-user send-time prediction.
Reading users and subscriptions
Both list routes take the REST API key.
Code
searchon users covers the external ID and the raw tag data; on subscriptions it covers token, external ID and subscription id.limitdefaults to 100 for users and 200 for subscriptions, with a hard ceiling of 1000 on both.- Push tokens are truncated to the first 20 characters on both list routes. The full token appears only in the admin NDJSON export.
?test=1on the subscriptions route restricts the result to test devices.
Pagination
There are two modes, chosen explicitly:
| Mode | How to ask | Behavior |
|---|---|---|
| Newest first | No cursor, no order | Ordered by most recent activity, no cursor returned — the ordering is neither unique nor stable, so a cursor would lie |
| Keyset walk | ?order=id, or any ?cursor= | Ascending on the primary key with an opaque cursor, stable across a table that is changing under you |
A short page means the walk is finished. A malformed cursor is a 400. Offset pagination is deliberately not offered, because it silently skips rows on a moving table.
Test subscriptions
You can nominate a device as a test subscription. Test devices ignore quiet hours and the frequency cap on every send, and anything currently held for quiet hours on that device is released the moment you add it. That makes them right for verifying a campaign and wrong for measuring one. See ../api-handbook/03-subscriptions-users.md.
Data collection
Three fields are gated by per-app collection switches: advertising id, location (latitude/longitude), and email. When a switch is off, values for that category are dropped, not rejected — a 400 would break every device on a released binary the instant an admin flips the switch. The dropped category names come back in not_collected.
Turning a switch off also erases the data already stored for that category, and the settings response reports how many rows were purged.
IP storage is a server-wide setting with three modes: store the full address, store a truncated one, or store nothing. The current mode is reported on the app's settings.
Limits
- Aliases: 10 per user,
external_idreserved. - Tag values are strings only.
- Status must be an integer, at most
511, and cannot claim a provider-owned or operator-owned code. - List routes truncate tokens; only the NDJSON export carries full ones.
- No offset pagination; page size is capped at 1000.
- Identity verification downgrades rather than rejecting — build your UI around that.
- The auth hash is never persisted by any SDK.
- There is no rate limit on registration or on sessions, but the default request body cap applies.
- Email, SMS and other channels are not deliverable platforms in OpenPush; the deliverable platforms are
ios,androidandweb.
FAQ
If a user installs on three devices, is that one user or three? One user, three subscriptions. Attach the same external ID from each install.
What happens to tags when I call logout?
The subscription is detached from the identified user and moved to a fresh anonymous one, so it no longer carries that person's tags — tags live on the user record. The subscription keeps its own state, including its status and token, and the SDK keeps its pending tag map for the process session.
A device shows as anonymous even though I called login. Why?
Almost always identity verification with a missing or wrong auth hash. Check the identity_rejected note; the device registered, the claim did not.
Can I set a subscription to -10 myself?
No. -10 is provider-written. -2 is the code for a user-initiated opt-out.
Why did my whole audience not get marked unreachable when my APNs key expired? By design. Configuration faults stop the send and change nothing; only genuine per-device provider verdicts change a status.
Does a provisional iOS device count as subscribed?
Yes. Provisional is bit 64 of a positive status.
Why is my web device showing platform android?
platform defaults to android when the field is omitted. Send it explicitly.
Related
- sdk-android.md
- sdk-ios.md
- sdk-unity.md
- web-push.md
- segments.md
- sending-messages.md
- Best-hour delivery
- security-and-limits.md
- import-export.md
- ../api-handbook/03-subscriptions-users.md
- ../api-handbook/01-apps-keys-settings.md