> ## Documentation Index
> Fetch the complete documentation index at: https://docs.brudcast.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Register device tokens on contacts

> Register your users' Firebase device tokens on their contacts from your backend, keep them current, and handle tokens that expire.

export const Screenshot = ({id, src, srcDark, alt, caption}) => {
  if (!src) {
    return <Placeholder id={id} kind="screenshot" label="Screenshot coming soon" description={alt} icon={<ImageIcon />} />;
  }
  return <figure className="brd-media" data-media-id={id}>
      <img className="brd-media-frame block dark:hidden" src={src} alt={alt} />
      <img className="brd-media-frame hidden dark:block" src={srcDark || src} alt={alt} />
      {caption && <figcaption className="brd-media-caption">{caption}</figcaption>}
    </figure>;
};

Brudcast can only notify a device it knows about. Your app gets a registration token from the
Firebase SDK, and your backend registers that token against the contact who uses the device. The
call comes from your backend because it needs your organization's API key, which must never ship
inside an app.

<Screenshot id="ss-push-register-device-tokens-hero-contact-devices" alt="A contact's push devices: one Android device that is active, and one iOS device marked Invalidated with the reason Firebase gave" />

```mermaid theme={"system"}
sequenceDiagram
  participant App as Your app
  participant BE as Your backend
  participant BR as Brudcast
  App->>App: Get a registration token from the Firebase SDK
  App->>BE: Send the token for the signed-in user
  BE->>BR: Register the device on the contact
  BR-->>BE: The device, with its id
```

<Info>
  **Before you start**, you need:

  * An app in **Channels > Push** that isn't disabled. See
    [Connect Firebase](/channels/push/connect-firebase).
  * A platform API key with the `contacts:write` scope, kept on your server. Listing apps also needs
    `senders:read`. See [API keys](/developers/api-keys).
  * The contact who uses the device, already in Brudcast.
</Info>

<Snippet file="auth-headers.mdx" />

## Find your app's ID

Every registration names the app the token was issued for. List your apps to get its `id`. The
`identifier` field is the Firebase project ID.

```bash theme={"system"}
curl https://core-service.prod.brudcast.com/api/v1/user/push-applications \
  -H "X-API-Key: bk_live_your_api_key" \
  -H "Accept: application/json"
```

```json theme={"system"}
{
  "success": true,
  "message": "Push applications retrieved successfully",
  "data": [
    {
      "id": "0192f3a4-1111-7abc-8def-0123456789ab",
      "name": "Acme Android",
      "provider": "fcm",
      "identifier": "acme-prod-1234",
      "platforms": ["android", "ios"],
      "status": "active",
      "isDefault": true,
      "activeTokenCount": 0
    }
  ],
  "meta": { "total": 1, "perPage": 20, "currentPage": 1, "lastPage": 1 }
}
```

## Register a device

`POST /contacts/{contactId}/push-notifications`

<ParamField path="contactId" type="string" required>
  The contact who uses the device.
</ParamField>

<ParamField body="pushApplicationId" type="string" required>
  The app the token was issued for. It must belong to your organization and must not be disabled.
</ParamField>

<ParamField body="deviceToken" type="string" required>
  The Firebase registration token. Up to 4,096 characters.
</ParamField>

<ParamField body="platform" type="string" required>
  `android`, `ios` or `web`.
</ParamField>

<ParamField body="appVersion" type="string">
  Your app's version, for your own records.
</ParamField>

<ParamField body="deviceInfo" type="object">
  Any JSON object that describes the device, such as its model or operating system version.
</ParamField>

<ParamField body="isPrimary" type="boolean">
  Marks this as the contact's primary device. Other devices keep their flag, so more than one can
  be primary.
</ParamField>

