# Transporter APIs — Fleet Owner (Transporter)

This document describes APIs for a **fleet owner**: role `TRANSPORTER` and `transporter_profile.account_type=FLEET_OWNER`. Fleet owners manage vehicles, win loads via bidding, own trips as `Trip.transporter`, and assign **linked** drivers to trips.

Shared patterns (envelope, auth, chats, billing shapes) match [`TRANSPORTER_INDIVIDUAL_DRIVER_API.md`](TRANSPORTER_INDIVIDUAL_DRIVER_API.md) unless noted below.

---

## Standard response envelope

```json
{
  "status_code": 200,
  "message": "Human-readable message.",
  "error": null,
  "data": {}
}
```

---

## Authentication

`Authorization: Bearer <token>`

### POST `/api/auth/login/`

**Auth:** none  

**Request (JSON):**

```json
{
  "email": "fleet@example.com",
  "password": "your-password"
}
```

**Response `200` — `data`:**

```json
{
  "token": "9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b",
  "user_id": 50,
  "email": "fleet@example.com",
  "first_name": "Fleet",
  "last_name": "Owner",
  "role": "TRANSPORTER",
  "persona_type": "TRANSPORTER_FLEET_OWNER",
  "account_type": "FLEET_OWNER",
  "tc_id": null,
  "tc_u_id": null
}
```

*(Registration does not auto-create Traccar `tc_id` / `tc_u_id` for `FLEET_OWNER`; those fields may remain empty unless set elsewhere.)*

### POST `/api/auth/logout/`

**Auth:** authenticated — empty body — **`200`**

### POST `/api/auth/forgot-password/`

Request a 6-digit password reset verification code sent to the fleet owner's registered email.

**Aliases:** `POST /api/auth/password/forgot/`, `POST /api/transporter/forgot-password/`  
**Auth:** none (public)

**Request (JSON):**

```json
{
  "email": "fleet@example.com"
}
```

**Response `200` — `data`:**

```json
{
  "email": "fleet@example.com"
}
```

### POST `/api/auth/verify-otp/`

Verify the 6-digit OTP code before setting a new password.

**Aliases:** `POST /api/auth/password/verify-otp/`  
**Auth:** none (public)

**Request (JSON):**

```json
{
  "email": "fleet@example.com",
  "otp": "492018"
}
```

**Response `200` — `data`:**

```json
{
  "email": "fleet@example.com",
  "reset_token": "random_secure_token_string",
  "verified": true
}
```

### POST `/api/auth/reset-password/`

Set a new password using the verified `otp` or `reset_token`.

**Aliases:** `POST /api/auth/password/reset/`, `POST /api/transporter/reset-password/`  
**Auth:** none (public)

**Request (JSON):**

```json
{
  "email": "fleet@example.com",
  "otp": "492018",
  "new_password": "NewSecurePassword123"
}
```

**Response `200` — `data`:**

```json
{
  "token": "new-auth-token",
  "user_id": 10,
  "email": "fleet@example.com",
  "role": "TRANSPORTER",
  "persona_type": "TRANSPORTER_FLEET_OWNER",
  "account_type": "FLEET_OWNER"
}
```

### PATCH `/api/auth/fcm-token/`

**Request (JSON):**

```json
{ "fcm_token": "<fcm-token>" }
```

**Response `200` — `data`:**

```json
{ "fcm_token_updated": true }
```

---

## Registration & profile

### POST `/api/transporter/register/`

**Auth:** none  

**Request (JSON) — fleet owner:**

```json
{
  "email": "newfleet@example.com",
  "password": "minimum8chars",
  "first_name": "Acme",
  "last_name": "Logistics",
  "phone": "+971501234567",
  "account_type": "FLEET_OWNER",
  "company_name": "Acme Transport LLC",
  "office_number": "+97141234567",
  "mobile_number": "+971501234567",
  "company_location": "Business Bay, Dubai",
  "local": false,
  "country_to_country": true,
  "language": "en"
}
```

Required: `local` and `country_to_country` (exactly one `true`). **`company_name` is required** for `FLEET_OWNER`.  
Optional company contact fields: `office_number`, `mobile_number`, `company_location` (also updatable later via profile).

**Response `201` — `data`:**

