Migrating a push audience off OneSignal without losing half of it
What actually transfers when you change push providers, what does not, and the two decisions that determine whether your migration is an import or a rebuild.
Changing push providers sounds like a data migration and is mostly not one. Your mobile tokens are not really your old provider's to keep, your web subscribers may be unmovable, and the part that goes wrong is almost never the CSV.
Here is the whole picture, in the order you need to make the decisions.
Step 0: find out whether your web audience can come with you
Do this before anything else, because it changes the plan rather than the schedule.
Mobile is portable. Android FCM tokens are issued by your Firebase project. Any provider holding your service account can send to them. APNs tokens are bound to your Apple team and bundle id. In both cases the old provider was borrowing credentials that belong to you, and taking them back is an upload.
Web push may not be portable. A browser subscription is cryptographically bound to the VAPID key pair that created it. If your old provider generated that pair and will not export the private half, every one of those subscriptions is permanently unreachable by anyone else. There is no workaround — we wrote about the mechanism here — and any vendor telling you otherwise is describing subscriptions that will silently fail with a 403 forever.
So: open a ticket with your current provider and ask, in writing, whether you can export your VAPID private key. Most providers, OneSignal included, do not offer this.
If the answer is no, your web migration is a rebuild: those users will resubscribe naturally as they return to your site, and how fast that happens depends entirely on your traffic pattern. Plan for it, budget for it, and do not discover it in week three.
Step 1: export, and look at what you got
A OneSignal CSV export gives you a row per subscriber with the device token, their device type, your external_user_id if you set one, tags, and — the column that matters most — notification_types.
notification_types encodes opt-out state. A negative value means that person turned notifications off.
If your import drops that column, you will send a push to people who unsubscribed, from a brand they have never heard of, on day one of your migration. That is the single worst possible opening move, and it happens because the column is not obviously important and nothing complains when you ignore it.
Import unsubscribed rows and mark them suppressed. Do not filter them out either — you want to know those people exist so you never re-add them from another source.
Step 2: keep your own user ids
Whatever you called a user in your old provider — external_user_id, external_id, an alias — map it to the same field in the new one.
This is the difference between a migration that touches your backend and one that does not. If your server code says "send to user 8812", and user 8812 still means the same thing after the move, the only change in your codebase is the SDK you import and the key you authenticate with. If you let the new provider mint fresh internal ids, you now have a join table, forever.
Step 3: run both providers at once, briefly
The safe cutover is not a switch, it is an overlap:
- Ship an app release with the new SDK alongside the old one. Both register. Users on the new build appear in both audiences.
- Import the historical CSV so users who have not updated yet are reachable from the new provider.
- Send a small real campaign from the new provider to a segment you can verify — internal staff, then 1%.
- Compare delivery numbers between the two for a week.
- Remove the old SDK in the next release.
The overlap is what makes step 4 possible, and step 4 is the only thing that tells you the migration worked. Deliverability claims are meaningless in comparison; the same three providers deliver everyone's notifications. What you are actually verifying is that your tokens, your segments and your payloads survived.
Step 4: expect your audience count to drop, and be pleased
After the first real send to an imported list, your reachable count will fall — often by a lot.
No provider can validate a push token without sending to it. So an import ingests whatever was in the file, and the first send is when reality arrives: uninstalled apps return UNREGISTERED, expired web subscriptions return 410 Gone, and those devices get retired.
Those users were already gone. Your old dashboard was counting them. The number did not get worse, it got true — and if the new number is materially smaller, that gap is worth understanding, because it was informing decisions before.
One caution on cleanup: FCM returns INVALID_ARGUMENT for both a dead token and a malformed payload. A cleanup job that treats every INVALID_ARGUMENT as a dead device can delete a healthy audience over a single content bug. The safe rule is to only retire on ambiguous rejections when another device on the same channel succeeded in the same send, which proves the payload was fine.
What this looks like in Notibase
We read a OneSignal export directly and map the columns ourselves, including notification_types — unsubscribed people come in suppressed rather than disappearing or getting messaged. You import your own VAPID key pair if you have one, rather than being forced to generate a new one. And nothing is written until you have seen a dry run that reports exactly what would happen, with every rejected row downloadable and its reason attached.
The full walkthrough is Importing an audience. If you want the version with the trade-offs rather than the pitch, our comparison with OneSignal includes the cases where you should not move.