PushMesh
Sign in Request access
Open section navigation

Index

Getting started

From zero to your first push with proof of delivery — through the React Native SDK or straight against the API, with commands you can copy and run.

Getting started

By the end of this page you will have a device registered, a push sent, and — the part that sets PushMesh apart — proof that the message reached the device, not just proof that the provider agreed to deliver it.

Keep these two words in mind; the rest of the documentation uses them constantly:

NumberWhat it means
successfulFCM/APNs accepted the message. Every push service can tell you this.
recebidosthe device returned proof that the notification arrived. This is what you came here for.

The hosts

HostWhat it is for
https://api.pushmesh.iothe public API. It is the address for everything in this reference: your server, the SDK and any custom client talk here
https://app.pushmesh.iothe dashboard — where the application, the key and the delivery credentials are created
https://pushmesh.iothe website

Always pass baseUrl to the SDK. The version published on npm today (@pushmesh/sdk@0.7.2) has a different host baked in as the default for when baseUrl is omitted, and that host is not live. Omit the field and the device never registers, while the server returns no error at all — the call never gets out. The examples on this page pass baseUrl explicitly, and you should do the same until a new release of the package fixes the default.


Before you start

Three things, all of them from the dashboard at https://app.pushmesh.io:

WhatWhat it is forWhere to get it
app_id (UUID)identifies your application in every callshown in the application’s page in the dashboard
API key pm_live_…authenticates calls made by your servershown exactly once, when the application is created
Delivery credentials (FCM and/or APNs)what actually lets a push leave for the devicedashboard → application settings → FCM / APNs

The delivery credential is a required step, and what it requires is not obvious. On Android it is the .json file of a Firebase service account (the legacy server key does not work); on iOS it is the APNs .p8 key plus Key ID, Team ID, Bundle ID and the environment choice — where the classic trap lives (TestFlight is production). Without it, the send returns 200, recipients looks right, and nothing goes out. What to have in hand, field by field, is in Delivery credentials — worth reading before you create the account.

The key is shown once. Only a cryptographic digest of it is stored; nobody — support included — can show it to you again. If you lose it, rotate it: rotation keeps the previous key valid for 24 h, so nothing breaks.

Never ship the pm_live_ key inside your published app. An APK or IPA is a file anyone can download and open. The routes the device itself calls (registration, receipts, In-App) were designed to take no credential for exactly this reason: the app_id travels in the body, and what protects them is application validation plus a per-origin request cap.

Put the two values in your environment so you can paste the commands below:

export PUSHMESH_KEY="pm_live_a1b2c3d4_put_your_43_character_secret_here"
export APP_ID="00000000-0000-0000-0000-000000000000"

1. Check that you are talking to the API

curl -s https://api.pushmesh.io/
{ "servico": "pushmesh", "versao": "<service version>", "git": "<deployed commit>" }

Now check that your key is accepted. This route needs no parameters — it returns how your application is configured to receive confirmations:

curl -s -H "Authorization: Basic $PUSHMESH_KEY" \
  https://api.pushmesh.io/api/v1/confirmacoes
{ "modo": "consulta", "fila_pendente": 0, "streams_ativos": 0 }
  • 401 → the key is missing, incomplete, or does not belong to this account.
  • 403 → the key is valid, but the application is paused.
  • 200 → you are good to go.

Also notice the X-Pm-Request-Id response header (use curl -i to see it). It is present on every response, success or failure, and it is the identifier support will ask for when tracing your call.


2. Register a device

Registering means telling PushMesh: “this push token belongs to this application”. What comes back is a player_id — the device’s public identifier, and what you use to send a message to it.

The package is published on npm and handles registration, token refresh, delivery receipts and click receipts on its own.

npm install @pushmesh/sdk @notifee/react-native @react-native-async-storage/async-storage
cd ios && bundle exec pod install   # iOS
import PushMesh from '@pushmesh/sdk';

// once, at app boot
await PushMesh.init({
  appId: 'YOUR_APP_ID',
  baseUrl: 'https://api.pushmesh.io',   // required on 0.7.2 — see the note above
  appVersion: '1.0.0',
});