```json
{
  "token": "9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b",
  "user_id": 50,
  "email": "newfleet@example.com"
}
```

### GET `/api/transporter/gcc-countries/`

**Auth:** none  

Dropdown options for GCC ID country selection:

```json
[
  {"code": "AE", "name": "United Arab Emirates"},
  {"code": "SA", "name": "Saudi Arabia"},
  {"code": "OM", "name": "Oman"},
  {"code": "QA", "name": "Qatar"},
  {"code": "BH", "name": "Bahrain"},
  {"code": "KW", "name": "Kuwait"}
]
```

### GET `/api/transporter/profile/`

**Auth:** transporter  

**Response `200` — `data`:**

```json
{
  "user_id": 50,
  "email": "fleet@example.com",
  "first_name": "Acme",
  "last_name": "Logistics",
  "phone": "+971501234567",
  "language": "en",
  "account_type": "FLEET_OWNER",
  "company_name": "Acme Transport LLC",
  "office_number": "+97141234567",
  "mobile_number": "+971501234567",
  "company_location": "Business Bay, Dubai",
  "documents_verified": true,
  "tc_id": "",
  "tc_u_id": null,
  "created_at": "2026-04-01T10:00:00Z",
  "updated_at": "2026-04-20T08:00:00Z"
}
```

### PATCH `/api/transporter/profile/`

**Auth:** transporter  

**Request (JSON, partial):**

```json
{
  "first_name": "Acme",
  "last_name": "Updated",
  "phone": "+971509998877",
  "company_name": "Acme Transport LLC",
  "office_number": "+97141234567",
  "mobile_number": "+971501234567",
  "company_location": "Business Bay, Dubai"
}
```

**Response `200`:** same shape as GET `data`.

---

## Documents (KYC)

Fleet owners upload **only three** required documents. Verification (`documents_verified`) requires all three approved.

### GET `/api/transporter/documents/`

**Response `200` — `data`:** array of documents (includes `gcc_country` / `gcc_country_name` when set).

### POST `/api/transporter/documents/`

**Request (JSON) — Company License or Owner Passport:**

```json
{
  "document_type": "COMPANY_LICENSE",
  "file": "data:application/pdf;base64,JVBERi0xLjQK...",
  "expiry_date": "2027-12-31"
}
```

**Request (JSON) — GCC ID (two-sided + country required):**

```json
{
  "document_type": "COUNTRY_GCC",
  "gcc_country": "AE",
  "file": "data:image/jpeg;base64,/9j/4AAQ...",
  "file_back": "data:image/jpeg;base64,/9j/4AAQ...",
  "expiry_date": "2027-12-31"
}
```

**Document types (fleet owner — only these):**
- `COMPANY_LICENSE` — Company License (single file)
- `COUNTRY_GCC` — GCC ID (two-sided: `file` + `file_back`, plus `gcc_country` from Gulf countries)
- `PASSPORT_COPY` — Company Owner’s Passport Copy (single file)

Legacy company types (`COMPANY_REGISTRATION`, `COMPANY_OWNERSHIP`, `COMPANY_ADDRESS_PROOF`, `OTHER_COMPLIANCE`, `TRADE_LICENSE`) and individual-driver types are **not** accepted for `FLEET_OWNER`.

**Response `201`:** single document object.

---

## Vehicles

Fleet owners use the same vehicle endpoints; vehicles are scoped to **`owner = request.user`**. A vehicle may list multiple **`vehicle_types`**; load matching uses **any** type against shipment `vehicle_type_required`.

### GET `/api/transporter/vehicles/`

**Response `200` — `data`:** array, e.g.:

```json
[
  {
    "id": 10,
    "owner_id": 50,
    "vehicle_type": "Flat Bed 12m",
    "vehicle_types": ["Flat Bed 12m", "Container 40 Feet / 20 Feet"],
    "registration_number": "DXB-100",
    "load_capacity": "8000.00",
    "max_length_m": "13.000",
    "max_width_m": "2.500",
    "max_height_m": "3.000",
    "special_features": "Tail lift",
    "is_active": true,
    "is_verified": true,
    "assigned_driver_id": 88,
    "assigned_driver_email": "driver@example.com",
    "verified_at": "2026-04-10T09:00:00Z",
    "created_at": "2026-04-01T10:00:00Z",
    "updated_at": "2026-04-20T08:00:00Z"
  }
]
```

