---
feature: Support and contact
slug: support
surface: client
consumer: flutter
version: 1.3.0
status: ready
brd_version: "1.3"
brd_ids: [CUST-024]
business_rules: [—]
decisions: [DEC-21, TD-162, TD-163]
schema_tables: [support_tickets, support_messages, support_reasons, orders]
screens: "`customer--account--02-*` (SCR-24)"
related_docs: [../admin/support.md, ../provider/support.md, ../delivery/support.md]
last_updated: 2026-11-09
---

# Support and contact — Flutter integration guide (client)

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

## Version history

| Version | Date | Type | Summary | App action required |
|---------|------|------|---------|---------------------|
| 1.3.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.2.0 | 2026-11-08 | MINOR | FAQ list is available from `GET /client/faqs` (TD-175). | Build the FAQ list of the help screen from `GET /faqs`. |
| 1.1.0 | 2026-11-03 | MINOR | The admin desk is live: `replied` / `closed` now move, and the requester gets a notice and a push on a reply and on a status change. Every ticket carries a `trip` key (always `null` here). | Open the thread from the notification's `data.ticket_id`; ignore `trip`. |
| 1.0.0 | 2026-11-02 | Initial | Predefined reasons, open a request with text and optional attachments, track its status and thread, and follow up. | Build the support form, the request list with status chips and the request thread screen. |

## Changelog

### 1.2.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.1.0 — 2026-11-03

- **Added:** notifications `support_ticket_replied` and `support_ticket_status_changed` (§6); `trip` key on every ticket (`null` in this app).
- **Changed:** `replied` and `closed` are now set by the admin desk through the API (ADMIN-026).
- **Fixed:** — · **Removed / Deprecated:** — (the base `POST /client/contact` form is deprecated but unchanged, TD-163).
- **Breaking:** No.
- **App action required:** Open the thread from the notification's `data.ticket_id`; ignore `trip`.
- **BRD / decisions:** CUST-024, ADMIN-026, DEC-21, TD-163.

### 1.0.0 — 2026-11-02

- **Added:** `GET /support/reasons`, `GET /support/tickets`, `POST /support/tickets`, `GET /support/tickets/{ticket}`, `POST /support/tickets/{ticket}/messages`.
- **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:** CUST-024, —, DEC-21, TD-162.

---

## 1. Business summary

A customer can send the Joshen team a support request or complaint from the app: she picks a **reason** from a predefined list, writes an explanation and may attach files, and may link one of her own orders. The request is saved as a **ticket** with a short code (`SUP-1000`+) for the admin dashboard to follow up. She tracks its status — `under_review`, `replied`, `closed` — and reads the thread of replies. 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 instead. A reason flagged urgent makes the ticket urgent for the desk. Admin replies appear in the thread as the **Support team**; the admin's identity is never shown.

The older `POST /contact` form is unchanged; this is the tracked replacement.

## 2. User flow and screens

| Step | Screen / widget | Endpoint | Notes |
|------|-----------------|----------|-------|
| 1 | Reason picker | `GET /support/reasons` | Active reasons for this app only; `is_urgent` is informational. |
| 2 | Request form: reason, text, order, files | `POST /support/tickets` | 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:customer` (customer token). 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/client/support/reasons`

```json
{
  "status": "success",
  "message": "Data retrieved successfully",
  "data": {
    "reasons": [
      {
        "id": "01a19c09-050b-7124-be97-8dc07b2f3770",
        "label": "Sample-ngee38861912",
        "is_urgent": false
      },
      {
        "id": "01a19c08-eb2e-7265-8f90-79088ac37940",
        "label": "Late or No-Show Specialist",
        "is_urgent": true
      },
      {
        "id": "01a19c08-eb32-716f-971c-c6c184865fae",
        "label": "Safety Concern or Misconduct",
        "is_urgent": true
      },
      {
        "id": "01a19c08-eb35-70c3-864e-e6b94e9c33c8",
        "label": "Payment or Billing Issue",
        "is_urgent": false
      },
      {
        "id": "01a19c08-eb39-715c-ac99-03903934fac3",
        "label": "Refund Request",
        "is_urgent": false
      },
      {
        "id": "01a19c08-eb3c-7248-80c6-c0853ce154b6",
        "label": "App or Account Problem",
        "is_urgent": false
      },
      {
        "id": "01a19c08-eb44-7394-82a5-eefd78e8c50d",
        "label": "General Inquiry or Help",
        "is_urgent": false
      }
    ]
  }
}
```

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

### 3.2 Open a request

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

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

**Success — `201`**