// when the user signs in, so you can target them by your own user id
await PushMesh.login('user-123');

// the device identifier, if you want to target it directly
const playerId = await PushMesh.getPlayerId();

Ask for permission — init does not do it for you

This is the second required step people usually miss. The SDK does not request notification permission during init, on purpose: your application chooses when to ask, not the library. On Android 13 or newer and on any iOS, without that request the device registers fine, the send returns successful: 1 — and recebidos stays at 0 forever.

// at the right moment in your flow (never in the app's first second)
if (await PushMesh.permissions.canRequestNatively()) {
  // the native prompt is still available: one tap, without leaving the app
  await PushMesh.permissions.checkAndReport();
} else {
  // a decision was already made — only the system settings can reverse it
  await PushMesh.permissions.openNotificationSettings();
}

canRequestNatively() is the “smart button”: it returns true only while the system still accepts the native dialog. Sending someone who has never seen the prompt straight to Settings is terrible — on iOS, an app that never asked for permission does not even have a page in Settings, so the person lands at the root and gets lost.

An honest note about notification_types: the SDK reads the real system permission and reports changes to the server, but no route in this API currently writes the “blocked” (-2) or “never asked” (0) states. A registered device is born with notification_types: 1 and stays that way; what changes over its life is invalid_identifier. For “can I still reach this device?”, the truthful question today is invalid_identifier === false.

What the SDK does from here on with no further code from you:

  • registers the device and keeps the token fresh;
  • displays the incoming notification;
  • sends the delivery receipt when the push arrives and the click receipt when the user taps — this is what feeds the recebidos number;
  • queues receipts locally while the device is offline and resends them on the next launch.

If your app already has its own Firebase setup, hand the SDK the token you already obtain (init({ appId, getToken })) and everything else stays the same. There is also a command-line check that inspects the integration from the outside:

npx pushmesh-doctor --base-url https://api.pushmesh.io --app-id "$APP_ID"

Path B — straight against the API

This is the path for a client you write yourself (another platform, an embedded device, a test). This route takes no Authorization header:

curl -s -X POST https://api.pushmesh.io/api/v1/players \
  -H "Content-Type: application/json" \
  -d '{
    "app_id": "'"$APP_ID"'",
    "device_type": 1,
    "identifier": "DEVICE_PUSH_TOKEN",
    "external_user_id": "user-123",
    "app_version": "1.0.0",
    "language": "en",
    "timezone": -10800
  }'
{ "success": true, "id": "0198e7c4-77a1-7bd2-b0f1-6b1d3f2a9c40" }

That id is the player_id. Keep it.

FieldTypeRequiredLimitWhat it does
app_idUUIDyesthe application that owns the device. Unknown ⇒ 400; paused ⇒ 403.
device_typeintegeryes0 iOS · 1 Android · 5 Chrome · 7 Safari · 8 Firefoxthe platform. Any other value ⇒ a named 400.
identifierstringyes194 bytesthe device push token. Empty or longer ⇒ 400.
external_user_idstringno128 bytes; up to 10 devices per valueyour own person identifier — it lets you target users without knowing their tokens.
app_versionstringnoyour application version.
device_modelstringnodevice model.
tagsJSON objectno2 KB serializedfree key/value pairs for segmentation. An array or string ⇒ 400.
languagestringno32 bytesdevice language.
timezoneintegerno±86400offset in seconds (-10800 = UTC−3). A time zone name here ⇒ 400.

Three behaviours that will save you an afternoon:

  1. Success is 200, not 201 — including the very first time.
  2. Registering the same token again returns the same player_id. The route is idempotent per application + token: call it on every app launch without worrying.
  3. On POST, an omitted field clears the stored value for external_user_id, app_version, device_model, tags, language and timezone. Always send the device’s full picture — or make partial changes through PUT /api/v1/players/{id}, where a missing field never clears anything. Both verbs are covered in Devices.