### POST `/api/transporter/vehicles/`

**Request (JSON):**

```json
{
  "vehicle_types": ["Flat Bed 12m", "Container 40 Feet / 20 Feet"],
  "registration_number": "DXB-200",
  "load_capacity": "10000.00",
  "max_length_m": "14.000",
  "special_features": ""
}
```

Legacy `vehicle_type` (string) is still accepted and stored as a one-element `vehicle_types` list.

Optional write-only **`noc_file`** (base64) and **`noc_expiry_date`** may be included; both are optional.

**Response `201`:** one vehicle object (created unverified until admin verifies; includes `vehicle_types`).

### GET `/api/transporter/vehicles/{vehicle_id}/`

### PATCH `/api/transporter/vehicles/{vehicle_id}/`

**Response `200`:** vehicle object.

### POST `/api/transporter/vehicles/{vehicle_id}/assign-driver/`

Assigns a user to the vehicle as `assigned_driver`. **Does not** check `TransporterDriverLink`; driver must still be a `TRANSPORTER` role user.

**Request (JSON):**

```json
{ "driver_id": 88 }
```

Unassign:

```json
{ "driver_id": null }
```

**Response `200` — `data`:** vehicle object.

### GET `/api/transporter/vehicles/{vehicle_id}/documents/`

### POST `/api/transporter/vehicles/{vehicle_id}/documents/`

**POST request (JSON):**

```json
{
  "document_type": "NOC",
  "file": "data:application/pdf;base64,JVBERi0xLjQK...",
  "expiry_date": "2028-01-01"
}
```

---

## Driver ↔ fleet owner link (data model)

Trip assignment validates an **active** `TransporterDriverLink(transporter=fleet_owner, driver=driver)`.  

Fleet drivers are managed via **`/api/transporter/drivers/`** (list/create/detail/documents). Without an active link, trip assign returns **`400`** `Invalid driver link.`

### GET `/api/transporter/drivers/{driver_id}/current-location/`

**Auth:** fleet owner (`FLEET_OWNER`)

Returns the latest Traccar position for a **linked active** fleet driver. Looks up `driver.id` → driver’s `transporter_profile.tc_id` → Traccar.

**Response `200` — `data`:**

```json
{
  "driver": {
    "id": 55,
    "email": "driver1@fleet.com",
    "first_name": "Ali",
    "last_name": "Khan",
    "phone": "555-0101",
    "account_type": "TRANSPORTER_DRIVER"
  },
  "tc_id": "12345",
  "tc_u_id": "DRV55",
  "position": { "lat": 25.2048, "lon": 55.2708 },
  "recorded_at": "2026-07-27T10:00:00.000Z",
  "raw": {}
}
```

**Errors:**
- `404` — driver not in your fleet / inactive link
- `400` — missing `tc_id`, Traccar fetch failure, or invalid coordinates

---

## Load discovery & bidding

Same URLs as other transporters; typically requires **`documents_verified=true`**.

### GET `/api/transporter/load-discovery/`

**Fleet owner:** uses the **owner profile `tc_id`**, same GPS path as other transporters. Missing `tc_id` → `400`. Linked-driver GPS is **not** used here.

- `country_code` is required (pickup country).
- Optional `load_type=local` or `load_type=country_to_country` for a strict shipment-type filter (`country_to_country` returns only C2C loads).
- **Local** owners: only local loads within the zone radius of that GPS.
- **C2C** owners: nearby local loads in that radius, plus C2C loads with pickup in `country_code` and drop-off in another country.

For driver-location + assigned-vehicle matching, individual drivers use **`GET /api/transporter/available-shipments/?country_code=`**.

```json
{
  "driver_location": { "lat": 24.861, "lon": 67.002 },
  "country_code": "AE",
  "radius_km": 50.0,
  "radius_source": "zone",
  "count": 1,
  "loads": [
    {
      "id": 100,
      "distance_from_driver_km": 0.12,
      "matched_vehicle_type": "Flatbed",
      "nearby_drivers": []
    }
  ]
}
```

Each element of `loads` is a **`ShipmentSerializer`** shipment plus extra keys such as `distance_from_driver_km`, `matched_vehicle_type`, `max_capacity_for_type`, `favorite_for_this_shipper`, `nearby_drivers`.