```json
{
  "status": "success",
  "message": "Your request was sent to the support team.",
  "data": {
    "id": "01a19c09-0538-72f6-a669-17acd9f3c9b6",
    "code": "SUP-1005",
    "status": {
      "value": "under_review",
      "label": "Under review"
    },
    "priority": {
      "value": "normal",
      "label": "Normal"
    },
    "reason": {
      "id": "01a19c09-050b-7124-be97-8dc07b2f3770",
      "label": "Sample-ngee38861912"
    },
    "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-02T11:33:19+00:00",
    "updated_at": "2026-11-02T11:33:19+00:00",
    "closed_at": null,
    "messages": []
  }
}
```

Postman: `client.support.tickets.store` › "201 Request sent"

| Case | Response |
|------|----------|
| Missing text / invalid input | `422` `VALIDATION_FAILED` — Postman: `client.support.tickets.store` › "422 Missing text" |
| Inactive reason or another app's | `422` `SUPPORT_REASON_INVALID` — Postman: `client.support.tickets.store` › "422 Reason not available" |
| Too many requests | `429` `RATE_LIMITED` — Postman: `client.support.tickets.store` › "429 Too many requests" |

### 3.3 My requests

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

```json
{
  "status": "success",
  "message": "Data retrieved successfully",
  "data": {
    "data": [
      {
        "id": "01a19c09-0538-72f6-a669-17acd9f3c9b6",
        "code": "SUP-1005",
        "status": {
          "value": "under_review",
          "label": "Under review"
        },
        "priority": {
          "value": "normal",
          "label": "Normal"
        },
        "reason": {
          "id": "01a19c09-050b-7124-be97-8dc07b2f3770",
          "label": "Sample-ngee38861912"
        },
        "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-02T11:33:19+00:00",
        "updated_at": "2026-11-02T11:33:19+00:00",
        "closed_at": null
      }
    ],
    "links": {
      "first": "http://localhost/api/v1/client/support/tickets?page=1",
      "last": "http://localhost/api/v1/client/support/tickets?page=1",
      "prev": null,
      "next": null
    },
    "meta": {
      "current_page": 1,
      "from": 1,
      "last_page": 1,
      "links": [
        {
          "url": null,
          "label": "&laquo; Previous",
          "page": null,
          "active": false
        },
        {
          "url": "http://localhost/api/v1/client/support/tickets?page=1",
          "label": "1",
          "page": 1,
          "active": true
        },
        {
          "url": null,
          "label": "Next &raquo;",
          "page": null,
          "active": false
        }
      ],
      "path": "http://localhost/api/v1/client/support/tickets",
      "per_page": 15,
      "to": 1,
      "total": 1
    }
  }
}
```

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

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

### 3.4 One request with its thread

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

```json
{
  "status": "success",
  "message": "Data retrieved successfully",
  "data": {
    "id": "01a19c09-0538-72f6-a669-17acd9f3c9b6",
    "code": "SUP-1005",
    "status": {
      "value": "under_review",
      "label": "Under review"
    },
    "priority": {
      "value": "normal",
      "label": "Normal"
    },
    "reason": {
      "id": "01a19c09-050b-7124-be97-8dc07b2f3770",
      "label": "Sample-ngee38861912"
    },
    "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-02T11:33:19+00:00",
    "updated_at": "2026-11-02T11:33:19+00:00",
    "closed_at": null,
    "messages": []
  }
}
```

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

### 3.5 Follow-up

`POST /api/v1/client/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": "01a19c09-0573-73f2-9951-ce7810921b42",
    "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-02T11:33:19+00:00"
  }
}
```

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

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

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

The FAQ part of the help screen is `GET /client/faqs` (public, unpaginated, ordered; see [client/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`).
- The ticket code is `SUP-` + (1000 + row id), assigned in the same transaction.
- A user only reaches her own tickets, per app: the same person's customer and provider tickets are separate lists.
- The ticket row is locked before the closed check (`SUPPORT_TICKET_CLOSED`).

## 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 (`app = customer`) and as an FCM push; open the thread from `data.ticket_id` ([notifications.md](notifications.md)). Closing with a last message sends only the second event.

## 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.
- [ ] Refresh the list on the `support_ticket_*` push and when the screen opens.

## 8. Acceptance scenarios and open decisions

AC-CUST-024 (reason + text + optional attachment creates an admin-visible request with a trackable status). Tests: `tests/Feature/Api/V1/Shared/SupportTicketTest.php`. Admin side: [../admin/support.md](../admin/support.md). Open: the DEC-21 hotline/FAQ/live-chat parts of SCR-24 are out of scope here.
