Migrate from OneSignal
Export mobile subscriptions, import devices, and switch each updated app to Carillon without overlapping sends.
We recommend keeping OneSignal running for app versions that have not migrated. An updated version registers with Carillon before disabling its OneSignal subscription. You can operate both services during rollout, with a separate audience for each.
Before you start
- Create a Carillon app and upload its APNs or FCM credential. Use the same iOS Bundle ID, Apple team and Firebase project as the existing app.
- You will need access to OneSignal, your notification backend and the mobile app source.
- It helps to inventory segments, scheduled campaigns, deep links, tags and notification preferences. These are not transferred by the device CSV.
- We recommend starting with a small test cohort and checking background, foreground and tap behavior on both platforms and on an older app version. Imported tokens alone do not provide Carillon open tracking or guarantee that existing notification handlers display its payloads.
1. Export mobile subscriptions
In OneSignal, open Audience → Subscriptions, select the required columns and export. Export subscriptions, not just user identifiers. See OneSignal's export guide.
You should import only currently subscribed iOS and Android push records with a provider token. Web push, email, SMS, Fire OS and Huawei subscriptions are outside this migration path. We recommend keeping the original file privately for reconciliation; it contains device identifiers.
2. Convert the file
Carillon accepts its own CSV schema, not an unchanged OneSignal export. Unzip the export locally if needed. Check the CSV headers first: dashboard and API exports use different names.
Dashboard export
For an export with push_token, channel and subscribed, use this mapping:
| OneSignal | Carillon | Conversion |
|---|---|---|
push_token | token | APNs or FCM token; exclude empty values |
channel | platform | iOS → ios; Google Android → android; exclude other channels |
external_id | external_id | Optional; keep your existing user identifier |
timezone_id | timezone_id | Optional IANA zone; do not substitute a numeric offset |
language_code | locale | Optional valid BCP 47 tag |
| Your iOS build environment | environment | production for App Store/TestFlight; sandbox for development builds |
The rows eligible for conversion are those where subscribed is Yes, channel is iOS or
Google Android, and push_token is non-empty. Subscribed email rows are still outside
this migration path. If these headings or status values differ, you should check the
export settings before converting.
device describes the hardware and OS; use channel to select the platform.
Neither player_id nor onesignal_id is a provider token or a replacement for your
own external_id. The export does not establish the APNs environment: confirm it
from the app build configuration. Android uses production.
API export
For the CSV export API,
the equivalent fields are identifier → token, device_type (0 → ios,
1 → android), external_user_id → external_id, and language → locale.
timezone_id keeps its name. Exclude other device types and empty tokens. Keep only
invalid_identifier=f (subscribed); t means unsubscribed. Request
notification_types if you need the detailed status.
Request an export with an agent
An agent can request, download and convert the export using your OneSignal App ID and App API key. Provide the key through a local environment variable or secret manager, not chat. Carillon MCP authentication does not grant access to OneSignal. Run the commands in a private working directory outside version control.
With ONESIGNAL_APP_ID and ONESIGNAL_APP_API_KEY already set:
umask 077
curl --fail-with-body --silent --show-error \
--request POST \
"https://api.onesignal.com/players/csv_export?app_id=${ONESIGNAL_APP_ID}" \
--header "Authorization: Key ${ONESIGNAL_APP_API_KEY}" \
--header 'Content-Type: application/json' \
--data '{"extra_fields":["external_user_id","timezone_id","notification_types"]}' \
--output onesignal-export.jsonA successful response contains csv_file_url, pointing to a .csv.gz archive.
The agent should:
- Save that URL privately and poll it with a bounded delay until the file is ready.
A temporary
404means generation may still be running; retry the download, not the export request. Stop after a chosen timeout and report the pending export. - Download the archive without forwarding the OneSignal API key to the download host. Decompress locally and parse it with a CSV parser, not comma splitting.
- Apply the API field mapping and subscription filter above. Report counts of kept, excluded and invalid rows without printing tokens or personal identifiers.
- Confirm the target Carillon app and APNs environment, then follow the import procedure when authorized to import.
API export limits and filters
- One export can run at a time per OneSignal account. Finish downloading it before requesting another.
- Generation is approximately 2,000 records per second; this is an estimate, not an SLA.
- The generated file expires after three days.
segment_namelimits the export to a segment.include_unsubscribed=falseonly filters subscriptions whensegment_nameis supplied; always check row status.last_active_sinceselects by last session, not by last modification. It is not a reliable change feed for token or preference updates.
These constraints come from the export endpoint reference. It does not specify a daily export quota or a paid-plan requirement; check access with the actual account rather than assuming unlimited exports.
The general rate-limit guide
also advises keeping export files below 100 GB. Follow the stricter single-export
rule above. On 429, respect Retry-After and back off; stop on 400, 401 or 403
and fix the request or credentials. Do not retry indefinitely.
Subscription status
Subscription eligibility depends on the status at export time, not just the presence of a token.
unsubscribed_at records a past unsubscribe; a subscription may have resubscribed
since then. That date alone is not a reason to exclude a currently subscribed row. See OneSignal's status fields.
We recommend using a fresh export and accounting for preference changes after the snapshot;
an old file does not establish today's subscription state or token validity.
Carillon's importer has no opt-out column and new imported devices default to opted in. You should therefore exclude unsubscribed users before importing. Unknown columns, including OneSignal status fields and tags, are ignored. Tags need a separate mapping through the SDK or device API.
Use the CSV format and upload commands. There is no
carillon import --from onesignal command.
3. Import and inspect the report
Upload the converted file with your Carillon app's secret key. Wait for the import to
finish, then check created, already_present, invalid and ignored_columns.
Fix rejected rows and upload them again. Provider credentials and imported tokens must
refer to the same mobile app.
Importing does not disable OneSignal or send a notification. During this staged rollout, imported-only devices should stay outside Carillon campaigns while OneSignal owns their sends.
4. Release both SDKs, then hand over each device
Follow the Swift, Kotlin or React Native integration guide. Test native notification delegates, Firebase messaging services and tap handlers together; do not assume two SDKs compose automatically. Preserve token refresh forwarding for whichever provider still owns sends.
Carillon swizzles nothing: its SDK receives tokens, notifications and taps only through the delegate and service calls you forward to it explicitly. OneSignal's swizzled delegate calls through to the original implementation, so both SDKs keep working when your own delegate forwards to Carillon. Verify that on a physical device for each platform, in the background, in the foreground and on tap, before the first cohort ships; a simulated trace does not prove it.
We recommend the following handover sequence for each installation eligible to receive push:
- Preserve the user's notification preference before changing OneSignal's status.
- Register the device with Carillon and confirm a successful server registration. Calling SDK initialization alone is not confirmation; check its registration result or device ID and the dashboard. Keep OneSignal active if registration fails.
- Disable that installation's OneSignal push subscription with the installed SDK's
OneSignal.User.pushSubscription.optOut()method. - Confirm the old subscription is unsubscribed before marking this device ready for Carillon campaigns. Persist the handover state and retry incomplete handovers after a restart or network failure.
OneSignal's optOut() changes its subscription status, not the OS notification
permission. logout() alone is not a replacement. See the
OneSignal mobile SDK reference.
This migration opt-out is different from a user refusal. Your app should track the user's
actual preference separately from the migration state and preserve existing opt-outs.
Calling OneSignal optIn() on every startup after handover would re-enable the old subscription.
There is no atomic transaction across the two services. A backend-controlled handover flag, a paused campaign window and a pilot rollout make failures easier to reconcile. Carillon does not provide this cross-provider handover controller.
5. Keep the two sending audiences separate
A first SDK registration changes the Carillon device's source from import to sdk.
It keeps the same device row when the app and token match. However, source=sdk only
proves registration: it does not prove that OneSignal opt-out completed.
You can use an application-controlled device tag after confirmed handover, for example
push_provider=carillon, or target explicit device IDs from your backend's handover
records. A Carillon audience can require both conditions:
{
"filters": [
{ "field": "source", "value": "sdk" },
{ "field": "tag", "key": "push_provider", "value": "carillon" }
]
}Use this as a saved audience definition. OneSignal sends only to installations that have not handed over. Apply the same rule to transactional sends, automations and scheduled campaigns, not just manual broadcasts. Do not use a user-level flag when that user has devices on different app versions.
6. Repeat imports while older versions remain
You can import a fresh eligible export whenever needed. Existing tokens are counted as
already_present and left untouched, including SDK-updated fields and handover tags.
New tokens are added as source=import; they are not immediately part of the Carillon
handover audience. Token rotation can create a new row, even for the same user.
An import is additive, not a preference sync: an existing device is not updated when its OneSignal status changes, and a row missing from the next export is not deleted. Real opt-outs need separate reconciliation; an old snapshot does not establish current consent.
7. Retire OneSignal
We recommend tracking remaining eligible installations and your supported app versions. Dormant devices may never update, so a cutoff policy is usually more practical than waiting for a database count to reach zero. Imported-only devices need separate verification before you route their sends through Carillon without an app update.
Once the remaining population has migrated or is covered by that policy, disable the old campaigns, journeys and backend sends. Remove the OneSignal SDK, services, extensions and configuration from a later app release, keeping anything Carillon still needs. Test push and taps again, then retire the OneSignal credentials and account when no supported installation depends on them. Removing an SDK from a new release does not remove it from already installed older versions.
We recommend having a rollback plan during the pilot: restoring OneSignal sending requires reconciling both subscription state and the Carillon audience so the two do not overlap.
Target existing user IDs
If your OneSignal sends use include_external_user_ids or include_aliases.external_id, keep the
same IDs in Carillon's device external_id and send with audience.external_ids, up to 2000 per
request. One user can have several devices. The list is resolved within the app selected
by your secret key, and unknown IDs are ignored. See user ID targeting.
Replace the country filter
OneSignal detects a subscription's country and offers it as a segment filter. Carillon does not
infer a country from the locale, the timezone or the IP address. Set a country tag from your own
user record instead, with setTags() at sign-in and whenever the record changes, and filter on it.
not_in matches devices that do not carry the tag at all. A segment such as "every country
except France and Belgium" must combine not_in with exists: true on the same tag, otherwise
every device that never set country is included:
{
"filters": [
{ "field": "tag", "key": "country", "not_in": ["FR", "BE"] },
{ "field": "tag", "key": "country", "exists": true }
]
}See audience filters.
Map targeting and scheduling
| OneSignal | Carillon |
|---|---|
include_external_user_ids, include_aliases.external_id | audience.external_ids, up to 2000 per request |
included_segments | audience: { "all": true } for everyone, or audience.audience_id for a saved audience |
include_subscription_ids, include_player_ids | audience.device_ids, up to 100 per request |
send_after | send_at, up to 30 days ahead; see scheduling |
throttle_rate_per_minute | delivery_rate, in notifications per second, capped by the app's ceiling |
DELETE /notifications/:id | POST /v1/messages/{id}/cancel |
Map notification options
| OneSignal | Carillon |
|---|---|
headings, contents, subtitle | Base title/body/subtitle and payload.localizations |
url, app_url | A key of your own in data, such as data.url, read in onOpened. Carillon never opens a URL itself |
big_picture, ios_attachments | payload.image; add the iOS extension. See image formats |
large_icon | No equivalent |
collapse_id, thread_id | payload.collapse_id, payload.thread_id |
android_group | thread_id groups on iOS and in the SDK's foreground display on Android; Android notifications rendered by the system are not grouped |
android_channel_id | payload.android.notification.channel_id; create the channel in your app |
ios_sound, android_sound | payload.sound; default when omitted. Android 8+ uses the channel's sound |
ios_badgeType: SetTo | payload.badge |
ios_badgeType: Increase | No increment operation: badge sets an absolute value. Send the unread count and reset it in your app |
ttl, priority | payload.ttl in seconds (default 72 hours), payload.priority (high or normal) |
Map delivery statistics
GET /v1/messages/{id} returns the trace described in delivery.
OneSignal GET /notifications/:id | Carillon trace |
|---|---|
successful | outcomes.sent |
failed, errored | outcomes.failed, with error_code on each delivery |
converted | outcomes.opened |
remaining | remaining |
platform_delivery_stats | platforms.ios and platforms.android |
received | No equivalent. Neither Apple nor Google confirms arrival on the handset; sent means the provider accepted the notification |
We recommend replacing the OneSignal service extension with Carillon's extension when you switch
iOS image handling. Test foreground, background and tap behavior after a native rebuild.
Use onReceived to suppress banners for your own in-app UI, and onOpened to route using your
own data keys. Carillon does not navigate to a URL automatically.
If the subscription ID was your identity
Some integrations used the OneSignal subscription or player ID as a stable identifier for the
installation, sometimes as a login factor. Store the Carillon device ID after registration and
observe changes with deviceId() and onDeviceIdChanged (getDeviceId in React Native). The
SDK preserves it across token rotations using an installation proof, but it is a public,
mutable push address: it travels in registration responses, can be read by anyone with the app,
and changes after a reinstall. It is not an authentication factor. Issue your own secret to the
installation, store it in the platform keychain or keystore, and authenticate with that. See
devices.