# Transporter APIs — Individual Driver

This document lists **individual-driver** transporter APIs: base path `/api/transporter/…` plus shared **auth**, **chat**, **FCM**, and **`/api/auth/`** helpers.

**Audience:** users with role `TRANSPORTER` and `account_type=DRIVER`, operating **without** fleet-owner-only trip buckets (fleet routes are omitted here).

---

## Standard response envelope

Almost every endpoint returns JSON in this shape (`Response` HTTP status equals `status_code`):

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

On validation errors, `error` often holds field errors (object) and `data` is `null`.

---

## Authentication (required for protected routes)

Send the token as: `Authorization: Bearer <token>` unless noted otherwise.

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

**Auth:** none

**Request (JSON):**

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

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

```json
{
  "token": "9944b09199c62bcf9418ad846dd0e4bbdfc6ee4b",
  "user_id": 42,
  "email": "driver@example.com",
  "first_name": "Ali",
  "last_name": "Driver",
  "role": "TRANSPORTER",
  "persona_type": "TRANSPORTER_INDIVIDUAL_DRIVER",
  "account_type": "DRIVER",
  "tc_id": "111",
  "tc_u_id": "AB12CD34"
}
```

*(For non-transporter users `tc_id` / `tc_u_id` may be null.)*

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

**Auth:** authenticated

**Request:** empty body.

**Response `200`:** message only; `data` typically `null`.

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

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

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

**Request (JSON):**

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

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

```json
{
  "email": "driver@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": "driver@example.com",
  "otp": "492018"
}
```

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

```json
{
  "email": "driver@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": "driver@example.com",
  "otp": "492018",
  "new_password": "NewSecurePassword123"
}
```

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

```json
{
  "token": "new-auth-token",
  "user_id": 12,
  "email": "driver@example.com",
  "role": "TRANSPORTER",
  "persona_type": "TRANSPORTER_INDIVIDUAL_DRIVER",
  "account_type": "DRIVER"
}
```

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

**Auth:** authenticated

**Request (JSON):**

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

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

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

---

## Registration & profile

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

**Auth:** none

**Request (JSON):**

```json
{
  "email": "newdriver@example.com",
  "password": "minimum8chars",
  "first_name": "Ali",
  "last_name": "Raza",
  "phone": "+923001234567",
  "account_type": "DRIVER",
  "company_name": "",
  "local": true,
  "country_to_country": false,
  "language": "en"
}
```

- Required: `local` and `country_to_country` (exactly one `true`). `local=true` limits load discovery / bidding to local shipments; `country_to_country=true` allows both local and cross-border shipments.
- Optional: `company_name` (string).

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

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

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

**Auth:** transporter

**Response `200` — `data` (Transporter profile):**

```json
{
  "user_id": 12,
  "email": "driver@example.com",
  "first_name": "Ali",
  "last_name": "Raza",
  "phone": "+923001234567",
  "language": "en",
  "account_type": "DRIVER",
  "company_name": "",
  "documents_verified": true,
  "tc_id": "111",
  "tc_u_id": "AB12CD34",
  "vehicle": {
    "id": 5,
    "owner_id": 12,
    "vehicle_type": "Flatbed",
    "registration_number": "ABC-1234",
    "load_capacity": "5000.00",
    "max_length_m": null,
    "max_width_m": null,
    "max_height_m": null,
    "special_features": "",
    "is_active": true,
    "is_verified": true,
    "assigned_driver_id": null,
    "assigned_driver_email": null,
    "verified_at": "2026-04-10T12:00:00Z",
    "created_at": "2026-04-05T10:00:00Z",
    "updated_at": "2026-04-10T12:00:00Z"
  },
  "vehicles": [],
  "created_at": "2026-04-01T10:00:00Z",
  "updated_at": "2026-04-20T08:00:00Z"
}
```

- **`vehicle`** — primary truck: first **active + verified** owned/assigned vehicle, else most recently updated; `null` if none.
- **`vehicles`** — all vehicles you **own** or are **assigned** to (same shape as `GET /api/transporter/vehicles/` list items).

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

