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

# The push channel model: device registry, fan-out, delivery, and VoIP call push

> How a push notification travels from your API call to a device — the device-token registry across APNs, FCM, Huawei Push Kit and Web Push, the targeting and capability flags on each send, per-device delivery results, scheduled sends, token hygiene, and the separate VoIP registry that rings agents on inbound calls.

# The push channel model

When you send a push through Orbit, one API call fans out across four transports — APNs for iOS, FCM for Android, Huawei Push Kit (HMS), and Web Push (VAPID) — delivered through the device registry your clients build up over time. This page explains that model end to end: what a push-capable device is, which capability flags travel with a send, how targeting resolves to device rows and how partial failures read back, scheduled sends, and the separate VoIP registry used to ring agents on inbound calls.

The [Push channel guide](/channels/push) walks through the API surface with copy-pasteable requests; this page is the model behind it. It is also deliberately separate from the [notification model](/concepts/notification-model): that page covers *operator* alerts inside your own workspace, while this one covers pushes you send to your end users' devices.

## Section 1 — What a push-capable device is

A push-capable device is a row in the device registry, created when your client SDK calls `POST /api/v1/push/device-tokens` (the older `POST /api/v1/push/register` alias behaves identically). The platform picked determines what the client supplies:

* **iOS (`ios`) and Android (`android`)** — the native APNs or FCM registration token.
* **Huawei (`huawei`)** — the HMS Push Kit registration token.
* **Web (`web`)** — the browser's `PushSubscription.toJSON()` envelope (endpoint URL plus the `p256dh` and `auth` keys), which Orbit stores as one canonical token. Web push subscribers fetch the VAPID public key from `GET /api/v1/push/config` before subscribing. Subscription endpoints must be publicly reachable URLs; a browser endpoint that resolves to an internal address is rejected at registration.

Each successful registration returns a device id plus the canonical token, and an optional `user_id` ties the device to an end-user identity. If you omit `user_id`, the authenticated caller is used. That link is what makes user-level targeting work: sending to `user_ids` resolves to every registered device that user owns, so an iPhone, an iPad, and a browser all ring from one call. `GET /api/v1/push/device-tokens` lists the registry (optionally filtered by user).

Two registry behaviors matter for day-2 operation. Re-registering an existing token updates the row instead of duplicating it, and supplying `app_install_id` on registration retires the install's older tokens — the mechanism that keeps a reinstalled app from accumulating stale rows.

## Section 2 — Capability flags

Every send carries a `title` and `body` (plus a small `data` map your app reads on tap), and Orbit keeps the payload under the ceiling APNs and FCM enforce: a request whose total payload exceeds roughly 3.8 KB is rejected with a validation error rather than partially delivered.

On top of the body, individual capability flags shape how the OS presents the notification:

* **`badge`** — the iOS Lock Screen badge number, stored on the notification record so `GET /api/v1/push/notifications` can report it.
* **`mutable_content`** — lets the iOS Notification Service Extension rewrite the payload before display; required for rich media handling on the device.
* **`thread_id`** — groups related notifications together on iOS.
* **`interruption_level` / `relevance_score`** — iOS presentation urgency and ordering hints.
* **`apns_priority` (1, 5, or 10), `apns_collapse_id`, and `apns_expiration`** — APNs delivery priority, coalescing, and expiry.
* **`actions`** — up to four in-notification buttons (for example "Pay now" / "Dismiss").

**Silent push** means a push that carries no visible alert. The classic use is badge hygiene: `POST /api/v1/push/badge/clear` (optionally with a `user_id`) sends a background push with `badge: 0` to refresh the icon without showing a banner. Silent pushes are also how VoIP and background-sync payloads travel — the payload is data-only, and the device handles it without a banner.

Per-platform overrides (`ios`, `android`, `webpush` objects on the send) let a single request set platform-specific presentation without splitting into one request per transport.

## Section 3 — Targeting model

A send must name a target, and there are three forms:

1. **`device_token_ids`** — explicit registry ids. Use it when you already know exactly which devices should get the notification.
2. **`user_ids`** — user-level fan-out, resolved to every registered device those users own. One recipient equals one frequency-cap decision, so a user with several devices consumes a single cap slot and either all of their devices receive the push or none are partially capped.
3. **Broadcast (`user_ids: ["*"]`)** — every deliverable device in the tenant, capped at 100,000 devices. A broadcast above the limit is rejected with a `BROADCAST_TOO_LARGE` validation error so you split it into explicit user lists instead of silently truncating the audience.

