# Tarivo API Contract

Document status: Active API contract
Version: 2.0
Base path: `/api/v1`
Authentication: Laravel Sanctum
Format: JSON

## 1. API Rules

- All protected endpoints require authentication.
- Mutating endpoints require authorization and validation.
- Retriable mutating endpoints accept `Idempotency-Key`.
- Lists are paginated by default.
- Errors never expose stack traces or raw provider responses.
- Public API changes must be versioned.

Implementation status is tracked in `TASKS.md`. This document defines the target
API contract. Until an endpoint is delivered and tested in the repository, treat
it as planned even if its request or response shape is specified here.

## 2. Standard Envelope

Success:

```json
{
  "success": true,
  "message": "Ride request created.",
  "data": {}
}
```

Validation failure:

```json
{
  "success": false,
  "message": "Validation failed.",
  "errors": {
    "pickup_latitude": ["The pickup latitude field is required."]
  }
}
```

Conflict:

```json
{
  "success": false,
  "message": "The trip cannot be started from its current status.",
  "code": "invalid_trip_transition",
  "data": {
    "current_status": "completed"
  }
}
```

## 3. Status Codes

- `200`: request completed
- `201`: resource created
- `202`: async job accepted
- `204`: resource deleted or no content
- `400`: invalid request
- `401`: unauthenticated
- `403`: unauthorized
- `404`: resource not found
- `409`: state conflict or sync conflict
- `422`: validation failed
- `429`: rate limit exceeded
- `500`: server error

## 4. Authentication

- `POST /auth/register`
- `POST /auth/login`
- `POST /auth/logout`
- `POST /auth/forgot-password`
- `POST /auth/reset-password`
- `POST /auth/verify-email`
- `POST /auth/verify-phone`
- `POST /auth/resend-verification`
- `GET /auth/me`
- `PATCH /auth/profile`
- `PATCH /auth/password`
- `DELETE /auth/account`

Target request shape for `POST /auth/register`:

```json
{
  "name": "Nadia Tarivo",
  "email": "nadia@example.com",
  "phone": "+212600000001",
  "password": "Secure12345",
  "password_confirmation": "Secure12345",
  "preferred_language": "fr",
  "timezone": "Africa/Casablanca",
  "role": "passenger",
  "device_name": "web"
}
```

Rules:

- `role` is optional and may only be `passenger` or `driver` for public
  registration.
- `email` is optional for local Moroccan usage, but `phone` is required and
  unique.
- Passwords require confirmation, letters and numbers, with a minimum length of
  10.

Target request shape for `POST /auth/login`:

```json
{
  "login": "+212600000001",
  "password": "Secure12345",
  "device_name": "web"
}
```

`login` accepts either phone or email.

Token response data for register and login:

```json
{
  "user": {
    "id": 1,
    "uuid": "8f0277c7-4a72-48bc-9f8a-9cdde2d29d88",
    "name": "Nadia Tarivo",
    "email": "nadia@example.com",
    "phone": "+212600000001",
    "preferred_language": "fr",
    "timezone": "Africa/Casablanca",
    "status": "active",
    "roles": ["passenger"],
    "email_verified_at": null,
    "phone_verified_at": null,
    "created_at": "2026-07-10T02:30:00.000000Z"
  },
  "token": "plain-text-sanctum-token",
  "token_type": "Bearer"
}
```

Verification endpoints may initially return `202` placeholders while SMS/email
providers are not connected. Requests are validated, rate limited and audited.

Implemented status:

- Implemented and tested now: `POST /auth/register`, `POST /auth/login`,
  `POST /auth/logout`, `GET /auth/me`, `POST /auth/verify-email`,
  `POST /auth/verify-phone` and `POST /auth/resend-verification`.
- Planned but not implemented yet: forgot password, reset password, profile,
  password change and account deletion endpoints.

Security:

- Login, registration and verification endpoints are rate limited.
- Logout invalidates the active token.
- Account deletion is soft-delete first unless legal retention requires longer
  storage.

## 5. Dashboard Summary

- `GET /dashboard/summary`

Target response data:

```json
{
  "user": {
    "id": 1,
    "uuid": "8f0277c7-4a72-48bc-9f8a-9cdde2d29d88",
    "name": "Nadia Tarivo",
    "roles": ["support"]
  },
  "dashboard": {
    "scope": "operations",
    "metrics": {
      "active_trips": 2,
      "requested_rides": 4,
      "unread_notifications": 1,
      "online_drivers": 3
    },
    "active_trips": [
      {
        "id": 42,
        "uuid": "37d46dc2-c0d4-4b9e-a4dd-069399922929",
        "code": "TRV-42",
        "status": "started",
        "passenger_name": "Nadia Passenger",
        "driver_name": "Yassine Driver",
        "vehicle_type": "Petit Taxi",
        "pickup_label": "Casa Port",
        "dropoff_label": "Maarif",
        "estimated_duration_seconds": 900,
        "final_price": null,
        "updated_at": "2026-07-10T10:30:00.000000Z"
      }
    ],
    "drivers": [
      {
        "id": 5,
        "name": "Yassine Driver",
        "city": "Casablanca",
        "availability": "busy",
        "status": "approved",
        "vehicle_type": "Petit Taxi",
        "rating_average": "4.90",
        "rating_count": 12
      }
    ],
    "notifications": [
      {
        "id": 7,
        "type": "incoming_ride_request",
        "title": "Incoming ride request",
        "body": "Casa Centre",
        "priority": "critical",
        "persistence": "blocking",
        "is_read": false,
        "is_acted": false,
        "created_at": "2026-07-10T10:30:00.000000Z"
      }
    ],
    "system": {
      "api": "healthy",
      "queue": "database",
      "reverb": "configured"
    }
  }
}
```

Rules:

- The endpoint requires Sanctum authentication.
- Support, admin and super admin users receive `operations` scope with
  platform-level active trips, requested rides and online or busy drivers.
- Passengers receive `passenger` scope limited to their own rides, trips and
  notifications.
- Drivers receive `driver` scope limited to their assigned trips, eligible ride
  responses, own driver profile and own notifications.
- Dashboard metrics are derived from Laravel state. The frontend must not
  fabricate operational counts, payment states or driver availability.
- Modules not yet delivered, such as offline sync, payments or Google Maps
  route/ETA, may be shown as pending UI states until their backend APIs are
  implemented and verified.

## 6. Reference Data and Settings

- `GET /cities`
- `GET /vehicle-types`
- `GET /settings/public`
- `GET /health`
- `GET /version`

Target response shape for `GET /cities`:

```json
{
  "success": true,
  "message": "Cities retrieved.",
  "data": {
    "cities": [
      {
        "id": 1,
        "name_fr": "Casablanca",
        "name_ar": "الدار البيضاء",
        "name_en": "Casablanca",
        "slug": "casablanca",
        "country_code": "MA",
        "latitude": "33.5731000",
        "longitude": "-7.5898000",
        "is_active": true
      }
    ]
  }
}
```

Target response shape for `GET /vehicle-types`:

```json
{
  "success": true,
  "message": "Vehicle types retrieved.",
  "data": {
    "vehicle_types": [
      {
        "id": 1,
        "name": "Petit Taxi",
        "slug": "petit-taxi",
        "base_fare": "7.00",
        "price_per_km": "3.00",
        "price_per_minute": "0.70",
        "capacity": 3,
        "is_active": true
      }
    ]
  }
}
```

Rules:

- Public reference data endpoints return active records only.
- Cities are limited to Moroccan launch cities in the initial seed data.
- Vehicle type pricing values are decimals in MAD.

Admin:

- `GET /admin/settings`
- `PATCH /admin/settings`
- `GET /admin/external-api-logs`

## 7. Drivers and Vehicles

- `GET /drivers`
- `GET /drivers/{driver}`
- `POST /drivers`
- `PATCH /drivers/{driver}`
- `PATCH /drivers/status`
- `PATCH /drivers/availability`
- `GET /drivers/earnings`
- `GET /drivers/statistics`

Target request shape for `POST /drivers`:

```json
{
  "city_id": 1
}
```

Target response data for driver profile endpoints:

```json
{
  "driver": {
    "id": 1,
    "user_id": 10,
    "city_id": 1,
    "status": "pending",
    "availability": "offline",
    "rating_average": "0.00",
    "rating_count": 0,
    "approved_at": null,
    "rejected_reason": null
  }
}
```

Target driver rules:

- Public driver profile creation is restricted to authenticated users with the
  `driver` role and no existing driver profile.
- Driver profiles are created with `pending` status and `offline`
  availability.
- Drivers can update their own launch city while support/admin roles can view
  records according to policy.
- `PATCH /drivers/availability` accepts `offline` or `online`; only
  approved drivers can go online, and `busy` is reserved for trip lifecycle
  services.
- Driver status approval is tied to the document review module.

Vehicles:

- `GET /vehicles`
- `GET /vehicles/{vehicle}`
- `POST /vehicles`
- `PATCH /vehicles/{vehicle}`
- `DELETE /vehicles/{vehicle}`

Target request shape for `POST /vehicles`:

```json
{
  "vehicle_type_id": 1,
  "city_id": 1,
  "brand": "Dacia",
  "model": "Logan",
  "color": "White",
  "registration_number": "CASA-12345",
  "seats": 3
}
```

Target vehicle rules:

- Vehicle create, update and delete actions require an authenticated driver
  profile and policy authorization.
- Vehicle type and city must be active reference records.
- Seat count cannot exceed the selected vehicle type capacity.
- Vehicle records are created with `pending` status and are soft deleted.

Driver documents:

- `GET /drivers/documents`
- `POST /drivers/documents`
- `GET /drivers/documents/{document}`
- `DELETE /drivers/documents/{document}`
- `POST /admin/drivers/documents/{document}/review`

Target request shape for `POST /drivers/documents`:

```text
multipart/form-data
type=identity_card
file=@identity.pdf
expires_at=2027-07-10
```

Rules:

- The authenticated user must have the `driver` role and an existing driver
  profile.
- `type` must be one of `identity_card`, `driving_license`, `insurance`,
  `vehicle_registration` or `vehicle_photo`.
- `file` must be PDF, JPEG, PNG or WebP and no larger than 10 MB.
- Files are stored on the private local disk with server-generated paths.
- API responses expose file metadata but never expose the private storage path.
- Upload creates a `driver_documents` record, a `files` record and a queued
  `ai_processing_jobs` record.
- Pending or approved documents cannot be duplicated for the same driver and
  document type.

Target response data for driver document endpoints:

```json
{
  "document": {
    "id": 1,
    "driver_id": 1,
    "file_id": 1,
    "type": "identity_card",
    "status": "pending",
    "ai_status": "queued",
    "ai_processing_job_id": 1,
    "rejection_reason": null,
    "expires_at": "2027-07-10",
    "reviewed_by": null,
    "reviewed_at": null,
    "file": {
      "id": 1,
      "uuid": "5fb1b533-d18c-4dc6-8f84-58f2cb14a7e6",
      "owner_id": 10,
      "original_name": "identity.pdf",
      "mime_type": "application/pdf",
      "size_bytes": 122880,
      "checksum": "sha256-checksum",
      "visibility": "private",
      "status": "uploaded"
    },
    "ai_processing_job": {
      "id": 1,
      "uuid": "a43e4f1d-fc31-4d57-9216-75cdb73aa0f7",
      "provider": "openai",
      "task": "driver_document_validation",
      "status": "queued",
      "result": null,
      "error_code": null
    }
  }
}
```

Target request shape for `POST /admin/drivers/documents/{document}/review`:

```json
{
  "status": "rejected",
  "rejection_reason": "The document image is unreadable.",
  "expires_at": "2027-07-10"
}
```

Review rules:

- Only `admin` and `super_admin` roles can review driver documents.
- `status` may only be `approved` or `rejected`.
- `rejection_reason` is required for rejected documents.
- Document review creates a persistent notification for the driver.
- Document review is audited.
- A driver is approved only after every required document type is approved.
- A rejected document marks the driver as rejected with the review reason.
- AI failure never auto-rejects or auto-approves a driver; admin review is
  authoritative.

Implemented status:

- Implemented and tested now: `GET /cities`, `GET /vehicle-types`,
  `GET /drivers`, `POST /drivers`, `GET /drivers/{driver}`,
  `PATCH /drivers/{driver}`, `PATCH /drivers/availability`,
  `GET /vehicles`, `POST /vehicles`, `GET /vehicles/{vehicle}`,
  `PATCH /vehicles/{vehicle}`, `DELETE /vehicles/{vehicle}`,
  `GET /drivers/documents`, `POST /drivers/documents`,
  `GET /drivers/documents/{document}`,
  `DELETE /drivers/documents/{document}` and
  `POST /admin/drivers/documents/{document}/review`.