**Auth:** transporter

**Request (JSON, partial):** any writable profile fields, nested under `user` / `role` as supported by the serializer, e.g.:

```json
{
  "first_name": "Ali",
  "last_name": "Updated",
  "phone": "+923009998877",
  "language": "en",
  "company_name": ""
}
```

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

---

## Documents (KYC)

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

**Auth:** transporter

**Response `200` — `data`:** array of document objects:

```json
[
  {
    "id": 3,
    "document_type": "DRIVER_LICENSE",
    "file": "kyc/2026/04/28/license-front.pdf",
    "file_url": "https://your-domain.com/media/kyc/2026/04/28/license-front.pdf",
    "file_back": "kyc/2026/04/28/license-back.pdf",
    "file_back_url": "https://your-domain.com/media/kyc/2026/04/28/license-back.pdf",
    "verified": false,
    "expiry_date": "2027-01-15", 
    "submitted_at": "2026-04-28T12:00:00Z",
    "reviewed_at": null
  }
]
```

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

**Auth:** transporter

**Request (JSON):** base64 upload payload per `TransporterDocumentUploadSerializer`:

```json
{
  "document_type": "DRIVER_LICENSE",
  "file": "data:application/pdf;base64,JVBERi0xLjQK...",
  "file_back": "data:application/pdf;base64,JVBERi0xLjQK...",
  "expiry_date": "2027-06-01"
}
```

**Types (individual driver only):** `DRIVER_LICENSE`, `PASSPORT_COPY`, `PERMIT`, `COUNTRY_GCC`, and optional `NOC`. `VEHICLE_REGISTRATION` and `INSURANCE` are not accepted on KYC (use vehicle documents for `NOC`). Two-sided types use `file` + `file_back`. For `COUNTRY_GCC`, also send `gcc_country` (`AE`/`SA`/`OM`/`QA`/`BH`/`KW`). Fleet-owner company types are not accepted for `DRIVER` accounts.

**Response `201`:** single document object (same fields as list item).

---

## Vehicles

A vehicle may list multiple **`vehicle_types`**. Load discovery / available-shipments match when shipment `vehicle_type_required` equals **any** of those types.

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

**Auth:** transporter

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

```json
[
  {
    "id": 5,
    "owner_id": 12,
    "vehicle_type": "Flat Bed 12m",
    "vehicle_types": ["Flat Bed 12m", "Container 40 Feet / 20 Feet"],
    "registration_number": "REG-100",
    "load_capacity": "4000.00",
    "max_length_m": "12.000",
    "max_width_m": "2.500",
    "max_height_m": "3.000",
    "special_features": "",
    "is_active": true,
    "is_verified": true,
    "assigned_driver_id": null,
    "assigned_driver_email": null,
    "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/`

**Auth:** transporter

**Request (JSON):** preferred `vehicle_types` array (legacy `vehicle_type` string still accepted).

```json
{
  "vehicle_types": ["Flat Bed 12m", "Container 40 Feet / 20 Feet"],
  "registration_number": "REG-200",
  "load_capacity": "5000.00",
  "max_length_m": "13.000",
  "special_features": "",
  "noc_file": null,
  "noc_expiry_date": null
}
```

Optional write-only **`noc_file`** (base64) and **`noc_expiry_date`** may be sent on create/patch. Both are optional; omit them to create a vehicle without a NOC. When `noc_file` is sent, a `VehicleDocument` with `document_type=NOC` is created/updated.

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

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

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

**Auth:** transporter (owner’s vehicles only)

**PATCH request (JSON, partial):** same writable fields as create.

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

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

Used when the owner assigns another user as driver for a vehicle (optional for a solo individual driver).

**Auth:** transporter

**Request (JSON):**

```json
{
  "driver_id": 99
}
```

Unassign:

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

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

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

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

**Auth:** transporter

**POST request (JSON):**

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

