# Client

Routes that **require Client authentication** (`user_type: individual`) and are specific to the Client role: session management, profile/settings, addresses, cart/orders, wallet, and Client-only notification preferences. Catalog browsing, support conversations, media upload, and FAQs/pages are used by Client too but live in `06-shared-general.md` since Technician/Company use the identical routes/shape — see that file for those and [WORKFLOWS.md](WORKFLOWS.md) for the full order lifecycle this feeds into (Admin assignment, Technician execution).

**37 endpoints in this section.**

## 01. Authentication (Session)

#### Refresh Access Token

**What this does:** Issues a fresh JWT for the currently authenticated user, extending the session without re-entering credentials.

| | |
|---|---|
| **Method** | `POST` |
| **Endpoint** | `{{base_url}}/api/v1/client/auth/refresh` |
| **Route name** | `client.auth.refresh` |
| **Guard** | JWT bearer (`auth:api`) |
| **Allowed roles** | Individual client (`user_type: individual`) |
| **Status codes** | `200 OK`, `401 Unauthorized` |

**Headers:**
- `Accept: application/json`
- `Content-Type: application/json`
- `Accept-Language: {{locale}}`

**Payload:**
No fields. Send an empty JSON object `{}`.

**Example Request Body:**
```json
[]
```

**Actual Success Response(s):**

*{ 200 OK — valid token }*
```json
{
    "status": "success",
    "message": "Token refreshed.",
    "data": {
        "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJodHRwOi8vbG9jYWxob3N0L2FwaS92MS9jbGllbnQvYXV0aC9yZWZyZXNoIiwiaWF0IjoxNzg3MjIzMTE0LCJleHAiOjE3OTI0MDcxMTQsIm5iZiI6MTc4NzIyMzExNCwianRpIjoiYVZoN3BSMVkzVVNRdVlKTSIsInN1YiI6IjY1IiwicHJ2IjoiMjNiZDVjODk0OWY2MDBhZGIzOWU3MDFjNDAwODcyZGI3YTU5NzZmNyJ9.6f0W0toUzyKgHdCAGdCaAekEVYTZ8WrKcHUHe2LQI24"
    }
}
```

**Actual Error Response(s):**

*{ 401 Unauthorized — 401 Unauthenticated - no token }*
```json
{
    "status": "fail",
    "message": "Unauthenticated, you have to login first",
    "data": null
}
```

**What happens next:** Replace the stored token with the new one; continue using the API as before.

---

#### Logout

**What this does:** Invalidates the current JWT (blacklists it) and, if `device_id` is supplied, removes that device's push-notification registration.

| | |
|---|---|
| **Method** | `POST` |
| **Endpoint** | `{{base_url}}/api/v1/client/auth/logout` |
| **Route name** | `client.auth.logout` |
| **Guard** | JWT bearer (`auth:api`) |
| **Allowed roles** | Individual client (`user_type: individual`) |
| **Status codes** | `200 OK`, `401 Unauthorized` |

**Headers:**
- `Accept: application/json`
- `Content-Type: application/json`
- `Accept-Language: {{locale}}`

**Payload:**
Send as `application/json`.

**Field documentation:**
### `device_id`
- **Description:** Device ID
- **Type:** `integer`
- **Required:** optional
- **Example:** `device-uuid-123`
- **Validation rules:** `nullable|string|max:255`
- `nullable` — may be null.
- `string` — must be text.
- `max:255` — must not exceed 255.

**Example Request Body:**
```json
{
    "device_id": "device-uuid-123"
}
```

**Actual Success Response(s):**

*{ 200 OK — valid token }*
```json
{
    "status": "success",
    "message": "Logged out successfully.",
    "data": null
}
```

**Actual Error Response(s):**

*{ 401 Unauthorized — 401 Unauthenticated - no token }*
```json
{
    "status": "fail",
    "message": "Unauthenticated, you have to login first",
    "data": null
}
```

**What happens next:** The token is no longer usable — call login again to obtain a new one.

---

## 02. Profile & Settings

#### Update Profile

**What this does:** Updates the client's own editable profile fields (name, phone, gender, birth date, locale, avatar image).

| | |
|---|---|
| **Method** | `PUT` |
| **Endpoint** | `{{base_url}}/api/v1/client/profile` |
| **Route name** | `client.profile.update` |
| **Guard** | JWT bearer (`auth:api`); account must be `active`; `geo.context` |
| **Allowed roles** | Individual client (`user_type: individual`) |
| **Status codes** | `200 OK`, `422 Unprocessable Entity` |

**Headers:**
- `Accept: application/json`
- `Content-Type: application/json`
- `Accept-Language: {{locale}}`

**Payload:**
Send as `application/json`.

**Field documentation:**
### `full_name`
- **Description:** Full Name
- **Type:** `string`
- **Required:** optional
- **Example:** `Example Name`
- **Validation rules:** `sometimes|string|max:100`
- `sometimes` — validated only when the field is present.
- `string` — must be text.
- `max:100` — must not exceed 100.
### `phone`
- **Description:** Phone
- **Type:** `string`
- **Required:** optional
- **Example:** `1234567890`
- **Validation rules:** `sometimes|string|max:20|unique:users,phone,"1",id`
- `sometimes` — validated only when the field is present.
- `string` — must be text.
- `max:20` — must not exceed 20.
- `unique:users,phone,"1",id` — must be unique (users, phone, "1", id).
### `locale`
- **Description:** Locale
- **Type:** `string`
- **Required:** optional
- **Example:** `value`
- **Validation rules:** `nullable|string|max:5`
- `nullable` — may be null.
- `string` — must be text.
- `max:5` — must not exceed 5.
### `image`
- **Description:** Image
- **Type:** `string`
- **Required:** optional
- **Example:** `value`
- **Validation rules:** `nullable|string|custom:MediaExists`
- `nullable` — may be null.
- `string` — must be text.
- `custom:MediaExists` — custom validation rule.
### `current_password`
- **Description:** Current Password
- **Type:** `string`
- **Required:** conditional
- **Example:** `Password123!`
- **Validation rules:** `required_with:password|string`
- `required_with:password` — required when password is present.
- `string` — must be text.
### `password`
- **Description:** New Password
- **Type:** `string`
- **Required:** conditional
- **Example:** `Password123!`
- **Validation rules:** `sometimes|required_with:current_password|string|min:8|confirmed`
- `sometimes` — validated only when the field is present.
- `required_with:current_password` — required when current_password is present.
- `string` — must be text.
- `min:8` — must be at least 8.
- `confirmed` — requires a matching *_confirmation field.
### `password_confirmation`
- **Description:** Password confirmation
- **Type:** `string`
- **Required:** conditional
- **Example:** `Password123!`
- **Validation rules:** `required_with:password|string`
- `required_with:password` — required when password is present.
- `string` — must be text.

**Example Request Body:**
```json
{
    "full_name": "Profile Updated",
    "phone": "1044433399"
}
```

**Actual Success Response(s):**

*{ 200 OK — valid update }*
```json
{
    "status": "success",
    "message": "Profile updated successfully",
    "data": {
        "id": 67,
        "name": "Profile Updated",
        "email": "profile.update@example.com",
        "phone": "1044433399",
        "user_type": "individual",
        "user_type_label": "Individual",
        "status": "active",
        "status_label": "Active",
        "avatar": null,
        "is_email_verified": true,
        "is_phone_verified": false,
        "last_login_at": null,
        "created_at": "2026-08-20T10:51:55.000000Z",
        "completed_orders_total": 0,
        "cancelled_orders_total": 0
    }
}
```

**Actual Error Response(s):**

*{ 422 Unprocessable Entity — 422 Validation Error - phone already taken }*
```json
{
    "status": "fail",
    "message": "This phone number is already registered.",
    "data": null
}
```

*{ 422 Unprocessable Entity — 422 Error - wrong current_password }*
```json
{
    "status": "fail",
    "message": "The current password is incorrect.",
    "data": null
}
```

**What happens next:** Fetch `GET /client/profile` to see the updated record.

---

#### Update Setting

**What this does:** Updates an existing setting.

| | |
|---|---|
| **Method** | `PUT` |
| **Endpoint** | `{{base_url}}/api/v1/client/settings` |
| **Route name** | `client.settings.update` |
| **Guard** | JWT bearer (`auth:api`); account must be `active`; `geo.context` |
| **Allowed roles** | Individual client (`user_type: individual`) |
| **Status codes** | `200 OK`, `422 Unprocessable Entity` |

**Headers:**
- `Accept: application/json`
- `Content-Type: application/json`
- `Accept-Language: {{locale}}`

**Payload:**
Send as `application/json`.

**Field documentation:**
### `allow_notifications`
- **Description:** Allow Notifications
- **Type:** `boolean`
- **Required:** optional
- **Example:** `true`
- **Validation rules:** `sometimes|boolean`
- `sometimes` — validated only when the field is present.
- `boolean` — must be true, false, 1, or 0.
### `locale`
- **Description:** Language
- **Type:** `string`
- **Required:** optional
- **Example:** `en`
- **Validation rules:** `sometimes|string|in:en,ar`
- `sometimes` — validated only when the field is present.
- `string` — must be text.
- `in:en,ar` — must be one of: en, ar.

**Example Request Body:**
```json
{
    "locale": "ar",
    "allow_notifications": false
}
```

**Actual Success Response(s):**

*{ 200 OK — valid update }*
```json
{
    "status": "success",
    "message": "Profile settings updated successfully",
    "data": {
        "id": 79,
        "name": "Alphonso Rutherford",
        "email": "settings.update@example.com",
        "phone": "1022211101",
        "user_type": "individual",
        "user_type_label": "Individual",
        "status": "active",
        "status_label": "Active",
        "avatar": null,
        "is_email_verified": true,
        "is_phone_verified": false,
        "last_login_at": null,
        "created_at": "2026-08-20T10:52:01.000000Z",
        "completed_orders_total": 0,
        "cancelled_orders_total": 0
    }
}
```

**Actual Error Response(s):**

*{ 422 Unprocessable Entity — 422 Validation Error - unsupported locale }*
```json
{
    "status": "fail",
    "message": "The selected language is not supported.",
    "data": null
}
```

**What happens next:** Fetch `show` to confirm the change.

---

#### Change Email Profile

**What this does:** Requests an email change — sends a verification code to the new address without changing anything yet.

| | |
|---|---|
| **Method** | `POST` |
| **Endpoint** | `{{base_url}}/api/v1/client/profile/change-email` |
| **Route name** | `client.profile.change-email` |
| **Guard** | JWT bearer (`auth:api`); account must be `active`; `geo.context` |
| **Allowed roles** | Individual client (`user_type: individual`) |
| **Status codes** | `200 OK`, `422 Unprocessable Entity` |

**Headers:**
- `Accept: application/json`
- `Content-Type: application/json`
- `Accept-Language: {{locale}}`

**Payload:**
Send as `application/json`.

**Field documentation:**
### `new_email`
- **Description:** New Email
- **Type:** `string`
- **Required:** required
- **Example:** `user@example.com`
- **Validation rules:** `required|email|unique:users,email,"1",id`
- `required` — must be provided.
- `email` — must be a valid email address.
- `unique:users,email,"1",id` — must be unique (users, email, "1", id).

**Example Request Body:**
```json
{
    "new_email": "profile.new-email@example.com"
}
```

**Actual Success Response(s):**

*{ 200 OK — available new email }*
```json
{
    "status": "success",
    "message": "A verification code has been sent to the new email.",
    "data": {
        "verification_token": "e56443d5-117c-4a4c-92da-a06e9dc88ece"
    }
}
```

**Actual Error Response(s):**

*{ 422 Unprocessable Entity — 422 Validation Error - email already taken }*
```json
{
    "status": "fail",
    "message": "The New Email has already been taken.",
    "data": null
}
```

**What happens next:** Call `POST /client/profile/verify-email-change` with the code to finalize.

---

#### Change Password Profile

**What this does:** Changes the account password after verifying `current_password`.

| | |
|---|---|
| **Method** | `POST` |
| **Endpoint** | `{{base_url}}/api/v1/client/profile/change-password` |
| **Route name** | `client.profile.change-password` |
| **Guard** | JWT bearer (`auth:api`); account must be `active`; `geo.context` |
| **Allowed roles** | Individual client (`user_type: individual`) |
| **Status codes** | `200 OK`, `422 Unprocessable Entity` |

**Headers:**
- `Accept: application/json`
- `Content-Type: application/json`
- `Accept-Language: {{locale}}`

**Payload:**
Send as `application/json`.

