Migrating from OneSignal
Moving an app off OneSignal means bringing four things across: the subscriber list, the historical record (messages, templates, segments), the client SDK, and any server code that calls the sending API. OpenPush handles the first two with dedicated importers, gives the third a source-compatibility layer so most call sites compile unchanged, and covers the fourth partially — see What is and is not API-compatible, which is the section to read before you plan the cutover.
The most important property of the migration: Android devices that have not yet taken your OpenPush SDK update keep rendering pushes, because imported Android subscriptions are sent with FCM's standard notification block — and Firebase itself renders that block on the device, whichever push SDK the app was built against. No OneSignal code is involved in the legacy path, and nothing about it is provided or endorsed by OneSignal. That gives you a real overlap window instead of a hard switch, but it rests on device-side behaviour OpenPush does not control, so confirm it against your own build before planning a cutover around it.
OneSignal is a trademark of OneSignal, Inc. OpenPush is an independent project and is not affiliated with, endorsed by, or sponsored by OneSignal, Inc.; references here are for identification and interoperability only.
All examples use https://app.openpush.ai, the OpenPush API base URL.
Migration order
- Pull the subscriber list.
- Pull the history — messages, templates, segments.
- Rebuild segment rules for anything that came in as a shell.
- Configure APNs and FCM credentials on the OpenPush app.
- Ship an app update with the OpenPush SDK.
- Repoint your backend at the OpenPush API.
- Send to the migrated Android population while the SDK update rolls out.
Pulling the subscriber list
The subscriber pull runs from the console, under the app's migration section. It needs two things from OneSignal:
- Your OneSignal app id.
- A OneSignal REST API key with export permission — a key you are authorised to use.
Before you start. Confirm that your own OneSignal agreement, including any negotiated enterprise terms, permits exporting your data this way. The pull uses only OneSignal's official, published REST endpoints — their CSV Export API, and the documented notifications, templates, and segments read endpoints — with the credentials you supply, to retrieve data you own. It does not scrape the dashboard, walk an undocumented endpoint, or read anything beyond the export feature OneSignal built for this purpose. Your contract with them still governs, and only you can see it.
Note. The REST key is used for the duration of the pull and is never persisted. Only the signed export URL OneSignal returns is stored, and that column is withheld from every OpenPush export. You do not need to rotate the key afterwards on OpenPush's account, though rotating it on OneSignal's side after the migration is good hygiene regardless.
Where the requests come from. Every call in the pull is made by the OpenPush server, not your browser — the export is server-to-server. The export request carries your REST key from the server's address rather than yours, and it lands against whatever quota OneSignal applies to your account. The signed URL it returns is then polled repeatedly until the file is ready: roughly once every five seconds on the scheduler tick, for up to two hours. That is a lot of requests to their infrastructure for one export, and how they are counted is OneSignal's call, not OpenPush's. If you would rather not hand a REST key to another system at all, export the CSV from the OneSignal dashboard yourself and upload the file — the column mapping below is identical and nothing else about the import changes.
What the pull does:
- Requests a player CSV export from OneSignal, asking for the extra fields
external_user_id,country,timezone_id,notification_types, and the OneSignal id. - Polls until the export is ready. A
404during this phase means "still generating", not "missing". The poll times out after two hours by default. - Streams the gzipped file, decompresses it to a spool file, and hands it to the ordinary CSV import queue — so job status, progress, and the summary object all work exactly as they do for a file you upload yourself.
- If the download is truncated, it is trimmed back to the last complete CSV record and the shortfall is reported in the summary as
partial_export.
If you would rather export from the OneSignal dashboard yourself and upload the file, that works too — the same column mapping applies.
CSV column mapping
Headers are matched case-insensitively after trimming; the first non-empty alias wins. These are the ones that matter for a OneSignal export:
| OpenPush target | OneSignal headers accepted |
|---|---|
| Push token (the upsert key) | identifier, push_token, token, device_token, registration_id |
| External ID | external_user_id, external_id, externaluserid |
OneSignal id → stored as the tag onesignal_id | id, player_id, onesignal_id, subscription_id |
| Subscription status | notification_types, notification_type, status |
| Platform | device_type, device_platform, platform, channel |
| Tags | tags (JSON object, or k=v;k=v) plus any tag_<key> column |
| Language / country / timezone | language|lang, country|country_code, timezone|timezone_id|tz |
| First / last session | first_session|created_at, last_active|last_session|last_seen |
| Session count | session_count, sessions, amount_spent_sessions |
| Device model / OS / app version | device_model|model, device_os|os_version, app_version|game_version |
| Advertising id, latitude, longitude, email (gated) | idfa|gaid|ad_id, lat|latitude, long|lng|longitude, email |
The full list, including the columns not specific to OneSignal, is in Import and export.
Your OneSignal player id survives the move as the tag onesignal_id on the subscription, so you can look a record up by its old identifier and can target legacy cohorts with a tag segment.
Status derivation
OneSignal's notification_types value is translated rather than flattened:
| Incoming value | Result |
|---|---|
| Positive and ≤511 | Kept verbatim as the iOS authorization bitmask — the granularity is preserved, not collapsed to "subscribed" |
| Positive and >511 | Normalised to 1 (Subscribed) |
invalid_identifier | -10 (Uninstalled) |
| Named negatives | Carried across: 0, -2, -10, -18, -19, -22, -31, -99 |
Text like unsub… / opt… | -2 (Unsubscribed) |
Text like never answered | -19 (Never Answered) |
Text like never / permission / denied | -18 on iOS, 0 elsewhere |
Every positive status is subscribed and targetable — that is OpenPush's rule, and it is also the convention the incoming notification_types values already encode, so an imported status carries its meaning across unchanged. On iOS the positive value doubles as the notification-authorization bitmask, and the bit layout is Apple's, from UNAuthorizationOptions — not any push vendor's: bit 1 Badge, 2 Sound, 4 Alert, 8 CarPlay, 16 Critical, 32 App Settings, 64 Provisional, 128 Announcement, 256 Time Sensitive. Preserving it means a provisional-authorized device arrives as provisional, not as generic subscribed.
Platform channels that are skipped
OpenPush delivers to three platforms: iOS, Android, and web. Rows on any other channel are recognised, skipped, and counted — they never silently become push subscriptions:
Amazon · Windows Phone · Chrome extension · Windows · Alexa · Email · Huawei · SMS
The counts land in the import summary under skipped_channels, so you can reconcile your list totals.
Note. Huawei is inconsistent by channel encoding: a numeric Huawei channel is skipped, while the literal string
huaweiin a platform column is mapped to Android. If your export uses the string form, expect Huawei rows to arrive as Android subscriptions with tokens FCM cannot deliver to.
Email and SMS are not OpenPush channels at all, on any path.
The privacy gate
Advertising id, latitude/longitude, and email pass through the same per-app collection switches the SDK obeys. If a switch is off, values in those columns are dropped, not rejected, and the categories are named in the summary under not_collected. Turn the switches on before importing if you intend to keep that data.
Pulling the history
A second console-side importer pulls your OneSignal history over their REST API. It brings three things, and explicitly not subscribers or users (that is what the CSV pull is for):
| Entity | Source | Notes |
|---|---|---|
| Messages | Their notifications list | 50 per page. Cancelled, below-threshold, and no-content sends are skipped. |
| Templates | Their templates list | Fetches per-template detail when the list omits content. |
| Segments | Their segments list | 300 per page. See the caveat below. |
Raw records are archived to disk as they arrive, and every later processing step reads those files rather than the network — so a re-run does not re-hit OneSignal's API.
Rate handling differs between the two pulls, so do not assume the history behaviour on the CSV path:
- The history pull is the patient one. It honours
Retry-Afteron a429(falling back to exponential backoff when the header is absent), retries5xxwith exponential backoff, gives up loudly after five attempts on a page rather than hammering, and paces itself with a short delay between pages. - The subscriber CSV pull has no
429handling at all, and no backoff. It requests the export once, then polls the signed URL, treating404as "still generating"; any other error status stops that pull and marks the import failed with the error text. (The one exception is a transport failure part-way through a download that has already delivered bytes — that prefix is kept and reported aspartial_export, as described above.) If OneSignal rate-limits the export request or the download, expect a failed import rather than a retry: wait, then start the pull again.
How messages are normalised
| OneSignal | OpenPush |
|---|---|
headings / contents | title / body, plus per-language variants |
big_picture, global_image, chrome_web_image, first iOS attachment | image_url |
url, app_url, web_url | deep_link |
ttl | ttl_s |
priority 10 / 5 | high / normal |
collapse_id | collapse_key |
successful, failed, errored, converted, received, remaining, platform delivery stats | Carried into the message's stored stats |
Messages and templates are deduplicated on their OneSignal source id, so a re-run updates rather than duplicates.
Segments arrive paused
Segments imported through this path arrive without their rules. As of August 2026, the segments endpoint OpenPush reads does not include rule definitions in what it returns, so segments pulled from it arrive in OpenPush as shells: status Paused, rules empty, and they are listed in the summary under needing_rules. They are deduplicated by name and an existing OpenPush segment is never overwritten. If your own export does come back carrying rules, open an issue — that is a change worth handling.
You have to rebuild those rules by hand in the OpenPush segment builder, then set the segment Active. That is deliberate — a paused, empty segment is visible and harmless, whereas an empty active segment would match everyone.
Where segment filters are available (from the chart CSV path, or filters you supply on a send), they are converted from OneSignal's flat filter list into OpenPush's grouped rules, with location filters becoming a radius rule. Filters OpenPush cannot express are counted and reported under declined_filters — they are never approximated or broadened into something that matches more people than you asked for. Check that list after every history import.
For direct message targeting, the REST and MCP APIs accept a top-level filters array. Each item uses OpenPush's saved-segment field and op vocabulary; tag predicates also require key. Predicates combine with AND by default, and {"operator":"or"} starts another OR group. The Messages API guide documents the supported fields and operators.
The legacy Android bridge
This is the part that makes an overlapping migration possible.
An Android subscription imported from a OneSignal CSV is stamped with a migration marker. When OpenPush sends to a marked subscription, it adds FCM's standard notification block (title and body) alongside the data payload — which is what a device still running the OneSignal SDK needs in order to render the push at all.
In practice:
- Ship your OpenPush SDK update whenever you are ready. You do not have to wait for adoption before you start sending.
- Devices that have updated receive the OpenPush data payload and render through the OpenPush SDK, with full click and receipt reporting.
- Devices that have not updated still see the notification, because FCM itself displays the
notificationblock.
Caveats worth knowing:
- The bridge is title and body only. Action buttons, images, and custom data are not rendered by the legacy path.
- The marker is a one-way tombstone: a later re-import cannot clear it, and it is withheld from the REST subscription view and from every export. You cannot inspect or unset it through the API.
- There is no equivalent bridge on iOS. iOS devices need the OpenPush SDK (or at minimum, an APNs alert payload) to display.
What is and is not API-compatible
Be precise here, because the compatibility surface is narrower than the SDK story suggests.
Routes that are OneSignal-shaped
Exactly two, both for Live Activities:
Code
These accept Authorization: Key <REST key> as well as X-OP-API-Key, and return the {"errors": [...]} envelope. The start route — and only the start route — also supports an idempotency_key body field (a UUID, 30-day replay window, Idempotent-Replayed: true on a hit). Both are rate limited to 60 requests per 60 seconds per app.
They also silently ignore unknown body keys, so OneSignal fields OpenPush does not implement fail quietly. Read the field tables in Live Activities rather than assuming your existing payload works.
Routes that are NOT OneSignal-shaped
There is no /api/v1 prefix, no /players route, and no OneSignal-shaped Create Message route. OpenPush's send endpoint is POST /v1/apps/{app_id}/messages and it takes title, body, and include_segments — not contents, headings, and included_segments. Your server-side send code needs rewriting. See Sending messages.
The Authorization: Key header spelling is accepted only on the two Live Activity routes. Every other admin route needs X-OP-API-Key.
Conveniences that do carry over
| Area | What is compatible |
|---|---|
| Auth header | Authorization: Key <key> on the compat Live Activity routes (also Basic, Bearer, or a bare value) |
| Error envelope | {"errors": [...]} on those routes |
| Status vocabulary | The same subscription status codes, and the invalid_identifier → -10 import rule |
| Scheduling fields | delayed_option and delivery_time_of_day are the field names on message create |
| Priority and collapse | collapse_id accepted as an alias for collapse_key; APNs 10/5 and FCM HIGH/NORMAL accepted for priority |
| Identity Verification | OpenPush verifies an HMAC-SHA256 of the external ID keyed with a REST API key. If your server already computes that construction, it carries over unchanged |
| Multi-language fallback | Base-language fallback on content (pt-BR falls back to pt) |
| SDK symbols | A source-compatibility symbol layer on all three SDKs, covering most OneSignal v5 call sites. Members with no OpenPush equivalent are compile errors, not silent no-ops |
Identity Verification
Here is OpenPush's own construction, stated in full so you can implement it without reference to anyone else's docs: your server computes HMAC-SHA256(external_id) keyed with an OpenPush REST API key, renders the digest as lowercase hex, and hands it to the client, which sends it as external_id_auth_hash alongside the identity claim. The comparison is a constant-time match on that exact lowercase-hex string, so an uppercase or base64 digest will not verify.
If your existing server code already computes exactly that, it carries over unchanged. If it computes something else — a signed token of any kind, for instance — recompute the hash server-side in the form above; there is no other accepted format.
Two OpenPush specifics:
- The hash is checked against every active REST key, so you can rotate keys without a flag day.
- A failed check is a downgrade, not a rejection. The identity fields are stripped, the device still registers, push delivery to that token continues, and the response carries an
identity_rejectednote. A signing mistake therefore costs you identity resolution, not delivery — but it does cost you that: an affected device registers anonymously and will not matchexternal_idtargeting, or carry the tags and aliases in the rejected claim, until the signing is fixed. A device already attached to a user keeps the identity it has.
SDK compatibility
All three SDKs ship a source-compatibility symbol layer that keeps the OneSignal v5 spellings resolvable, so most call sites compile unchanged. It is a symbol layer, not an emulation: it covers the call sites, not every behaviour behind them. Two design stances to be aware of:
- Unsupported members are compile-time errors carrying replacement text, not silent no-ops. If a OneSignal call has no OpenPush equivalent, your build tells you at the call site instead of shipping a no-op into production.
- The outcome methods forward to real events:
addOutcome,addUniqueOutcome, andaddOutcomeWithValueall become custom events.
In-app messaging is implemented. OpenPush ships a server-side in-app messaging engine with its own REST routes and a presenter in all three SDKs, so the compat layer's in-app members — paused, addTrigger, addTriggers, removeTrigger, removeTriggers, clearTriggers — resolve onto it and work rather than refusing at compile time. See In-app messages.
What still has no OpenPush counterpart and will not compile: the app inbox, and the consent gate (gate your own initialize call instead — nothing is collected and no request is made until you make it). Per-SDK details are in Android SDK, iOS SDK, and Unity SDK.
Terminology mapping
| OneSignal | OpenPush | Note |
|---|---|---|
| Player / device | Subscription | One addressable device record. A user may own several. |
| Player id | Subscription id | The old player id is preserved as the tag onesignal_id. |
| OneSignal id | — | No system-generated per-user public id. Use your external ID. |
external_user_id | External ID | Your stable user identifier. |
| Alias | Alias | Same idea, key/value; capped at 10 per user. external_id is reserved. |
| Data tags | Tags | Key/value on the user. String values only — no numeric, boolean, or date tag types. |
| Segment | Segment | Filter rules; AND within a group, OR between groups, two levels only. |
| Notification (the API resource) | Message | "Notification" is reserved for the on-device artifact. |
contents / headings | body / title | Plus per-language variants. |
included_segments | include_segments | On the native send route. |
| Template | Template | |
| Journey | Journey | |
| Custom event | Custom event | Feeds journey triggers only. |
| Outcome | Custom event | The SDK outcome methods forward to events. |
| Live Activity | Live Activity | |
| Delivered | Provider Accepted | OpenPush distinguishes provider-accepted, device-received, confirmed, and clicked. Do not read "delivered" as "shown". |
| Conversion / influenced open | — | Not implemented. |
| In-app message | In-app message | Implemented — see In-app messages. |
| App inbox · Email · SMS · Event stream / webhook | — | Not implemented. |
Migration checklist
- Create the OpenPush app and note its app id.
- Set the per-app collection switches (advertising id, location, email) before importing, or those columns will be dropped.
- Pull the subscriber CSV with your OneSignal app id and REST key, or upload an export yourself.
- Reconcile the import summary:
rows,imported,skipped,invalid_rows,skipped_channels,not_collected. - Pull the history: messages, templates, segments.
- Review
declined_filtersand rebuild those rules by hand. - Rebuild every segment listed under
needing_rules, then set it Active. - Upload the APNs p8 key. Upload the FCM service account under the Android platform section, not Web.
- Configure quiet hours and the frequency cap — the cap is on by default at 10 sends per device per 24 hours.
- Decide on Identity Verification and wire the HMAC on your server if you want it.
- Integrate the OpenPush SDK, keeping the OneSignal facade imports where they compile.
- Register a test device and send a test message.
- Rewrite server-side send calls against
POST /v1/apps/{app_id}/messages. - If you send Live Activities, repoint at the compat routes and confirm your body fields are actually accepted.
- Send to the imported Android population and confirm legacy-SDK devices render.
- Take an NDJSON export as a post-migration baseline — and read what it omits.
- Rotate your OneSignal REST key once the migration is complete.
FAQ
Will my OneSignal-SDK users stop receiving pushes the day I switch?
No, on Android. Imported Android subscriptions are sent an FCM notification block, which Firebase renders as a title and body on the device without involving your push SDK at all. That is standard, documented FCM behaviour rather than anything OpenPush or OneSignal controls — confirm it against your own build before you rely on it for a cutover window. iOS devices need the OpenPush SDK.
Do my segments come across working? Segments pulled through the history importer arrive paused with empty rules, because the response OpenPush receives does not include rule definitions (as of August 2026). Rebuild them and activate them.
Can I keep calling OneSignal's Create Notification API shape? No. Only two Live Activity routes are OneSignal-shaped. The send endpoint is different and your server code needs updating.
What happened to my player ids?
Each one is preserved as the tag onesignal_id on the imported subscription, so you can look records up by the old identifier or build a segment on it.
Are my email and SMS subscribers imported? No. Those channels are recognised, skipped, and counted in the import summary. OpenPush is push-only.
Related
- Import and export
- Users and subscriptions
- Segments
- Sending messages
- Live Activities
- Security and limits
OneSignal is a trademark of OneSignal, Inc. OpenPush is an independent project and is not affiliated with, endorsed by, or sponsored by OneSignal, Inc. References to OneSignal are for identification and interoperability purposes only.