<CodeGroup>
  ```bash cURL theme={"system"}
  curl -X POST https://core-service.prod.brudcast.com/api/v1/user/contacts/your_contact_id/push-notifications \
    -H "X-API-Key: bk_live_your_api_key" \
    -H "Content-Type: application/json" \
    -H "Accept: application/json" \
    -d '{
      "pushApplicationId": "your_push_application_id",
      "deviceToken": "the_device_registration_token",
      "platform": "android",
      "appVersion": "4.2.0"
    }'
  ```

  ```javascript Node.js theme={"system"}
  const response = await fetch(
    `https://core-service.prod.brudcast.com/api/v1/user/contacts/${contactId}/push-notifications`,
    {
      method: "POST",
      headers: {
        "X-API-Key": process.env.BRUDCAST_API_KEY,
        "Content-Type": "application/json",
        Accept: "application/json",
      },
      body: JSON.stringify({
        pushApplicationId,
        deviceToken,
        platform: "android",
        appVersion: "4.2.0",
      }),
    },
  );
  const { data: device } = await response.json();
  ```

  ```python Python theme={"system"}
  import os
  import requests

  response = requests.post(
      f"https://core-service.prod.brudcast.com/api/v1/user/contacts/{contact_id}/push-notifications",
      headers={"X-API-Key": os.environ["BRUDCAST_API_KEY"], "Accept": "application/json"},
      json={
          "pushApplicationId": push_application_id,
          "deviceToken": device_token,
          "platform": "android",
          "appVersion": "4.2.0",
      },
  )
  response.raise_for_status()
  device = response.json()["data"]
  ```

  ```php PHP theme={"system"}
  $ch = curl_init("https://core-service.prod.brudcast.com/api/v1/user/contacts/{$contactId}/push-notifications");
  curl_setopt_array($ch, [
      CURLOPT_POST => true,
      CURLOPT_RETURNTRANSFER => true,
      CURLOPT_HTTPHEADER => [
          "X-API-Key: " . getenv("BRUDCAST_API_KEY"),
          "Content-Type: application/json",
          "Accept: application/json",
      ],
      CURLOPT_POSTFIELDS => json_encode([
          "pushApplicationId" => $pushApplicationId,
          "deviceToken" => $deviceToken,
          "platform" => "android",
          "appVersion" => "4.2.0",
      ]),
  ]);
  $device = json_decode(curl_exec($ch), true)["data"];
  ```
</CodeGroup>

The response is `201`, whether the device is new or already known.

```json theme={"system"}
{
  "success": true,
  "message": "Push notification device added successfully",
  "data": {
    "id": "0192f3a4-2222-7abc-8def-0123456789ab",
    "contactId": "0192f3a4-3333-7abc-8def-0123456789ab",
    "organizationId": "0192f3a4-4444-7abc-8def-0123456789ab",
    "pushApplicationId": "0192f3a4-1111-7abc-8def-0123456789ab",
    "pushApplication": {
      "id": "0192f3a4-1111-7abc-8def-0123456789ab",
      "name": "Acme Android",
      "provider": "fcm",
      "identifier": "acme-prod-1234",
      "status": "active"
    },
    "deviceToken": "the_device_registration_token",
    "platform": "android",
    "status": "active",
    "appVersion": "4.2.0",
    "deviceInfo": null,
    "isPrimary": false,
    "lastUsedAt": "2026-09-11T10:15:00.000+00:00",
    "expiresAt": null,
    "invalidatedAt": null,
    "lastFailureReason": null,
    "optedOutAt": null,
    "createdAt": "2026-09-11T10:15:00.000+00:00",
    "updatedAt": "2026-09-11T10:15:00.000+00:00"
  }
}
```

Keep the device `id`. You need it to update or remove the device, and the
`push.token_invalidated` webhook names devices by it.

## Registering again is safe

Brudcast keeps one record per token per app. Registering a token that the app already knows updates
that record instead of adding a second one.

* If the token belonged to another contact, it moves to this one. This covers a shared device, or
  one user signing out and another signing in.
* The device becomes `active` again, and any earlier opt-out or expiry is cleared.
* `appVersion`, `deviceInfo` and `isPrimary` keep their earlier values unless you send new ones.

So register the device every time a user signs in, and whenever Firebase gives your app a new token.
The token on an existing record can't be edited, so when Firebase issues a new token, register the
new one and remove or deactivate the old record.

<Warning>
  Registering reactivates a device. Don't register a device whose user turned notifications off in
  your app. [Mark it inactive](#when-a-user-turns-notifications-off) instead, and register it again
  only when they turn notifications back on.
</Warning>

## Register devices when you create a contact

`POST /contacts` also accepts devices, in a `pushNotifications` array alongside the contact's other
fields. Each entry takes the same fields as a single registration, including the required
`pushApplicationId`. The same one-record-per-token rule and the same subscribers quota apply.

```json theme={"system"}
{
  "pushNotifications": [
    {
      "pushApplicationId": "your_push_application_id",
      "deviceToken": "the_device_registration_token",
      "platform": "ios"
    }
  ]
}
```

## The subscribers quota

A subscriber is a contact with at least one active device. Push plans cap how many subscribers your
organization can have, and the free plan allows 1,000.

* A contact counts once, however many devices it has.
* Registering a device for a contact that already has an active device never counts against the
  cap.
* Inactive and expired devices don't count, so a contact whose devices have all expired or opted
  out frees its place.
* Without a push plan, registration still works up to the free plan's 1,000 subscribers, so your
  app can start collecting tokens before you buy push.

When a registration would take you past the cap, the call fails with status `400` and code
`QUOTA_EXCEEDED`, and the device isn't saved. Upgrade your push plan in **Billing & Plans**, or
remove devices you no longer need.

## When a user turns notifications off

When a user switches notifications off in your app, mark the device `inactive`. Brudcast records
when in `optedOutAt` and stops sending to that device.

```bash theme={"system"}
curl -X PATCH https://core-service.prod.brudcast.com/api/v1/user/contacts/your_contact_id/push-notifications/your_device_id \
  -H "X-API-Key: bk_live_your_api_key" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{ "status": "inactive" }'
