Skip to main content

Push notifications end to end

This guide walks the full push lifecycle over the API and SDKs: register device tokens, choose a targeting mode, read per-device delivery results, schedule sends, and track delivery and open engagement. Follow it once and push becomes a closed loop — tokens in, notifications out, engagement acks back. For request and response schemas, see the push API reference. This page covers the workflow; the push channel page covers the full field catalog and provider credential setup for APNs, FCM, HMS, and VAPID. Both are linked rather than repeated below.

1. Register device tokens

A device becomes reachable once it registers a token against an Orbit user. The SDK does this at first run — wire it before your first send, otherwise every send returns NO_DEVICE_TOKENS. Browser (Web Push). Use the OrbitPush surface of the Web SDK. It resolves the VAPID public key, subscribes via pushManager.subscribe, and POSTs the subscription envelope for you:
register() returns null when the user denies notification permission or the browser lacks PushManager (Safari < 16.4) — handle that branch in your onboarding UI. Serve the service-worker bootstrap from your site root so it can claim scope: "/". iOS / Android / Huawei (native). The device obtains its APNs, FCM, or HMS Push Kit token from the OS push SDK, then registers it with POST /api/v1/push/device-tokens. A plain-HTTP call works for any native client:
  • platform — one of ios, android, huawei, web. Web clients pass subscription (the PushSubscription.toJSON() envelope) instead of token; registers are validated against SSRF so the endpoint must be a public HTTPS URL.
  • app_install_id — a stable per-install identifier. Pass it and a reinstall that rotates the token retires the previous token instead of double-registering.
  • Tokens scoped to (org, app, user); user_id defaults to the authenticated caller when omitted.
Register again on OS token refresh and on reinstall — the same token upserts (idempotent), and only a rotated token with app_install_id triggers retirement of the old row. To stop delivery without deleting, toggle PATCH /api/v1/push/device-tokens/:id with { "enabled": false }; to retire permanently, DELETE it.
Test one end-to-end — once your first token lands, fire POST /api/v1/push/test-send to validate the whole credential chain against the calling user’s own devices, with no campaign stats pollution.

2. Choose how to target

POST /api/v1/push/send takes exactly one of three targeting shapes: Disabled tokens and STOP / channel-push / channel-all suppression entries are filtered server-side before the fan-out, so a broadcast only reaches deliverable devices.

3. Handle per-device errors

An immediate send returns 201 whether it reached one device or one hundred. Read notifications[] per device — a top-level success response does not mean every device accepted:
Top-level request failures are validation errors, not per-device results:

4. Schedule and batch

Supply send_at (ISO-8601 with offset) on /push/send to hold the payload for a future instant. The request returns 202 with a scheduled-push id; the payload replays through the send path at send time, so the same targeting, caps, and suppression gates run identically. Manage the queued push with the scheduled surface:
  • GET /api/v1/push/scheduled — list queue rows (filter by status: scheduled, sent, cancelled, failed).
  • GET /api/v1/push/scheduled/:id — read one, including its full payload, attempt count, and last error.
  • DELETE /api/v1/push/scheduled/:id — cancel while it is still scheduled; returns 409 once the send has left.
Batch deliberately rather than with endpoint loops: for broad campaigns, prefer one broadcast (under the 100,000 cap) or explicit user_ids batches over per-recipient sends — one send request issues one provider interaction. Scheduled sends have automatic back-pressure in the queued row, and the page lists recent results.

5. Track engagement

The two engagement events close the loop once the client SDK is integrated:
  • push.delivered — the device received the render.
  • push.opened — the user tapped the notification.
The Web service worker fires both automatically once a registration lands; the SDK acks POST /api/v1/push/notifications/:id/ack with event: "delivered" | "opened" and an optional timestamp. The ack is idempotent — the webhook fires only on the first genuine delivered or opened transition, so duplicate acks do not re-emit. Subscribe to both events in the webhook event catalog; also read GET /api/v1/push/notifications for the per-notification delivered_at / opened_at timestamps when reconciling analytics.

6. Production checklist

The channel page covers full provider setup (Apple APNs, Web VAPID, Huawei HMS); before launch, verify against the credential group each platform you enable depends on:
  • Credential groups are set atomically. APNs requires DEVOTEL_APNS_KEY_ID + DEVOTEL_APNS_TEAM_ID + DEVOTEL_APNS_PRIVATE_KEY together; VAPID requires DEVOTEL_VAPID_PUBLIC_KEY + DEVOTEL_VAPID_PRIVATE_KEY + DEVOTEL_VAPID_SUBJECT; HMS requires DEVOTEL_HMS_APP_ID + DEVOTEL_HMS_APP_SECRET. A partial group fails every device in that transport with a per-device failed.
  • APNs host is right for the environment. Sandbox APNs tokens only resolve against the sandbox host, and DEVOTEL_APNS_PRODUCTION=true only matters in a non-production NODE_ENV — pick the matching pair before firing test sends.
  • VAPID keys rotated only when necessary. A key rotation invalidates every browser subscription; clients must re-subscribe.
  • Token rotation and reinstalls handled. Pass app_install_id at registration so rotated tokens retire their predecessors, and re-register on the SDK’s refresh callback.
  • Suppression and frequency caps are tested. A 422 NO_DEVICE_TOKENS on broadcast or a device-level skipped row is the expected posture once a user opts out.
  • Engagement wired. Install the service worker (or native ack hooks) so push.delivered / push.opened reach your analytics loop, and subscribe to the webhook events above.