- Upload responses expose file metadata but not private storage paths.
- Planned but not implemented yet: persistent notification creation after
  document review and the admin review queue UI/API filters.

## 8. Rides and Trips

Ride requests:

- `GET /rides`
- `GET /rides/{ride}`
- `POST /rides`
- `POST /rides/{ride}/accept`
- `POST /rides/{ride}/reject`
- `POST /rides/{ride}/cancel`
- `POST /rides/{ride}/share`

Target request shape for `POST /rides`:

```json
{
  "city_id": 1,
  "vehicle_type_id": 1,
  "pickup_label": "Casa Port, Casablanca",
  "pickup_latitude": 33.5973,
  "pickup_longitude": -7.6149,
  "dropoff_label": "Maarif, Casablanca",
  "dropoff_latitude": 33.5836,
  "dropoff_longitude": -7.6373,
  "payment_method": "cash",
  "scheduled_at": "2026-07-11T10:30:00+01:00"
}
```

Target response data for ride request endpoints:

```json
{
  "ride": {
    "id": 1,
    "uuid": "f32f2f8b-1a46-49ce-a6b4-3e4cccaef100",
    "passenger_id": 10,
    "city_id": 1,
    "vehicle_type_id": 1,
    "pickup_label": "Casa Port, Casablanca",
    "pickup_latitude": "33.5973000",
    "pickup_longitude": "-7.6149000",
    "dropoff_label": "Maarif, Casablanca",
    "dropoff_latitude": "33.5836000",
    "dropoff_longitude": "-7.6373000",
    "requested_at": "2026-07-10T10:30:00.000000Z",
    "scheduled_at": null,
    "estimated_distance_meters": 3082,
    "estimated_duration_seconds": 462,
    "estimated_price": "21.67",
    "payment_method": "cash",
    "status": "requested"
  }
}
```

Target ride request rules:

- `POST /rides` requires an authenticated user with the `passenger` role.
- `city_id` and `vehicle_type_id` must reference active launch records.
- Coordinates are validated by backend Form Request.
- Payment method must be `cash`, `wallet` or `card`.
- The backend computes distance, duration and price estimates from coordinates
  and vehicle pricing. The estimator is isolated in a service so Google Maps
  routing can replace it without moving business logic into the frontend.
- Ride request creation is stored in `ride_requests` and audited as
  `ride_request.created`.
- Ride request creation persists blocking `incoming_ride_request`
  notifications for approved online drivers with matching approved vehicles in
  the ride city.
- Passengers may list and view only their own ride requests.
- Support, admin and super admin users may list and view ride requests for
  operations.

Target response data for `POST /rides/{ride}/accept`:

```json
{
  "trip": {
    "id": 1,
    "uuid": "37d46dc2-c0d4-4b9e-a4dd-069399922929",
    "ride_request_id": 1,
    "passenger_id": 10,
    "driver_id": 5,
    "vehicle_id": 7,
    "status": "accepted",
    "started_at": null,
    "completed_at": null,
    "cancelled_at": null,
    "final_distance_meters": null,
    "final_duration_seconds": null,
    "final_price": null
  }
}
```

Target driver response rules:

- `POST /rides/{ride}/accept` requires an authenticated `driver` user with a
  driver profile.
- The driver must be approved and online.
- The driver must have an approved vehicle matching the ride city and vehicle
  type.
- A driver with an active trip cannot accept another ride.
- Accepting a ride atomically marks the ride request `accepted`, creates a
  `trips` row, creates a `trip_status_events` row, records the driver response,
  updates driver availability to `busy`, resolves incoming ride notifications,
  notifies the passenger with `ride_accepted`, broadcasts
  `trip.status.updated` after commit and audits the action.
- `POST /rides/{ride}/reject` records a durable driver rejection without closing
  the ride for other eligible drivers and marks that driver's incoming ride
  notification acted.
- Duplicate driver responses and stale ride acceptance attempts return `409`.

Trips:

- `GET /trips`
- `GET /trips/{trip}`
- `POST /trips/{trip}/arrived`
- `POST /trips/{trip}/start`
- `POST /trips/{trip}/pause`
- `POST /trips/{trip}/resume`
- `POST /trips/{trip}/finish`
- `POST /trips/{trip}/cancel`

Rules:

- `GET /trips` and `GET /trips/{trip}` are required for passengers, drivers
  and operations roles.
- `POST /trips/{trip}/arrived` transitions `accepted` or `driver_arriving` to
  `driver_arrived`.
- `POST /trips/{trip}/start` transitions `driver_arrived` to `started`.
- `POST /trips/{trip}/pause` transitions `started` to `paused`.
- `POST /trips/{trip}/resume` transitions `paused` to `started`.
- `POST /trips/{trip}/finish` transitions `started` to `completed`, stores
  final distance, duration and server-calculated price, and releases the driver
  to `online` when still approved.
- `POST /trips/{trip}/cancel` transitions active trips to `cancelled`, stores
  cancellation reason in the trip status event, and releases the driver.
- Driver lifecycle actions are restricted to the assigned driver plus admin
  roles. Passenger cancellation is allowed for the passenger's own trip.
- Invalid or duplicate state changes return `409`.
- Direct lifecycle endpoints record trip status events and audit logs. Offline
  replay idempotency is handled by the sync module.
- Trip arrived, started, completed and cancelled transitions persist
  participant notifications and dispatch sanitized Reverb broadcasts after
  database commit.

Realtime channels:

- `private-user.{userId}`: only the authenticated user can subscribe. Used for
  persistent notification delivery and participant trip status updates.
- `private-trip.{tripId}`: only the passenger, assigned driver user, support,
  admin and super admin users can subscribe.
- `private-admin.operations`: restricted to support, admin and super admin
  users.

Broadcast events:

- `.notification.created` on `private-user.{userId}` with the persisted
  notification id, uuid, type, title, body, data, priority, persistence,
  sound key and timestamps.
- `.trip.status.updated` on `private-trip.{tripId}` and participant user
  channels with trip id, uuid, ride request id, passenger id, driver id,
  vehicle id, from status, to status, optional reason and lifecycle timestamps.
- `.trip.location.updated` on `private-trip.{tripId}` with sanitized
  `trip_locations` fields: id, trip id, driver id, latitude, longitude,
  heading, speed, accuracy and timestamps.

Implemented status:

- Implemented and tested now: `GET /rides`, `POST /rides`, `GET /rides/{ride}`,
  `POST /rides/{ride}/accept`, `POST /rides/{ride}/reject`,
  `POST /rides/{ride}/cancel`, `GET /trips`, `GET /trips/{trip}`,
  `POST /trips/{trip}/arrived`, `POST /trips/{trip}/start`,
  `POST /trips/{trip}/pause`, `POST /trips/{trip}/resume`,
  `POST /trips/{trip}/finish` and `POST /trips/{trip}/cancel`.
- Invalid ride and trip state transitions return `409` with a stable `code`.
- Planned but not implemented yet: persisted notifications and Reverb
  broadcasts for ride/trip lifecycle events.

## 9. Tracking and Maps

- `POST /trips/{trip}/location`
- `GET /trips/{trip}/tracking`
- `GET /trips/{trip}/route`
- `GET /trips/{trip}/eta`

Location request minimum fields:

```json
{
  "latitude": 33.5731,
  "longitude": -7.5898,
  "accuracy": 14.2,
  "heading": 90,
  "speed": 22.5,
  "recorded_at": "2026-07-10T10:30:00Z"
}
```

Rules:

- Location updates are authenticated, authorized and rate limited.
- Only trip participants and authorized support/admin users can read tracking.
- `POST /trips/{trip}/location` is required for the assigned driver on
  active trips and is rate limited with the `tracking` limiter.
- Completed or cancelled trips reject new location updates with `409` and code
  `trip_tracking_closed`.
- Location writes persist `trip_locations`, audit `trip.location_recorded` and
  broadcast `.trip.location.updated` on `private-trip.{tripId}`.
- `GET /trips/{trip}/tracking` is required for trip participants and
  operations roles. It returns route context, the last known driver location,
  the latest location history and a stream state.

Target response data for `GET /trips/{trip}/tracking`:

```json
{
  "tracking": {
    "trip_id": 42,
    "status": "started",
    "last_location": {
      "id": 8,
      "trip_id": 42,
      "driver_id": 5,
      "latitude": "33.5731000",
      "longitude": "-7.5898000",
      "heading": "90.00",
      "speed": "22.50",
      "accuracy": "14.20",
      "recorded_at": "2026-07-10T10:30:00.000000Z",
      "created_at": "2026-07-10T10:30:01.000000Z"
    },
    "recent_locations": [],
    "route": {
      "pickup_label": "Casa Port, Casablanca",
      "pickup_latitude": "33.5973000",
      "pickup_longitude": "-7.6149000",
      "dropoff_label": "Maarif, Casablanca",
      "dropoff_latitude": "33.5836000",
      "dropoff_longitude": "-7.6373000",
      "estimated_distance_meters": 3200,
      "estimated_duration_seconds": 900
    },
    "stream_state": "last_known_location"
  }
}
```

## 10. Notifications

- `GET /notifications`
- `GET /notifications/unread-count`
- `PATCH /notifications/{notification}/read`
- `PATCH /notifications/{notification}/acted`
- `PATCH /notifications/read-all`
- `DELETE /notifications/{notification}`
- `GET /notification-preferences`
- `PATCH /notification-preferences`

Target response data for `GET /notifications`:

```json
{
  "notifications": [
    {
      "id": 1,
      "uuid": "4de3d80f-37b2-4b1a-8a8f-61f0f566f370",
      "user_id": 10,
      "type": "driver_arrived",
      "title": "Driver arrived",
      "body": "Your driver has arrived at pickup.",
      "data": {
        "trip_id": 5
      },
      "priority": "high",
      "persistence": "persistent",
      "sound_key": "driver_arrived",
      "read_at": null,
      "acted_at": null,
      "expires_at": null,
      "is_read": false,
      "is_acted": false,
      "is_expired": false,
      "created_at": "2026-07-10T10:30:00.000000Z",
      "updated_at": "2026-07-10T10:30:00.000000Z"
    }
  ],
  "meta": {
    "current_page": 1,
    "last_page": 1,
    "per_page": 15,
    "total": 1
  }
}
```

Target notification API rules:

- Endpoints require an authenticated Sanctum user.
- Users can list, read, act on and delete only their own notifications.
- `GET /notifications/unread-count` excludes expired notifications.
- `PATCH /notifications/{notification}/acted` also marks the notification read
  if it was still unread.
- `PATCH /notifications/read-all` only updates the authenticated user's
  notifications and returns `updated_count`.
- Notification creation broadcasts `.notification.created` on
  `private-user.{userId}` after the database transaction commits. The broadcast
  payload mirrors the API resource's safe notification fields and does not
  replace the persisted notification center.
- Read, acted, read-all and delete actions are audited.

Critical notification types:

- `incoming_ride_request`
- `incoming_call`
- `ride_accepted`
- `driver_arrived`
- `trip_started`
- `trip_completed`
- `trip_cancelled`
- `payment_failed`
- `document_rejected`
- `support_reply`
- `offline_sync_conflict`

Persistence values:

- `ephemeral`
- `persistent`
- `blocking`

Priority values:

- `normal`
- `high`
- `critical`

## 11. Chat and Calls

Conversations:

- `GET /conversations`
- `GET /conversations/{conversation}`
- `POST /conversations`
- `PATCH /conversations/{conversation}/archive`

Messages:

- `POST /messages`
- `PATCH /messages/{message}`
- `DELETE /messages/{message}`
- `POST /messages/upload`
- `POST /messages/read`
- `POST /messages/typing`

Target conversation request for `POST /conversations`:

```json
{
  "type": "ride",
  "trip_id": 42
}
```

Target message request for `POST /messages`:

```json
{
  "conversation_id": 7,
  "type": "text",
  "body": "I am waiting near the main entrance."
}
```

Target chat rules:

- Conversation and message endpoints require Sanctum authentication.
- `POST /conversations` MVP supports `ride` conversations for an
  authorized trip participant or operations user.
- Ride conversation creation is idempotent per trip and automatically adds the
  passenger and assigned driver as participants. Operations users who create the
  conversation are added as participants.