```

Set `status` back to `active` when they turn notifications on again. That clears `optedOutAt`.

Brudcast can't see a permission the user revokes in the device's own settings. Check the permission
when your app opens, and mark the device inactive if it's gone.

To remove a device completely, for example when the user signs out, send
`DELETE /contacts/{contactId}/push-notifications/{id}`.

| Device status | Meaning                                                  | Receives campaigns |
| ------------- | -------------------------------------------------------- | ------------------ |
| `active`      | Registered and accepted by Firebase so far               | Yes                |
| `inactive`    | Your app reported that the user turned notifications off | No                 |
| `expired`     | Firebase no longer accepts the token                     | No                 |

## Expired tokens

Tokens stop working when the app is uninstalled, when its data is cleared, or when Firebase replaces
the token. When Firebase rejects a token as unregistered, invalid, or issued by a different Firebase
project, Brudcast expires the device automatically.

* `status` becomes `expired`.
* `invalidatedAt` records when, and `lastFailureReason` holds Firebase's reason. The contact's page
  shows the device as invalidated, with the reason.
* Later campaigns skip the device, and it stops counting as a subscriber.

You don't need to clean anything up. If the same token is registered again, the device becomes
active again.

### The push.token\_invalidated webhook

To hear about each expiry, for example to delete the token from your own database, subscribe a
webhook endpoint to `push.token_invalidated` under **Developers > Webhooks**. The event fires once
per device, when Brudcast expires it.

```json theme={"system"}
{
  "delivery_id": "0190f5c1-2b3c-7d4e-9f5a-6b7c8d9e0f1a",
  "event": "push.token_invalidated",
  "data": {
    "tokenId": "0190f5b0-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
    "contactId": "0190f4ff-9e8d-7c6b-5a4f-3e2d1c0b9a87",
    "applicationId": "0190f5a2-7c1e-7b3a-9d4e-2f6a8b1c3d5e",
    "platform": "android",
    "reason": "The reason Firebase gave",
    "occurredAt": "2026-09-11T10:02:17.000Z"
  },
  "timestamp": 1757584937
}
```

| Field           | Contains                                              |
| --------------- | ----------------------------------------------------- |
| `tokenId`       | The device's `id`, as returned when you registered it |
| `contactId`     | The contact the device belongs to                     |
| `applicationId` | The app the token was registered against              |
| `platform`      | `android`, `ios` or `web`                             |
| `reason`        | Firebase's reason for rejecting the token             |
| `occurredAt`    | When Firebase rejected it, as an ISO 8601 timestamp   |

The event doesn't carry the token itself. Match on `tokenId`. See
[Webhook event types](/developers/webhooks/event-types).

## Errors

| Status and code                 | When                                                                 |
| ------------------------------- | -------------------------------------------------------------------- |
| `400` `BUSINESS_RULE_VIOLATION` | The app doesn't belong to your organization, or it is disabled       |
| `400` `QUOTA_EXCEEDED`          | The device would take you past the subscribers cap                   |
| `401`                           | The API key is missing, malformed, revoked or expired                |
| `403`                           | The key lacks the `contacts:write` scope                             |
| `404` `BUSINESS_RULE_VIOLATION` | The contact doesn't exist in your organization                       |
| `422`                           | A field failed validation, for example a missing `pushApplicationId` |
| `429`                           | Rate limit exceeded. Wait and retry                                  |

## Related

<Columns cols={2}>
  <Card title="Connect Firebase" icon="key-round" href="/channels/push/connect-firebase">
    Create the Firebase service account key and add your app.
  </Card>

  <Card title="Write the notification" icon="pen-line" href="/channels/push/send-your-first-push#write-the-notification">
    Send to the devices you've registered.
  </Card>

  <Card title="Sync contacts from your app" icon="refresh-cw" href="/developers/guides/sync-contacts-from-your-app">
    Create and update contacts from your backend.
  </Card>

  <Card title="Push troubleshooting" icon="wrench" href="/channels/push/send-your-first-push#troubleshooting">
    Invalid tokens and campaigns that reach no one.
  </Card>
</Columns>