Full query-parameter matrix: **`TRANSPORTER_INDIVIDUAL_DRIVER_API.md`** § Load discovery.

### GET `/api/transporter/available-shipments/`

**Fleet owner:** lists published loads near **linked drivers' live GPS** (pickup country of each driver + zone radius). Local owners only see nearby local loads. C2C owners also see C2C loads with pickup in the driver's country and drop-off in another country. Every call fetches live Traccar. `driver_location` / `driver_locations` are included.

No approved linked driver, or no verified active fleet vehicle: `shipments`, `driver_location`, and `driver_locations` are `null`.

**Query:** `country_code` is ignored for located drivers. Optional `load_type=local` or `load_type=country_to_country` (strict; C2C filter excludes local). Loads follow each approved driver's live GPS country (so a Dubai driver is not dropped when the owner profile/GCC country is Pakistan).

**Response excerpt:**

```json
{
  "driver_location": { "lat": 31.5204, "lon": 74.3587 },
  "driver_locations": [
    {
      "id": 55,
      "email": "driver1@fleet.com",
      "location": { "lat": 31.5204, "lon": 74.3587 }
    }
  ],
  "count": 1,
  "shipments": [
    {
      "id": 100,
      "nearest_distance_km": null,
      "nearby_drivers": []
    }
  ]
}
```

See **`API_DOCS.md`** § available-shipments for the full field list.

### POST `/api/transporter/shipments/{shipment_id}/bid/`

**Request (JSON):**

```json
{
  "action": "ACCEPT",
  "message": "We accept the posted rate."
}
```

**Response `201` — `data`:** bid object (same as individual driver doc).

### GET `/api/transporter/shipments/{shipment_id}/bids/`

**Response `200` — `data`:** array of bids.

---

## Fleet owner — trip list buckets

All require **`documents_verified=true`** and **`account_type=FLEET_OWNER`**.

Filters: **`Trip.transporter = request.user`**.

### GET `/api/transporter/trips/my/`

All trips owned by the fleet owner.

**Response `200` — `data`:** array of `TripListSerializer`:

```json
[
  {
    "id": 12,
    "shipment_id": 200,
    "status": "ASSIGNED",
    "status_timeline": [
      { "status": "ASSIGNED", "recorded_at": "2026-04-28T08:00:00Z" }
    ],
    "assigned_driver_id": 88,
    "shipper_rating": false,
    "current_lat": null,
    "current_lon": null,
    "eta": null,
    "created_at": "2026-04-28T08:00:00Z",
    "updated_at": "2026-04-28T09:00:00Z"
  }
]
```

### GET `/api/transporter/trips/assigned/`

Same as `my`, but only rows with **`assigned_driver != null`**.

### GET `/api/transporter/trips/active/`

Statuses: `EN_ROUTE`, `ARRIVED_PICKUP`, `LOADED`, `IN_TRANSIT`, `ARRIVED_DELIVERY`, `DELIVERED`.

### GET `/api/transporter/trips/completed/`

**`status = COMPLETED`** only.

Same payload as individual driver **`GET /api/transporter/my-trips/completed/`**: `data.count`, `data.trips[]`, full trip detail (`shipment`, `accepted_bid`, `shipper`, `assigned_driver`, `tc_id`, `tc_u_id`), and a **`payment`** object per trip for confirm-cash/card UI (`can_confirm_cash`, `can_confirm_card`, `awaiting_shipper_payment`, etc.).

---

## Assign driver to a trip (fleet owner only)

### POST `/api/transporter/trips/{trip_id}/assign-driver/`

**Auth:** fleet owner + verified; trip must have **`transporter = you`**.

**Assign:**

```json
{
  "driver_id": 88
}
```

**Unassign:**

```json
{
  "driver_id": null
}
```

**Success `200` — `data`:** full trip detail (`TransporterTripDetailSerializer`), e.g. includes `shipment`, `accepted_bid`, `transporter`, `shipper`, `assigned_driver`, `tc_id`, `tc_u_id`.

Assigning a driver also grants them access to the existing shipper ↔ fleet-owner trip chat (`Conversation` linked to the trip). Unassigning removes that access. `TRIP_ASSIGNED` push includes `conversation_id`.