**Response:** list or created document per `VehicleDocumentSerializer` (includes `file_url`).

---

## Load discovery & bidding

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

**Auth:** transporter with `documents_verified=true`

**Query parameter:**

| Parameter | Notes |
|-----------|--------|
| `country_code` | **Required.** ISO 3166-1 alpha-2; pickup country. Local drivers: zone radius around GPS. C2C drivers: that country for pickup; C2C drop-off must be another country |
| `load_type` | Optional. `local` or `country_to_country`. Strict filter on shipment type (`country_to_country` returns only C2C loads, not local). Omit for profile-based visibility. Invalid → `400` |

Requires `tc_id` on the **individual driver** profile and a successful Traccar position fetch (otherwise `400`).

- Profile **local:** only local loads inside the zone radius of the driver's GPS.
- Profile **C2C:** local loads inside that radius, plus C2C loads with pickup in `country_code` and drop-off in a different country.

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

```json
{
  "driver_location": { "lat": 24.861, "lon": 67.01 },
  "country_code": "PK",
  "radius_km": 50.0,
  "radius_source": "zone",
  "count": 2,
  "loads": [
    {
      "id": 100,
      "pickup_address": "...",
      "pickup_lat": "31.5497000",
      "pickup_lon": "74.3436000",
      "pickup_country_code": "PK",
      "delivery_address": "...",
      "delivery_lat": "33.6844000",
      "delivery_lon": "73.0479000",
      "delivery_country_code": "PK",
      "cargo_type": "General",
      "weight": "1 ton",
      "dimensions": "",
      "vehicle_type_required": "Flatbed",
      "special_instructions": "",
      "distance_km": "290.50",
      "suggested_price": "12000.00",
      "status": "PUBLISHED",
      "pickup_scheduled_at": "2026-04-25T08:00:00Z",
      "created_at": "2026-04-22T10:00:00Z",
      "updated_at": "2026-04-22T10:00:00Z",
      "distance_from_driver_km": 12.34,
      "matched_vehicle_type": "Flatbed",
      "max_capacity_for_type": 5000.0,
      "favorite_for_this_shipper": false
    }
  ]
}
```

The individual-driver load list with GPS country + vehicle matching is **`GET /api/transporter/available-shipments/?country_code=`**.

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

**Auth:** transporter with `documents_verified=true`

**Query parameter:**

| Parameter | Notes |
|-----------|--------|
| `country_code` | **Required.** ISO 3166-1 alpha-2 pickup country |
| `load_type` | Optional. `local` or `country_to_country`. Strict shipment-type filter. Invalid → `400` |

- Live Traccar from the driver's `tc_id` on **every** request. GPS must be in `country_code` or the list is empty.
- Pickup within that country's zone radius of the driver.
- Verified+active vehicle required; `vehicle_type_required` must match **any** of the vehicle’s `vehicle_types`. No matching vehicle → empty `shipments`.

**Response `200` — `data`:** `{ "driver_location": { "lat", "lon" }, "count": N, "shipments": [ ... ] }` with `nearest_distance_km` set on each matching row.

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

**Auth:** transporter with `documents_verified=true`

**Request (JSON):**

Accept shipper rate:

```json
{
  "action": "ACCEPT",
  "message": "I accept the proposed rate."
}
```

Counter-offer:

```json
{
  "action": "COUNTER",
  "amount": "11500.00",
  "message": "Can we meet at 11500?"
}
```

**Response `201` — `data` (bid):**

```json
{
  "id": 55,
  "transporter": 12,
  "transporter_email": "driver@example.com",
  "transporter_name": "Ali Raza",
  "amount": "11500.00",
  "status": "PENDING",
  "counter_amount": null,
  "message": "Can we meet at 11500?",
  "created_at": "2026-04-28T12:00:00Z"
}
```

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

**Auth:** transporter with `documents_verified=true`

**Response `200` — `data`:** array of bids (same shape as above).

---

## Chats (bid negotiation & trip lifecycle)

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

### GET `/api/chats/`