Testing without a device: an identifier starting with test: (for example test:ok) registers normally and returns a player_id — useful to validate your registration code. But it is flagged as a sandbox device and is deliberately excluded from every send. To see a real receipt in step 4, use a real device.


3. Send the push

Now with the key, because the sender is your server:

curl -s -X POST https://api.pushmesh.io/api/v1/notifications \
  -H "Authorization: Basic $PUSHMESH_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "app_id": "'"$APP_ID"'",
    "include_player_ids": ["0198e7c4-77a1-7bd2-b0f1-6b1d3f2a9c40"],
    "headings": { "en": "Order shipped", "pt": "Pedido a caminho" },
    "contents": { "en": "It arrives tomorrow", "pt": "Chega amanhã" }
  }'
{ "id": "0198e7d1-1c40-7f2a-9b31-77aa10c4e001", "recipients": 1 }

That id is the push. It is also the pm_msg_id that travels inside the push payload, and it is how the device’s receipt finds its way back.

Minimum rules for the body:

FieldTypeRequiredWhat it does
app_idUUIDyesmust be the key’s application, otherwise 401.
contentsobject {lang: text}yesthe message body. An empty object ⇒ 400.
headingsobject {lang: text}nothe title.
one targetyesinclude_player_ids (up to 2,000), include_external_user_ids (up to 2,000) or included_segments (up to 10 names). None or more than one ⇒ 400.

For a broad send, swap the target for a segment:

"included_segments": ["Subscribed Users"]

Accepted names are Subscribed Users (and its synonym Total Subscriptions), Active Users (seen in the last 7 days) and Engaged Users (returned a receipt in the last 7 days). An unknown name returns 400 listing the valid ones.

Rehearse first, if you like. With the X-Dry-Run: true header the call resolves the real audience, returns the real recipients, and delivers nothing and stores nothing:

curl -s -X POST https://api.pushmesh.io/api/v1/notifications \
  -H "Authorization: Basic $PUSHMESH_KEY" \
  -H "X-Dry-Run: true" \
  -H "Content-Type: application/json" \
  -d '{"app_id":"'"$APP_ID"'","included_segments":["Subscribed Users"],"contents":{"en":"test"}}'
{ "id": "", "recipients": 12840, "dry_run": true }

Two 200 responses that look like errors and are not

{ "id": "", "recipients": 0, "errors": ["All included players are not subscribed"] }

Nobody in the target was reachable. This is not a 4xx and retrying will not help: either the identifiers do not exist in this application, or the devices are not subscribed, or they were sandbox devices.

{
  "id": "0198e7d1-1c40-7f2a-9b31-77aa10c4e001",
  "recipients": 1,
  "errors": { "invalid_player_ids": ["11111111-1111-1111-1111-111111111111"] }
}

A partial send: the valid ones went out, and the response names the ones that did not. Note that errors is an object here and was an array above — if you deserialize it into a fixed type, accept the union.


4. Read the receipt

curl -s -H "Authorization: Basic $PUSHMESH_KEY" \
  "https://api.pushmesh.io/api/v1/notifications/0198e7d1-1c40-7f2a-9b31-77aa10c4e001?app_id=$APP_ID"
{
  "id": "0198e7d1-1c40-7f2a-9b31-77aa10c4e001",
  "successful": 1,
  "failed": 0,
  "errored": 0,
  "remaining": 0,
  "converted": 0,
  "recebidos": 1,
  "queued_at": 1756300000,
  "completed_at": 1756300003,
  "contents": { "en": "It arrives tomorrow", "pt": "Chega amanhã" },
  "platform_delivery_stats": {
    "android": { "successful": 1, "failed": 0 },
    "ios": { "successful": 0, "failed": 0 }
  },
  "pm_metricas": { "via": "rest", "cobertura_recibo_pct": 100.0, "ttfd_ms": null, "duracao_ms": 3000 }
}

The example above is abridged: the response also carries the title, the target and the scheduling window. The full list is in Delivery receipts.

