---
feature: Support and contact
slug: support
surface: delivery
consumer: flutter
version: 1.2.0
status: ready
brd_version: "1.3"
brd_ids: [DRIV-019]
business_rules: [—]
decisions: [DEC-21, TD-162, TD-163]
schema_tables: [support_tickets, support_messages, support_reasons, trips]
screens: "Not captured (SCR-24 entry points expected)"
related_docs: [../admin/support.md, ../client/support.md, ../provider/support.md, trips.md]
last_updated: 2026-11-09
---

# Support and contact — Flutter integration guide (driver)

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

## Version history

| Version | Date | Type | Summary | App action required |
|---------|------|------|---------|---------------------|
| 1.2.0 | 2026-11-09 | MINOR | Support attachments may be a video (`mp4`, `mov`) as well as an image or a PDF; tickets and messages return `has_media` (R05, TD-183). | Let the user pick a video; show an attachment indicator from `has_media`. |

| 1.1.0 | 2026-11-08 | MINOR | FAQ list is available from `GET /delivery/faqs` (TD-175). | Build the FAQ list of the help screen from `GET /faqs`. |
| 1.0.0 | 2026-11-03 | Initial | Predefined driver reasons, open a request with text, optional attachments and an optional link to her own trip, track its status and thread, follow up. | Build the support form (with a trip picker), the request list with status chips and the request thread screen. |

## Changelog

### 1.1.0 — 2026-11-08

- **Added:** Pointer to the new public FAQ endpoint. No contract change in this doc.
- **Breaking:** No. **App action required:** Build the FAQ list of the help screen from `GET /faqs`.

### 1.0.0 — 2026-11-03

- **Added:** `GET /support/reasons`, `GET /support/tickets`, `POST /support/tickets`, `GET /support/tickets/{ticket}`, `POST /support/tickets/{ticket}/messages`; `trip_id` on the open request; default driver reasons.
- **Changed:** — · **Fixed:** — · **Removed / Deprecated:** —
- **Breaking:** No.
- **App action required:** Build the support form, the request list with status chips and the request thread screen.
- **BRD / decisions:** DRIV-019, DEC-21, TD-162, TD-163.

---

## 1. Business summary

A driver can send the Joshen team a support request or complaint: she picks a **reason** from a predefined list, writes an explanation, may attach files, and may link **one of her own trips** — that link is how she raises a dispute about a trip. The request becomes a **ticket** with a short code (`SUP-1000`+) that the admin desk follows up. She tracks its status — `under_review`, `replied`, `closed` — and reads the thread. While a ticket is not closed she can add follow-ups (a follow-up puts a `replied` ticket back to `under_review`); a closed ticket refuses more messages and she opens a new request. An urgent reason (an incident, or a provider or customer who cannot be reached) makes the ticket urgent for the desk. Admin replies appear as the **Support team**; the admin's identity is never shown.

Support is open to a driver in **every account status** (pending, needs changes, suspended): it is the way to ask for help with the account itself. She receives an inbox notice and a push when the team replies or changes the status (see §6).

## 2. User flow and screens

| Step | Screen / widget | Endpoint | Notes |
|------|-----------------|----------|-------|
| 1 | Reason picker | `GET /support/reasons` | Active reasons for the driver app only; `is_urgent` is informational. |
| 2 | Request form: reason, text, trip, files | `POST /support/tickets` | Choose a trip from `GET /trips`; upload files first through the media endpoint and send the hashes. |
| 3 | My requests | `GET /support/tickets` | Status chips via `filters[status]`. |
| 4 | Request thread | `GET /support/tickets/{ticket}` | Includes `messages`; hide the reply box when `can_reply` is `false`. |
| 5 | Follow-up | `POST /support/tickets/{ticket}/messages` | Text and optional files. |

## 3. Endpoints

Auth for all: Bearer token · `app:driver` (any account status — no approval needed). Ids are uuids; every key is always present. Send files as media hashes (`attachments`, ≤ 5; image, video or document).

### 3.1 Reasons

`GET /api/v1/delivery/support/reasons`

```json
{
  "status": "success",
  "message": "Data retrieved successfully",
  "data": {
    "reasons": [
      {
        "id": "01a1a0fc-76af-707d-bf5a-2362d2283613",
        "label": "Sample-ilyu30706937",
        "is_urgent": false
      },
      {
        "id": "01a1a0fc-7178-721b-a226-f6272c3c73ff",
        "label": "Incident or Safety Problem During a Trip",
        "is_urgent": true
      },
      {
        "id": "01a1a0fc-717c-71c1-8fba-b8aaf0e1bb33",
        "label": "Provider or Customer Unreachable",
        "is_urgent": true
      },
      {
        "id": "01a1a0fc-7180-731b-8068-1b18150f35d6",
        "label": "Trip Problem or Dispute",
        "is_urgent": false
      },
      {
        "id": "01a1a0fc-7184-70d4-8f0f-2421cde2d6c8",
        "label": "Payout or Transfer Issue",
        "is_urgent": false
      },
      {
        "id": "01a1a0fc-7187-7346-82d6-fe1ec32ea167",
        "label": "App or Account Problem",
        "is_urgent": false
      },
      {
        "id": "01a1a0fc-718e-72e9-aa5a-e6e1007f701b",
        "label": "General Inquiry or Help",
        "is_urgent": false
      }
    ]
  }
}
```

