Push Credentials

The FCM v1 service account and Apple APNs .p8 key the push service sends with — where to get them, how to save them, and how the live validation probe works.

Universal push sends through your Firebase project and your Apple developer key. Two credential sets per app:

PlatformCredentialWhere it comes from
AndroidFCM v1 service-account JSONFirebase console → Project settings → Service accounts → Generate new private key (projectId, client_email, private_key).
iOSAPNs .p8 key + its Key ID, your Team ID and the app's Bundle IDApple Developer portal → Keys → + → enable APNs → download the .p8 (Apple only lets you download it once).

Legacy keys are gone:

The legacy FCM server key (AAAA…) is no longer used — Google shut it down. FCM v1 with a service account is the only supported Android credential.

Saving credentials

Save both sets per app in the dashboard (App Settings → Push credentials). Each save runs a live probe immediately — Google must mint an OAuth token from the service account, and Apple must answer the deliberately-invalid probe token with BadDeviceToken, which proves the key — so a save and its verification are one round trip, with a plain-English result.

Scripts and agents can do the same:

POST /api/apps/:appId/save-credentials-firebase   (dashboard session or MCP token; owner / admin)
POST /api/apps/:appId/save-credentials-apns       (dashboard session or MCP token; owner / admin)

Both are targeted saves: each updates only the columns it owns, so saving Firebase can never clear the Apple key and vice versa. A teammate with the developer role gets 403 admin_only, and both routes refuse an app token (it ships inside your apps, so it can never change credentials). Secrets are never echoed back; the .p8 key is stored encrypted (AES-256-GCM).

Validating at any time

POST https://app.nativenotify.com/api/universal/credentials/validate

Body: appId, appToken (or call it with a dashboard session / MCP token and just the appId). Same live probe as the saves, same owner/admin gate.

{
  "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 probe carries the reasonCode Apple or Google returned (for example InvalidProviderToken).

  • FCM — the stored JSON really can mint an OAuth token for the project. If it cannot, Android sends for that app fail with the credential reason AccessTokenUnavailable — they are attempted, not skipped.
  • APNs — the stored .p8, Key ID, Team ID and Bundle ID build a valid JWT and Apple accepts it. An authentication or topic error such as InvalidProviderToken or BadTopic means the credentials are wrong, or the bundle id does not match the app.

What happens without credentials

Sends do not fail because of a missing credential — they skip that transport's tokens and report it: skipped: { apns: N, fcm: M } in the send response. A skipped token is an app-config gap, never counted as the token's fault. Credentials that are saved but wrong are different: those tokens are attempted and fail with a credential reason.

Web push needs no credentials of yours — the VAPID keypair is generated server-side (see Web Push).

Classic apps and the Expo path

Classic apps that send iOS pushes straight to APNs keep their Apple key in App Settings → iOS (Legacy): drop the AuthKey_XXXXXXXXXX.p8 file there and press Upload key — the Key ID fills in from the file name — then fill in the Team ID and Bundle Identifier if they are empty. The same upload, for scripts:

POST https://app.nativenotify.com/api/app/:appId/apns-key   (dashboard session or MCP token; owner / admin)
FieldRequiredNotes
p8ContentyesThe full text of the .p8 file. Must be an Apple push key (EC P-256).
keyIdnoThe 10-character Key ID (ABCDE12345 in AuthKey_ABCDE12345.p8).
teamIdnoThe 10-character Team ID.
bundleIdnoThe iOS bundle identifier.

A successful upload answers 200 with { ok, fileName, keyId, teamId, bundleId } — identifiers you did not send keep their stored value, and the key itself is never returned. Errors: 400 missing_field / invalid_field, 403 (an app token, or admin_only), 404 app_not_found, 503 encryption_unavailable (nothing was changed).

Saving universal credentials changes an app's SDK-path sends too:

Universal-push credentials are stored in the same app fields as the classic FCM v1 / APNs settings. Once they are saved, that app's SDK-path sends (Expo push tokens) also deliver through FCM v1 and APNs directly: those deliveries have no Expo receipts, and the rich Expo fields do not apply to them. Apps that use only one path are unaffected.