**Error `400`** if driver not linked:

```json
{
  "status_code": 400,
  "message": "Driver is not linked to this transporter.",
  "error": "Invalid driver link.",
  "data": null
}
```

---

## Trip detail, navigation, execution

Fleet owner may access if **`request.user.id == trip.transporter_id`** OR **`request.user.id == trip.assigned_driver_id`** (same rule as drivers). Typically the owner uses the **transporter** path.

### GET `/api/transporter/trips/{trip_id}/`

**Response `200` — `data` (abbreviated):**

```json
{
  "id": 12,
  "shipment": {
    "id": 200,
    "pickup_address": "Depot A",
    "pickup_lat": "25.2048000",
    "pickup_lon": "55.2708000",
    "pickup_country_code": "AE",
    "delivery_address": "Site B",
    "delivery_lat": "24.4539000",
    "delivery_lon": "54.3773000",
    "delivery_country_code": "AE",
    "cargo_type": "Pallets",
    "weight": "2 tons",
    "dimensions": "",
    "vehicle_type_required": "Flatbed",
    "special_instructions": "",
    "distance_km": "120.00",
    "suggested_price": "800.00",
    "status": "ASSIGNED",
    "pickup_scheduled_at": "2026-04-29T06:00:00Z",
    "created_at": "2026-04-28T07:00:00Z",
    "updated_at": "2026-04-28T08:00:00Z"
  },
  "accepted_bid": {
    "id": 70,
    "transporter": 50,
    "transporter_email": "fleet@example.com",
    "transporter_name": "Acme Logistics",
    "amount": "750.00",
    "status": "ACCEPTED",
    "counter_amount": null,
    "message": "",
    "created_at": "2026-04-28T07:30:00Z"
  },
  "transporter": 50,
  "status": "ASSIGNED",
  "shipper_rating": false,
  "status_timeline": [],
  "current_lat": null,
  "current_lon": null,
  "eta": null,
  "created_at": "2026-04-28T08:00:00Z",
  "updated_at": "2026-04-28T08:00:00Z",
  "shipper": {
    "id": 3,
    "email": "shipper@example.com",
    "first_name": "Sara",
    "last_name": "Khan",
    "phone": "+923001111111",
    "account_type": "BUSINESS",
    "company_name": "Acme Logistics"
  },
  "assigned_driver": {
    "id": 88,
    "email": "driver@example.com",
    "first_name": "Driver",
    "last_name": "One",
    "phone": "+923001234567",
    "account_type": "DRIVER",
    "company_name": ""
  },
  "tc_id": "333",
  "tc_u_id": "FL00WN01"
}
```

`tc_id` / `tc_u_id` reflect the **trip owner** (`Trip.transporter`) profile used for Traccar hints.

### GET `/api/transporter/trips/{trip_id}/navigation/`

**Response `200` — `data`:**

```json
{
  "trip_id": 12,
  "shipment_id": 200,
  "pickup": {
    "address": "Depot A",
    "lat": "25.2048000",
    "lon": "55.2708000"
  },
  "delivery": {
    "address": "Site B",
    "lat": "24.4539000",
    "lon": "54.3773000"
  },
  "transporter_profile": {
    "tc_id": "333",
    "tc_u_id": "FL00WN01"
  },
  "traccar_device_url_hint": "https://traccar.example.com/#/devices/333",
  "google_maps_directions_url": "https://www.google.com/maps/dir/?api=1&origin=25.2048,55.2708&destination=24.4539,54.3773&travelmode=driving",
  "naive_route_distance_km": 95.4,
  "naive_eta_minutes": 127
}
```

### POST `/api/transporter/trips/{trip_id}/location/`

```json
{ "lat": "25.2048", "lon": "55.2708" }
```

### PATCH `/api/transporter/trips/{trip_id}/status/`

```json
{
  "status": "EN_ROUTE",
  "lat": 25.2048,
  "lon": 55.2708
}
```

**Response `200` — `data`:** `TripListSerializer` row.

### POST `/api/transporter/trips/{trip_id}/pod/`

**Auth:** transporter + verified; user must be `Trip.transporter` or `Trip.assigned_driver`.

**Request:** `multipart/form-data`

