---
feature: Notifications and FCM Push Delivery
slug: notifications
surface: client
consumer: flutter
version: 1.4.0
status: ready
brd_version: "1.3"
brd_ids: [CUST-022]
business_rules: [BR-21, BR-22]
decisions: [DEC-13, TD-19, TD-23, TD-25, TD-141, TD-142, TD-143, TD-166, TD-167]
schema_tables: [notifications, notification_deliveries, devices, device_apps]
screens: [customer--notifications--*]
related_docs: [auth.md, orders.md, ../API_CONVENTIONS.md]
last_updated: 2026-11-08
---

# Notifications — Flutter Integration Guide (client)

> **Base URL:** `{HOST}/api/v1` · Shared rules (headers, envelope, errors, pagination): [API_CONVENTIONS.md](../API_CONVENTIONS.md)

## Version history

| Version | Date | Type | Summary | App action required |
|---------|------|------|---------|---------------------|
| 1.4.0 | 2026-11-08 | MINOR | New inbox event `general`: a notification an admin sent from the dashboard (TD-173). | Show it as a plain inbox item; a tap may open the inbox. |
| 1.3.1 | 2026-11-04 | PATCH | Fix: a template switched off no longer hides notifications already in the inbox; only `send_in_app = 0` hides rows (TD-168). `booking_reminder` now sends `data.scheduled_at` in ISO 8601 with offset (was `Y-m-d H:i:s`). Section 5 open items are current. | Parse `scheduled_at` as ISO 8601. |
| 1.3.0 | 2026-11-04 | MINOR | Added missing BRD scenarios (`order_payment_succeeded`, `order_payment_failed`, `order_rating_requested`, `wallet_refund_credited`); chat messages routed via template engine; inactive templates filtered from inbox. | Handle new event keys in deep link / routing handlers if needed. |
| 1.2.0 | 2026-11-04 | MINOR | Dynamic placeholder template engine; automated booking reminders (`orders:remind-bookings`); and in-app delivery audit records. | None. |
| 1.1.0 | 2026-11-04 | MINOR | Per-category push / SMS switches (`GET` / `PUT …/notifications/preferences/categories`), event catalog, device lifecycle, inbox success examples. | Build the notification settings screen; send `device_token` on logout. |
| 1.0.0 | 2026-10-28 | Initial | Device registration, shared inbox and FCM delivery of persisted notifications. | Register the device token after login and on token refresh. |

## Changelog

### 1.4.0 — 2026-11-08

- **Added:** event `general` in this app's inbox and push. An admin wrote it in the dashboard and sent it to every reachable customer, to those in chosen cities, or to people she picked (TD-173). `title` and `body` are the admin's text in the user's language (Arabic when no English was written). `data` = `{"broadcast_id": "<uuid>"}`. Category `offers`: switching offers off mutes its push; the inbox row is always written.
- **Breaking:** No.
- **App action required:** Show `general` as a plain inbox item. The customer inbox design (`customer--notifications--01-inbox.png`) shows this kind of item as the offer card with the gift icon; use that style for `type = general`. Nothing to call: no endpoint changed.

### 1.3.1 — 2026-11-04

- **Fixed:** switching a template off (`is_active = false`) stops new notifications but no longer hides ones already in the inbox, so the history and `unread_count` stay stable. Rows of a template with `send_in_app = 0` are still hidden (they exist only to carry the push). `booking_reminder` now sends `data.scheduled_at` in ISO 8601 with offset (was `Y-m-d H:i:s`).
- **Breaking:** No. **App action required:** parse `scheduled_at` as ISO 8601.

### 1.3.0 — 2026-11-04
- **Added:** New notification scenarios for customer app: `order_payment_succeeded` (with payment method and amount), `order_payment_failed` (with failure reason), `order_rating_requested` (upon order completion), and `wallet_refund_credited` (upon wallet refund credit).
- **Changed:** Order chat messages are now delivered through the template engine (`chat_message`); customer profile creation triggers `user_registered`; inactive or `send_in_app = false` templates are filtered from the inbox.
- **Breaking:** No.
- **App action required:** Handle new event keys in push/inbox routing handlers if needed.
- **BRD / decisions:** CUST-022, ADMIN-020, TD-167.

### 1.2.0 — 2026-11-04
- **Added:** Dynamic placeholder-based notification templating (TD-167); automated 2-hour scheduled booking reminder notifications (`booking_reminder`); `in_app` delivery audit logging.
- **Changed:** Notification copy now uses customizable admin templates with locale resolution.
- **Breaking:** No.
- **App action required:** None.
- **BRD / decisions:** CUST-022, DEC-13, DEC-15, TD-167.