- Only conversation participants can open the conversation, send messages,
  mark it read or send typing state.
- `POST /messages` MVP supports text messages. Messages are persisted
  with `sent` status before broadcast.
- `POST /messages/read` updates the authenticated participant's
  `last_read_at`, marks other participants' sent or delivered messages as
  `read`, audits `conversation.read` and broadcasts `.conversation.read`.
- `POST /messages/typing` broadcasts ephemeral `.conversation.typing` state and
  does not create durable business state.
- Conversation creation, message send and read receipt APIs are rate limited
  with the `communication` limiter.

Target response data for `GET /conversations/{conversation}`:

```json
{
  "conversation": {
    "id": 7,
    "uuid": "c81f50a8-c1f8-49ee-a68f-dc5d7e30178a",
    "type": "ride",
    "trip_id": 42,
    "support_ticket_id": null,
    "status": "active",
    "participants": [
      {
        "id": 3,
        "conversation_id": 7,
        "user_id": 10,
        "last_read_at": "2026-07-10T10:30:00.000000Z",
        "muted_until": null
      }
    ],
    "messages": [
      {
        "id": 12,
        "uuid": "4b8782b5-46d6-427e-8e4f-1bd4ca6f2387",
        "conversation_id": 7,
        "sender_id": 10,
        "type": "text",
        "body": "I am waiting near the main entrance.",
        "file_id": null,
        "status": "sent",
        "created_at": "2026-07-10T10:30:00.000000Z"
      }
    ]
  }
}
```

Calls:

- `POST /calls`
- `POST /calls/{call}/accept`
- `POST /calls/{call}/decline`
- `POST /calls/{call}/end`
- `POST /calls/{call}/signal`

Call types:

- `audio`
- `video`

Target request shape for `POST /calls`:

```json
{
  "conversation_id": 7,
  "recipient_id": 10,
  "type": "audio"
}
```

Target response data for call endpoints:

```json
{
  "call": {
    "id": 18,
    "uuid": "f0b5e9d7-1a8e-486f-8f46-4cb8a232a1f4",
    "conversation_id": 7,
    "caller_id": 9,
    "recipient_id": 10,
    "type": "audio",
    "status": "ringing",
    "started_at": "2026-07-10T10:30:00.000000Z",
    "answered_at": null,
    "ended_at": null,
    "ended_reason": null
  }
}
```

Target signal request for `POST /calls/{call}/signal`:

```json
{
  "type": "offer",
  "payload": {
    "sdp": "v=0..."
  }
}
```

Target call rules:

- Call endpoints require Sanctum authentication and the `communication` rate
  limiter.
- The caller and recipient must both be participants in the conversation.
- Only the recipient can accept or decline a ringing call.
- Caller or recipient can end a ringing or accepted call.
- Invalid call transitions return `409` with `invalid_call_transition`.
- Starting a call stores `call_sessions`, creates a blocking `incoming_call`
  notification and audits `call.started`.
- Accept, decline and end actions are audited and broadcast as `.call.updated`.
- Signaling is allowed only for caller or recipient while the session is
  `ringing` or `accepted`; signals are audited and broadcast as `.call.signal`
  on `presence-call.{callSessionId}` without being stored as durable media.

## 12. Uploads and AI

Files:

- `POST /files`
- `GET /files/{file}`
- `DELETE /files/{file}`

AI:

- `POST /ai/ocr`
- `POST /ai/image-analysis`
- `POST /ai/document-validation`
- `POST /ai/complaint-summary`
- `GET /ai/jobs/{job}`

Rules:

- Uploads validate file type, MIME type, size and ownership.
- AI endpoints return `202` when queued.
- AI results are not treated as final approval for sensitive driver decisions.

## 13. Offline Sync

- `POST /sync/actions`
- `POST /sync/actions/batch`
- `GET /sync/actions`
- `GET /sync/actions/{syncAction}`

Request:

```json
{
  "idempotency_key": "driver-42-trip-981-start-20260710T103000Z",
  "action_type": "trip_start",
  "payload": {
    "trip_id": 981,
    "client_created_at": "2026-07-10T10:30:00Z"
  }
}
```

Supported action types:

- `trip_start`
- `trip_complete`

Target response data:

```json
{
  "sync_action": {
    "id": 22,
    "uuid": "7c05255f-e913-4c6d-80db-bbde8a14e226",
    "user_id": 10,
    "idempotency_key": "driver-42-trip-981-start-20260710T103000Z",
    "action_type": "trip_start",
    "payload": {
      "trip_id": 981,
      "client_created_at": "2026-07-10T10:30:00Z"
    },
    "status": "applied",
    "conflict_reason": null,
    "processed_at": "2026-07-10T10:31:00.000000Z"
  },
  "trip": {},
  "duplicate": false
}
```

Target rules:

- Sync endpoints require Sanctum authentication.
- `idempotency_key` is required and globally unique.
- `trip_start` replays the same backend transition as
  `POST /trips/{trip}/start`.
- `trip_complete` replays the same backend transition as
  `POST /trips/{trip}/finish`.
- Duplicate idempotency keys owned by the same user return the existing
  `sync_actions` row without repeating trip side effects.
- The backend revalidates trip authorization and current state during replay.
- Single-action conflicts return HTTP `409` with code
  `offline_sync_conflict`; batch requests return per-action `conflict` status
  in the result list.
- Applied and conflict outcomes are audited as `sync.applied` and
  `sync.conflict`.

## 14. Payments, Wallet and Invoices

Payments:

- `GET /payments`
- `POST /payments`
- `GET /payments/{payment}`
- `POST /payments/{payment}/confirm-cash`
- `POST /payments/webhooks/stripe`

Target request shape for `POST /payments`:

```json
{
  "trip_id": 42,
  "provider": "cash"
}
```

Target response data:

```json
{
  "payment": {
    "id": 18,
    "uuid": "a0c68efa-a0b8-4f28-9a54-8182276ce0ad",
    "trip_id": 42,
    "user_id": 10,
    "provider": "cash",
    "provider_reference": "cash-trip-42",
    "amount": "29.00",
    "currency": "MAD",
    "status": "pending",
    "paid_at": null,
    "invoice": null
  }
}
```

Cash payment rules:

- Payment endpoints require Sanctum authentication.
- `POST /payments` MVP supports `cash` only.
- Cash payment amount is always derived from the completed trip's
  `final_price`; frontend amount or status is ignored.
- Payment creation requires a completed trip and participant or operations
  authorization.
- Creating a cash payment for the same trip reuses the existing cash payment.
- Assigned driver, admin or super admin can confirm cash collection.
- Cash confirmation marks the payment `completed`, records a completed cash
  transaction and creates one invoice for the payment.
- Re-confirming an already completed cash payment returns the existing
  completed payment and does not duplicate transaction or invoice rows.
- Wallet payment, Stripe payment creation and Stripe webhook verification are
  required payment capabilities and must follow the rules below.

Wallet:

- `GET /wallet`
- `GET /wallet/history`
- `POST /wallet/deposit`
- `POST /wallet/withdraw`

Transactions:

- `GET /transactions`
- `GET /transactions/{transaction}`

Invoices:

- `GET /invoices`
- `GET /invoices/{invoice}`
- `GET /invoices/{invoice}/download`

Webhook rules:

- Stripe signatures are verified.
- Provider event IDs are stored.
- Processing is idempotent.
- Payment failures generate persistent notifications.

## 15. Realtime Channels

- `private-user.{userId}`
- `private-trip.{tripId}`
- `private-conversation.{conversationId}`
- `presence-call.{callSessionId}`
- `private-admin.operations`

Every channel authorization callback must check the authenticated user's
relationship to the resource.

Target conversation broadcasts:

- `.message.sent` on `private-conversation.{conversationId}` with sanitized
  message id, uuid, conversation id, sender id/name, type, body, status and
  timestamps.
- `.conversation.read` on `private-conversation.{conversationId}` with
  conversation id, reader id and read timestamp.
- `.conversation.typing` on `private-conversation.{conversationId}` with
  conversation id, user id and typing state.
- `.call.ringing` on `private-conversation.{conversationId}` and
  `private-user.{recipientId}` with sanitized call session fields.
- `.call.updated` on `private-conversation.{conversationId}` and
  `presence-call.{callSessionId}` with sanitized call session fields.
- `.call.signal` on `presence-call.{callSessionId}` with call id,
  conversation id, sender id, signal type and short-lived signal payload.
