# Push Credentials

The FCM v1 service account and Apple APNs .p8 key the universal 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 for each app from the dashboard — the Agent Notify getting-started flow walks through the Firebase and Apple wizards, and each save **runs a live probe immediately** (Google must mint an OAuth token from the service account; Apple must answer the deliberately-invalid probe token with `BadDeviceToken`, which proves the key). Save + verify happen in one round trip, and the response narrates the plain-English result.

Behind the scenes the dashboard calls:

```text
POST /api/agent/apps/:appId/save-credentials-firebase   (session, admin)
POST /api/agent/apps/:appId/save-credentials-apns       (session, admin)
```

Both are **targeted saves**: each one updates only the columns it owns, so saving Firebase can never clear the Apple key and vice versa. Secrets are never echoed back in any response. The `.p8` content is stored on the server in its key directory; only the file name reaches the database.

## Validating at any time

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

Body: `appId`, `appToken`. This is the same live probe the saves run, and it is **admin-only** — the same role gate that protects changing push credentials on the dashboard.

```json
{
  "ok": true,
  "appId": 123,
  "checkedAt": "2026-09-23T09:42:10.000Z",
  "credentials": {
    "fcm": { "...": "live probe result" },
    "apns": { "...": "live probe result" }
  }
}
```

What the probes prove:

- **FCM** — the stored service-account JSON really can mint an OAuth token for the project. If it cannot, the credentials are wrong (wrong project, revoked key, malformed JSON) and Android sends will be skipped for that app.
- **APNs** — the stored `.p8`, Key ID, Team ID and Bundle ID build a valid JWT and Apple accepts it (the probe token is deliberately invalid, so `BadDeviceToken` is the *success* answer; anything else 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** the tokens of that transport and report it: `skipped: { apns: N, fcm: M }` in the [send response](/docs/universal-push/sending). A skipped token is an app-config gap and is never counted as the token's fault.

## The Expo path's credentials

If you send through the Expo push service (the SDK path — `ExponentPushToken` registrations), credentials are handled the Expo way: FCM/APNs keys live in your Expo/EAS project, and — when enhanced push security is enabled — the app's Expo access token is configured on the app in the dashboard. Universal-push credentials do not affect that path, and vice versa.
