Skip to main content

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. 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 reference (sections Apple Push setup, Google / FCM, Web Push setup, and Huawei Push setup).
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.
Validate the full chain end-to-end with POST /api/v1/push/test-send: 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.
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 reference. Swap the targeted user_ids list for the wildcard ["*"] and the same shape becomes a broadcast — one request that fans out to every deliverable device in your tenant:
An immediate send answers 201 with one notifications[] verdict row per targeted device — a failed or skipped row never fails the request:
Supply a future send_at on the same payload and the response flips to 202 with the scheduled push id; the scheduler dispatches it at that time.

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.

BROADCAST_TOO_LARGE (422)

The audience gate refuses the whole wildcard before any device is targeted; error.message carries the deliverable-device count it counted:

CHANNEL_RATE_LIMITED (429)

The velocity gate names its channel in error.details so a push refusal is never confused with an SMS or email one. The response also carries the standard rate-limit headers — back off for Retry-After seconds before retrying:

Raising the push envelope

Read your current overrides, the cluster defaults, and the sanity ceiling in one call:
The push envelope lives under the same endpoint as every outgoing channel (owner-only to write). PUT replaces the whole overrides map — send every channel you want active or the omitted ones revert to the cluster default on the next send:
The PUT echoes the same { overrides, defaults, channels, max_per_minute } shape as the GET, so you can confirm the write landed without a second read. 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 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.
    The 201 echoes the stored row, including the id you use for PATCH / DELETE below:
    Repeat the same POST after an OS token refresh or on every app start — the upsert returns the same id, it never inserts twice. For web push, send "platform": "web" with the browser PushSubscription.toJSON() envelope under subscription instead of token.
  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 200 returns the updated row with its new enabled flag; a disabled token stays on file but every send path skips it, and the audience gate no longer counts it. DELETE on the same URL drops the row entirely with a 204 and no body.
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. The log is newest-first and capped at 200 rows per page; cursor is the created_at of the last row you already hold, so page forward until a page returns fewer rows than your limit.
    Each row carries the per-device status, delivered_at / opened_at receipts, and the error string when the provider refused that device — the row-level trail behind the notifications[] verdicts the send itself returned.
  • 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 runbook.

6. When to prefer push vs SMS or the inbox

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