# Delivery and reliability

Understand what a send proves, how retries behave, and how device availability affects an update. Use these rules when turning the guides into unattended workflows.

HTML: https://notify.belweave.ai/docs/delivery

## Acceptance is not a receipt

```json
{
  "ok": true,
  "eventId": "evt_example_pickup",
  "delivered": 1
}
```

Save `eventId` if you will inspect an answer or request withdrawal. `delivered` counts push requests accepted across the configured providers: Expo for iOS and FCM for native Android. It is not a delivery receipt, proof of display, read state, or a human answer. Notification detail responses now include `pushReceipts` for the reader’s iOS devices: `pending`, `accepted` by Apple, `error` with a safe provider code, or `unknown` when no receipt arrives within 24 hours. Checks begin after one minute and retry every five minutes; diagnostics are retained for 30 days. Older sends have no receipts. A receipt still cannot prove on-screen display. `DeviceNotRegistered` retires only the matching Expo token; a newer registration and Live Activity credentials are preserved.

Interactive service questions persist their original push targets with the question before sending. While retries remain, HTTP `202` includes `queued: N` alongside the persisted `delivered` count. Repeat the identical payload and idempotency key to read that same event’s current counts. Retries preserve the interaction and response credential, recheck authorization and expiry, and never add newly registered devices. Exhausted delivery with no provider acceptance returns `502`. A provider request can repeat after an ambiguous network result; deduplicate by interaction ID.

> With no eligible devices, the event can still be created with `delivered: 0` and an explanatory `message`. Decide explicitly whether your workflow requires an available device. For questions, proceed only after an approved or otherwise appropriate terminal answer.

## Retry without duplicate sends

Give each logical send a stable `Idempotency-Key` when a network retry is possible. The header accepts 1–200 characters, and its namespace belongs to the service. Reuse a key only for an identical payload.

- An already completed matching request returns its original event and `idempotent: true`.
- A matching request still being processed returns `202 Accepted`; wait before checking again.
- A key associated with a different payload returns `409 Conflict`.
- An empty key or a key longer than 200 characters returns `400`.

## Choose recipients

Omit targeting to use the account’s active supported iOS and native Android devices, ordered by most recent activity. Personal accounts support unlimited devices.

Set `deviceIds` to 1–50 stable IDs from the dashboard when only selected devices should receive the update. A foreign or unknown selection returns `400 Invalid device selection`. Owned devices that are inactive or unsupported are skipped. Workplace `group` targeting uses a separate permission check.

```json
{
  "body": "Package PKG-42 needs a pickup decision.",
  "deviceIds": ["dev_your_iphone_id"]
}
```

Targeting has no paid personal-plan gate. Availability still depends on the device’s registration, platform capabilities, and operating-system notification settings.

## Remove an obsolete alert

If a package alert becomes obsolete, use its saved `eventId` to request removal from the service owner’s active supported devices. Withdrawal also cancels an associated pending question. To stop answers without requesting alert removal, use [question cancellation](https://notify.belweave.ai/docs/ask-decision#response-status).

```bash
curl -X POST \
  'https://notify.belweave.ai/hooks/whk_your_token/events/evt_example_pickup/withdraw'
```

```json
{
  "ok": true,
  "eventId": "evt_example_pickup",
  "status": "withdrawn",
  "accepted": 1
}
```

> `accepted` counts provider acceptance of the removal command, not confirmed device removal. Background work may run late or never run, including after an iOS force-quit. Use the webhook belonging to the original event. Withdrawal commands target the owner’s devices, not every original family/group recipient; those recipients may retain their alert. A settled repeat returns the status with `accepted: 0` and `idempotent: true`, without another command.

## Rate limits and usage

| Limit | Personal · free | Family |
| --- | --- | --- |
| Requests per minute, per service | 300 | 300 |
| Requests per minute, per account | 1,500 | 1,500 |
| Notifications per month | Unlimited | Unlimited |
| Active devices | Unlimited | Unlimited |

Hosted defaults are shown above. Notifications, questions, and activity operations share rolling 60-second counters. A per-minute rejection returns `429`, `Retry-After: 60`, and `retryAfterSeconds`. There is no monthly notification cap.

Personal and Family include unlimited notifications and devices for free. Per-minute rate limits still apply to notifications, questions, and activity operations. Teams is TBD and is not currently purchasable.

## Expiry and stale content

Set `expiresInSeconds` to the longest useful lifetime for the task: 60–28,800 seconds, with 28,800 as the default. After the deadline, state becomes `expired` and further updates are refused.

`staleAfterSeconds` expresses how long an update should be trusted: 0–28,800 seconds, default 14,400. On iOS, stale content remains visible and updateable but is marked as potentially outdated. Each update moves the stale deadline forward without exceeding expiry; omitting this field retains the previous stale window.

## Activity slots and replacement

Notify allows up to five concurrent activities per target device, including interactive activities. Distinct keys can coexist. Starting the same active key for the same service or token without replacement returns this `409` response:

```json
{
  "ok": false,
  "error": "A Live Activity with this key is still active",
  "code": "ACTIVE_ACTIVITY_CONFLICT",
  "activityId": "act_example_pickup"
}
```

Use `code` to identify a key conflict and update the existing task. Device capacity instead returns `422`, `code: ACTIVITY_CAPACITY_EXCEEDED`, and `limit: 5`; end one of your running tasks before starting another. Interactive Live Activity asks use the same rejection: if any target is full, the whole mixed-target request rolls back without a queued receipt or push. Retry the same payload and idempotency key after a slot is released. Five is Notify’s admission policy, not a hardware specification.

A deliberate replacement uses the same `key` with `replace: true`. It ends only the matching activity owned by this service or token before the new start. `replaced` reports how many were displaced, or zero if no replacement was needed. Device removal remains subject to provider and OS behavior.

Replacement preserves other keys and other requesters’ activities. The matching activity is ended everywhere, including outside the new target set, so its alias can be reused. A stable key plus replacement suits repeated runs of the same task.

## Activity delivery behavior

On iOS, starts include an alert; update/end pushes are silent. Routine progress updates use APNs priority 5. Starts, endings, and interactive state use priority 10. Priority does not grant sound, haptics, or display. Android updates modify the same ongoing progress notification; OS settings still control presentation.

Send updates at meaningful transitions. Activity operations share notification rate limits. There is no monthly allowance to exhaust.

> Read `activity.readiness` to distinguish ready devices, queued starts and accepted starts awaiting an update token. After 90 seconds without registration, `update_token_delayed` is a diagnostic warning, not proof of Apple throttling; the activity still accepts late registration until its deadline. Open Notify and check Live Activity settings if it persists. Reuse one activity for each continuing task and send meaningful progress changes. Unlimited monthly usage does not mean unlimited delivery frequency.
