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

# Push broadcasts: credentials, payload shape, and the pre-send caps

> Set up push broadcasts before you send one — APNs, FCM, HMS, and VAPID credentials, the request payload shape per provider, the two pre-send caps (audience size, per-minute rate), device-token lifecycle and pruning, delivery receipts, and when push beats SMS or the inbox.

# Push broadcasts: setup and pre-send caps

A push **broadcast** is a wildcard send — `user_ids: ["*"]` on
`POST /api/v1/push/send` — that fans one notification out to every opted-in
device in your tenant. Run this checklist once, before the first wildcard
send, so the two pre-send gates (audience size, per-minute rate) never trip
on a live campaign.

Each step below is a **tenant-owned control** you configure once; nothing on
this page is platform-internal.

## 1. Provider credentials

Orbit holds your provider credentials and fans a send out to the right
transport from the single `POST /api/v1/push/send` endpoint. Configure each
provider you need — a send fans out per-device, so an iOS-only tenant needs
only APNs, and unconfigured providers return per-device `status: "failed"`
entries rather than a top-level error.

| Provider | Audience | Credentials | Where you configure it |
| - | - | - | - |
| **APNs** (iOS) | iPhone / iPad / Mac / Watch / Vision | Token-based `.p8` Auth Key: Key ID, Team ID, and the `.p8` private key | Your operator's deployment environment |
| **FCM** (Android) | Android with Google Play Services | Firebase **project ID**; OAuth2 access tokens are minted automatically on GKE from the Workload Identity metadata server, with a static-access-token fallback off-cluster | Your operator's deployment environment |
| **Web Push** (VAPID) | Browsers | One VAPID keypair (`npx web-push generate-vapid-keys`) plus a `mailto:` subject | Your operator's deployment environment |
| **HMS** (Huawei) | Huawei devices without Google Play | AppGallery Connect **App ID** + **App secret** (OAuth2 `client_credentials`) | Your operator's deployment environment |

Bring-your-own credentials cover **all four providers** in the same channel,
and the per-send pricing page still shows a flat platform fee — no per-push
provider charge on the unified send. The detailed table per provider, with
the exact variable names and where to source each value, lives on the
[Push channel](/channels/push) reference (sections *Apple Push setup*,
*Google / FCM*, *Web Push setup*, and *Huawei Push setup*).

<Note>
  Rotate only when a key is compromised. Rotating APNs / VAPID / HMS
  credentials **invalidates every existing subscription** — end users must
  re-subscribe. FCM access tokens rotate automatically (GKE metadata-server
  tokens refresh on a 1-hour TTL); no manual action is needed on GKE.
</Note>

Validate the full chain end-to-end with
[`POST /api/v1/push/test-send`](/api-reference/endpoints/push):
it fires one fixed test notification to **your own** registered devices and
returns a per-token verdict per provider — never a broadcast, and never
counted as a tracked send.

## 2. Payload shape per provider

One request shape covers every provider; the per-platform override objects
fine-tune a single transport without forking your send code.

```json theme={null}
{
  "user_ids": ["user_8a1f2c"],
  "title": "Order update",
  "body": "Order #12345 is ready for pickup",
  "data": { "order_id": "12345" },
  "image_url": "https://cdn.example.com/orders/12345.png",
  "deep_link": "myapp://orders/12345",
  "badge": 1,
  "notification_channel": "orders",
  "mutable_content": true,
  "interruption_level": "active",
  "apns_priority": 10,
  "actions": [
    { "id": "view", "title": "View order" },
    { "id": "dismiss", "title": "Dismiss", "destructive": true }
  ],
  "ios":     { "sound": "default", "thread_id": "orders" },
  "android": { "channel_id": "orders", "priority": "high" },
  "webpush": { "icon_url": "https://cdn.example.com/icon.png", "require_interaction": true }
}
```

Key rules per provider:

* **APNs (iOS)** — `title` (≤ 500) + `body` (≤ 4000) are required;
  `badge`, `thread_id`, `interruption_level`, `relevance_score`, and
  `mutable_content` map straight onto the APNs payload; keep the whole
  payload under the 4 KB provider cap.
* **FCM (Android)** — `channel_id` picks the Android notification channel,
  `priority` is `normal` or `high`, and `ttl_seconds` bounds how long FCM
  stores an offline device.
* **Web Push (VAPID)** — `topic` must match `^[A-Za-z0-9_-]{1,32}$`, and
  `actions` is capped at **2** per RFC 8294; a `topic` with disallowed
  characters is rejected with HTTP 422 before the provider is ever called.