**Field documentation:**
### `current_password`
- **Description:** Current Password
- **Type:** `string`
- **Required:** required
- **Example:** `Password123!`
- **Validation rules:** `required|string`
- `required` — must be provided.
- `string` — must be text.
### `password`
- **Description:** New Password
- **Type:** `string`
- **Required:** required
- **Example:** `Password123!`
- **Validation rules:** `required|string|min:8|confirmed`
- `required` — must be provided.
- `string` — must be text.
- `min:8` — must be at least 8.
- `confirmed` — requires a matching *_confirmation field.

**Example Request Body:**
```json
{
    "current_password": "password",
    "password": "brandnewpass123",
    "password_confirmation": "brandnewpass123"
}
```

**Actual Success Response(s):**

*{ 200 OK — correct current password }*
```json
{
    "status": "success",
    "message": "Password changed successfully.",
    "data": null
}
```

**Actual Error Response(s):**

*{ 422 Unprocessable Entity — 422 Error - wrong current password }*
```json
{
    "status": "fail",
    "message": "The current password is incorrect.",
    "data": null
}
```

*{ 422 Unprocessable Entity — 422 Validation Error - password too short }*
```json
{
    "status": "fail",
    "message": "The password must be at least 8 characters.",
    "data": null
}
```

**What happens next:** The existing token remains valid; no re-login is required unless you also want a fresh token.

---

#### Delete Profile

**What this does:** Permanently deletes the authenticated client's own account.

| | |
|---|---|
| **Method** | `DELETE` |
| **Endpoint** | `{{base_url}}/api/v1/client/profile` |
| **Route name** | `client.profile.delete` |
| **Guard** | JWT bearer (`auth:api`); account must be `active`; `geo.context` |
| **Allowed roles** | Individual client (`user_type: individual`) |
| **Status codes** | `200 OK`, `401 Unauthorized` |

**Headers:**
- `Accept: application/json`
- `Content-Type: application/json`
- `Accept-Language: {{locale}}`

**Payload:**
No request body. Use the documented path and query parameters.

**Actual Success Response(s):**

*{ 200 OK — authenticated }*
```json
{
    "status": "success",
    "message": "Account deleted successfully",
    "data": null
}
```

**Actual Error Response(s):**

*{ 401 Unauthorized — 401 Unauthenticated - no token }*
```json
{
    "status": "fail",
    "message": "Unauthenticated, you have to login first",
    "data": null
}
```

**What happens next:** The current token is invalidated along with the account — no further calls are possible with it.

---

#### Verify Email Change Profile

**What this does:** Confirms the code sent to the new email address and applies the change.

| | |
|---|---|
| **Method** | `POST` |
| **Endpoint** | `{{base_url}}/api/v1/client/profile/verify-email-change` |
| **Route name** | `client.profile.verify-email-change` |
| **Guard** | JWT bearer (`auth:api`); account must be `active`; `geo.context` |
| **Allowed roles** | Individual client (`user_type: individual`) |
| **Status codes** | `200 OK`, `422 Unprocessable Entity` |

**Headers:**
- `Accept: application/json`
- `Content-Type: application/json`
- `Accept-Language: {{locale}}`

**Payload:**
Send as `application/json`.

**Field documentation:**
### `token`
- **Description:** Token
- **Type:** `string`
- **Required:** required
- **Example:** `<verification-token-from-previous-response>`
- **Validation rules:** `required|string`
- `required` — must be provided.
- `string` — must be text.
### `code`
- **Description:** Verification Code
- **Type:** `string`
- **Required:** required
- **Example:** `123456`
- **Validation rules:** `required|string|size:6`
- `required` — must be provided.
- `string` — must be text.
- `size:6` — custom validation rule.

**Example Request Body:**
```json
{
    "token": "e56443d5-117c-4a4c-92da-a06e9dc88ece",
    "code": "123456"
}
```

**Actual Success Response(s):**

*{ 200 OK — valid code }*
```json
{
    "status": "success",
    "message": "Email changed successfully.",
    "data": {
        "id": 74,
        "name": "Concepcion Corwin",
        "email": "profile.new-email@example.com",
        "phone": "1044433329",
        "user_type": "individual",
        "user_type_label": "Individual",
        "status": "active",
        "status_label": "Active",
        "avatar": null,
        "is_email_verified": true,
        "is_phone_verified": false,
        "last_login_at": null,
        "created_at": "2026-08-20T10:51:58.000000Z",
        "completed_orders_total": 0,
        "cancelled_orders_total": 0
    }
}
```

**Actual Error Response(s):**

*{ 422 Unprocessable Entity — 422 Error - invalid token }*
```json
{
    "status": "fail",
    "message": "Expired or invalid verification code.",
    "data": null
}
```

**What happens next:** Future logins must use the new email.

---

#### Show Profile Details

**What this does:** Returns the individual client's own profile — base account fields plus role-specific profile data (and, for admin, `roles`/`permissions` when loaded).

| | |
|---|---|
| **Method** | `GET` |
| **Endpoint** | `{{base_url}}/api/v1/client/profile` |
| **Route name** | `client.profile.view` |
| **Guard** | JWT bearer (`auth:api`); account must be `active`; `geo.context` |
| **Allowed roles** | Individual client (`user_type: individual`) |
| **Status codes** | `200 OK`, `401 Unauthorized` |

**Headers:**
- `Accept: application/json`
- `Content-Type: application/json`
- `Accept-Language: {{locale}}`

**Payload:**
No request body. Use the documented path and query parameters.

**Actual Success Response(s):**

*{ 200 OK — authenticated }*
```json
{
    "status": "success",
    "message": "Profile retrieved successfully",
    "data": {
        "id": 66,
        "name": "Profile Original",
        "email": "profile.user@example.com",
        "phone": "1044433322",
        "user_type": "individual",
        "user_type_label": "Individual",
        "status": "active",
        "status_label": "Active",
        "avatar": null,
        "is_email_verified": true,
        "is_phone_verified": false,
        "last_login_at": null,
        "created_at": "2026-08-20T10:51:54.000000Z",
        "completed_orders_total": 0,
        "cancelled_orders_total": 0
    }
}
```

**Actual Error Response(s):**

*{ 401 Unauthorized — 401 Unauthenticated - no token }*
```json
{
    "status": "fail",
    "message": "Unauthenticated, you have to login first",
    "data": null
}
```

**What happens next:** Use `PUT` on the same endpoint to update editable fields.

---

#### Show Setting Details

**What this does:** Returns the client's notification/locale preference settings.

| | |
|---|---|
| **Method** | `GET` |
| **Endpoint** | `{{base_url}}/api/v1/client/settings` |
| **Route name** | `client.settings.view` |
| **Guard** | JWT bearer (`auth:api`); account must be `active`; `geo.context` |
| **Allowed roles** | Individual client (`user_type: individual`) |
| **Status codes** | `200 OK`, `401 Unauthorized` |

**Headers:**
- `Accept: application/json`
- `Content-Type: application/json`
- `Accept-Language: {{locale}}`

**Payload:**
No request body. Use the documented path and query parameters.

**Actual Success Response(s):**

*{ 200 OK — authenticated }*
```json
{
    "status": "success",
    "message": "Settings retrieved successfully",
    "data": {
        "locale": "en",
        "allow_notifications": true
    }
}
```

**Actual Error Response(s):**

*{ 401 Unauthorized — 401 Unauthenticated - no token }*
```json
{
    "status": "fail",
    "message": "Unauthenticated, you have to login first",
    "data": null
}
```

**What happens next:** Use `PUT` on the same endpoint to change them.

---

## 03. Addresses

#### List All Addresses

**What this does:** Returns a paginated, filterable, searchable list of addresses.

| | |
|---|---|
| **Method** | `GET` |
| **Endpoint** | `{{base_url}}/api/v1/client/addresses?page=1&per_page=15&search=&sort[created_at]=desc&paginate=false&filters[is_active]=1` |
| **Route name** | `client.addresses.index` |
| **Guard** | JWT bearer (`auth:api`); account must be `active`; `geo.context` |
| **Allowed roles** | Individual client (`user_type: individual`) |
| **Status codes** | `200 OK`, `401 Unauthorized` |

**Headers:**
- `Accept: application/json`
- `Content-Type: application/json`
- `Accept-Language: {{locale}}`

**Payload:**
No request body. Use the documented path and query parameters.

**Query parameters:**
- `page` — Page number (supported)
- `per_page` — Items per page (max: 100) (optional)
- `search` — Search term (searches across searchable fields) (optional)
- `sort[created_at]` — Sort by field: asc or desc (optional)
- `paginate` — Set to false to disable pagination (optional)
- `filters[is_active]` — Filter by active status: 1 or 0 (optional)

**Actual Success Response(s):**

*{ 200 OK — list addresses for authenticated user }*
```json
{
    "status": "success",
    "message": "Addresses retrieved",
    "data": [
        {
            "id": 15,
            "user_id": 35,
            "title": "Home",
            "address": "123 Faker Street, Riyadh",
            "country_id": 15,
            "governorate_id": null,
            "region_id": null,
            "lat": 24.7136,
            "lng": 46.6753,
            "phone": "500000001",
            "phone_code": "966",
            "notes": "Ring the bell twice",
            "is_default": true,
            "created_at": "2026-08-20T10:51:06.000000Z",
            "updated_at": "2026-08-20T10:51:06.000000Z",
            "country": {
                "id": 15,
                "phone_code": null,
                "phone_starting_number": null,
                "phone_length": null,
                "short_name": null,
                "continent": "asia",
                "is_active": true,
                "created_at": "2026-08-20T10:51:06.000000Z",
                "updated_at": "2026-08-20T10:51:06.000000Z",
                "name": null,
                "slug": null,
                "currency": null,
                "nationality": null
            },
            "governorate": null,
            "region": null
        }
    ]
}
```

**Actual Error Response(s):**

*{ 401 Unauthorized — 401 Unauthenticated - no token }*
```json
{
    "status": "fail",
    "message": "Unauthenticated, you have to login first",
    "data": null
}
```

**What happens next:** Use `show` on a specific id for full details.

---

#### Show Address Details

**What this does:** Returns full details for one address.

| | |
|---|---|
| **Method** | `GET` |
| **Endpoint** | `{{base_url}}/api/v1/client/addresses/:address` |
| **Route name** | `client.addresses.show` |
| **Guard** | JWT bearer (`auth:api`); account must be `active`; `geo.context` |
| **Allowed roles** | Individual client (`user_type: individual`) |
| **Status codes** | `200 OK`, `404 Not Found` |

**Headers:**
- `Accept: application/json`
- `Content-Type: application/json`
- `Accept-Language: {{locale}}`

**Path Parameters:**
- `address` — Address ID — chained from an earlier request in this flow ({{address_id}})

**Payload:**
No request body. Use the documented path and query parameters.

**Actual Success Response(s):**

*{ 200 OK — show own address }*
```json
{
    "status": "success",
    "message": "Address retrieved",
    "data": {
        "id": 16,
        "user_id": 36,
        "title": "Home",
        "address": "123 Faker Street, Riyadh",
        "country_id": 16,
        "governorate_id": null,
        "region_id": null,
        "lat": 24.7136,
        "lng": 46.6753,
        "phone": "500000001",
        "phone_code": "966",
        "notes": "Ring the bell twice",
        "is_default": true,
        "created_at": "2026-08-20T10:51:07.000000Z",
        "updated_at": "2026-08-20T10:51:07.000000Z",
        "country": {
            "id": 16,
            "phone_code": null,
            "phone_starting_number": null,
            "phone_length": null,
            "short_name": null,
            "continent": "asia",
            "is_active": true,
            "created_at": "2026-08-20T10:51:07.000000Z",
            "updated_at": "2026-08-20T10:51:07.000000Z",
            "name": null,
            "slug": null,
            "currency": null,
            "nationality": null
        },
        "governorate": null,
        "region": null
    }
}
```

**Actual Error Response(s):**

*{ 404 Not Found — nonexistent address id }*
```json
{
    "status": "fail",
    "message": "Resource not found",
    "data": null
}
```

*{ 404 Not Found — address belongs to a different user }*
```json
{
    "status": "fail",
    "message": "Resource not found",
    "data": null
}
```

**What happens next:** Use `update` to modify it, or `destroy` to remove it.

---

#### Create New Address

**What this does:** Creates a new address.

| | |
|---|---|
| **Method** | `POST` |
| **Endpoint** | `{{base_url}}/api/v1/client/addresses` |
| **Route name** | `client.addresses.store` |
| **Guard** | JWT bearer (`auth:api`); account must be `active`; `geo.context` |
| **Allowed roles** | Individual client (`user_type: individual`) |
| **Status codes** | `201 Created`, `422 Unprocessable Entity` |

**Headers:**
- `Accept: application/json`
- `Content-Type: application/json`
- `Accept-Language: {{locale}}`

**Payload:**
Send as `application/json`.