Postman: `delivery.support.reasons` › "200 Reasons for my app"

### 3.2 Open a request

`POST /api/v1/delivery/support/tickets` — rate limit `support-ticket`, 10 per minute per user.

| Field | Type | Required | Rules | Description |
|-------|------|----------|-------|-------------|
| `reason_id` | uuid | yes | active reason of the driver app | From `GET support/reasons`. |
| `body` | string | yes | 3–4000 | Explanation. |
| `trip_id` | uuid | no | one of her own trips | Links the request to a trip (her dispute); someone else's is a 404. |
| `attachments` | array | no | ≤ 5 media hashes | Images, videos (`mp4`, `mov`) or documents. |

`order_id` is **not accepted** from the driver app (`422`); the order of the linked trip is visible to the desk.

**Success — `201`** (linked to a trip)

```json
{
  "status": "success",
  "message": "Your request was sent to the support team.",
  "data": {
    "id": "01a1a0fc-7407-721b-8817-58edccda4ca2",
    "code": "SUP-1005",
    "status": {
      "value": "under_review",
      "label": "Under review"
    },
    "priority": {
      "value": "urgent",
      "label": "Urgent"
    },
    "reason": {
      "id": "01a1a0fc-73e9-71ad-9f44-b1af7cc9362d",
      "label": "Sample-mucg43289533"
    },
    "body": "The customer was not at the address",
    "attachments": [
      "https://ui-avatars.com/api/?name=N&background=f3f4f6&color=9ca3af&size=200"
    ],
    "order": null,
    "trip": {
      "id": "01a1a0fc-73ed-7155-9434-92842fab89fc",
      "code": "TRP-hmzt47694437"
    },
    "can_reply": true,
    "created_at": "2026-11-03T10:37:41+00:00",
    "updated_at": "2026-11-03T10:37:41+00:00",
    "closed_at": null,
    "messages": []
  }
}
```

Postman: `delivery.support.tickets.store` › "201 Request linked to my trip" · "201 Request sent" (no trip: `trip` is `null`)

| Case | Response |
|------|----------|
| Missing text / invalid input | `422` `VALIDATION_FAILED` — Postman: `delivery.support.tickets.store` › "422 Missing text" |
| `order_id` sent | `422` `VALIDATION_FAILED` — Postman: `delivery.support.tickets.store` › "422 Order link not accepted" |
| Inactive reason or another app's | `422` `SUPPORT_REASON_INVALID` — Postman: `delivery.support.tickets.store` › "422 Reason not available" |
| Trip is not hers | `404` — Postman: `delivery.support.tickets.store` › "404 Trip is not mine" |
| Too many requests | `429` `RATE_LIMITED` — Postman: `delivery.support.tickets.store` › "429 Too many requests" |

### 3.3 My requests

`GET /api/v1/delivery/support/tickets` — `page`, `per_page` (1–100, default 15), `filters[status]` (`under_review`, `replied`, `closed`). Newest first; paginated resource in `data.data` (with `links` and `meta`). The example keeps one row.

```json
{
  "status": "success",
  "message": "Data retrieved successfully",
  "data": {
    "data": [
      {
        "id": "01a1a0fc-76c4-73fb-a639-fa8d583cbcfd",
        "code": "SUP-1011",
        "status": {
          "value": "under_review",
          "label": "Under review"
        },
        "priority": {
          "value": "normal",
          "label": "Normal"
        },
        "reason": {
          "id": "01a1a0fc-76af-707d-bf5a-2362d2283613",
          "label": "Sample-ilyu30706937"
        },
        "body": "I need help with an order",
        "attachments": [
          "https://ui-avatars.com/api/?name=N&background=f3f4f6&color=9ca3af&size=200"
        ],
        "order": null,
        "trip": null,
        "can_reply": true,
        "created_at": "2026-11-03T10:37:42+00:00",
        "updated_at": "2026-11-03T10:37:42+00:00",
        "closed_at": null
      }
    ]
  }
}
```

Postman: `delivery.support.tickets.index` › "200 My requests"

Errors: `422` `VALIDATION_FAILED` — Postman: `delivery.support.tickets.index` › "422 Unknown status filter".

### 3.4 One request with its thread