| Field | Required | Notes |
|-------|----------|--------|
| `receiver_name` | Yes (initial submission) | |
| `receiver_signature` | No | image file |
| `delivery_lat` | No | |
| `delivery_lon` | No | |
| `delivered_at` | No | ISO datetime string |
| `photos` / `pod_photos` / `pods` | No | multiple files (upload multiple PODs/photos) |
| `delivery_photo` | No | single file |

**Response `201` — `data`:**

```json
{
  "id": 30,
  "receiver_name": "Receiver",
  "receiver_signature": "pod/signatures/2026/04/28/sig.png",
  "signature_url": "https://your-domain.com/media/pod/signatures/2026/04/28/sig.png",
  "delivery_lat": "24.4539000",
  "delivery_lon": "54.3773000",
  "delivered_at": "2026-04-28T18:00:00Z",
  "photos": [],
  "created_at": "2026-04-28T18:00:00Z"
}
```

---

## Payments (fleet owner as trip owner)

These use **`Trip.objects.get(..., transporter=request.user)`** — only the **fleet owner** on the trip, not the assigned driver.

### GET `/api/transporter/trips/{trip_id}/payment-summary/`

**Response `200` — `data`:**

```json
{
  "trip_id": 12,
  "payments": [],
  "invoices": []
}
```

*(Arrays contain `PaymentSerializer` / `InvoiceSerializer` objects when present.)*

### POST `/api/transporter/trips/{trip_id}/confirm-cash/`

**Request:** empty body.

**Response `200`:** e.g. message “Cash collection confirmed.”, **`data`** may be `{}`.

---

## Billing & earnings (fleet owner account)

Wallet and ledger entries belong to the **fleet owner user** (`request.user`), not to assigned drivers.

### GET `/api/transporter/wallet/`

**Response `200` — `data`:**

```json
{
  "balance": "5000.00",
  "currency": "USD",
  "updated_at": "2026-04-28T12:00:00Z"
}
```

### GET `/api/transporter/ledger/`

**Response `200` — `data`:** array of entries:

```json
[
  {
    "id": 1001,
    "amount": "250.00",
    "entry_type": "WALLET_CREDIT_EARNING",
    "trip": 12,
    "payment": null,
    "note": "",
    "idempotency_key": "earn-12-1",
    "created_at": "2026-04-28T10:00:00Z"
  }
]
```

### GET `/api/transporter/earnings/`

**Response `200` — `data`:**

```json
{
  "total_credited": "15000.00",
  "entries": []
}
```

### GET `/api/transporter/earnings/by-trip/`

**Response `200` — `data`:**

```json
{
  "by_trip": [
    { "trip_id": 12, "total_credited": "250.00" },
    { "trip_id": 11, "total_credited": "180.00" }
  ]
}
```

### GET `/api/transporter/withdrawals/`

**Response `200` — `data`:** array of withdrawals:

```json
[
  {
    "id": 2,
    "amount": "1000.00",
    "currency": "USD",
    "status": "COMPLETED",
    "note": "Bank transfer",
    "created_at": "2026-04-01T08:00:00Z",
    "updated_at": "2026-04-02T09:00:00Z"
  }
]
```

### POST `/api/transporter/withdrawals/`

**Request (JSON):**

```json
{
  "amount": "1000.00",
  "note": "Monthly withdrawal"
}
```

**Response `201` — `data`:**

```json
{
  "id": 3,
  "amount": "1000.00",
  "currency": "USD",
  "status": "PENDING",
  "note": "Monthly withdrawal",
  "created_at": "2026-04-28T12:00:00Z",
  "updated_at": "2026-04-28T12:00:00Z"
}
```

---

## Chats

Base path **`/api/chats/`** (not under `/api/transporter/`).

When you assign a fleet driver to a trip, that driver is included in the same shipper ↔ owner conversation (see `assigned_driver_id` on chat summaries).

### GET `/api/chats/`

**Response `200` — `data`:** array of summaries:

```json
[
  {
    "id": 15,
    "shipment_id": 200,
    "trip_id": 12,
    "shipper_id": 3,
    "transporter_id": 50,
    "assigned_driver_id": 88,
    "counterparty": {
      "id": 3,
      "email": "shipper@example.com",
      "first_name": "Sara",
      "last_name": "Khan",
      "phone": "+923001111111",
      "role": "SHIPPER",
      "profile": {
        "company_name": "Acme",
        "account_type": "BUSINESS",
        "kyc_verified": true,
        "credit_approved": false
      }
    },
    "last_message_at": "2026-04-28T14:30:00Z",
    "created_at": "2026-04-28T12:00:00Z"
  }
]
```