The full field table with every per-platform override lives on the
[Push channel](/channels/push#fields) reference.

## 3. Broadcast caps (pre-send gates)

Two gates refuse a broadcast **before dispatch** — neither reaches the
provider, no wallet hold is taken, and both are deterministic (retrying the
identical shape computes the same refusal). Read the `error.details`
envelope to tell which gate fired.

| Code | HTTP | Gate | Ceiling |
| - | - | - | - |
| `BROADCAST_TOO_LARGE` | 422 | **Audience size** on the dedicated `POST /api/v1/push/send` wildcard | 100,000 **deliverable** devices (enabled tokens minus suppression opt-outs) — a platform constant, never tenant-configurable |
| `CHANNEL_RATE_LIMITED` | 429 | **Per-minute velocity** from the cross-channel fraud guard, on the unified `POST /api/v1/messages` pipeline | Your `push` per-channel envelope — `GET` / `PUT /api/v1/settings/compliance/channel-rate-overrides` |

Fix the gate at **its own** surface — split a 422 into explicit `user_ids`
batches, or throttle / raise the `push` override for a 429. The full
refusal-decoding runbook, with the `details` envelope shapes and the
recovery sequence, is the
[push broadcast pre-send gates](/troubleshooting/push-broadcast-caps)
page.

## 4. Device-token lifecycle and pruning

Device tokens are the audience a broadcast counts; keep them current or the
audience gate over- or under-counts.

1. **Register once per (org, app, user)** — the mobile SDK (`@devotel/orbit-mobile`)
   or web SDK (`@devotel/orbit-web`) calls
   `POST /api/v1/push/device-tokens` with the device's APNs / FCM / VAPID
   subscription. Re-registering the same token is an idempotent upsert — it
   refreshes `last_seen_at` and never duplicates the row.
2. **Rotate automatically** — the SDK hands a refreshed platform token to
   the same endpoint; the platform resolves the rotation server-side.
3. **Prune dead tokens** — a token the provider reports as permanently gone
   (APNs `410`, FCM `UNREGISTERED`) is **removed automatically** on the
   delivery path, so the next send skips it. Disable a noisy device without
   deleting it via `PATCH /push/device-tokens/:id` (`enabled: false`), or
   delete outright via `DELETE /push/device-tokens/:id`.

The audience gate counts only **deliverable** tokens — active suppression
and disabled rows never count — so pruning keeps your broadcast-size
estimate honest.

## 5. Delivery receipts

A send returns **201** immediately with a per-device verdict array;
durability lives in the delivery log, not the send response.

* **Per-device verdicts** — each entry in `notifications[]` carries
  `status` (`sent` / `failed` / `skipped`), `delivered_at`, `opened_at`,
  and any provider `error`. `total` and `sent` aggregate the page.
* **Delivery log** — page through `GET /api/v1/push/notifications` with a
  `cursor` to reconcile a broadcast after the fact.
* **Webhooks** — when the SDK is integrated, `push.delivered` and
  `push.opened` fire as webhook events so your pipeline gets receipts
  without polling.

Decode per-device failures (token expiry, unconfigured provider, VAPID
rotation) on the [push token expiry](/troubleshooting/push-token-expiry)
runbook.

## 6. When to prefer push vs SMS or the inbox

| Reach | Prefer push | Prefer SMS | Prefer the inbox / email |
| - | - | - | - |
| **Your own app users with the SDK registered** | **Yes** — free of carrier fees, per-device verdicts, no PII on the lock screen if you only send `data` | Only when the app is not installed | No — the inbox is for conversations |
| **Cold audiences / marketing blasts** | Only if every recipient registered a device — otherwise 422 `NO_DEVICE_TOKENS` | **Yes** — phone-number reach | For long-form content |
| **Transactional one-to-one** (OTP, order updates) | Yes when the recipient has the app | **Yes** for the no-app segment | No |
| **Two-way conversation** | No — push is one-way | Yes | Yes — inbox / email threads |

Push is the right surface for **your installed base** (no carrier cost,
per-device receipts, capability flags like badge / sound / mutable-content).
SMS still wins for reach across phone numbers, and the inbox wins when the
user needs to reply.

## See also

* [Push channel](/channels/push) — the full send/reference surface, per-platform field tables, and the common-errors table.
* [Push broadcast pre-send gates](/troubleshooting/push-broadcast-caps) — decode `BROADCAST_TOO_LARGE` and `CHANNEL_RATE_LIMITED`.
* [Push token expiry](/troubleshooting/push-token-expiry) — per-device provider verdicts and token pruning.
* [Per-channel rate overrides](/compliance/channel-rate-overrides) — the tenant-owned `push` per-minute envelope.
* [Push notification categories](/guides/push-notification-categories) — curated APNs / FCM category registry.