### 1.1.0 — 2026-11-04
- **Added:** `GET` and `PUT /api/v1/client/notifications/preferences/categories` (TD-166); the shared event catalog, push format and device lifecycle in [API_CONVENTIONS §7.1–§7.3](../API_CONVENTIONS.md); captured examples for the inbox routes. The push sender now honours every category switch (before, only `chat` was checked).
- **Changed / removed:** None. Nothing existing was renamed or removed.
- **Breaking:** No. **App action required:** add the settings screen; send `device_token` when logging out (DEC-29).
- **BRD / decisions:** CUST-022, DEC-13, TD-166; open: DEC-28, DEC-29.

### 1.0.0 — 2026-10-28
- **Added:** Device registration, shared inbox and FCM delivery of persisted notifications (TD-141–143).
- **App action required:** Register the device token after login and when it rotates.
- **BRD / decisions:** CUST-022, DEC-13, TD-23, TD-25, TD-141–143.

## 1. Business summary

The customer app can register a push token and access its persisted notification inbox. The broader event catalog and notification preferences UI contract are not complete.

## 2. User flow and screens

Register the device after login, list the inbox, and open a notification to mark it read. The shared inbox is scoped to the signed-in user and token app.

## 3. Endpoints

All routes require an active authenticated account and a bearer token for this app. Paths below include the `/api/v1` prefix. Set `Accept-Language: ar|en` as needed.

| Method | Path | Route name | Behavior and example |
|--------|------|------------|----------------------|
| `POST` | `/api/v1/client/notifications/device` | `client.notifications.device` | Register or refresh the device. Postman: `client.notifications.device` › "200 Device". |
| `PUT` | `/api/v1/client/notifications/preferences` | `client.notifications.preferences` | Set the required boolean `allow_notifications`. Example: see below. |
| `GET` | `/api/v1/client/notifications/preferences/categories` | `client.notifications.preferences.categories.show` | Read the five category switches of this app. Postman: `client.notifications.preferences.categories.show` › "200 Defaults before any change". |
| `PUT` | `/api/v1/client/notifications/preferences/categories` | `client.notifications.preferences.categories.update` | Partial update. Postman: `client.notifications.preferences.categories.update` › "200 Switch orders push off and chat SMS off". |
| `GET` | `/api/v1/notifications` | `shared.notifications.index` | List notifications and unread count. Example: see below. |
| `GET` | `/api/v1/notifications/{id}` | `shared.notifications.show` | Read a notification by UUID and mark it read. Example: see below. |
| `DELETE` | `/api/v1/notifications/{id}` | `shared.notifications.destroy` | Delete one notification by UUID. Example: see below. |
| `POST` | `/api/v1/notifications/mark-all-read` | `shared.notifications.mark-all-read` | Mark all as read. Example: see below. |
| `DELETE` | `/api/v1/notifications/clear-all` | `shared.notifications.clear-all` | Clear the inbox. Example: see below. |

Device registration requires `device_token` (string, maximum 4096 characters) and `device_type` (`ios`, `android`, or `huawei`); `app_version` is optional (string, maximum 20 characters) and `device_id` is prohibited.

Device response (Postman: `client.notifications.device` › "200 Device"):

```json
{
    "status": "success",
    "message": "Device registered.",
    "data": null
}
```

### Captured examples

Inbox routes: Postman `shared.notifications.index` › "200 Inbox with unread count", `shared.notifications.show` › "200 Open one notification and mark it read", `shared.notifications.destroy` › "200 Delete one notification", `shared.notifications.mark-all-read` › "200 Mark all as read", `shared.notifications.clear-all` › "200 Clear the inbox". The list shape is shown in [API_CONVENTIONS §7](../API_CONVENTIONS.md). A notification of another app answers 404 (Postman: `shared.notifications.show` › "404 Another app's notification").

### 3.1 Notification preferences (DEC-13, TD-166)

The server keeps one switch pair per category for **this app only**: `orders`, `payments`, `chat`, `offers`, `account`. Provider and driver settings of the same phone are separate. A category with no saved row reads as on. The read never creates rows.

`GET` response (Postman: `client.notifications.preferences.categories.show`):

