> ## 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 notifications end to end: tokens, targeting, results, engagement

> Take push from token registration to engagement tracking — register device tokens with the SDKs, target users or broadcast, read per-device results, schedule sends, and wire push.delivered / push.opened acks.

# 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](/api-reference/endpoints/push). This page covers the workflow; the [push channel page](/channels/push) 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:

```ts theme={null}
import { OrbitPush } from '@devotel-orbit/web';

if (OrbitPush.isSupported()) {
  const push = new OrbitPush({
    publicKey: 'dv_live_pk_...',
    vapidPublicKey: 'BPa6...',
  });
  const registration = await push.register();
  // registration.deviceTokenId is the Orbit id you can also target directly.
}
```

`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:

```bash theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/push/device-tokens \
  -H "X-API-Key: $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "user_id": "user_8a1f2c",
    "platform": "ios",
    "token": "<APNs / FCM / HMS token from the device>",
    "app_install_id": "install_abc123"
  }'
```

* `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:

| Shape              | When to use it                                                                                                   |
| ------------------ | ---------------------------------------------------------------------------------------------------------------- |
| `device_token_ids` | Address specific devices — e.g. after resolving a device id client-side from the SDK registration return.        |
| `user_ids`         | Reach a user's devices wherever they registered — typical for transactional flows like "order ready for pickup". |
| `user_ids: ["*"]`  | Broadcast to every opted-in device in the tenant, capped at 100,000 deliverable tokens.                          |

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:

```json theme={null}
{
  "data": {
    "notifications": [
      { "id": "pushNotif_2", "deviceTokenId": "deviceToken_2", "status": "sent" },
      { "id": "pushNotif_3", "deviceTokenId": "deviceToken_3", "status": "failed", "error": "BadDeviceToken" },
      { "id": "pushNotif_4", "deviceTokenId": "deviceToken_4", "status": "skipped", "error": "Frequency cap exceeded for this recipient" }
    ],
    "total": 3,
    "sent": 1,
    "devices_targeted": 3
  }
}
```

| Status    | Meaning                                                                                      | Fix                                                                                                       |
| --------- | -------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `sent`    | Provider accepted the send.                                                                  | None.                                                                                                     |
| `failed`  | Provider rejected this device — unconfigured credentials, expired token, mis-shapen payload. | Check the per-device `error` string; provider-reported permanent failures retire the token automatically. |
| `skipped` | The platform gated this device (frequency cap, suppression).                                 | Re-time the send or review your frequency-cap configuration.                                              |

Top-level request failures are validation errors, not per-device results:

| Code                          | Trigger                                                                            |
| ----------------------------- | ---------------------------------------------------------------------------------- |
| `VALIDATION_ERROR` (422)      | Neither `device_token_ids` nor `user_ids` supplied, or a field fails schema.       |
| `NO_DEVICE_TOKENS` (422)      | Target resolves to zero deliverable tokens after suppression / disabled filtering. |
| `BROADCAST_TOO_LARGE` (422)   | `user_ids: ["*"]` would exceed the 100,000-recipient broadcast cap.                |
| `INVALID_PUSH_ENDPOINT` (422) | On registration — the web-push `subscription.endpoint` is not publicly reachable.  |
| `NOT_FOUND` (404)             | Device token / scheduled push id is unknown to this tenant.                        |

## 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](/webhooks/events); also read `GET /api/v1/push/notifications` for the per-notification `delivered_at` / `opened_at` timestamps when reconciling analytics.

```bash theme={null}
curl https://api.orbit.devotel.io/api/v1/push/notifications \
  -H "X-API-Key: $ORBIT_API_KEY"
```

## 6. Production checklist

The channel page covers full provider setup ([Apple APNs](/channels/push#apple-push-setup-apns), [Web VAPID](/channels/push#web-push-setup-vapid), [Huawei HMS](/channels/push#huawei-push-setup-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.
