Save the FCM v1 service account and APNs .p8 key your sends use, and check them with a live probe.
What you need
| Platform | Credential | Where to get it |
|---|---|---|
| Android | FCM v1 service-account JSON (project_id, client_email, private_key) | Firebase console → Project settings → Service accounts → Generate new private key. |
| iOS | APNs .p8 key, its Key ID, your Team ID and the app's Bundle ID | Apple Developer portal → Keys → + → enable APNs → download the .p8. Apple allows one download. |
The legacy FCM server key (AAAA…) is no longer used; FCM v1 is the only Android credential.
Before you start
- An Apple Developer Program membership ($99 a year). A free Apple ID can run an app on your own device but cannot create an APNs key, so sending to an iPhone needs the paid account.
- Your app's Bundle ID, registered with Apple: Identifiers → + → App IDs → App → an Explicit Bundle ID (the one in Xcode under Signing & Capabilities) → tick Push Notifications → Register. Xcode registers it when you pick your Team and leave automatic signing on. An ID that starts with
com.exampleis a template value; use your own, such ascom.yourname.appname. - The Push Notifications capability on the app target (Xcode → Signing & Capabilities → + Capability). The iOS Simulator and a real iPhone both receive push once it is on. The error
no valid “aps-environment” entitlement string foundmeans the build does not have it. - The APNs key: Keys → + → tick Apple Push Notifications service (APNs) → download the
.p8. Your Team ID is under Membership details.
- A Firebase project (console.firebase.google.com, free) with your app registered under its exact package name.
google-services.json(Firebase console → Project settings → Your apps) in the app module folder, usuallyapp/.- The service-account key: Firebase console → Project settings → Service accounts → Generate new private key.
Save them in Native Notify
In Native Notify, open your project's Settings → Push credentials and save both. Each save runs the live check below and returns its result.
For scripts and agents:
POST /api/apps/:appId/save-credentials-firebase { serviceAccountJson, projectId? }
POST /api/apps/:appId/save-credentials-apns { p8Content, keyId, teamId, bundleId }
Both take a Native Notify dashboard session or MCP token. Only the owner or an admin may save: a developer-role teammate gets 403 admin_only, and an app token is refused because it ships inside your apps. serviceAccountJson is the JSON as text or an object, projectId defaults to its project_id, and keyId and teamId are 10 characters each.
Each answers 200 with saved: true, a check block like those below and ok, the live check result. ok: false means the check failed, but the credential is saved. Each save updates only its own credential, so saving Firebase never clears the Apple key. Secrets are never echoed, and the .p8 key is stored encrypted (AES-256-GCM).
Validate any time
POST https://app.nativenotify.com/api/universal/credentials/validate
Send appId and appToken, or call it with a Native Notify dashboard session or MCP token and only the appId. Same owner / admin gate as the saves.
{
"ok": true,
"appId": 123,
"checkedAt": "2026-09-23T09:42:10.000Z",
"credentials": {
"fcm": {
"configured": true,
"ok": true,
"detail": "Google accepted the stored service-account JSON and minted an OAuth access token (project my-app). The FCM credentials are valid.",
"projectId": "my-app"
},
"apns": {
"configured": true,
"ok": true,
"detail": "Apple authenticated the request: it rejected the deliberately-invalid probe token as expected (BadDeviceToken). The .p8 key, key id and team id are valid for sending.",
"reasonCode": "BadDeviceToken"
}
},
"note": "ok = every CONFIGURED credential passed its live check. A credential that is not configured reports configured:false."
}
ok is true only when every configured credential passed. A platform with nothing saved reports configured: false, and an app with nothing configured answers ok: false. A failed check carries the reasonCode Apple or Google returned, such as InvalidProviderToken.
- FCM: Google must mint an OAuth token from the stored JSON. If it can't, Android sends fail with the credential reason
AccessTokenUnavailable. They are attempted, not skipped. - APNs: the stored
.p8, Key ID, Team ID and Bundle ID must build a JWT, and Apple must answer a deliberately invalid probe token withBadDeviceToken, which proves the key.InvalidProviderTokenorBadTopicmeans a wrong credential, or a bundle id that doesn't match the app.
Without credentials
A send doesn't fail for a missing credential. It skips that transport's tokens and reports skipped: { apns: N, fcm: M } in the send response, never counting them against the device. Saved but wrong credentials are different: those tokens are attempted and fail with a credential reason. Web push needs none, because the VAPID keypair is generated server-side (Web push).
Classic apps
To move a Classic app to the current service, see Migrate a Classic app; it tests a new key with a real push before saving it.
Classic apps that send iOS pushes straight to APNs keep their Apple key in the Classic view of Native Notify, under the app's App Settings → iOS (Legacy). Upload the AuthKey_XXXXXXXXXX.p8 file there; the Key ID fills in from the file name, then fill in the Team ID and Bundle Identifier if they're empty. For scripts:
POST https://app.nativenotify.com/api/app/:appId/apns-key (dashboard session or MCP token; owner / admin)
| Field | Required | Notes |
|---|---|---|
p8Content | yes | The full text of the .p8 file. Must be an Apple push key (EC P-256). |
keyId | no | The 10-character Key ID, as in AuthKey_ABCDE12345.p8. |
teamId | no | The 10-character Team ID. |
bundleId | no | The iOS bundle identifier. |
A successful upload answers 200 with { ok, fileName, keyId, teamId, bundleId }. Identifiers you didn't send keep their stored value, and the key is never returned. Errors: 400 missing_field or invalid_field, 403 (an app token, or admin_only), 404 app_not_found, and 503 encryption_unavailable (nothing changed).
Saving these also changes SDK-path sends
Once saved, they make the app's SDK-path sends (Expo push tokens) deliver through FCM v1 and APNs directly, with no Expo receipts; the rich fields still apply, and apps that use only one path are unaffected.