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.
Save them
Save both under App Settings → Push credentials. 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 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 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
Classic apps that send iOS pushes straight to APNs keep their Apple key under 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 and no rich Expo fields; apps that use only one path are unaffected.