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 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: 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 callsPOST /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’sPushSubscription.toJSON()envelope (endpoint URL plus thep256dhandauthkeys), which Orbit stores as one canonical token. Web push subscribers fetch the VAPID public key fromGET /api/v1/push/configbefore subscribing. Subscription endpoints must be publicly reachable URLs; a browser endpoint that resolves to an internal address is rejected at registration.
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 atitle 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 soGET /api/v1/push/notificationscan 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, andapns_expiration— APNs delivery priority, coalescing, and expiry.actions— up to four in-notification buttons (for example “Pay now” / “Dismiss”).
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:device_token_ids— explicit registry ids. Use it when you already know exactly which devices should get the notification.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.- Broadcast (
user_ids: ["*"]) — every deliverable device in the tenant, capped at 100,000 devices. A broadcast above the limit is rejected with aBROADCAST_TOO_LARGEvalidation error so you split it into explicit user lists instead of silently truncating the audience.
NO_DEVICE_TOKENS validation error.
Two optional fields change how the notification is classified on the device:
notification_channel— names a category registered throughPOST /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 Androidchannel_iddefault — 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 anotifications 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 futuresend_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; 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
voippriority / FCMhighpriority. 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 APNsBadDeviceToken, 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/:idwithenabled: falseskips the device on every send while keeping the token on file;DELETE /api/v1/push/device-tokens/:idremoves it outright (for example on sign-out).
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 — the channel guide: setup, sample requests, and the send surface.
- The notification model — operator-facing alerts (the dashboard bell, email, and digests), a separate system from pushed-to-customer notifications.
- Send gating and quiet hours — tenant-owned timing policy that scheduled
send_atpairs with. - Consent and suppression — the suppression entries the send path screens every target against.
- Webhook fan-out and event sinks — subscribing to the
push.queued/push.sent/push.failed/push.delivered/push.openedlifecycle.