Skip to main content

Subscriptions API

Devices. Public or secret key, scope subscriptions:write. These are the calls the SDKs make; you only need them for custom clients or imports.

POST /v1/subscriptions — register (upsert)

Rate-limited per client IP. Upserts by (appId, token) for mobile or (appId, endpoint) for web, so calling it on every launch is correct.

Mobile body

{
"platform": "ios", // or "android"
"token": "<APNs device token / FCM registration token>",
"externalId": "user_123", // optional → login at subscribe time
"tags": { "plan": "premium" }, // optional, merged into the user
"language": "tr", "timezone": "Europe/Istanbul", "country": "TR",
"appVersion": "3.4.1", "sdkVersion": "0.1.0", "osVersion": "17.5", "deviceModel": "iPhone 15 Pro"
}

Web body

{
"platform": "web",
"endpoint": "https://fcm.googleapis.com/fcm/send/…",
"keys": { "p256dh": "…", "auth": "…" },
"browser": "chrome",
"standalone": true, // installed as a PWA
"externalId": "user_123", "tags": {}, "language": "en", "timezone": "Europe/London"
}

Response 200

{ "id": "66f1…", "platform": "ios", "userId": "66f2…" }

Re-subscribing revives an opted-out or invalidated row. If externalId is given the identity rules run exactly as for login.

PATCH /v1/subscriptions/:id — device fields

{ "optedIn": false, "appVersion": "3.5.0", "osVersion": "18.0", "deviceModel": "…" }

Device-level only. Anything about the person goes through /user below. 200 { "id", "optedIn" }.

POST /v1/subscriptions/:id/session — the app came to the foreground

{ "appVersion": "3.5.0", "osVersion": "18.0" }

Body optional. Refreshes lastActiveAt and, if the previous session started 30 minutes or more ago, increments sessionCount and sets lastSessionAt — the OneSignal session rule, applied on the server so the SDKs can ping on every foreground without counting twice. POST /v1/subscriptions (the upsert every SDK runs at launch) applies the same rule, so an app on an older SDK still gets counts, just coarser ones.

200 { "id", "sessionCount", "lastSessionAt" }. The counters are segment fields: sessionCount gte 5, lastSessionAt lt -30d.

POST /v1/subscriptions/:id/login

{ "externalId": "user_123" }

200 { "userId": "…", "externalId": "user_123" }. Renames the anonymous user or moves the device — see Identity.

POST /v1/subscriptions/:id/logout

No body. 200 { "userId": "<new anonymous user>" }.

PATCH /v1/subscriptions/:id/user — owning user's properties

Also available as POST (Android's HttpURLConnection cannot send PATCH). Same body as PATCH /v1/users/:id:

{ "tags": { "plan": "platinum", "streak": null }, "language": "tr", "timezone": "Europe/Istanbul", "country": "TR" }

Tags merge; null removes a key. The write reaches every device of the user. 200 { "userId", "tags" }.

DELETE /v1/subscriptions/:id

204. Removes the device; an anonymous owner with no devices left is removed too.

POST /v1/subscriptions/rotate — web endpoint changed

Sent by the service worker on pushsubscriptionchange:

{ "old": "https://…/old-endpoint", "new": { "endpoint": "https://…/new", "keys": { "p256dh": "…", "auth": "…" } } }

200 { "id" } — the row is updated in place, the subscriber is not lost. 404 if the old endpoint is unknown.

Errors

404 not_found for an unknown or foreign id (an id from another app is indistinguishable from a non-existent one), 422 invalid_body with field details, 429 rate_limited.