Migrating from another provider
What changes when you switch push providers — what the API accepts as-is, what has to change in your app, and the steps nobody automates.
Migrating from another provider
Switching push providers usually means rewriting the entire integration layer.
Not here: PushMesh’s /api/v1 speaks the classic push format the market
already uses — the player model, with include_player_ids, contents,
included_segments and friends.
In practice, the server side is repointing a base URL and swapping a key. The app side takes more work, and this page is honest about it: there are manual steps, and one stage that depends on your app update curve. Nobody migrates a device base with an API key.
The honest picture in three lines
| Layer | Real effort |
|---|---|
| Server (sending, polling, reporting) | low — base URL + key |
| App (SDK) | medium — swap the package and the init call |
| Device base | a period, not a command — devices register as the app updates |
Part 1 — The server
What is already in the format you know
Sending, POST /api/v1/notifications, with the classic names:
| Group | Accepted fields |
|---|---|
| Target (exactly one) | include_player_ids, include_external_user_ids, included_segments |
| Content | contents, headings, subtitle, data |
| Tap destination | url, app_url, web_url |
| Delivery | ttl, priority (5 or 10), collapse_id, send_after |
| Platform | isIos, isAndroid, isAnyWeb |
| Android | android_channel_id, android_accent_color, android_sound, big_picture, large_icon, small_icon |
| iOS | ios_sound, ios_attachments, content_available, mutable_content |
| Identity | name, external_id, idempotency_key |
| Channel | channel_for_external_user_ids (accepted: "push") |
Semantics are preserved, including the quirky ones:
-
url/app_url/web_urlbecome the tap destination, in that precedence; -
platform flags follow exclusion logic: absent includes, explicit
falseexcludes.isIos: truerestricts nothing; -
nobody reachable is not an error — it is a 200 with an empty
id:{ "errors": ["All included players are not subscribed"], "id": "", "recipients": 0 } -
a partial send is also 200, with
errorsas an object:{ "errors": { "invalid_player_ids": ["1111…"] }, "id": "0198…", "recipients": 1 }
Recognised segments: Subscribed Users, Total Subscriptions (an alias of the
first), Active Users (seen in the last 7 days) and Engaged Users (receipt in
the last 7 days).
The swap
# before
curl -X POST https://<previous-provider>/api/v1/notifications \
-H "Authorization: Basic $OLD_KEY" ...
# after
curl -X POST https://api.pushmesh.io/api/v1/notifications \
-H "Authorization: Basic $PUSHMESH_KEY" ...
The key looks like pm_live_<identifier>_<secret> and travels in
Authorization: Basic. Bearer <key> and Basic with base64 of
user:key are also accepted — whatever your HTTP client already does will
work.
Differences worth testing before the cutover
| Behaviour | What to expect |
|---|---|
| Successful send | 200, always — not 201 |
errors inside a 200 | an array when nobody is reachable, an object on a partial send |
Empty id | means “nobody reachable” or a dry run — it is not a failure |
| Error text | in Portuguese; branch on the HTTP status, never on the sentence |
| Content limit | 3,891 bytes on the rendered payload, already including 96 bytes the server injects |
app_id | required in the body/query, and must match the key |
A typed deserialiser that pins errors to a list of strings breaks in exactly
the most common migration scenario — a list carrying stale identifiers, which
answers with errors as an object. Fix that before you switch.
What is refused on purpose
These fields return a named 400 rather than being silently accepted. Accepting and ignoring them would send the wrong message to your whole base without telling anyone:
| Field | What to do |
|---|---|
template_id | message templates do not exist yet: build it in contents/headings |
custom_data | use data |
existing_android_channel_id | use android_channel_id |
filters | no equivalent today — resolve the audience on your side and send the list |
excluded_segments | same |
buttons | action buttons do not exist yet |
delayed_option, delivery_time_of_day | per-user preferred-time scheduling does not exist yet; use send_after |
throttle_rate_per_minute | delivery pacing belongs to the app, not to a single send |
android_visibility, android_led_color, android_group, ios_category | not configurable per send yet |
include_aliases, include_subscription_ids | these belong to the newer user model; this compatibility layer is the player model |
web_buttons is the single exception: accepted and ignored, because legacy
integrations send an empty array.
Read that table before deciding. If your operation depends on filters or
on message templates, migrating means rebuilding that on your side — better to
find out now than the night before.
Part 2 — The app
React Native
npm install @pushmesh/sdk
The SDK ships a classic surface precisely so you do not have to rewrite the app’s calls:
import { ClassicApi } from '@pushmesh/sdk/compat';
await ClassicApi.initialize(APP_ID, { baseUrl: 'https://api.pushmesh.io' });
await ClassicApi.login('user-42');
await ClassicApi.Notifications.requestPermission();
await ClassicApi.User.addTags({ plan: 'pro' });
ClassicApi.InAppMessages.addTrigger('screen', 'checkout');
It covers initialize, login/logout, permission, notification listeners,
user tags and in-app triggers. Where the names line up, the change is the
import.
The SDK’s native API is the same thing under its own names:
import { PushMesh } from '@pushmesh/sdk';
await PushMesh.init({ appId: APP_ID, baseUrl: 'https://api.pushmesh.io' });
await PushMesh.login('user-42');
baseUrlis not optional in the version published today. The@pushmesh/sdk@0.7.2package has a different host baked in as the default for when the field is omitted, and that host is not live: the device never registers and no server error appears, because the call never gets out. PassbaseUrl: 'https://api.pushmesh.io'on both paths —PushMesh.initandClassicApi.initialize— until a new release fixes the default. It is the first thing to check if the new base does not grow after you publish the app.
What needs your attention in the app
- Remove the previous SDK. Two push SDKs fighting over the device token is the recipe for push disappearing with no error at all.
- Register your Firebase parameters in the dashboard (Android). The SDK fetches them at boot — the app stops needing the config file bundled in. And do not confuse them with the credential that actually sends: both are in Delivery credentials.
- Confirm token delivery in your refresh flow.
- Do not put the API key in the app. The routes the app uses require no credential for exactly this reason: a published APK is a published secret.
Part 3 — The device base (the part that is not magic)
There is no import-your-base button. There are two paths, and you will probably use both.
Path A — organic registration (the default, safe one)
You ship the app version carrying the new SDK. Every device that opens the app
registers itself, and its player_id is born from that registration. The base
grows at the speed of your version adoption.
- Upside: zero risk, zero work, and the base that forms contains only live devices.
- Cost: it takes as long as your update curve — typically weeks.
- Identity continuity: send
external_user_idat login and your usual identifier keeps working. That is what lets your own segmentation survive the switch.
Path B — bulk registration of tokens you already hold
If you keep your push token list, you can register them directly:
curl -X POST https://api.pushmesh.io/api/v1/players \
-H "Content-Type: application/json" \
-d '{
"app_id": "'"$APP_ID"'",
"device_type": 1,
"identifier": "<the device push token>",
"external_user_id": "user-42"
}'
The route is idempotent per token: registering twice returns the same
player_id.
Before relying on this, check that the tokens are still valid:
- Android: the token is bound to the Firebase project. Keep the same project and the tokens keep working. Change projects and they do not — and there is no way to convert them.
- iOS: the token is bound to the app and the notification environment, not to the provider. Keeping the same app and supplying the same authentication key, they keep working.
Also check the time cost, which tends to surprise: the registration route is capped at 120 requests per minute per source address, and an app is capped at 50,000 device registrations per hour. A large base, coming from a single server, takes days. If that is your situation, talk to us before you start rather than discovering it against a wall of 429s.
One note that prevents a wrong expectation: a token registered this way is only truly reachable once the device is running an app able to receive through PushMesh. Registering a token does not install the SDK.
Part 4 — Running both in parallel
The safest way to switch is not to switch all at once.
-
Ship the app with the new SDK, keeping the previous provider live.
-
Let adoption climb. Nothing changes in your sending yet.
-
Measure the new base with the export, which returns who is alive:
curl -X POST "https://api.pushmesh.io/api/v1/players/csv_export?app_id=$APP_ID" \ -H "Authorization: Basic $PUSHMESH_KEY"{ "csv_file_url": "https://…/exports/…/players-20260827T150000Z-….csv.gz", "pm_pronto_em_s_estimado": 30, "pm_validade_h": 72 }The file is gzipped CSV with the columns
id,external_user_id,notification_types,invalid_identifier,last_active. Fetch the URL: it returns 404 while the file is still being built and 200 once it is ready. The URL is the credential — the object is readable by anyone holding the address, and it carries your user identifiers. Do not paste it into a ticket or a chat. -
Send in parallel to a small slice, on both sides, and compare. Here you get an advantage the comparison usually reveals: alongside
successful(the provider accepted it), PushMesh returnsrecebidos— the confirmation that came back from the device itself. -
Move the traffic once the new base covers what you need.
-
Turn off the previous provider.
Checklist
Server
- Base URL pointed at
https://api.pushmesh.io - New key in the vault; no key in the repository
- No field from the refused table in your payload
-
errorshandled as a union of array and object - Branching on HTTP status, not on error text
-
Idempotency-Keyon sends that must not duplicate
App
- Previous SDK removed
-
@pushmesh/sdkinstalled and initialised at boot withbaseUrl - Firebase parameters registered in the dashboard (Android)
-
external_user_idsent at login - No API key embedded in the app
Base
- Path chosen (organic, bulk, or both)
- If bulk: token validity confirmed and pacing agreed
- Parallel period defined
- Export reviewed before the cutover
Where to get help
If anything on this page does not match what you are seeing in practice, the
page is what is wrong — and we want to know. Every API response carries the
X-Pm-Request-Id header; quote that identifier and we will find the exact
call.