```json
{
    "status": "success",
    "message": "Data retrieved successfully",
    "data": {
        "allow_notifications": true,
        "sms_delivery_available": false,
        "categories": [
            {
                "category": {
                    "value": "orders",
                    "label": "Orders and trips"
                },
                "push_enabled": true,
                "sms_enabled": true
            },
            {
                "category": {
                    "value": "payments",
                    "label": "Payments and payouts"
                },
                "push_enabled": true,
                "sms_enabled": true
            },
            {
                "category": {
                    "value": "chat",
                    "label": "Chat messages"
                },
                "push_enabled": true,
                "sms_enabled": true
            },
            {
                "category": {
                    "value": "offers",
                    "label": "Offers"
                },
                "push_enabled": true,
                "sms_enabled": true
            },
            {
                "category": {
                    "value": "account",
                    "label": "Account and support"
                },
                "push_enabled": true,
                "sms_enabled": true
            }
        ]
    }
}
```

`PUT` body: `{"categories": {"orders": {"push_enabled": false}, "chat": {"sms_enabled": false}}}`. Send at least one category; each category needs at least one of `push_enabled` / `sms_enabled` (booleans); an unknown category or field is a 422. Categories and fields you omit stay unchanged. The response is the full state (Postman: `client.notifications.preferences.categories.update` › "200 …"):

```json
{
    "status": "success",
    "message": "Preferences updated",
    "data": {
        "allow_notifications": true,
        "sms_delivery_available": false,
        "categories": [
            {
                "category": {
                    "value": "orders",
                    "label": "Orders and trips"
                },
                "push_enabled": false,
                "sms_enabled": false
            },
            {
                "category": {
                    "value": "payments",
                    "label": "Payments and payouts"
                },
                "push_enabled": true,
                "sms_enabled": true
            },
            {
                "category": {
                    "value": "chat",
                    "label": "Chat messages"
                },
                "push_enabled": true,
                "sms_enabled": false
            },
            {
                "category": {
                    "value": "offers",
                    "label": "Offers"
                },
                "push_enabled": true,
                "sms_enabled": true
            },
            {
                "category": {
                    "value": "account",
                    "label": "Account and support"
                },
                "push_enabled": true,
                "sms_enabled": true
            }
        ]
    }
}
```

Validation (Postman: `client.notifications.preferences.categories.update` › "422 Unknown category"):

```json
{
    "status": "fail",
    "message": "The categories field must be an array.",
    "data": {
        "errors": {
            "categories": [
                "The categories field must be an array."
            ]
        }
    }
}
```

Rules to know:
- **Master switch.** `allow_notifications` is account-wide. `PUT /api/v1/client/notifications/preferences` (unchanged) still sets it; when it is `false`, no push reaches any of the person's apps whatever the categories say. When `true`, the category decides.
- **Only push is filtered.** The inbox row is always written and realtime events are unaffected. A muted category simply produces no FCM message.
- **Nothing is unmutable yet.** Whether some messages (for example account review results or payment failures) must always reach the user is an open owner decision (DEC-28); until then every category can be switched off.
- **SMS.** `sms_enabled` is stored for later. No SMS is sent today (provider open, DEC-15) and the response says `sms_delivery_available: false`; do not promise SMS in the UI.
- **OS permission.** Whether the phone allows this app to show notifications is an operating-system setting the server cannot see. Ask for it in the app and show your own hint when it is denied.
- **Event to category.** See the catalog in [API_CONVENTIONS §7.1](../API_CONVENTIONS.md).

## 4. Push delivery

Creating a persisted `Notification` dispatches `SendPushNotificationJob`. Delivery requires `FIREBASE_ENABLED=true`, valid Firebase credentials, the user's `allow_notifications` flag and the event's category switch for this app (§3.1). It targets devices registered for the notification's `customer` app. Invalid or unknown tokens are pruned. Per-token transient failures are not retried (TD-143).

The push includes a notification title and body, plus data containing the notification UUID (`notification_id`), the app (`app`), and any data stored on the notification.

## 5. Event catalog, device lifecycle and what is left

- Every event key this app can receive, its trigger, category and `data` keys: [API_CONVENTIONS §7.1](../API_CONVENTIONS.md). The push `data` does not carry the event key; route a tapped push by the `data` keys or by `notification_id`.
- Device-token behaviour (rotation, several devices, two apps on one phone, logout, invalid tokens, retries): [API_CONVENTIONS §7.2](../API_CONVENTIONS.md). Send `device_token` in the logout body so the token is detached.
- **Still open.** SMS delivery (DEC-15; SMS is only logged), WhatsApp (not integrated), promotions beyond the admin's `general` notification (the `offers` category holds only `general`), the owner's answers on DEC-28 and DEC-29, and a verified FCM delivery on staging (DevOps checklist in the handoff reply). Templates and the admin notification screens are built ([admin/notifications.md](../admin/notifications.md), TD-167).
