# 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:

| Platform | Credential                                                                        | Where it comes from                                                                                                               |
| -------- | --------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| Android  | **FCM v1 service-account JSON**                                                   | Firebase console → Project settings → Service accounts → *Generate new private key* (`projectId`, `client_email`, `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 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:

```text
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

```text
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.

```json
{
  "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](/docs/push/sending). 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](/docs/push/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:

```text
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 (`ABCDE12345` 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 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](/docs/rich-notifications) do not apply to them. Apps that use only one path are unaffected.