All three forms screen targets at send time: devices that opted out (a suppression entry for the user's address on the push channel, or channel-wide) are excluded server-side, and devices an operator paused stay registered but are skipped. When no target resolves to a device, the send returns a `NO_DEVICE_TOKENS` validation error.

Two optional fields change how the notification is classified on the device:

* **`notification_channel`** — names a category registered through `POST /api/v1/push/categories`. A category bundles an identifier (wire-bound, so the iOS/Android client matches it), an optional sound, and action buttons. Naming one on a send expands it into the APNs category and becomes the Android `channel_id` default — so interactive buttons and notification settings respect the same label on both platforms. Category records are immutable post-create; the supported update pattern is delete and recreate.
* **`deep_link`** — passed through to the client verbatim; on tap your app receives it and routes to the right screen.

## Section 4 — Delivery and per-device results

The send response is per-device by design. It returns a `notifications` array with one row per targeted device — the notification id, the device id, the transport actually used (`apns`, `fcm`, `webpush`, or `hms`), a status of `sent`, `failed`, or `skipped`, and an `error` when that device did not deliver — plus the totals (`total`, `sent`, `devices_targeted`). A `skipped` row with a frequency-cap message tells you exactly which recipient hit your configured cap and when to retry. Partial failure is expected: one dead device never fails the batch, and the same per-device outcome is mirrored into the tenant message log so the Messages page shows push alongside every other channel.

Every outcome is also emitted to webhook subscribers as a `push.sent` or `push.failed` event per device, and engagement continues after the send. The client SDK calls `POST /api/v1/push/notifications/:id/ack` with `event: "delivered"` when the device receives the push and `event: "opened"` when the user taps it. The ack is idempotent — a repeated ack does not re-fire — and the matching `push.delivered` / `push.opened` webhook emits only on the first genuine transition. `GET /api/v1/push/notifications` pages the full log with delivered and opened timestamps for reconciliation.

Because silent and background pushes can wake a device, the SDK also reports device-side telemetry (`POST /api/v1/push/telemetry` — battery level, wake count, delivery latency), aggregated per user and platform at `GET /api/v1/push/telemetry/summary`. That aggregate is the honest check on background push: median battery level at push wake plus average wake count, so you can answer "is background push draining our users' devices?" with a number.

## Section 5 — Scheduled push

Supplying a future `send_at` on `POST /api/v1/push/send` schedules the notification instead of dispatching it. A strictly-future timestamp persists the full payload, returns `202` with the scheduled push id, and fires a `push.queued` webhook; at send time the scheduler replays that exact payload back through the send path, so suppression, frequency caps, and the multi-transport fan-out apply identically to scheduled and immediate sends. A `send_at` that is at or before now falls through to an immediate send rather than being silently dropped to clock skew.

Manage the queue with `GET /api/v1/push/scheduled` (optionally filtered by status) and `GET /api/v1/push/scheduled/:id`. `DELETE /api/v1/push/scheduled/:id` cancels a still-queued push, but cancel is a guarded status change: a push the scheduler already dispatched or failed returns a conflict so the caller knows it has already left — never a silent no-op.

For time-of-day policy, pair `send_at` with the tenant-owned controls in [Send gating and quiet hours](/concepts/send-gating-and-quiet-hours); scheduling makes the send land inside the window rather than postponing it by hand.

## Section 6 — VoIP push

VoIP push is a second, separate registry — not the device registry above — and it exists because an inbound call has a different delivery contract than a notification. When the ACD queue offers a call to an agent, the ring path fans the call out to the agent's VoIP devices so a locked or force-backgrounded phone rings with the native incoming-call UI (CallKit on iOS, ConnectionService on Android) instead of a passive banner.

The differences from notification push are deliberate:

* **Separate registry and transports.** Agents register PushKit (`voip`) tokens on iOS or high-priority FCM tokens on Android through the VoIP device endpoints (`POST /push/voip-devices`, with list and unregister siblings). Notification tokens and VoIP tokens are owned by different platform APIs, so mixing them into one registry would misroute one or the other.
* **Data-only payload with a bounded TTL.** The payload carries the call id, caller id and name, the queue name, a reservation token, and a time-to-live, sent at APNs `voip` priority / FCM `high` priority. The TTL is clamped to a 5–120 second window (default 30) so the push dies with the ring window instead of ringing a phone after the caller already rolled to the next agent.
* **Fail-open.** Zero registered VoIP devices or unconfigured push credentials degrade the dispatcher without stalling the ring cycle — the queue's existing hunt and no-answer logic keeps working.
* **Existing call endpoints own accept and decline.** Answering or declining routes through the same call endpoints the softphone uses, keyed on the call id the push carries — there is no parallel resolve endpoint to diverge from the call's lifecycle.

## Section 7 — Token hygiene

Push providers report a dead token by name (for example APNs `BadDeviceToken`, FCM `UNREGISTERED`, or a Web Push endpoint that is gone). Orbit treats those permanent-failure signals automatically:

* On the notification registry, a permanently dead token is deleted in-line during the send, so the next send does not repeat the failure. Transient errors (timeouts, rate limits) never trigger cleanup.
* On the VoIP registry, a dead token is marked revoked but the row is kept, so an operator can see why an agent's device stopped ringing rather than watch it vanish.
* Prefer pausing over deleting when the device is still valid: `PATCH /api/v1/push/device-tokens/:id` with `enabled: false` skips the device on every send while keeping the token on file; `DELETE /api/v1/push/device-tokens/:id` removes it outright (for example on sign-out).

Registration-side hygiene — the re-register upsert and the `app_install_id` reinstall dedup from Section 1 — handles the other half: browsers rotate web push subscriptions and mobile OSes renew native tokens, and both paths converge into a single live row per install.

## Where this fits

* **[Push](/channels/push)** — the channel guide: setup, sample requests, and the send surface.
* **[The notification model](/concepts/notification-model)** — operator-facing alerts (the dashboard bell, email, and digests), a separate system from pushed-to-customer notifications.
* **[Send gating and quiet hours](/concepts/send-gating-and-quiet-hours)** — tenant-owned timing policy that scheduled `send_at` pairs with.
* **[Consent and suppression](/concepts/consent-and-suppression-model)** — the suppression entries the send path screens every target against.
* **[Webhook fan-out and event sinks](/concepts/webhook-fan-out-and-event-sinks)** — subscribing to the `push.queued` / `push.sent` / `push.failed` / `push.delivered` / `push.opened` lifecycle.