**Auth:** authenticated

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

```json
[
  {
    "id": 9,
    "shipment_id": 100,
    "trip_id": 7,
    "shipper_id": 3,
    "transporter_id": 12,
    "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/`

**Auth:** authenticated (must be shipper or transporter on the shipment)

**Request (JSON):**

```json
{
  "shipment_id": 100,
  "transporter_id": 12
}
```

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

```json
{
  "conversation_id": 9,
  "shipment_id": 100,
  "trip_id": 7,
  "shipper_id": 3,
  "transporter_id": 12,
  "counterparty": { }
}
```

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

**Auth:** authenticated participant

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

```json
[
  {
    "id": 501,
    "conversation_id": 9,
    "shipment_id": 100,
    "trip_id": 7,
    "sender": 12,
    "sender_email": "driver@example.com",
    "text": "On my way",
    "message_type": "TEXT",
    "voice_file": null,
    "voice_url": null,
    "created_at": "2026-04-28T14:30:00Z"
  }
]
```

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

**Auth:** authenticated participant

**Text message (JSON):**

```json
{
  "text": "Hello",
  "message_type": "TEXT"
}
```

**Voice message (multipart):** `message_type=VOICE` and file field `voice_file` (see `MessageCreateSerializer`).

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

---

## Trip lists (individual driver)

Individual drivers own trips as **`Trip.transporter`** (after a bid is accepted). Use these lists before calling trip detail/execution endpoints.

All three endpoints use the same response envelope: `{ "count": N, "trips": [ ... ] }` with full `TransporterTripDetailSerializer` objects.

**Auth (all):** individual driver (`account_type=DRIVER`, no active fleet link). **Does not require** `documents_verified`.

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

Trips not started yet — status **`ASSIGNED` only** (`transporter = request.user`).

**Response `200`:** `{ "count": 1, "trips": [ ... ] }` — if none: `{ "count": 0, "trips": [] }`.

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

**Auth:** individual driver (`account_type=DRIVER`, no active fleet link)  

**Does not require** `documents_verified`.

Returns trips where `transporter = request.user` and status is one of:  
`EN_ROUTE`, `ARRIVED_PICKUP`, `LOADED`, `IN_TRANSIT`, `ARRIVED_DELIVERY`, `DELIVERED` (excludes `ASSIGNED` — use `my-trips/assigned/`).

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

```json
{
  "count": 1,
  "trips": [
    {
      "id": 7,
      "shipment": { "id": 100, "pickup_address": "Warehouse A", "status": "IN_TRANSIT" },
      "accepted_bid": { "id": 55, "amount": "4200.00", "status": "ACCEPTED" },
      "transporter": 12,
      "status": "IN_TRANSIT",
      "shipper_rating": false,
      "status_timeline": [],
      "shipper": { "id": 3, "email": "shipper@example.com" },
      "assigned_driver": null,
      "tc_id": "444",
      "tc_u_id": "XY99ZZ11"
    }
  ]
}
```

If none: `{ "count": 0, "trips": [] }`.

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

Finished trips — status **`COMPLETED` only** (`transporter = request.user`). Does not include `CLOSED`.

Each trip includes a **`payment`** object for building the payment / confirm-cash UI without a separate call.

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

```json
{
  "count": 1,
  "trips": [
    {
      "id": 7,
      "status": "COMPLETED",
      "shipment": { "id": 100, "pickup_address": "Warehouse A" },
      "accepted_bid": { "id": 55, "amount": "4200.00" },
      "payment": {
        "status": "pending",
        "raw_status": "PENDING_COD",
        "method": "CASH",
        "amount": "4200.00",
        "payment_id": 91,
        "can_confirm_cash": true,
        "awaiting_shipper_payment": false
      }
    }
  ]
}
```

**`payment.status` (use for UI):**

| Value | Meaning |
|--------|---------|
| `pending` | Not settled yet — shipper has not paid, cash awaiting collection, or payment in progress |
| `completed` | Settled (`raw_status` = `CAPTURED`) |