`GET /api/v1/delivery/support/tickets/{ticket}` — someone else's ticket, or one from her other apps, is a `404`.

```json
{
  "status": "success",
  "message": "Data retrieved successfully",
  "data": {
    "id": "01a1a0fc-76c4-73fb-a639-fa8d583cbcfd",
    "code": "SUP-1011",
    "status": {
      "value": "under_review",
      "label": "Under review"
    },
    "priority": {
      "value": "normal",
      "label": "Normal"
    },
    "reason": {
      "id": "01a1a0fc-76af-707d-bf5a-2362d2283613",
      "label": "Sample-ilyu30706937"
    },
    "body": "I need help with an order",
    "attachments": [
      "https://ui-avatars.com/api/?name=N&background=f3f4f6&color=9ca3af&size=200"
    ],
    "order": null,
    "trip": null,
    "can_reply": true,
    "created_at": "2026-11-03T10:37:42+00:00",
    "updated_at": "2026-11-03T10:37:42+00:00",
    "closed_at": null,
    "messages": []
  }
}
```

Postman: `delivery.support.tickets.show` › "200 Request with its thread"

### 3.5 Follow-up

`POST /api/v1/delivery/support/tickets/{ticket}/messages` — body `body` (required, ≤ 4000), optional `attachments`; rate limit shared with 3.2.

```json
{
  "status": "success",
  "message": "Your message was added.",
  "data": {
    "id": "01a1a0fc-76eb-7162-b858-c781e5373843",
    "sender": {
      "value": "user",
      "label": "You"
    },
    "body": "Any news?",
    "attachments": [
      "https://ui-avatars.com/api/?name=N&background=f3f4f6&color=9ca3af&size=200"
    ],
    "created_at": "2026-11-03T10:37:42+00:00"
  }
}
```

Postman: `delivery.support.tickets.reply` › "201 Follow-up added"

| Case | Response |
|------|----------|
| Empty text | `422` `VALIDATION_FAILED` — Postman: `delivery.support.tickets.reply` › "422 Empty follow-up" |
| Ticket is closed | `409` `SUPPORT_TICKET_CLOSED` — Postman: `delivery.support.tickets.reply` › "409 Request is closed" |
| Too many | `429` `RATE_LIMITED` — Postman: `delivery.support.tickets.reply` › "429 Too many follow-ups" |

Any other app's token (customer or provider) gets `403`; no token `401`.

### 3.x FAQ list (TD-175)

The FAQ part of the help screen is `GET /delivery/faqs` (public, unpaginated, ordered; see [delivery/content.md](content.md)). Show it above or next to "Contact support"; the hotline and live chat parts of SCR-24 stay out of scope (DEC-21).

## 4. Status values

| Status | Meaning | `can_reply` |
|--------|---------|-------------|
| `under_review` | Waiting for the Joshen team (also after her follow-up). | true |
| `replied` | The team answered. | true |
| `closed` | Finished; open a new request. | false |

Priority: `normal` or `urgent` (an urgent reason).

## 5. Business rules the backend enforces

- Reasons are per app (DEC-21); a reason of another app or an inactive one is refused (`SUPPORT_REASON_INVALID`).
- A driver links only her **own** trip (`trips.driver_id`); the link is optional.
- The ticket code is `SUP-` + (1000 + row id), assigned in the same transaction.
- A user only reaches her own tickets, per app.
- The ticket row is locked before the closed check (`SUPPORT_TICKET_CLOSED`).
- Default driver reasons are inserted by a migration only where none exist (incident or safety — urgent; provider or customer unreachable — urgent; trip problem or dispute; payout or transfer issue; app or account problem; general inquiry).

## 6. Notifications

| Event key | When | Payload (`data`) |
|-----------|------|------------------|
| `support_ticket_replied` | The team replied | `ticket_id`, `code`, `status` |
| `support_ticket_status_changed` | The team set the request back to `under_review` or closed it | `ticket_id`, `code`, `status` (`under_review` \| `closed`) |

Both arrive in the shared inbox with `app = driver` and as an FCM push; open the thread from `data.ticket_id` ([notifications.md](notifications.md)).

## 7. Integration checklist

- [ ] Upload attachments first, then send their hashes.
- [ ] Hide the reply box when `can_reply` is `false`; handle 409 anyway.
- [ ] Show the admin as "Support team" (`messages[].sender`), never by name.
- [ ] Offer a trip picker (her own trips) on the form; do not send `order_id`.
- [ ] Open the thread from the notification's `data.ticket_id`.

## 8. Acceptance scenarios and open decisions

AC-DRIV-019 (reason + text + optional attachment creates an admin-visible request with a trackable status). Tests: `tests/Feature/Api/V1/Shared/SupportTicketTest.php`. Open: the support screens are not captured yet; hotline / FAQ / live-chat parts of SCR-24 are out of scope.