**Field documentation:**
### `title`
- **Description:** Title
- **Type:** `string`
- **Required:** required
- **Example:** `Example Title`
- **Validation rules:** `required|string|max:100`
- `required` — must be provided.
- `string` — must be text.
- `max:100` — must not exceed 100.
### `address`
- **Description:** Address
- **Type:** `string`
- **Required:** required
- **Example:** `123 Main Street, Building A`
- **Validation rules:** `required|string|max:500`
- `required` — must be provided.
- `string` — must be text.
- `max:500` — must not exceed 500.
### `country_id`
- **Description:** Country
- **Type:** `integer`
- **Required:** required
- **Example:** `1`
- **Validation rules:** `required|exists:countries,id`
- `required` — must be provided.
- `exists:countries,id` — must reference an existing record (countries, id).
### `governorate_id`
- **Description:** Governorate
- **Type:** `integer`
- **Required:** optional
- **Example:** `1`
- **Validation rules:** `nullable|exists:governorates,id`
- `nullable` — may be null.
- `exists:governorates,id` — must reference an existing record (governorates, id).
### `region_id`
- **Description:** Region
- **Type:** `integer`
- **Required:** optional
- **Example:** `1`
- **Validation rules:** `nullable|exists:regions,id`
- `nullable` — may be null.
- `exists:regions,id` — must reference an existing record (regions, id).
### `lat`
- **Description:** Latitude
- **Type:** `number`
- **Required:** required
- **Example:** `10`
- **Validation rules:** `required|numeric|between:-90,90`
- `required` — must be provided.
- `numeric` — must be numeric.
- `between:-90,90` — must be between -90, 90.
### `lng`
- **Description:** Longitude
- **Type:** `number`
- **Required:** required
- **Example:** `10`
- **Validation rules:** `required|numeric|between:-180,180`
- `required` — must be provided.
- `numeric` — must be numeric.
- `between:-180,180` — must be between -180, 180.
### `phone`
- **Description:** Phone
- **Type:** `string`
- **Required:** optional
- **Example:** `1234567890`
- **Validation rules:** `nullable|string|max:20`
- `nullable` — may be null.
- `string` — must be text.
- `max:20` — must not exceed 20.
### `phone_code`
- **Description:** Phone code
- **Type:** `string`
- **Required:** optional
- **Example:** `20`
- **Validation rules:** `nullable|string|max:5`
- `nullable` — may be null.
- `string` — must be text.
- `max:5` — must not exceed 5.
### `notes`
- **Description:** Notes
- **Type:** `string`
- **Required:** optional
- **Example:** `value`
- **Validation rules:** `nullable|string|max:500`
- `nullable` — may be null.
- `string` — must be text.
- `max:500` — must not exceed 500.
### `is_default`
- **Description:** Default
- **Type:** `boolean`
- **Required:** optional
- **Example:** `true`
- **Validation rules:** `sometimes|boolean`
- `sometimes` — validated only when the field is present.
- `boolean` — must be true, false, 1, or 0.

**Example Request Body:**
```json
{
    "title": "Home",
    "address": "123 Faker Street, Riyadh",
    "country_id": 1,
    "lat": 24.7136,
    "lng": 46.6753,
    "phone_code": "966",
    "phone": "500000001",
    "notes": "Ring the bell twice"
}
```

**Actual Success Response(s):**

*{ 201 Created — valid address (first address auto-default) }*
```json
{
    "status": "success",
    "message": "Address created",
    "data": {
        "title": "Home",
        "address": "123 Faker Street, Riyadh",
        "country_id": 14,
        "lat": 24.7136,
        "lng": 46.6753,
        "phone": "500000001",
        "phone_code": "966",
        "notes": "Ring the bell twice",
        "user_id": 33,
        "is_default": true,
        "updated_at": "2026-08-20T10:51:05.000000Z",
        "created_at": "2026-08-20T10:51:05.000000Z",
        "id": 14,
        "country": {
            "id": 14,
            "phone_code": null,
            "phone_starting_number": null,
            "phone_length": null,
            "short_name": null,
            "continent": "asia",
            "is_active": true,
            "created_at": "2026-08-20T10:51:04.000000Z",
            "updated_at": "2026-08-20T10:51:04.000000Z",
            "name": null,
            "slug": null,
            "currency": null,
            "nationality": null
        },
        "governorate": null,
        "region": null
    }
}
```

**Actual Error Response(s):**

*{ 422 Unprocessable Entity — 422 Validation Error - missing required fields }*
```json
{
    "status": "fail",
    "message": "Please enter an address.",
    "data": null
}
```

**What happens next:** Use the returned id with `show`/`update`/`destroy` for this address.

---

#### Update Address

**What this does:** Updates an existing address.

| | |
|---|---|
| **Method** | `PUT` |
| **Endpoint** | `{{base_url}}/api/v1/client/addresses/:address` |
| **Route name** | `client.addresses.update` |
| **Guard** | JWT bearer (`auth:api`); account must be `active`; `geo.context` |
| **Allowed roles** | Individual client (`user_type: individual`) |
| **Status codes** | `200 OK`, `422 Unprocessable Entity` |

**Headers:**
- `Accept: application/json`
- `Content-Type: application/json`
- `Accept-Language: {{locale}}`

**Path Parameters:**
- `address` — Address ID — chained from an earlier request in this flow ({{address_id}})

**Payload:**
Send as `application/json`.

**Field documentation:**
### `title`
- **Description:** Title
- **Type:** `string`
- **Required:** optional
- **Example:** `Example Title`
- **Validation rules:** `sometimes|string|max:100`
- `sometimes` — validated only when the field is present.
- `string` — must be text.
- `max:100` — must not exceed 100.
### `address`
- **Description:** Address
- **Type:** `string`
- **Required:** optional
- **Example:** `123 Main Street, Building A`
- **Validation rules:** `sometimes|string|max:500`
- `sometimes` — validated only when the field is present.
- `string` — must be text.
- `max:500` — must not exceed 500.
### `country_id`
- **Description:** Country
- **Type:** `integer`
- **Required:** optional
- **Example:** `1`
- **Validation rules:** `sometimes|exists:countries,id`
- `sometimes` — validated only when the field is present.
- `exists:countries,id` — must reference an existing record (countries, id).
### `governorate_id`
- **Description:** Governorate
- **Type:** `integer`
- **Required:** optional
- **Example:** `1`
- **Validation rules:** `nullable|exists:governorates,id`
- `nullable` — may be null.
- `exists:governorates,id` — must reference an existing record (governorates, id).
### `region_id`
- **Description:** Region
- **Type:** `integer`
- **Required:** optional
- **Example:** `1`
- **Validation rules:** `nullable|exists:regions,id`
- `nullable` — may be null.
- `exists:regions,id` — must reference an existing record (regions, id).
### `lat`
- **Description:** Latitude
- **Type:** `number`
- **Required:** optional
- **Example:** `10`
- **Validation rules:** `sometimes|numeric|between:-90,90`
- `sometimes` — validated only when the field is present.
- `numeric` — must be numeric.
- `between:-90,90` — must be between -90, 90.
### `lng`
- **Description:** Longitude
- **Type:** `number`
- **Required:** optional
- **Example:** `10`
- **Validation rules:** `sometimes|numeric|between:-180,180`
- `sometimes` — validated only when the field is present.
- `numeric` — must be numeric.
- `between:-180,180` — must be between -180, 180.
### `phone`
- **Description:** Phone
- **Type:** `string`
- **Required:** optional
- **Example:** `1234567890`
- **Validation rules:** `nullable|string|max:20`
- `nullable` — may be null.
- `string` — must be text.
- `max:20` — must not exceed 20.
### `phone_code`
- **Description:** Phone code
- **Type:** `string`
- **Required:** optional
- **Example:** `20`
- **Validation rules:** `nullable|string|max:5`
- `nullable` — may be null.
- `string` — must be text.
- `max:5` — must not exceed 5.
### `notes`
- **Description:** Notes
- **Type:** `string`
- **Required:** optional
- **Example:** `value`
- **Validation rules:** `nullable|string|max:500`
- `nullable` — may be null.
- `string` — must be text.
- `max:500` — must not exceed 500.
### `is_default`
- **Description:** Default
- **Type:** `boolean`
- **Required:** optional
- **Example:** `true`
- **Validation rules:** `sometimes|boolean`
- `sometimes` — validated only when the field is present.
- `boolean` — must be true, false, 1, or 0.

**Example Request Body:**
```json
{
    "title": "Office",
    "address": "456 Updated Ave"
}
```

**Actual Success Response(s):**

*{ 200 OK — partial update }*
```json
{
    "status": "success",
    "message": "Address updated",
    "data": {
        "id": 18,
        "user_id": 40,
        "title": "Office",
        "address": "456 Updated Ave",
        "country_id": 18,
        "governorate_id": null,
        "region_id": null,
        "lat": 24.7136,
        "lng": 46.6753,
        "phone": "500000001",
        "phone_code": "966",
        "notes": "Ring the bell twice",
        "is_default": true,
        "created_at": "2026-08-20T10:51:09.000000Z",
        "updated_at": "2026-08-20T10:51:09.000000Z",
        "country": {
            "id": 18,
            "phone_code": null,
            "phone_starting_number": null,
            "phone_length": null,
            "short_name": null,
            "continent": "asia",
            "is_active": true,
            "created_at": "2026-08-20T10:51:09.000000Z",
            "updated_at": "2026-08-20T10:51:09.000000Z",
            "name": null,
            "slug": null,
            "currency": null,
            "nationality": null
        },
        "governorate": null,
        "region": null
    }
}
```

**Actual Error Response(s):**

*{ 422 Unprocessable Entity — 422 Validation Error - lat out of range }*
```json
{
    "status": "fail",
    "message": "Latitude must be between -90 and 90.",
    "data": null
}
```

**What happens next:** Fetch `show` to confirm the change.

---

#### Default Address

**What this does:** Marks one of the client's addresses as the default delivery address.

| | |
|---|---|
| **Method** | `POST` |
| **Endpoint** | `{{base_url}}/api/v1/client/addresses/:address/default` |
| **Route name** | `client.addresses.default` |
| **Guard** | JWT bearer (`auth:api`); account must be `active`; `geo.context` |
| **Allowed roles** | Individual client (`user_type: individual`) |
| **Status codes** | `200 OK`, `404 Not Found` |

**Headers:**
- `Accept: application/json`
- `Content-Type: application/json`
- `Accept-Language: {{locale}}`

**Path Parameters:**
- `address` — Address ID — chained from an earlier request in this flow ({{address_id}})

**Payload:**
No fields. Send an empty JSON object `{}`.

**Example Request Body:**
```json
[]
```

**Actual Success Response(s):**

*{ 200 OK — set a non-default address as default }*
```json
{
    "status": "success",
    "message": "Default address updated",
    "data": {
        "id": 22,
        "user_id": 44,
        "title": "Office",
        "address": "123 Faker Street, Riyadh",
        "country_id": 21,
        "governorate_id": null,
        "region_id": null,
        "lat": 24.7136,
        "lng": 46.6753,
        "phone": "500000001",
        "phone_code": "966",
        "notes": "Ring the bell twice",
        "is_default": true,
        "created_at": "2026-08-20T10:51:11.000000Z",
        "updated_at": "2026-08-20T10:51:15.000000Z"
    }
}
```

**Actual Error Response(s):**

*{ 404 Not Found — nonexistent address id }*
```json
{
    "status": "fail",
    "message": "Resource not found",
    "data": null
}
```

**What happens next:** The default address can be omitted from `address_id` on some flows where a default is assumed; otherwise still pass it explicitly on checkout.

---

#### Delete Address

**What this does:** Permanently deletes a address.

| | |
|---|---|
| **Method** | `DELETE` |
| **Endpoint** | `{{base_url}}/api/v1/client/addresses/:address` |
| **Route name** | `client.addresses.destroy` |
| **Guard** | JWT bearer (`auth:api`); account must be `active`; `geo.context` |
| **Allowed roles** | Individual client (`user_type: individual`) |
| **Status codes** | `200 OK`, `404 Not Found` |

**Headers:**
- `Accept: application/json`
- `Content-Type: application/json`
- `Accept-Language: {{locale}}`

**Path Parameters:**
- `address` — Address ID — chained from an earlier request in this flow ({{address_id}})

**Payload:**
No request body. Use the documented path and query parameters.

**Actual Success Response(s):**

*{ 200 OK — delete own address }*
```json
{
    "status": "success",
    "message": "Address deleted",
    "data": null
}
```

**Actual Error Response(s):**

*{ 404 Not Found — nonexistent address id }*
```json
{
    "status": "fail",
    "message": "Resource not found",
    "data": null
}
```

**What happens next:** It no longer appears in `index`.

---

## 04. Requests & Orders — Creating

#### Show Cart Details