### POST `/api/chats/open/`

**Request (JSON):**

```json
{
  "shipment_id": 200,
  "transporter_id": 50
}
```

**Response `200` — `data`:**

```json
{
  "conversation_id": 15,
  "shipment_id": 200,
  "trip_id": 12,
  "shipper_id": 3,
  "transporter_id": 50,
  "counterparty": {}
}
```

### GET `/api/chats/{conversation_id}/messages/`

**Response `200` — `data`:** array of messages (`MessageSerializer`):

```json
[
  {
    "id": 900,
    "conversation_id": 15,
    "shipment_id": 200,
    "trip_id": 12,
    "sender": 50,
    "sender_email": "fleet@example.com",
    "text": "Driver assigned for tomorrow.",
    "message_type": "TEXT",
    "voice_file": null,
    "voice_url": null,
    "created_at": "2026-04-28T14:30:00Z"
  }
]
```

### POST `/api/chats/{conversation_id}/messages/`

**Request (JSON) — text:**

```json
{
  "text": "We will load at 08:00.",
  "message_type": "TEXT"
}
```

**Response `201`:** single message object (same shape as above).

---

## Ratings, analytics, compliance

### POST `/api/transporter/trips/{trip_id}/shipper-review/`

Submit a one-time review for the trip shipper after the trip is `DELIVERED`, `COMPLETED`, or `CLOSED`.

**Auth:** transporter; user must be `Trip.transporter` or `Trip.assigned_driver`.

**Request:**

```json
{
  "shipper_rating": 5,
  "comment": "Clear instructions and quick payment."
}
```

**Response `201` — `data`:**

```json
{
  "id": 1,
  "trip": 7,
  "shipper": 3,
  "transporter": 12,
  "reviewer": 12,
  "shipper_rating": 5,
  "comment": "Clear instructions and quick payment.",
  "created_at": "2026-04-27T10:00:00Z"
}
```

### GET `/api/transporter/ratings/summary/`

**Response `200` — `data`:**

```json
{
  "average_transporter_rating": 4.7,
  "review_count": 34
}
```

### GET `/api/transporter/ratings/received/`

**Response `200` — `data`:** array of reviews:

```json
[
  {
    "id": 5,
    "trip": 12,
    "shipper": 3,
    "transporter": 50,
    "driver": 88,
    "transporter_rating": 5,
    "driver_rating": 5,
    "comment": "Professional fleet.",
    "created_at": "2026-04-27T10:00:00Z"
  }
]
```

### GET `/api/transporter/analytics/`

**Response `200` — `data`:** includes windowed bid stats, acceptance rate, rating aggregates, on-time metrics (see `transporter_analytics` in `api/engagement_api.py`).

### GET `/api/transporter/compliance/summary/`

**Response `200` — `data`:**

```json
{
  "kyc_documents": {
    "expiring_within_window": 0,
    "expired": 0,
    "no_expiry_set": 0
  },
  "vehicle_documents": {
    "expiring_within_window": 1,
    "expired": 0,
    "no_expiry_set": 0
  },
  "document_reminder_horizon_days": 30
}
```

---

## Not used as “fleet owner only” UI routes

These require **`IsFleetDriverTransporter`** (active driver link). Fleet owners **without** a driver link use **`/api/transporter/trips/*`** buckets instead.

- `GET /api/transporter/driver/trips/assigned/`
- `GET /api/transporter/driver/trips/active/`
- `GET /api/transporter/driver/trips/completed/`

See **`TRANSPORTER_FLEET_DRIVER_API.md`**.

---

## Additional reference

For the **full load-discovery query-parameter table** and a line-by-line **`analytics`** response, see **`docs/TRANSPORTER_INDIVIDUAL_DRIVER_API.md`**. Core JSON envelopes match everywhere; this file emphasizes **fleet-owner routes** (`trips/my`, `trips/assign-driver`, payments) and **constraints** (`tc_id` for load-discovery, `TransporterDriverLink` for trip assign).