**Other `payment` fields:**

| Field | Description |
|--------|-------------|
| `raw_status` | Backend payment status (`PENDING_COD`, `CAPTURED`, etc.) or `null` if no payment yet |
| `method` | `WALLET`, `CASH`, `CREDIT`, or `null` |
| `amount` | Expected or actual settlement amount (string) |
| `payment_id` | Payment row id, or `null` |
| `can_confirm_cash` | `true` when transporter should call `POST /api/transporter/trips/{id}/confirm-cash/` |
| `can_confirm_card` | `true` when transporter should call `POST /api/transporter/trips/{id}/confirm-card/` |
| `awaiting_shipper_payment` | `true` when shipper has not initiated payment yet |

**Confirm cash (when `can_confirm_cash` is true):**  
`POST /api/transporter/trips/{trip_id}/confirm-cash/` (empty body) — records that **physical cash** was collected. Credits the transporter’s **platform wallet** only; does **not** debit the shipper’s wallet.

---

## Trip detail & execution

Trip detail, navigation, location, status, and POD use `IsTransporter` (no `documents_verified` required when you are `Trip.transporter` or `assigned_driver`).

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

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

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

```json
{
  "id": 7,
  "shipment": {
    "id": 100,
    "pickup_address": "Warehouse A",
    "pickup_lat": "31.5497000",
    "pickup_lon": "74.3436000",
    "pickup_country_code": "PK",
    "delivery_address": "Store B",
    "delivery_lat": "33.6844000",
    "delivery_lon": "73.0479000",
    "delivery_country_code": "PK",
    "cargo_type": "General",
    "weight": "1 ton",
    "dimensions": "",
    "vehicle_type_required": "Flatbed",
    "special_instructions": "",
    "distance_km": "290.50",
    "suggested_price": "12000.00",
    "status": "IN_TRANSIT",
    "pickup_scheduled_at": "2026-04-25T08:00:00Z",
    "created_at": "2026-04-22T10:00:00Z",
    "updated_at": "2026-04-28T12:00:00Z"
  },
  "accepted_bid": {
    "id": 55,
    "transporter": 12,
    "transporter_email": "driver@example.com",
    "transporter_name": "Ali Raza",
    "amount": "11500.00",
    "status": "ACCEPTED",
    "counter_amount": null,
    "message": "...",
    "created_at": "2026-04-22T11:00:00Z"
  },
  "transporter": 12,
  "status": "IN_TRANSIT",
  "shipper_rating": false,
  "status_timeline": [
    { "status": "ASSIGNED", "recorded_at": "2026-04-22T11:05:00Z" },
    { "status": "IN_TRANSIT", "recorded_at": "2026-04-28T09:00:00Z" }
  ],
  "current_lat": "31.7000000",
  "current_lon": "74.5000000",
  "eta": null,
  "created_at": "2026-04-22T11:05:00Z",
  "updated_at": "2026-04-28T12: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": null,
  "tc_id": "111",
  "tc_u_id": "AB12CD34"
}
```

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

**Auth:** same as trip detail.

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

```json
{
  "trip_id": 7,
  "shipment_id": 100,
  "pickup": {
    "address": "Warehouse A",
    "lat": "31.5497000",
    "lon": "74.3436000"
  },
  "delivery": {
    "address": "Store B",
    "lat": "33.6844000",
    "lon": "73.0479000"
  },
  "transporter_profile": {
    "tc_id": "111",
    "tc_u_id": "AB12CD34"
  },
  "traccar_device_url_hint": "https://traccar.example.com/#/devices/111",
  "google_maps_directions_url": "https://www.google.com/maps/dir/?api=1&origin=31.5497,74.3436&destination=33.6844,73.0479&travelmode=driving",
  "naive_route_distance_km": 290.12,
  "naive_eta_minutes": 387
}
```

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

**Auth:** transporter verified

**Request (JSON):**

```json
{
  "lat": "31.5497",
  "lon": "74.3436"
}
```