**What this does:** Returns full details for one cart.

| | |
|---|---|
| **Method** | `GET` |
| **Endpoint** | `{{base_url}}/api/v1/client/cart` |
| **Route name** | `client.cart.show` |
| **Guard** | JWT bearer (`auth:api`); account must be `active`; `geo.context` |
| **Allowed roles** | Individual client (`user_type: individual`) |
| **Status codes** | `200 OK`, `401 Unauthorized` |

**Headers:**
- `Accept: application/json`
- `Content-Type: application/json`
- `Accept-Language: {{locale}}`

**Payload:**
No request body. Use the documented path and query parameters.

**Actual Success Response(s):**

*{ 200 OK — empty cart (never persisted) }*
```json
{
    "status": "success",
    "message": "Cart retrieved",
    "data": {
        "id": null,
        "items_count": 0,
        "subtotal": 0
    }
}
```

**Actual Error Response(s):**

*{ 401 Unauthorized — 401 - unauthenticated }*
```json
{
    "status": "fail",
    "message": "Unauthenticated, you have to login first",
    "data": null
}
```

**What happens next:** Use `update` to modify it, or `destroy` to remove it.

---

#### Update Item

**What this does:** Updates an existing cart.

| | |
|---|---|
| **Method** | `PATCH` |
| **Endpoint** | `{{base_url}}/api/v1/client/cart/items/:itemId` |
| **Route name** | `client.cart.items.update` |
| **Guard** | JWT bearer (`auth:api`); account must be `active`; `geo.context` |
| **Allowed roles** | Individual client (`user_type: individual`) |
| **Status codes** | `200 OK`, `404 Not Found` |

**Headers:**
- `Accept: application/json`
- `Content-Type: application/json`
- `Accept-Language: {{locale}}`

**Path Parameters:**
- `itemId` — ItemId ID — chained from an earlier request in this flow ({{cart_item_id}})

**Payload:**
Send as `application/json`.

**Field documentation:**
### `quantity`
- **Description:** Quantity
- **Type:** `integer`
- **Required:** required
- **Example:** `1`
- **Validation rules:** `required|integer|min:1|max:100`
- `required` — must be provided.
- `integer` — must be a whole number.
- `min:1` — must be at least 1.
- `max:100` — must not exceed 100.

**Example Request Body:**
```json
{
    "quantity": 5
}
```

**Actual Success Response(s):**

*{ 200 OK — quantity updated }*
```json
{
    "status": "success",
    "message": "Cart updated",
    "data": {
        "id": 5,
        "items_count": 5,
        "subtotal": 500,
        "items": [
            {
                "id": 5,
                "quantity": 5,
                "unit_price": "100.00",
                "line_total": 500,
                "service": {
                    "id": 11,
                    "name": "إصلاح مكيف",
                    "image": "https://ui-avatars.com/api/?name=C&background=fef3c7&color=d97706&size=200"
                },
                "created_at": "2026-08-20T10:50:39.000000Z"
            }
        ]
    }
}
```

**Actual Error Response(s):**

*{ 404 Not Found — 404 - item does not exist }*
```json
{
    "status": "fail",
    "message": "Resource not found",
    "data": null
}
```