FieldWhat it means
successfulthe provider accepted it. Not proof of arrival.
failedpermanent rejection (a dead token, for instance).
erroredall three delivery attempts were exhausted.
remainingstill being processed. It must reach zero.
convertedclicks confirmed by the device. 0 means nobody tapped.
recebidoshow many devices proved they received it.
pm_metricas.cobertura_recibo_pctrecebidos ÷ successful, as a percentage.

Two honest notes about these numbers:

  • Coverage rarely reaches 100%. A device that is switched off, offline, or no longer has your app installed returns no proof at all. A number below 100% is an honest picture of your base, not a platform defect.
  • recebidos: null is not zero. On the free plan the field comes back null alongside a recebidos_gate block explaining that real delivery confirmation is a paid-plan feature. If your code does recebidos ?? 0, a commercial gate silently becomes “zero deliveries” on your dashboard.

5. Who sends the receipt

If you use the SDK, nobody: it already did. This section is for people writing their own client.

Every push carries two fields inside data: pm_msg_id (the push) and pm_rcpt (a cryptographic proof valid only for that push on that device). When the notification arrives, your client echoes both back:

curl -s -X POST https://api.pushmesh.io/api/v1/receipts \
  -H "Content-Type: application/json" \
  -d '{
    "app_id": "'"$APP_ID"'",
    "notification_id": "PM_MSG_ID_FROM_THE_PUSH",
    "player_id": "0198e7c4-77a1-7bd2-b0f1-6b1d3f2a9c40",
    "rcpt": "PM_RCPT_FROM_THE_PUSH",
    "evento": "recebido"
  }'
{ "ok": true }
  • evento accepts recebido (the default) and clique. A clique with no prior receipt counts as both — nobody taps what never arrived.
  • Sending the same receipt again returns {"ok":true,"duplicado":true}: you can drop the item from your queue.
  • {"ok":true,"tardio":true} means “counted, through a slower path” — it is also a success, and must not be treated as a duplicate.
  • Do not try to compute the rcpt. The key that produces it never leaves the server; echo exactly what arrived in the push. An invalid proof returns 403, always with the same wording.

6. If nothing arrived

Work through this in order — it is the order in which things usually break:

SymptomLikely cause
No device registered, and no error anywhere in your logsthe SDK has no baseUrl and talked to its built-in default host, which is not live. Pass baseUrl: 'https://api.pushmesh.io' to init.
recipients: 0 with an empty idthe target does not exist in this application, the devices are not subscribed, or they were sandbox (test:) devices.
good recipients, successful: 0, remaining: 0the delivery credential (FCM/APNs) is missing or wrong in the dashboard. See Delivery credentials.
good successful, recebidos: 0the push went out but the device returned no proof: notification permission denied, app uninstalled, device offline — or your own client is not calling the receipts route.
403 mentioning app pausadothe application is paused on the account.
422 mentioning bytesthe rendered payload exceeded 3,891 bytes. The budget includes 96 bytes the server injects (pm_msg_id + pm_rcpt); the error tells you how many bytes to trim.
422 mentioning plano and usuários ativosthe account is on the free plan and went past its monthly allowance. The send is refused entirely, on purpose, and retrying does not help — explain carries the cap, the current number and the price of leaving the free plan. See Limits.
429you hit a cap. See Authentication and errors.
any error you do not recogniseread explain.causa and explain.como_corrigir in the body, and keep the request_id.

The errors and explain texts are written in Portuguese, with no language negotiation. Branch your code on the HTTP status and the structured fields — never on the message string.


What to do next

If you want to…Go to
understand the key, rotation and every errorAuthentication and errors
get your FCM and APNs credentials in orderDelivery credentials
every send field (scheduling, image, priority, idempotency)Send a push
the delivery proof in depth, history and windowed numbersDelivery receipts
manage and export the device baseDevices
be notified instead of pollingWebhooks
messages inside the applicationIn-App messages
migrate from another push serviceMigrating from another provider
every cap, with numbersLimits
retention, personal data and deletionData, retention and privacy
know what can change without noticeVersioning