Import existing device tokens
Contents
If you're moving to Workflows push notifications from another push provider, you can import the APNs device tokens and FCM registration tokens you already have. Imported devices can receive pushes right away, so users on older versions of your app don't need to open it again or update before they get notifications.
The mobile SDKs register new devices on their own. Use this import once, for devices that registered with your old provider before you added PostHog.
There are three ways to import:
- Upload a CSV in Workflows. Best for a one-off import.
- Ask your AI agent to move the devices from your old provider.
- Use the API for very large lists or a scripted import.
Before you start
- Connect an APNs or FCM channel. See Configure a workflows channel.
- You need to be an admin of the project, with edit access to persons.
- For each device, you need its token and the person's
distinct_idin PostHog. Use the samedistinct_idyour app passes toidentify, so the device lands on the right person.
Which token goes to which channel
| Channel | Token it takes | Looks like |
|---|---|---|
| APNs | The APNs device token, hex encoded | 64 hex characters |
| FCM | The FCM registration token | Around 150 letters, digits, - and _, often with a : |
- An iOS app that sends through Firebase has FCM tokens. Import those into the FCM channel.
- Expo push tokens (
ExponentPushToken[...]) can't be imported. They only work through Expo's own push service. Your app needs to read the native APNs or FCM token withgetDevicePushTokenAsyncand register it with the PostHog SDK instead.
Rows that don't import
PostHog checks every row and tells you why a row wasn't imported:
| Reason | What it means | What to do |
|---|---|---|
| No person has this distinct ID | No person in PostHog has this distinct_id | Check that it's the ID your app passes to identify. If the IDs are right and these people just haven't opened the app since you added PostHog, import them anyway and create new persons |
| The token doesn't fit this channel | For example an Expo token, an APNs token in an FCM channel, or an FCM token in an APNs channel | Import it into the right channel, or drop it |
| Not stored | PostHog didn't save the device | Import the row again |
| Person processing is off for this person | PostHog didn't update the person, so the device wasn't saved | Check that person profiles are processed for these people, then import again |
| The line can't be used (CSV only) | For example the line has no token or no distinct ID, or the distinct ID is longer than 200 bytes | Fix the line and upload it again |
Rows with an unknown distinct_id are rejected by default. If they were imported, PostHog would create a new person for each one, and a person your app never identifies never gets a push.
A person with several devices gets one row per device. Importing the same row twice stores it once.
Upload a CSV
Go to Workflows > Channels.
Find your APNs or FCM channel under Your connections and click Import devices.
Upload a CSV file. The first line must name the columns, and the file needs a
distinct_idcolumn and adevice_tokencolumn. Other columns are ignored.csvClick Import. PostHog imports the file in the background, so you can close the window once the upload is done. Click Import devices again to see the progress. You can click Stop import at any time. Devices sent before you stop stay imported.
When it's done, you see how many devices were imported, how many repeated lines were left out, and a table of the lines that weren't imported, with the reason. The table shows the first 1,000 lines.
If some lines failed because no person has the distinct ID, and you're sure the IDs are right, click Import anyway and create new persons to import just those lines. This option is available for 24 hours after the import. After that, upload the file again.
Files can be up to 20 MB and 400,000 lines. For a larger list, split it into several files and import them one at a time, or use the API. Each channel runs one import at a time.
Ask your AI agent
If your AI agent is connected to the PostHog MCP server, it can export devices from your old provider and import them into PostHog. It checks every row with a dry run before it imports anything.
For example, ask it:
Import our push devices from OneSignal into our PostHog APNs and FCM channels. Our OneSignal external user ID is the ID we pass to PostHog identify.
The agent uses the integrations-push-tokens-create tool and the importing-push-tokens skill, which covers exporting from OneSignal, Braze, Customer.io, Airship and your own Firebase backend. Tell the agent which ID your app passes to identify, because that's what pairs each device with the right person. If the dry run says many rows have no person, the ID mapping is wrong. Fix the mapping rather than creating new persons.
Use the API
You need a personal API key with the integration:write and person:write scopes. The key's user must be an admin of the project.
Find your channel's ID
List your push channels. Use kind=apns for APNs and kind=firebase for FCM:
Each result has an id and a config that shows the channel's bundle ID (APNs) or Firebase project ID (FCM). Pick the channel that matches your app. Use eu.posthog.com if your project is in the EU.
Check, then import
Send up to 1,000 devices per request. Set dry_run to true first to check every row without storing anything:
The response says how many devices would be stored, and why each other row would fail:
index is the row's position in subscriptions, counting from 0. reason is one of unknown_person, token_mismatch, not_stored or person_processing_disabled. See Rows that don't import. Fix the failed rows, then send the same request without dry_run to import.
Give each import request an import_id, a new UUID per batch. If a request times out and you send the batch again, use the same import_id, so a batch that was stored before the timeout isn't stored twice.
To import rows whose distinct_id has no person yet, set create_missing_persons to true. Only do this for IDs you're sure your app identifies.
Import a large list
Split the list into batches of 1,000. Imports are limited to 60 requests per minute per project. Above that the API answers 429 with a Retry-After header, so wait that many seconds and try again.
The Python script below reads a CSV with distinct_id and device_token columns and does all of this:
- It checks every row with a dry run. If any rows would fail, it writes them to
not_imported.csvwith the reason and stops, so you can fix them first. SetIMPORT_PASSING_ROWS = Trueto import the rows that passed anyway. - It imports the rows that passed, and sends
not_storedrows again up to five times. - Rows that still fail go into
not_imported.csv, and the script exits with an error so you know the import isn't complete.
Billing
Each device that's stored is recorded on its person as one event, so an import of 10,000 devices counts as 10,000 events toward your product analytics usage. Rows you import again count again. A dry run stores nothing and isn't billed.
Check the import
Each device is stored as a $device_push_subscription_… person property. To count the people who have at least one device, run this in the SQL editor:
Then send a test push to one of them from a workflow that uses the same channel.