**Response `201`:** typically `data: null`, message “Location updated successfully.”

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

**Auth:** transporter verified

**Request (JSON):**

```json
{
  "status": "EN_ROUTE",
  "lat": 25.2048,
  "lon": 55.2708,
  "recorded_at": "2026-04-28T10:00:00Z"
}
```

**Response `200` — `data`:** trip list item shape (`TripListSerializer`):

```json
{
  "id": 7,
  "shipment_id": 100,
  "status": "EN_ROUTE",
  "status_timeline": [],
  "assigned_driver_id": null,
  "shipper_rating": false,
  "current_lat": "25.2048000",
  "current_lon": "55.2708000",
  "eta": null,
  "created_at": "2026-04-22T11:05:00Z",
  "updated_at": "2026-04-28T10:00:00Z"
}
```

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

**Auth:** transporter verified

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

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

**Response `201` — `data` (POD):**

```json
{
  "id": 20,
  "receiver_name": "Receiver Name",
  "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": "33.6844000",
  "delivery_lon": "73.0479000",
  "delivered_at": "2026-04-28T16:00:00Z",
  "photos": [
    {
      "id": 1,
      "image": "pod/photos/2026/04/28/photo1.jpg",
      "image_url": "https://your-domain.com/media/pod/photos/2026/04/28/photo1.jpg",
      "created_at": "2026-04-28T16:01:00Z"
    }
  ],
  "created_at": "2026-04-28T16:00:00Z"
}
```

---

## Billing & earnings

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

**Auth:** transporter

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

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

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

**Auth:** transporter

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

```json
[
  {
    "id": 900,
    "amount": "100.00",
    "entry_type": "WALLET_CREDIT_EARNING",
    "trip": 7,
    "payment": null,
    "note": "",
    "idempotency_key": "earn-7-1",
    "created_at": "2026-04-28T12:00:00Z"
  }
]
```

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

**Auth:** transporter

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

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

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

**Auth:** transporter

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

```json
{
  "by_trip": [
    { "trip_id": 7, "total_credited": "150.00" }
  ]
}
```

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

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

**Auth:** transporter

**POST request (JSON):**

```json
{
  "amount": "500.00",
  "note": "Weekly payout"
}
```

**Response `201` — `data` (withdrawal):**

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

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

**Auth:** transporter; trip must belong to **you** as `Trip.transporter` (individual driver).

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

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

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

**Auth:** transporter; same trip ownership rule as payment summary.

**Request:** empty body.

**Response `200`:** `data` often `{}`.

### 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"
}
```

**Error `400`:** trip not in allowed status, duplicate review, or validation errors.

---

## Ratings, analytics, compliance

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

**Auth:** transporter

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

```json
{
  "average_transporter_rating": 4.6,
  "review_count": 12
}
```

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

**Auth:** transporter

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

```json
[
  {
    "id": 1,
    "trip": 7,
    "shipper": 3,
    "transporter": 12,
    "driver": 12,
    "transporter_rating": 5,
    "driver_rating": 5,
    "comment": "Great service",
    "created_at": "2026-04-27T10:00:00Z"
  }
]
```

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

**Auth:** transporter

**Response `200` — `data`:** analytics object (bids, acceptance rate, on-time metrics, etc.). Shape matches `transporter_analytics` in code (see `api/engagement_api.py`).

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

**Auth:** transporter

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

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

---

## Not included here (fleet-only)

These exist on the server but target **fleet owners** or **fleet drivers**, not the solo individual-driver flow:

- `POST /api/transporter/trips/{trip_id}/assign-driver/`
- `GET /api/transporter/trips/my/`, `/trips/assigned/`, `/trips/active/`, `/trips/completed/` (fleet **owner** buckets)
- `GET /api/transporter/driver/trips/*` (fleet **driver** buckets)

See `docs/TRANSPORTER_FLEET_OWNER_API.md` and `docs/TRANSPORTER_FLEET_DRIVER_API.md`.
