FCM setup (Android and web)
To deliver to Android — and to web, which also goes out through Firebase — OpenPush needs a Firebase Cloud Messaging credential. It uses the HTTP v1 API with a service-account JSON only. There is no legacy server key path anywhere in the server.
When to use
Do this once per app before any Android device can receive a push. If you also deliver to browsers, read web-push.md — web sends use this same credential, uploaded in a specific place. iOS uses a separate credential; see platform-setup-apns.md.
Prerequisites
- A Firebase project with Cloud Messaging enabled, containing your Android app
- Permission in Google Cloud to create a service account key for that project
- Your OpenPush app and console access with the
managerrole or higher
The one thing that catches everyone
Upload the Firebase service account under Android — even if you only care about web.
The sender reads the Android platform credential and nothing else. A service account uploaded under Web is stored, makes
webpush-configwork, and turns the Web readiness badge green — and is then never read by any send. Every actual delivery falls back to a platform-wide environment credential, or fails with "no FCM credentials", while the console still shows web as configured.If web is your only channel, upload the service account under Android anyway. Use the Web credential slot for the VAPID public key, site URL and
firebaseConfigthat browsers need, not for the sending credential.
Step 1 — create the service account key
In the Google Cloud console for your Firebase project, create (or reuse) a service account with the Firebase Cloud Messaging sender role and download a JSON key.
OpenPush validates the file at upload and requires all of the following:
| JSON field | Requirement |
|---|---|
type | Must be exactly service_account |
project_id | Present — this is the project OpenPush sends to |
client_email | Present — the service account identity |
private_key | Present, and must look like a PEM private key |
An OAuth client JSON, a Firebase config object, or a truncated paste is rejected at upload rather than accepted and failing later at send time.
Step 2 — upload it under Android
Uploading a platform credential is a console action: open your app's Settings → Platforms → Android and upload the JSON. There is no /v1 route for uploading platform credentials.
Verify: GET /v1/apps/{app_id}/settings reports Android as ready under platforms.
Code
The platform-wide answer — whether any FCM credential is configured — is on GET /healthz as fcm. Per-app readiness is the settings response above.
How the platform-wide fallback behaves
There is one more place an FCM credential can come from, and it is worth understanding even though it is not yours to set. OpenPush can hold a single platform-wide FCM credential, configured by the OpenPush team through the environment, which is used by any app that has not uploaded its own:
| Variable | Meaning |
|---|---|
FCM_SA_JSON | The service-account JSON contents |
FCM_SA_PATH | A path to the JSON file, as an alternative |
Your app's own credential always takes precedence, and uploading one is the supported path.
This fallback is also why a misplaced credential can look like it works: an app whose service account went into the Web slot silently falls back on the platform-wide credential, which may point at an entirely different Firebase project.
How OpenPush talks to FCM
- HTTP v1 only. Requests go to
https://fcm.googleapis.com/v1/projects/{project}/messages:sendwith thefirebase.messagingOAuth scope. There is nokey=AAAA…legacy path and nofcm/sendendpoint in the server. - OAuth access tokens are refreshed only when invalid, under a per-credential lock — roughly once per campaign, not once per device.
The message OpenPush builds
Every OpenPush Android push is a data-only message:
Code
datacarries the whole payload, string-coerced. Objects and arrays are serialized as deterministic JSON.androidcarries exactly three things:priority(HIGHby default,NORMALotherwise),ttlas a duration string, andcollapse_key. When no TTL is set the field is omitted entirely, so Google's own four-week default applies.- A
notificationblock is set only for devices carried over by the legacy OneSignal migration bridge, and even then onlytitleandbody.
Nothing else is set: no android.notification.channel_id, icon, colour, sound, tag, click action, ticker or image; no restricted_package_name; no apns or webpush sub-config; no fcm_options; no topic or condition targeting.
This is deliberate. A data-only message has no sender-side home for a notification channel or an accent colour, so your app draws the notification and the composer's Android options ride as op_android_* keys inside data. See sdk-android.md for the key names and how to read them.
The FCM data envelope is 4 KB. If a rendered payload exceeds it the server truncates deterministically — body first, then title — and marks the payload with op_render_truncated. TTL is capped at 28 days at the API boundary.
Error behavior
| FCM response | What OpenPush does |
|---|---|
NOT_FOUND or UNREGISTERED | Marks that subscription status -10 — uninstalled or token expired |
429, 500, 502, 503, 504 | Records a retry on the delivery row only; the subscription is never changed |
| A transport error | Same — delivery row only |
| An OAuth refresh failure | Treated as a configuration fault: the fan-out stops and no device is marked unreachable |
| Anything else | Recorded on the delivery row with FCM's status |
A per-device fault never raises. A credential fault stops the send instead of damaging your audience — a broken service account cannot mass-unsubscribe anyone.
Web delivery through the same credential
Web subscriptions route through this same FCM v1 path, using the browser's FCM web registration token. Web devices receive the same data-only payload as Android; no webpush sub-config is ever set. OpenPush has no VAPID or Web Push Protocol sender of its own. The full picture, including what to put in the Web credential slot, is in web-push.md.
Limits
- HTTP v1 service-account JSON only. No legacy server key, no API key.
- Console-only upload. Platform credentials are not exposed on
/v1. - The Web platform credential is never used for sending. Upload the service account under Android.
- Data-only messages: FCM will not draw a notification for you, and your app must render every push it receives — including when the app is backgrounded or killed.
- A force-stopped Android app receives nothing until the user launches it again. That is an Android behavior, not an OpenPush one.
- No topic or condition targeting. OpenPush targets by segment and by subscription; see segments.md.
- TTL is capped at 28 days.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Upload rejected as not a service account | You uploaded an OAuth client JSON or a firebaseConfig object | Download a service account key from Google Cloud, not a client config |
| Web badge is green, but no send works | The service account went into the Web slot | Upload it under Android — the sender reads only the Android credential |
| Sends succeed but reach the wrong project's devices | An app with a misplaced credential is falling back to the platform-wide one | Upload the correct service account under Android for that app |
| Every send fails with "no FCM credentials" | No Android credential on the app, and no platform-wide fallback either | Upload the service account under Android for that app |
| Notifications never appear, but receipts do not either | Nothing is rendering the data-only payload | Add a renderer — your own service, or the optional openpush-android-fcm module |
| Notifications appear twice | Two FirebaseMessagingService implementations in the merged manifest | Keep one; strip the other during manifest merge |
Devices turning to -10 in bulk | Genuine uninstalls or expired tokens reported by FCM | Expected churn; only NOT_FOUND and UNREGISTERED change status |
| A whole campaign stops with nothing marked bad | An OAuth refresh failure | Fix the service account and resend; no device state was changed |
| The message arrives with a truncated body | The rendered payload exceeded FCM's 4 KB data envelope | Shorten content, or move bulk data behind a deep link; check op_render_truncated |
FAQ
Can I keep using a legacy FCM server key? No. Only the HTTP v1 API with a service-account JSON is implemented.
Why is there a Web platform slot if it does not send?
It stores what browsers need to subscribe — the VAPID public key, your site URL, and the firebaseConfig object published by the web push config endpoint. The sending credential belongs under Android.
Can I set the notification channel or accent colour from the composer?
You can set them on the message, and they arrive in the data payload as op_android_* keys. Your app applies them when it renders. FCM is never asked to draw the notification.
Does one service account cover several OpenPush apps? Yes, if they all live in the same Firebase project. Upload it under Android on each OpenPush app.
How do I confirm which credential a send actually used?
Check the app's own platforms readiness first. If the app has no Android credential, the send used the platform-wide fallback.
Related
- quickstart.md
- sdk-android.md
- sdk-unity.md
- web-push.md
- platform-setup-apns.md
- sending-messages.md
- security-and-limits.md
- ../api-handbook/01-apps-keys-settings.md