*{ 404 Not Found — 404 - item belongs to another user's cart (scoped via cart->items()->findOrFail, not visible cross-user) }*
```json
{
    "status": "fail",
    "message": "Resource not found",
    "data": null
}
```

**What happens next:** Fetch `show` to confirm the change.

---

#### Checkout Cart

**What this does:** Pays for the individual client's cart (wallet — immediate, all-or-nothing debit — or gateway) and, only on confirmed payment, creates the `Order` (`status: pending`) with one `OrderItem` per cart line; empties the cart. Requires an `Idempotency-Key` header (safe to retry). VAT is added on top of the cart subtotal per `settings.vat_percent`.

| | |
|---|---|
| **Method** | `POST` |
| **Endpoint** | `{{base_url}}/api/v1/client/cart/checkout` |
| **Route name** | `client.cart.checkout` |
| **Guard** | JWT bearer (`auth:api`); account must be `active`; `geo.context`; requires `Idempotency-Key` header (`idempotency`) |
| **Allowed roles** | Individual client (`user_type: individual`) |
| **Status codes** | `201 Created`, `401 Unauthorized`, `422 Unprocessable Entity` |

**Headers:**
- `Accept: application/json`
- `Content-Type: application/json`
- `Accept-Language: {{locale}}`
- `Idempotency-Key: wallet-checkout-19`

**Payload:**
Send as `application/json`.

**Field documentation:**
### `payment_method`
- **Description:** Payment Method
- **Type:** `string`
- **Required:** required
- **Example:** `gateway`
- **Validation rules:** `required|in:"gateway","wallet"|custom:WalletCoversCartRule`
- `required` — must be provided.
- `in:"gateway","wallet"` — must be one of: "gateway", "wallet".
- `custom:WalletCoversCartRule` — custom validation rule.
### `booking_type`
- **Description:** Booking Type
- **Type:** `string`
- **Required:** required
- **Example:** `instant`
- **Validation rules:** `required|in:"instant","scheduled"`
- `required` — must be provided.
- `in:"instant","scheduled"` — must be one of: "instant", "scheduled".
### `address_id`
- **Description:** Address
- **Type:** `integer`
- **Required:** required
- **Example:** `1`
- **Validation rules:** `required|exists:addresses,id,user_id,"1"`
- `required` — must be provided.
- `exists:addresses,id,user_id,"1"` — must reference an existing record (addresses, id, user_id, "1").
### `scheduled_date`
- **Description:** Scheduled Date
- **Type:** `string`
- **Required:** conditional
- **Example:** `2026-08-21`
- **Validation rules:** `required_if:booking_type,scheduled|date|after_or_equal:today`
- `required_if:booking_type,scheduled` — required when booking_type, scheduled.
- `date` — must be a valid date.
- `after_or_equal:today` — custom validation rule.

**Example Request Body:**
```json
{
    "payment_method": "wallet",
    "booking_type": "instant",
    "address_id": 1
}
```

**Actual Success Response(s):**

*{ 201 Created — 201 - wallet payment (Idempotency-Key header sent) }*
```json
{
    "status": "success",
    "message": "Order placed successfully",
    "data": {
        "order_number": "ORD-260820-KMPW",
        "customer_id": 19,
        "order_source": "individual",
        "branch_id": null,
        "booking_type": "instant",
        "scheduled_date": null,
        "address_id": 2,
        "status": "pending",
        "subtotal_amount": "200.00",
        "vat_amount": "30.00",
        "fees_amount": "0.00",
        "total_amount": "230.00",
        "paid_amount": "230.00",
        "uuid": "c7e219d7-8c70-4c6e-8735-cd7b3df45678",
        "updated_at": "2026-08-20T10:50:54.000000Z",
        "created_at": "2026-08-20T10:50:54.000000Z",
        "id": 6,
        "items": [
            {
                "id": 3,
                "order_id": "c7e219d7-8c70-4c6e-8735-cd7b3df45678",
                "category_id": 16,
                "service_name": "إصلاح مكيف",
                "pricing_type": "fixed",
                "unit_price": "100.00",
                "quantity": 2,
                "line_total": "200.00",
                "is_quoted": false,
                "notes": null,
                "request_description": null,
                "created_at": "2026-08-20T10:50:54.000000Z",
                "updated_at": "2026-08-20T10:50:54.000000Z"
            }
        ]
    }
}
```

*{ 201 Created — 201 - gateway payment (dummy gateway confirms), scheduled booking }*
```json
{
    "status": "success",
    "message": "Order placed successfully",
    "data": {
        "order_number": "ORD-260820-NDLY",
        "customer_id": 20,
        "order_source": "individual",
        "branch_id": null,
        "booking_type": "scheduled",
        "scheduled_date": "2026-08-21T00:00:00.000000Z",
        "address_id": 3,
        "status": "pending",
        "subtotal_amount": "150.00",
        "vat_amount": "22.50",
        "fees_amount": "0.00",
        "total_amount": "172.50",
        "paid_amount": "172.50",
        "uuid": "3cabd499-6b25-4f27-902a-dfc510eb6499",
        "updated_at": "2026-08-20T10:50:54.000000Z",
        "created_at": "2026-08-20T10:50:54.000000Z",
        "id": 7,
        "items": [
            {
                "id": 4,
                "order_id": "3cabd499-6b25-4f27-902a-dfc510eb6499",
                "category_id": 17,
                "service_name": "إصلاح مكيف",
                "pricing_type": "fixed",
                "unit_price": "50.00",
                "quantity": 3,
                "line_total": "150.00",
                "is_quoted": false,
                "notes": null,
                "request_description": null,
                "created_at": "2026-08-20T10:50:54.000000Z",
                "updated_at": "2026-08-20T10:50:54.000000Z"
            }
        ]
    }
}
```

*{ 201 Created — 201 - idempotent replay: same Idempotency-Key returns cached first response, no duplicate order (X-Idempotent-Replay: true) }*
```json
{
    "status": "success",
    "message": "Order placed successfully",
    "data": {
        "order_number": "ORD-260820-7YMK",
        "customer_id": 26,
        "order_source": "individual",
        "branch_id": null,
        "booking_type": "instant",
        "scheduled_date": null,
        "address_id": 8,
        "status": "pending",
        "subtotal_amount": "40.00",
        "vat_amount": "6.00",
        "fees_amount": "0.00",
        "total_amount": "46.00",
        "paid_amount": "46.00",
        "uuid": "644b2de9-a3ee-42d6-8b80-55446defef66",
        "updated_at": "2026-08-20T10:50:59.000000Z",
        "created_at": "2026-08-20T10:50:59.000000Z",
        "id": 8,
        "items": [
            {
                "id": 5,
                "order_id": "644b2de9-a3ee-42d6-8b80-55446defef66",
                "category_id": 22,
                "service_name": "إصلاح مكيف",
                "pricing_type": "fixed",
                "unit_price": "40.00",
                "quantity": 1,
                "line_total": "40.00",
                "is_quoted": false,
                "notes": null,
                "request_description": null,
                "created_at": "2026-08-20T10:51:00.000000Z",
                "updated_at": "2026-08-20T10:51:00.000000Z"
            }
        ]
    }
}
```

**Actual Error Response(s):**

*{ 422 Unprocessable Entity — 422 - gateway declines the charge (payments.dummy_gateway_should_fail=true), no order created }*
```json
{
    "status": "fail",
    "message": "Payment could not be completed",
    "data": {
        "payment_id": 3
    }
}
```

*{ 422 Unprocessable Entity — 422 - empty cart (no cart row at all) }*
```json
{
    "status": "error",
    "message": "Your cart is empty",
    "data": {
        "error_code": "BUSINESS_RULE_VIOLATED",
        "correlation_id": "c0b2afe8-c192-4d05-90d1-37dcca7f29dd"
    }
}
```

*{ 422 Unprocessable Entity — 422 - wallet balance (10.00) does not cover cart total (575.00), rejected by WalletCoversCartRule at the FormRequest layer before the service runs }*
```json
{
    "status": "fail",
    "message": "Wallet balance (10.00 SAR) is less than the order total (500.00 SAR). The wallet cannot be used for partial payment.",
    "data": null
}
```

*{ 422 Unprocessable Entity — 422 - BUG: wallet (105.00) covers cart subtotal (100.00) but not the VAT-inclusive total (115.00); CheckoutRequest's WalletCoversCartRule validates against subtotal only and lets this through, then CheckoutCommandService::checkoutWithWallet rejects it one layer deeper — see CheckoutRequest.php:25-30 vs CheckoutCommandService.php:140-142 }*
```json
{
    "status": "fail",
    "message": "Wallet balance (105.00 SAR) is less than the order total (115.00 SAR). The wallet cannot be used for partial payment.",
    "data": null
}
```

*{ 422 Unprocessable Entity — 422 - address_id missing }*
```json
{
    "status": "fail",
    "message": "Please select an address.",
    "data": null
}
```

*{ 401 Unauthorized — 401 - unauthenticated }*
```json
{
    "status": "fail",
    "message": "Unauthenticated, you have to login first",
    "data": null
}
```

**What happens next:** The order now needs a technician — see `admin.orders.assign-technician` in Assignments. Track it via `client.orders.show` in Requests / Jobs.

---

#### Clear Cart

**What this does:** Empties the entire cart.

| | |
|---|---|
| **Method** | `DELETE` |
| **Endpoint** | `{{base_url}}/api/v1/client/cart` |
| **Route name** | `client.cart.clear` |
| **Guard** | JWT bearer (`auth:api`); account must be `active`; `geo.context` |
| **Allowed roles** | Individual client (`user_type: individual`) |
| **Status codes** | `200 OK` |

**Headers:**
- `Accept: application/json`
- `Content-Type: application/json`
- `Accept-Language: {{locale}}`

**Payload:**
No request body. Use the documented path and query parameters.

**Actual Success Response(s):**

*{ 200 OK — cart cleared }*
```json
{
    "status": "success",
    "message": "Cart cleared",
    "data": {
        "id": 11,
        "items_count": 0,
        "subtotal": 0,
        "items": []
    }
}
```

*{ 200 OK — clear with no pre-existing cart (lazily created then cleared) }*
```json
{
    "status": "success",
    "message": "Cart cleared",
    "data": {
        "id": 12,
        "items_count": 0,
        "subtotal": 0,
        "items": []
    }
}
```

**What happens next:** Cart is now empty — add items again before checking out.

---

#### Custom Request Cart

**What this does:** Raises a custom-price request directly as an `Order` (bypassing the cart) at `status: waiting_price`, for work not in the fixed-price catalog.

| | |
|---|---|
| **Method** | `POST` |
| **Endpoint** | `{{base_url}}/api/v1/client/cart/custom-request` |
| **Route name** | `client.cart.custom-request` |
| **Guard** | JWT bearer (`auth:api`); account must be `active`; `geo.context` |
| **Allowed roles** | Individual client (`user_type: individual`) |
| **Status codes** | `201 Created`, `401 Unauthorized`, `422 Unprocessable Entity` |

**Headers:**
- `Accept: application/json`
- `Content-Type: application/json`
- `Accept-Language: {{locale}}`

**Payload:**
Send as `application/json`.

**Field documentation:**
### `category_id`
- **Description:** Category
- **Type:** `integer`
- **Required:** optional
- **Example:** `2`
- **Validation rules:** `nullable|exists:categories,id`
- `nullable` — may be null.
- `exists:categories,id` — must reference an existing record (categories, id).
### `description`
- **Description:** Description
- **Type:** `string`
- **Required:** required
- **Example:** `Example content text`
- **Validation rules:** `required|string|max:2000`
- `required` — must be provided.
- `string` — must be text.
- `max:2000` — must not exceed 2000.
### `request_media`
- **Description:** Attachment
- **Type:** `string`
- **Required:** optional
- **Example:** `value`
- **Validation rules:** `nullable|string|custom:MediaExists`
- `nullable` — may be null.
- `string` — must be text.
- `custom:MediaExists` — custom validation rule.
### `address_id`
- **Description:** Address
- **Type:** `integer`
- **Required:** required
- **Example:** `1`
- **Validation rules:** `required|exists:addresses,id`
- `required` — must be provided.
- `exists:addresses,id` — must reference an existing record (addresses, id).

**Example Request Body:**
```json
{
    "description": "Water leak under the kitchen sink",
    "request_media": "2295ffcd31dbf66c64ca94f2add8268b",
    "address_id": 1
}
```

**Actual Success Response(s):**

*{ 201 Created — 201 - custom request with a previously uploaded media hash }*
```json
{
    "status": "success",
    "message": "Custom request submitted",
    "data": {
        "order_number": "ORD-260820-QZJN",
        "customer_id": 27,
        "order_source": "individual",
        "branch_id": null,
        "booking_type": "instant",
        "address_id": 9,
        "status": "waiting_price",
        "subtotal_amount": "0.00",
        "vat_amount": "0.00",
        "fees_amount": "0.00",
        "total_amount": "0.00",
        "paid_amount": "0.00",
        "uuid": "878d4cf3-06b3-4c05-b9dd-efe339ad0a5c",
        "updated_at": "2026-08-20T10:51:00.000000Z",
        "created_at": "2026-08-20T10:51:00.000000Z",
        "id": 9,
        "items": [
            {
                "id": 6,
                "order_id": "878d4cf3-06b3-4c05-b9dd-efe339ad0a5c",
                "category_id": null,
                "service_name": "Custom request",
                "pricing_type": "quote",
                "unit_price": "0.00",
                "quantity": 1,
                "line_total": "0.00",
                "is_quoted": false,
                "notes": null,
                "request_description": "Water leak under the kitchen sink",
                "created_at": "2026-08-20T10:51:00.000000Z",
                "updated_at": "2026-08-20T10:51:00.000000Z"
            }
        ]
    }
}
```

*{ 201 Created — 201 - custom request with no media and no category (generic label) }*
```json
{
    "status": "success",
    "message": "Custom request submitted",
    "data": {
        "order_number": "ORD-260820-L6HL",
        "customer_id": 28,
        "order_source": "individual",
        "branch_id": null,
        "booking_type": "instant",
        "address_id": 10,
        "status": "waiting_price",
        "subtotal_amount": "0.00",
        "vat_amount": "0.00",
        "fees_amount": "0.00",
        "total_amount": "0.00",
        "paid_amount": "0.00",
        "uuid": "d3cfe905-0eb4-4d80-a915-be7c84589794",
        "updated_at": "2026-08-20T10:51:01.000000Z",
        "created_at": "2026-08-20T10:51:01.000000Z",
        "id": 10,
        "items": [
            {
                "id": 7,
                "order_id": "d3cfe905-0eb4-4d80-a915-be7c84589794",
                "category_id": null,
                "service_name": "Custom request",
                "pricing_type": "quote",
                "unit_price": "0.00",
                "quantity": 1,
                "line_total": "0.00",
                "is_quoted": false,
                "notes": null,
                "request_description": "Need a quote for full house painting",
                "created_at": "2026-08-20T10:51:01.000000Z",
                "updated_at": "2026-08-20T10:51:01.000000Z"
            }
        ]
    }
}
```

*{ 201 Created — 201 - custom request scoped to a category (service_name snapshotted from category) }*
```json
{
    "status": "success",
    "message": "Custom request submitted",
    "data": {
        "order_number": "ORD-260820-HJ47",
        "customer_id": 29,
        "order_source": "individual",
        "branch_id": null,
        "booking_type": "instant",
        "address_id": 11,
        "status": "waiting_price",
        "subtotal_amount": "0.00",
        "vat_amount": "0.00",
        "fees_amount": "0.00",
        "total_amount": "0.00",
        "paid_amount": "0.00",
        "uuid": "52e578b2-39b1-4489-995f-9f3e74d34937",
        "updated_at": "2026-08-20T10:51:01.000000Z",
        "created_at": "2026-08-20T10:51:01.000000Z",
        "id": 11,
        "items": [
            {
                "id": 8,
                "order_id": "52e578b2-39b1-4489-995f-9f3e74d34937",
                "category_id": 23,
                "service_name": "إصلاح مكيف",
                "pricing_type": "quote",
                "unit_price": "0.00",
                "quantity": 1,
                "line_total": "0.00",
                "is_quoted": false,
                "notes": null,
                "request_description": "Rewiring the living room, need custom quote",
                "created_at": "2026-08-20T10:51:01.000000Z",
                "updated_at": "2026-08-20T10:51:01.000000Z"
            }
        ]
    }
}
```

**Actual Error Response(s):**

*{ 422 Unprocessable Entity — 422 - description is required }*
```json
{
    "status": "fail",
    "message": "Please enter a description.",
    "data": null
}
```

*{ 422 Unprocessable Entity — 422 - request_media hash not found (MediaExists rule) }*
```json
{
    "status": "fail",
    "message": "The media file was not found.",
    "data": null
}
```

*{ 422 Unprocessable Entity — 422 - address does not exist }*
```json
{
    "status": "fail",
    "message": "The selected address does not exist.",
    "data": null
}
```

*{ 401 Unauthorized — 401 - unauthenticated }*
```json
{
    "status": "fail",
    "message": "Unauthenticated, you have to login first",
    "data": null
}
```

**What happens next:** Awaits `admin.orders.quote` — an admin sets item prices, moving it to `priced` for the customer to accept/reject.

---

#### Add Item

**What this does:** Adds a bookable service (`category_id`) and `quantity` to the cart, or increments quantity if it's already in the cart.

| | |
|---|---|
| **Method** | `POST` |
| **Endpoint** | `{{base_url}}/api/v1/client/cart/items` |
| **Route name** | `client.cart.items.add` |
| **Guard** | JWT bearer (`auth:api`); account must be `active`; `geo.context` |
| **Allowed roles** | Individual client (`user_type: individual`) |
| **Status codes** | `201 Created`, `422 Unprocessable Entity` |

**Headers:**
- `Accept: application/json`
- `Content-Type: application/json`
- `Accept-Language: {{locale}}`

**Payload:**
Send as `application/json`.

**Field documentation:**
### `category_id`
- **Description:** Service
- **Type:** `integer`
- **Required:** required
- **Example:** `2`
- **Validation rules:** `required|exists:categories,id,is_active,"1",price,"NOT_NULL"`
- `required` — must be provided.
- `exists:categories,id,is_active,"1",price,"NOT_NULL"` — must reference an existing record (categories, id, is_active, "1", price, "NOT_NULL").
### `quantity`
- **Description:** Quantity
- **Type:** `integer`
- **Required:** optional
- **Example:** `1`
- **Validation rules:** `sometimes|integer|min:1|max:100`
- `sometimes` — validated only when the field is present.
- `integer` — must be a whole number.
- `min:1` — must be at least 1.
- `max:100` — must not exceed 100.

**Example Request Body:**
```json
{
    "category_id": 2,
    "quantity": 2
}
```

**Actual Success Response(s):**

*{ 201 Created — 201 - item added to empty cart }*
```json
{
    "status": "success",
    "message": "Item added to cart",
    "data": {
        "id": 1,
        "items_count": 2,
        "subtotal": 200,
        "items": [
            {
                "id": 1,
                "quantity": 2,
                "unit_price": "100.00",
                "line_total": 200,
                "service": {
                    "id": 1,
                    "name": "إصلاح مكيف",
                    "image": "https://ui-avatars.com/api/?name=C&background=fef3c7&color=d97706&size=200"
                },
                "created_at": "2026-08-20T10:50:36.000000Z"
            }
        ]
    }
}
```

*{ 201 Created — 201 - merges quantity into existing line (1 + 3 = 4) }*
```json
{
    "status": "success",
    "message": "Item added to cart",
    "data": {
        "id": 2,
        "items_count": 4,
        "subtotal": 400,
        "items": [
            {
                "id": 2,
                "quantity": 4,
                "unit_price": "100.00",
                "line_total": 400,
                "service": {
                    "id": 2,
                    "name": "إصلاح مكيف",
                    "image": "https://ui-avatars.com/api/?name=C&background=fef3c7&color=d97706&size=200"
                },
                "created_at": "2026-08-20T10:50:37.000000Z"
            }
        ]
    }
}
```

**Actual Error Response(s):**

*{ 422 Unprocessable Entity — 422 - category does not exist }*
```json
{
    "status": "fail",
    "message": "The selected category does not exist.",
    "data": null
}
```

*{ 422 Unprocessable Entity — 422 - category is inactive (excluded by Rule::exists where is_active=true) }*
```json
{
    "status": "fail",
    "message": "The selected category does not exist.",
    "data": null
}
```

*{ 422 Unprocessable Entity — 422 - visit service cannot join a cart that already has items }*
```json
{
    "status": "success",
    "message": "Item_cant_be_added_because_visit_service_must_be_single_service_in_order",
    "data": []
}
```

*{ 422 Unprocessable Entity — 422 - different parent category than existing cart line }*
```json
{
    "status": "success",
    "message": "Item_cant_be_added_because_its_another_category_please_empty_cart_first",
    "data": []
}
```

**What happens next:** Continue adding items, or proceed to `checkout` once done.

---

#### Remove Item

**What this does:** Removes one line item from the cart by its cart-item id.

| | |
|---|---|
| **Method** | `DELETE` |
| **Endpoint** | `{{base_url}}/api/v1/client/cart/items/:itemId` |
| **Route name** | `client.cart.items.remove` |
| **Guard** | JWT bearer (`auth:api`); account must be `active`; `geo.context` |
| **Allowed roles** | Individual client (`user_type: individual`) |
| **Status codes** | `200 OK`, `404 Not Found` |

**Headers:**
- `Accept: application/json`
- `Content-Type: application/json`
- `Accept-Language: {{locale}}`

**Path Parameters:**
- `itemId` — ItemId ID — chained from an earlier request in this flow ({{cart_item_id}})

**Payload:**
No request body. Use the documented path and query parameters.

**Actual Success Response(s):**

*{ 200 OK — item removed }*
```json
{
    "status": "success",
    "message": "Item removed from cart",
    "data": {
        "id": 9,
        "items_count": 0,
        "subtotal": 0,
        "items": []
    }
}
```

**Actual Error Response(s):**

*{ 404 Not Found — 404 - item does not exist }*
```json
{
    "status": "fail",
    "message": "Resource not found",
    "data": null
}
```

**What happens next:** Review the updated cart with `GET /client/cart`.

---

## 04. Requests & Orders — Lifecycle

#### List All Orders

**What this does:** Returns a paginated, filterable, searchable list of orders.

| | |
|---|---|
| **Method** | `GET` |
| **Endpoint** | `{{base_url}}/api/v1/client/orders?page=1&per_page=15&search=&sort[created_at]=desc&paginate=false&filters[is_active]=1` |
| **Route name** | `client.orders.index` |
| **Guard** | JWT bearer (`auth:api`); account must be `active`; `geo.context` |
| **Allowed roles** | Individual client (`user_type: individual`) |
| **Status codes** | `200 OK`, `401 Unauthorized` |

**Headers:**
- `Accept: application/json`
- `Content-Type: application/json`
- `Accept-Language: {{locale}}`

**Payload:**
No request body. Use the documented path and query parameters.

**Query parameters:**
- `page` — Page number (supported)
- `per_page` — Items per page (max: 100) (optional)
- `search` — Search term (searches across searchable fields) (optional)
- `sort[created_at]` — Sort by field: asc or desc (optional)
- `paginate` — Set to false to disable pagination (optional)
- `filters[is_active]` — Filter by active status: 1 or 0 (optional)

**Actual Success Response(s):**

*{ 200 OK — OK }*
```json
{
    "status": "success",
    "message": "Orders retrieved successfully.",
    "data": {
        "data": [
            {
                "id": 12,
                "uuid": "57ccff44-7848-4427-acfd-8b34ccb35ad8",
                "order_number": "ORD-260820-EJIN",
                "status": "pending",
                "scheduled_date": "2026-09-02",
                "total_amount": "436.57",
                "items": [
                    {
                        "id": 9,
                        "service_name": "error expedita voluptatem",
                        "pricing_type": "fixed",
                        "unit_price": "248.91",
                        "quantity": 3,
                        "line_total": "746.73",
                        "is_quoted": false,
                        "request_description": null,
                        "request_media_url": "https://ui-avatars.com/api/?name=N&background=f3f4f6&color=9ca3af&size=200"
                    }
                ],
                "created_at": "2026-08-20T10:52:19+00:00"
            },
            {
                "id": 13,
                "uuid": "cd381198-01c3-4fee-b2d7-fc598466594a",
                "order_number": "ORD-260820-FKLW",
                "status": "confirmed",
                "scheduled_date": null,
                "total_amount": "149.41",
                "items": [
                    {
                        "id": 10,
                        "service_name": "est maiores deserunt",
                        "pricing_type": "fixed",
                        "unit_price": "172.68",
                        "quantity": 3,
                        "line_total": "518.04",
                        "is_quoted": false,
                        "request_description": null,
                        "request_media_url": "https://ui-avatars.com/api/?name=N&background=f3f4f6&color=9ca3af&size=200"
                    }
                ],
                "created_at": "2026-08-20T10:52:19+00:00"
            }
        ],
        "links": {
            "first": "http://localhost/api/v1/client/orders?page=1",
            "last": "http://localhost/api/v1/client/orders?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/orders?page=1",
                    "label": "1",
                    "page": 1,
                    "active": true
                },
                {
                    "url": null,
                    "label": "Next &raquo;",
                    "page": null,
                    "active": false
                }
            ],
            "path": "http://localhost/api/v1/client/orders",
            "per_page": 15,
            "to": 2,
            "total": 2
        }
    }
}
```

**Actual Error Response(s):**

*{ 401 Unauthorized — 401 Unauthenticated }*
```json
{
    "status": "fail",
    "message": "Unauthenticated, you have to login first",
    "data": null
}
```

**What happens next:** Use `show` on a specific id for full details.

---

#### Show Order Details

**What this does:** Returns full details for one order.

| | |
|---|---|
| **Method** | `GET` |
| **Endpoint** | `{{base_url}}/api/v1/client/orders/:order` |
| **Route name** | `client.orders.show` |
| **Guard** | JWT bearer (`auth:api`); account must be `active`; `geo.context` |
| **Allowed roles** | Individual client (`user_type: individual`) |
| **Status codes** | `200 OK`, `404 Not Found` |

**Headers:**
- `Accept: application/json`
- `Content-Type: application/json`
- `Accept-Language: {{locale}}`

**Path Parameters:**
- `order` — Order UUID (orders.uuid) — copy the `uuid` field from a List/Show response, not the numeric id. — chained from an earlier request in this flow ({{order_id}})

**Payload:**
No request body. Use the documented path and query parameters.

**Actual Success Response(s):**

*{ 200 OK — OK }*
```json
{
    "status": "success",
    "message": "Order retrieved successfully.",
    "data": {
        "id": 14,
        "uuid": "e3302b99-57fe-45e5-ab4c-4e6f7fc0bab9",
        "order_number": "ORD-260820-PRTK",
        "status": "pending",
        "scheduled_date": null,
        "subtotal_amount": "135.88",
        "vat_amount": "20.38",
        "fees_amount": "0.00",
        "total_amount": "156.26",
        "paid_amount": "156.26",
        "address": {
            "id": 24,
            "title": "Home"
        },
        "technician": null,
        "items": [
            {
                "id": 11,
                "service_name": "molestias ut iure",
                "pricing_type": "fixed",
                "unit_price": "186.78",
                "quantity": 1,
                "line_total": "186.78",
                "is_quoted": false,
                "request_description": null,
                "request_media_url": "https://ui-avatars.com/api/?name=N&background=f3f4f6&color=9ca3af&size=200"
            }
        ],
        "status_history": [
            {
                "from_status": null,
                "to_status": "pending",
                "created_at": "2026-08-20T10:52:23+00:00"
            }
        ],
        "rating": null,
        "created_at": "2026-08-20T10:52:23+00:00"
    }
}
```

**Actual Error Response(s):**

*{ 404 Not Found — Not Found }*
```json
{
    "status": "fail",
    "message": "Resource not found",
    "data": null
}
```

*{ 404 Not Found — Another Customer's Order }*
```json
{
    "status": "fail",
    "message": "Resource not found",
    "data": null
}
```

**What happens next:** Use `update` to modify it, or `destroy` to remove it.

---

#### Accept Price Order

**What this does:** Accepts an admin-quoted price on a custom-price request and pays for it (wallet/gateway, same mechanics as checkout) — only valid while `status: priced`. On success the order moves to `pending`.

| | |
|---|---|
| **Method** | `POST` |
| **Endpoint** | `{{base_url}}/api/v1/client/orders/:id/accept-price` |
| **Route name** | `client.orders.accept-price` |
| **Guard** | JWT bearer (`auth:api`); account must be `active`; `geo.context` |
| **Allowed roles** | Individual client (`user_type: individual`) |
| **Status codes** | `200 OK`, `401 Unauthorized`, `404 Not Found`, `422 Unprocessable Entity` |

**Headers:**
- `Accept: application/json`
- `Content-Type: application/json`
- `Accept-Language: {{locale}}`

**Path Parameters:**
- `id` — Order UUID (orders.uuid) — copy the `uuid` field from a List/Show response, not the numeric id. — chained from an earlier request in this flow ({{order_id}})

**Payload:**
Example payload is included in the request body. No FormRequest validation rules were detected.
### `key`
- **Description:** Key
- **Type:** `string`
- **Required:** optional
- **Example:** `value`
- **Validation rules:** `not specified`
- `not specified` — no validation rule was declared.

**Example Request Body:**
```json
{
    "payment_method": "wallet"
}
```

**Actual Success Response(s):**

*{ 200 OK — wallet }*
```json
{
    "status": "success",
    "message": "Price accepted, your order is now being processed.",
    "data": {
        "id": 16,
        "uuid": "0f2d3ba1-dff2-4b96-af78-0e694d14f390",
        "order_number": "ORD-260820-EECE",
        "status": "pending",
        "scheduled_date": null,
        "subtotal_amount": "650.39",
        "vat_amount": "97.56",
        "fees_amount": "0.00",
        "total_amount": "100.00",
        "paid_amount": "100.00",
        "rating": null,
        "created_at": "2026-08-20T10:52:27+00:00"
    }
}
```

*{ 200 OK — gateway }*
```json
{
    "status": "success",
    "message": "Price accepted, your order is now being processed.",
    "data": {
        "id": 20,
        "uuid": "6be2d81c-4c3c-4843-aefe-f12dd60d9502",
        "order_number": "ORD-260820-BMXR",
        "status": "pending",
        "scheduled_date": "2026-08-24",
        "subtotal_amount": "365.91",
        "vat_amount": "54.89",
        "fees_amount": "0.00",
        "total_amount": "200.00",
        "paid_amount": "200.00",
        "rating": null,
        "created_at": "2026-08-20T10:52:35+00:00"
    }
}
```

**Actual Error Response(s):**

*{ 422 Unprocessable Entity — 422 Wrong Status }*
```json
{
    "status": "fail",
    "message": "This order has no pending price to accept or reject.",
    "data": null
}
```

*{ 422 Unprocessable Entity — 422 Validation Error }*
```json
{
    "status": "fail",
    "message": "The Payment Method field is required.",
    "data": null
}
```

*{ 404 Not Found — Not Found }*
```json
{
    "status": "fail",
    "message": "Resource not found",
    "data": null
}
```

*{ 401 Unauthorized — 401 Unauthenticated }*
```json
{
    "status": "fail",
    "message": "Unauthenticated, you have to login first",
    "data": null
}
```

*{ 422 Unprocessable Entity — 422 Insufficient Wallet Balance - caught downstream in OrderCommandService, NOT by AcceptPriceRequest (see bug report) }*
```json
{
    "status": "fail",
    "message": "Wallet balance (5.00 SAR) is less than the order total (1,000.00 SAR). The wallet cannot be used for partial payment.",
    "data": null
}
```

*{ 422 Unprocessable Entity — 422 Gateway Payment Failed }*
```json
{
    "status": "fail",
    "message": "Payment could not be completed",
    "data": {
        "payment_id": 7
    }
}
```

**What happens next:** Same as a fresh checkout from here: awaits `admin.orders.assign-technician`.

---

#### Cancel Order

**What this does:** Cancels the individual client's own order — only allowed before the technician has started work (`pending`/`waiting_price`/`priced`/`accepted`/`on_the_way`; not once `start`/`finish`). If `paid_amount > 0` and the order is an individual (non-job-order) order, the paid amount is refunded to the customer's wallet automatically.

| | |
|---|---|
| **Method** | `POST` |
| **Endpoint** | `{{base_url}}/api/v1/client/orders/:id/cancel` |
| **Route name** | `client.orders.cancel` |
| **Guard** | JWT bearer (`auth:api`); account must be `active`; `geo.context` |
| **Allowed roles** | Individual client (`user_type: individual`) |
| **Status codes** | `200 OK`, `401 Unauthorized`, `404 Not Found`, `422 Unprocessable Entity` |

**Headers:**
- `Accept: application/json`
- `Content-Type: application/json`
- `Accept-Language: {{locale}}`

**Path Parameters:**
- `id` — Order UUID (orders.uuid) — copy the `uuid` field from a List/Show response, not the numeric id. — chained from an earlier request in this flow ({{order_id}})

**Payload:**
Send as `application/json`.

**Field documentation:**
### `reason`
- **Description:** Cancellation Reason
- **Type:** `string`
- **Required:** required
- **Example:** `Manual adjustment by admin.`
- **Validation rules:** `required|string|max:500`
- `required` — must be provided.
- `string` — must be text.
- `max:500` — must not exceed 500.

**Example Request Body:**
```json
{
    "reason": "Changed my mind about the service."
}
```

**Actual Success Response(s):**

*{ 200 OK — OK }*
```json
{
    "status": "success",
    "message": "Order cancelled successfully.",
    "data": {
        "id": 28,
        "uuid": "58b5c4e0-7419-4ad6-a466-f3699a1e22b8",
        "order_number": "ORD-260820-QDQW",
        "status": "cancelled",
        "scheduled_date": null,
        "subtotal_amount": "370.60",
        "vat_amount": "55.59",
        "fees_amount": "0.00",
        "total_amount": "426.19",
        "paid_amount": "0.00",
        "rating": null,
        "created_at": "2026-08-20T10:52:46+00:00"
    }
}
```

*{ 200 OK — with refund to wallet }*
```json
{
    "status": "success",
    "message": "Order cancelled successfully.",
    "data": {
        "id": 29,
        "uuid": "4946c762-5927-4aa0-b018-9fffbdeb2668",
        "order_number": "ORD-260820-ZWYI",
        "status": "cancelled",
        "scheduled_date": null,
        "subtotal_amount": "167.74",
        "vat_amount": "25.16",
        "fees_amount": "0.00",
        "total_amount": "120.00",
        "paid_amount": "120.00",
        "rating": null,
        "created_at": "2026-08-20T10:52:49+00:00"
    }
}
```

**Actual Error Response(s):**

*{ 422 Unprocessable Entity — 422 Wrong Status }*
```json
{
    "status": "fail",
    "message": "This order can no longer be cancelled.",
    "data": null
}
```

*{ 422 Unprocessable Entity — 422 Validation Error }*
```json
{
    "status": "fail",
    "message": "The Cancellation Reason field is required.",
    "data": null
}
```

*{ 404 Not Found — Not Found }*
```json
{
    "status": "fail",
    "message": "Resource not found",
    "data": null
}
```

*{ 401 Unauthorized — 401 Unauthenticated }*
```json
{
    "status": "fail",
    "message": "Unauthenticated, you have to login first",
    "data": null
}
```

**What happens next:** Nothing further — the order is terminal. Company job orders never auto-refund to a wallet (billed to the company/contract instead); use `admin.wallets.block-refund` if manual reconciliation is needed.

---

#### Confirm Completion Order

**What this does:** Confirms a `finish`-status order as complete — moves it to `confirmed` (terminal) and, if `stars` is given, records a rating against the technician and recalculates their aggregate stats.

| | |
|---|---|
| **Method** | `POST` |
| **Endpoint** | `{{base_url}}/api/v1/client/orders/:id/confirm-completion` |
| **Route name** | `client.orders.confirm-completion` |
| **Guard** | JWT bearer (`auth:api`); account must be `active`; `geo.context` |
| **Allowed roles** | Individual client (`user_type: individual`) |
| **Status codes** | `200 OK`, `401 Unauthorized`, `404 Not Found`, `422 Unprocessable Entity`, `500 Internal Server Error` |

**Headers:**
- `Accept: application/json`
- `Content-Type: application/json`
- `Accept-Language: {{locale}}`

**Path Parameters:**
- `id` — Order UUID (orders.uuid) — copy the `uuid` field from a List/Show response, not the numeric id. — chained from an earlier request in this flow ({{order_id}})

**Payload:**
Send as `application/json`.

**Field documentation:**
### `stars`
- **Description:** Rating
- **Type:** `integer`
- **Required:** optional
- **Example:** `1`
- **Validation rules:** `nullable|integer|min:1|max:5`
- `nullable` — may be null.
- `integer` — must be a whole number.
- `min:1` — must be at least 1.
- `max:5` — must not exceed 5.
### `comment`
- **Description:** Comment
- **Type:** `string`
- **Required:** optional
- **Example:** `value`
- **Validation rules:** `nullable|string|max:500`
- `nullable` — may be null.
- `string` — must be text.
- `max:500` — must not exceed 500.

**Example Request Body:**
```json
{
    "stars": 1,
    "comment": "value"
}
```

**Actual Success Response(s):**

*{ 200 OK — no rating }*
```json
{
    "status": "success",
    "message": "Thanks for confirming — your order is complete.",
    "data": {
        "id": 25,
        "uuid": "3a6b0b05-03b4-4804-83e8-277674ba761e",
        "order_number": "ORD-260820-ZMUW",
        "status": "confirmed",
        "scheduled_date": "2026-08-24",
        "subtotal_amount": "722.29",
        "vat_amount": "108.34",
        "fees_amount": "0.00",
        "total_amount": "830.63",
        "paid_amount": "830.63",
        "rating": null,
        "created_at": "2026-08-20T10:52:46+00:00"
    }
}
```

> **Fixed:** an earlier pass of this doc captured a real `500` here — `ratings.technician_id` was constrained to `users.id`, but every writer (`OrderCommandService::confirmCompletion()`, `Order::technician_id`, `Technician::ratings()`, `Rating::technician()`) treats it as `technicians.id`, the same convention `orders.technician_id` uses. Rating a completed order failed its FK constraint every time `stars` was sent. Fixed by `database/migrations/2026_08_20_220000_fix_ratings_technician_id_foreign_key.php`, which re-points the constraint at `technicians.id`.

**Actual Error Response(s):**

*{ 422 Unprocessable Entity — 422 Wrong Status }*
```json
{
    "status": "fail",
    "message": "This order is not awaiting a completion confirmation.",
    "data": null
}
```

*{ 422 Unprocessable Entity — 422 Validation Error }*
```json
{
    "status": "fail",
    "message": "The Rating must not be greater than 5.",
    "data": null
}
```

*{ 404 Not Found — Not Found }*
```json
{
    "status": "fail",
    "message": "Resource not found",
    "data": null
}
```

*{ 401 Unauthorized — 401 Unauthenticated }*
```json
{
    "status": "fail",
    "message": "Unauthenticated, you have to login first",
    "data": null
}
```

**What happens next:** Order lifecycle is complete. The technician's `avg_rating`/`completed_orders_count` reflect the new rating immediately.

---

#### Reject Price Order

**What this does:** Rejects an admin-quoted price — cancels the order. Nothing was ever charged, so no refund is triggered.

| | |
|---|---|
| **Method** | `POST` |
| **Endpoint** | `{{base_url}}/api/v1/client/orders/:id/reject-price` |
| **Route name** | `client.orders.reject-price` |
| **Guard** | JWT bearer (`auth:api`); account must be `active`; `geo.context` |
| **Allowed roles** | Individual client (`user_type: individual`) |
| **Status codes** | `200 OK`, `401 Unauthorized`, `404 Not Found`, `422 Unprocessable Entity` |

**Headers:**
- `Accept: application/json`
- `Content-Type: application/json`
- `Accept-Language: {{locale}}`

**Path Parameters:**
- `id` — Order UUID (orders.uuid) — copy the `uuid` field from a List/Show response, not the numeric id. — chained from an earlier request in this flow ({{order_id}})

**Payload:**
Example payload is included in the request body. No FormRequest validation rules were detected.
### `key`
- **Description:** Key
- **Type:** `string`
- **Required:** optional
- **Example:** `value`
- **Validation rules:** `not specified`
- `not specified` — no validation rule was declared.

**Example Request Body:**
```json
{
    "key": "value"
}
```

**Actual Success Response(s):**

*{ 200 OK — OK }*
```json
{
    "status": "success",
    "message": "Price rejected, your order has been cancelled.",
    "data": {
        "id": 22,
        "uuid": "17c26507-0a3b-480a-9f57-56342dd14e59",
        "order_number": "ORD-260820-YNQG",
        "status": "cancelled",
        "scheduled_date": null,
        "subtotal_amount": "639.15",
        "vat_amount": "95.87",
        "fees_amount": "0.00",
        "total_amount": "80.00",
        "paid_amount": "735.02",
        "rating": null,
        "created_at": "2026-08-20T10:52:39+00:00"
    }
}
```

**Actual Error Response(s):**

*{ 422 Unprocessable Entity — 422 Wrong Status }*
```json
{
    "status": "fail",
    "message": "This order has no pending price to accept or reject.",
    "data": null
}
```

*{ 404 Not Found — Not Found }*
```json
{
    "status": "fail",
    "message": "Resource not found",
    "data": null
}
```

*{ 401 Unauthorized — 401 Unauthenticated }*
```json
{
    "status": "fail",
    "message": "Unauthenticated, you have to login first",
    "data": null
}
```

**What happens next:** The custom request is over; submit a new one if still needed.

---

## 05. Wallet

#### Balance Wallet

**What this does:** Returns the client's current wallet balance.

| | |
|---|---|
| **Method** | `GET` |
| **Endpoint** | `{{base_url}}/api/v1/client/wallet` |
| **Route name** | `client.wallet.balance` |
| **Guard** | JWT bearer (`auth:api`); account must be `active`; `geo.context` |
| **Allowed roles** | Individual client (`user_type: individual`) |
| **Status codes** | `200 OK`, `401 Unauthorized` |

**Headers:**
- `Accept: application/json`
- `Content-Type: application/json`
- `Accept-Language: {{locale}}`

**Payload:**
No request body. Use the documented path and query parameters.

**Actual Success Response(s):**

*{ 200 OK — individual wallet balance }*
```json
{
    "status": "success",
    "message": "Balance retrieved successfully",
    "data": {
        "balance": 150.5,
        "currency": "SAR",
        "is_locked": false,
        "can_withdraw": false
    }
}
```

*{ 200 OK — no wallet row yet (defaults returned) }*
```json
{
    "status": "success",
    "message": "Balance retrieved successfully",
    "data": {
        "balance": 0,
        "currency": "SAR",
        "is_locked": false
    }
}
```

**Actual Error Response(s):**

*{ 401 Unauthorized — 401 Unauthenticated - no token }*
```json
{
    "status": "fail",
    "message": "Unauthenticated, you have to login first",
    "data": null
}
```

**What happens next:** See `GET /client/wallet/transactions` for the ledger, or the Requests / Jobs section to spend the balance at checkout.

---

#### Transactions Wallet

**What this does:** Returns the client's wallet transaction history (credits/debits), optionally filtered by type/date.

| | |
|---|---|
| **Method** | `GET` |
| **Endpoint** | `{{base_url}}/api/v1/client/wallet/transactions?page=1&per_page=15&search=&sort[created_at]=desc&paginate=false&type=credit&from=2024-01-01&to=2024-12-31` |
| **Route name** | `client.wallet.transactions` |
| **Guard** | JWT bearer (`auth:api`); account must be `active`; `geo.context` |
| **Allowed roles** | Individual client (`user_type: individual`) |
| **Status codes** | `200 OK` |

**Headers:**
- `Accept: application/json`
- `Content-Type: application/json`
- `Accept-Language: {{locale}}`

**Payload:**
No request body. Use the documented path and query parameters.

**Query parameters:**
- `page` — Page number (supported)
- `per_page` — Items per page (max: 100) (optional)
- `search` — Search term (searches across searchable fields) (optional)
- `sort[created_at]` — Sort by field: asc or desc (optional)
- `paginate` — Set to false to disable pagination (optional)
- `type` — Filter by type: credit, debit (optional)
- `from` — Filter from date (optional)
- `to` — Filter to date (optional)

**Actual Success Response(s):**

*{ 200 OK — paginated transaction history }*
```json
{
    "status": "success",
    "message": "Transactions retrieved successfully",
    "data": {
        "data": [
            {
                "id": 2,
                "type": "refund_credit",
                "type_label": "Refund from cancelled order",
                "amount": "100.00",
                "balance_after": "100.00",
                "status": "completed",
                "notes": null,
                "related_order": null,
                "created_at": "2026-08-20T10:51:25+00:00"
            },
            {
                "id": 3,
                "type": "refund_credit",
                "type_label": "Refund from cancelled order",
                "amount": "100.00",
                "balance_after": "200.00",
                "status": "completed",
                "notes": null,
                "related_order": null,
                "created_at": "2026-08-20T10:51:25+00:00"
            }
        ],
        "links": {
            "first": "http://localhost/api/v1/client/wallet/transactions?page=1",
            "last": "http://localhost/api/v1/client/wallet/transactions?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/wallet/transactions?page=1",
                    "label": "1",
                    "page": 1,
                    "active": true
                },
                {
                    "url": null,
                    "label": "Next &raquo;",
                    "page": null,
                    "active": false
                }
            ],
            "path": "http://localhost/api/v1/client/wallet/transactions",
            "per_page": 20,
            "to": 2,
            "total": 2
        }
    }
}
```

*{ 200 OK — no wallet row (empty list) }*
```json
{
    "status": "success",
    "message": "Transactions retrieved successfully",
    "data": []
}
```

**What happens next:** Cross-reference `related_order` on each entry with `GET /client/orders/{id}`.

---

#### Withdraw Wallet

**What this does:** **Individuals cannot withdraw** — `WalletValidationService::validateWithdrawal()` rejects any non-freelance-technician wallet with a 422; this endpoint exists on the shared `WalletController` but is only functional for freelance technicians.

| | |
|---|---|
| **Method** | `POST` |
| **Endpoint** | `{{base_url}}/api/v1/client/wallet/withdraw` |
| **Route name** | `client.wallet.withdraw` |
| **Guard** | JWT bearer (`auth:api`); account must be `active`; `geo.context` |
| **Allowed roles** | Individual client (`user_type: individual`) |
| **Status codes** | `201 Created`, `422 Unprocessable Entity` |

**Headers:**
- `Accept: application/json`
- `Content-Type: application/json`
- `Accept-Language: {{locale}}`

**Payload:**
Send as `application/json`.

**Field documentation:**
### `amount`
- **Description:** Amount
- **Type:** `number`
- **Required:** required
- **Example:** `10`
- **Validation rules:** `required|numeric|min:1`
- `required` — must be provided.
- `numeric` — must be numeric.
- `min:1` — must be at least 1.
### `bank_name`
- **Description:** Bank Name
- **Type:** `string`
- **Required:** required
- **Example:** `Example Name`
- **Validation rules:** `required|string|max:100`
- `required` — must be provided.
- `string` — must be text.
- `max:100` — must not exceed 100.
### `account_number`
- **Description:** Account Number
- **Type:** `string`
- **Required:** required
- **Example:** `1234567890123`
- **Validation rules:** `required|string|max:100`
- `required` — must be provided.
- `string` — must be text.
- `max:100` — must not exceed 100.
### `iban`
- **Description:** IBAN
- **Type:** `string`
- **Required:** required
- **Example:** `EG123456789012345678901234`
- **Validation rules:** `required|string|max:50`
- `required` — must be provided.
- `string` — must be text.
- `max:50` — must not exceed 50.

**Example Request Body:**
```json
{
    "amount": 200,
    "bank_name": "Al Rajhi Bank",
    "account_number": "1234567890",
    "iban": "SA0380000000608010167519"
}
```

**Actual Success Response(s):**

*{ 201 Created — freelance technician withdraws successfully }*
```json
{
    "status": "success",
    "message": "Withdrawal request submitted successfully",
    "data": {
        "user_id": 50,
        "amount": 200,
        "bank_name": "Al Rajhi Bank",
        "account_number": "1234567890",
        "iban": "SA0380000000608010167519",
        "updated_at": "2026-08-20T10:51:35.000000Z",
        "created_at": "2026-08-20T10:51:35.000000Z",
        "id": 1
    }
}
```

**Actual Error Response(s):**

*{ 422 Unprocessable Entity — 422 Validation Error - individual accounts cannot withdraw (refund-only) }*
```json
{
    "status": "fail",
    "message": "Withdrawal is not available for individual accounts. Wallet is refund-only.",
    "data": null
}
```

*{ 422 Unprocessable Entity — 422 Validation Error - insufficient wallet balance }*
```json
{
    "status": "fail",
    "message": "Insufficient wallet balance",
    "data": null
}
```

*{ 422 Unprocessable Entity — 422 Validation Error - missing bank details }*
```json
{
    "status": "fail",
    "message": "Please enter the bank name.",
    "data": null
}
```

**What happens next:** See `technician.wallet.withdraw` in the Technician section for the actual working flow.

---

## 06. Notifications (Client-Specific)

#### List All Notifications

**What this does:** Returns a paginated, filterable, searchable list of notifications.

| | |
|---|---|
| **Method** | `GET` |
| **Endpoint** | `{{base_url}}/api/v1/client/notifications?page=1&per_page=15&search=&sort[created_at]=desc&paginate=false&filters[is_active]=1` |
| **Route name** | `client.notifications.index` |
| **Guard** | JWT bearer (`auth:api`); account must be `active`; `geo.context` |
| **Allowed roles** | Individual client (`user_type: individual`) |
| **Status codes** | `200 OK`, `401 Unauthorized` |

**Headers:**
- `Accept: application/json`
- `Content-Type: application/json`
- `Accept-Language: {{locale}}`

**Payload:**
No request body. Use the documented path and query parameters.

**Query parameters:**
- `page` — Page number (supported)
- `per_page` — Items per page (max: 100) (optional)
- `search` — Search term (searches across searchable fields) (optional)
- `sort[created_at]` — Sort by field: asc or desc (optional)
- `paginate` — Set to false to disable pagination (optional)
- `filters[is_active]` — Filter by active status: 1 or 0 (optional)

**Actual Success Response(s):**

*{ 200 OK — authenticated with notifications }*
```json
{
    "status": "success",
    "message": "Notifications retrieved",
    "data": {
        "current_page": 1,
        "data": [
            {
                "id": "a1a58d57-1743-468f-b399-7525426cf6ce",
                "admin_notification_id": null,
                "notifiable_type": "App\\Models\\User",
                "notifiable_id": 81,
                "type": "App\\Notifications\\OrderStatusChanged",
                "data": "{\"title\":\"Order Update\",\"body\":\"Your order status changed.\"}",
                "read_at": null,
                "created_at": "2026-08-20T10:52:02.000000Z",
                "updated_at": "2026-08-20T10:52:02.000000Z"
            }
        ],
        "first_page_url": "http://localhost/api/v1/client/notifications?page=1",
        "from": 1,
        "last_page": 1,
        "last_page_url": "http://localhost/api/v1/client/notifications?page=1",
        "links": [
            {
                "url": null,
                "label": "&laquo; Previous",
                "page": null,
                "active": false
            },
            {
                "url": "http://localhost/api/v1/client/notifications?page=1",
                "label": "1",
                "page": 1,
                "active": true
            },
            {
                "url": null,
                "label": "Next &raquo;",
                "page": null,
                "active": false
            }
        ],
        "next_page_url": null,
        "path": "http://localhost/api/v1/client/notifications",
        "per_page": 20,
        "prev_page_url": null,
        "to": 1,
        "total": 1
    }
}
```

**Actual Error Response(s):**

*{ 401 Unauthorized — 401 Unauthenticated - no token }*
```json
{
    "status": "fail",
    "message": "Unauthenticated, you have to login first",
    "data": null
}
```

**What happens next:** Use `show` on a specific id for full details.

---

#### Device Notification

**What this does:** Registers/updates the client's device for push notifications (device id, FCM/APNs token, platform).

| | |
|---|---|
| **Method** | `POST` |
| **Endpoint** | `{{base_url}}/api/v1/client/notifications/device` |
| **Route name** | `client.notifications.device` |
| **Guard** | JWT bearer (`auth:api`); account must be `active`; `geo.context` |
| **Allowed roles** | Individual client (`user_type: individual`) |
| **Status codes** | `200 OK`, `422 Unprocessable Entity` |

**Headers:**
- `Accept: application/json`
- `Content-Type: application/json`
- `Accept-Language: {{locale}}`

**Payload:**
Send as `application/json`.

**Field documentation:**
### `device_id`
- **Description:** Device ID
- **Type:** `integer`
- **Required:** required
- **Example:** `device-uuid-123`
- **Validation rules:** `required|string|max:255`
- `required` — must be provided.
- `string` — must be text.
- `max:255` — must not exceed 255.
### `device_token`
- **Description:** Device Token
- **Type:** `string`
- **Required:** required
- **Example:** `fcm-token-here`
- **Validation rules:** `required|string|max:500`
- `required` — must be provided.
- `string` — must be text.
- `max:500` — must not exceed 500.
### `device_type`
- **Description:** Device Type
- **Type:** `string`
- **Required:** required
- **Example:** `ios`
- **Validation rules:** `required|in:ios,android,web,huawei`
- `required` — must be provided.
- `in:ios,android,web,huawei` — must be one of: ios, android, web, huawei.

**Example Request Body:**
```json
{
    "device_id": "device-abc-123",
    "device_token": "fcm-token-xyz",
    "device_type": "android"
}
```

**Actual Success Response(s):**

*{ 200 OK — valid device registration }*
```json
{
    "status": "success",
    "message": "Device registered",
    "data": null
}
```

**Actual Error Response(s):**

*{ 422 Unprocessable Entity — 422 Validation Error - invalid device_type }*
```json
{
    "status": "fail",
    "message": "Invalid device type. Accepted values: ios, android, web.",
    "data": null
}
```

**What happens next:** Push notifications for order/wallet events will now be delivered to this device.

---

#### Preferences Notification

**What this does:** Updates the client's notification channel preferences (push/email/SMS toggles).

| | |
|---|---|
| **Method** | `PUT` |
| **Endpoint** | `{{base_url}}/api/v1/client/notifications/preferences` |
| **Route name** | `client.notifications.preferences` |
| **Guard** | JWT bearer (`auth:api`); account must be `active`; `geo.context` |
| **Allowed roles** | Individual client (`user_type: individual`) |
| **Status codes** | `200 OK`, `422 Unprocessable Entity` |

**Headers:**
- `Accept: application/json`
- `Content-Type: application/json`
- `Accept-Language: {{locale}}`

**Payload:**
Send as `application/json`.

**Field documentation:**
### `allow_notifications`
- **Description:** Allow Notifications
- **Type:** `boolean`
- **Required:** required
- **Example:** `true`
- **Validation rules:** `required|boolean`
- `required` — must be provided.
- `boolean` — must be true, false, 1, or 0.

**Example Request Body:**
```json
{
    "allow_notifications": false
}
```

**Actual Success Response(s):**

*{ 200 OK — valid preference }*
```json
{
    "status": "success",
    "message": "Preferences updated",
    "data": null
}
```

**Actual Error Response(s):**

*{ 422 Unprocessable Entity — 422 Validation Error - missing allow_notifications }*
```json
{
    "status": "fail",
    "message": "Please specify your notification preference.",
    "data": null
}
```

**What happens next:** Future notification scenarios respect these toggles.

---

#### Read Notification

**What this does:** Marks one or more notifications as read by `ids`.

| | |
|---|---|
| **Method** | `POST` |
| **Endpoint** | `{{base_url}}/api/v1/client/notifications/read` |
| **Route name** | `client.notifications.read` |
| **Guard** | JWT bearer (`auth:api`); account must be `active`; `geo.context` |
| **Allowed roles** | Individual client (`user_type: individual`) |
| **Status codes** | `200 OK`, `422 Unprocessable Entity` |

**Headers:**
- `Accept: application/json`
- `Content-Type: application/json`
- `Accept-Language: {{locale}}`

**Payload:**
Send as `application/json`.

**Field documentation:**
### `ids`
- **Description:** Ids
- **Type:** `array`
- **Required:** optional
- **Example:** `["00000000-0000-0000-0000-000000000000"]`
- **Validation rules:** `sometimes|array`
- `sometimes` — validated only when the field is present.
- `array` — must be an array.
### `ids.*`
- **Description:** Ids item
- **Type:** `string`
- **Required:** optional
- **Example:** `00000000-0000-0000-0000-000000000000`
- **Validation rules:** `uuid`
- `uuid` — custom validation rule.
### `all`
- **Description:** All
- **Type:** `boolean`
- **Required:** optional
- **Example:** `true`
- **Validation rules:** `sometimes|boolean`
- `sometimes` — validated only when the field is present.
- `boolean` — must be true, false, 1, or 0.

**Example Request Body:**
```json
{
    "ids": [
        "8b5066e9-46c6-4672-8b58-34605c5df413"
    ]
}
```

**Actual Success Response(s):**

*{ 200 OK — mark specific ids read }*
```json
{
    "status": "success",
    "message": "Notifications marked as read",
    "data": null
}
```

*{ 200 OK — mark all read }*
```json
{
    "status": "success",
    "message": "Notifications marked as read",
    "data": null
}
```

**Actual Error Response(s):**

*{ 422 Unprocessable Entity — 422 Validation Error - ids.* must be uuid }*
```json
{
    "status": "fail",
    "message": "The ids.0 must be a valid UUID.",
    "data": null
}
```

**What happens next:** They no longer count toward any unread badge the client app shows.

---

## 07. Home

#### Home

**What this does:** Returns the client's home-screen content — active promotional sliders (ordered by `sort_order`) and up to 6 active top-level service categories (ordered by `sort_order`, each with its `translations` and `children` loaded). Not a paginated resource — no `page`/`search`/`sort`/`filters` query parameters are read by this endpoint even though they may appear on other `index` routes.

| | |
|---|---|
| **Method** | `GET` |
| **Endpoint** | `{{base_url}}/api/v1/client/home` |
| **Route name** | `client.home.index` |
| **Guard** | JWT bearer (`auth:api`); account must be `active`; `geo.context` |
| **Allowed roles** | Individual client (`user_type: individual`) |
| **Status codes** | `200 OK`, `401 Unauthorized` |

**Headers:**
- `Accept: application/json`
- `Content-Type: application/json`
- `Accept-Language: {{locale}}`

**Payload:**
No request body, no query parameters.

**Actual Success Response(s):**

*{ 200 OK — OK }*
```json
{
    "status": "success",
    "message": "Home data retrieved successfully",
    "data": {
        "sliders": [],
        "categories": []
    }
}
```
Empty arrays above reflect the seeded database having no `is_active: true` sliders/root categories at capture time — the shape is `HomeSliderResource[]` / `HomeCategoryResource[]` (see `06-shared-general.md` §06 for the category resource shape) when rows exist.

**Actual Error Response(s):**

*{ 401 Unauthorized — 401 Unauthenticated }*
```json
{
    "status": "fail",
    "message": "Unauthenticated, you have to login first",
    "data": null
}
```

**What happens next:** Browse a category's services via `GET /client/categories/:id/services` (`06-shared-general.md` §06 Categories & Services), or drill into content via `GET /client/sliders` / `GET /client/pages/:type` (`06-shared-general.md` §10 Content & Reference).

---
