Frontend API Reference
Complete reference for frontend developers — authentication, endpoints, models, enums, and integration patterns.
Shipping System — Frontend API Reference
Detailed reference for frontend developers integrating with the Admin API.
Scope: The Admin API (
/api/admin/*) covers authentication, admin/role/permission management, drivers, vehicles, driver unavailabilities, driver–vehicle assignments, users, shipments, shipment stops, proof of deliveries, failed deliveries, routes, and route stops.
Table of Contents
- Quick Start
- Global Conventions
- Authentication
- Profile
- Admins
- Roles & Permissions
- Drivers
- Vehicles
- Driver Unavailabilities
- Driver–Vehicle Assignments
- Users (Customers)
- Shipments
- Shipment Stops
- Proof of Deliveries
- Failed Deliveries
- Routes
- Route Stops
- Domain Models Reference
- Enums Reference
- Permissions Reference
- Error Handling
- Frontend Integration Checklist
Quick Start
POST /api/admin/login
Content-Type: application/json
Accept-Language: en
{
"email": "mousa@example.com",
"password": "password"
}
Store the returned JWT and send it on every subsequent request:
GET /api/admin/drivers
Authorization: Bearer {token}
Accept: application/json
Accept-Language: en
Base URL: {APP_URL}/api — e.g. http://localhost:8000/api when running php artisan serve.
Global Conventions
Request Headers
| Header | Required | Description |
|---|---|---|
Authorization |
Yes (except login) | Bearer {jwt_token} |
Accept |
Recommended | application/json |
Accept-Language |
Optional | ar (default) or en — controls translated message text |
Content-Type |
Varies | application/json for JSON bodies; multipart/form-data when uploading images |
Response Envelope
Every API response uses this structure:
{
"status": "Success",
"message": "Data fetched successfully",
"data": {},
"statusCode": 200
}
| Field | Type | Description |
|---|---|---|
status |
"Success" | "Error" |
Outcome indicator |
message |
string | Human-readable, localized message |
data |
object | array | null | Payload (null on delete/logout) |
statusCode |
integer | HTTP status code echoed in body |
On login success, the JWT is also returned in the Authorization response header as Bearer {token}.
HTTP Methods
Updates use POST, not PUT or PATCH.
All update endpoints follow the pattern POST /api/admin/{resource}/{id}.
Content Types
| Scenario | Content-Type |
|---|---|
| JSON CRUD (no file) | application/json |
| Create/update with image (admin, driver, profile) | multipart/form-data |
| Assign/unassign vehicle | No body required |
When using multipart/form-data:
- Send scalar fields as form fields.
- Send
working_daysas repeated fields or JSON string (array). - Send
roles/permissionsas repeated fields or JSON array string. - Send
imageas a file field (max 5 MB, must be an image).
Listing, Filtering & Sorting
Most GET list endpoints accept query string parameters:
| Parameter | Type | Description |
|---|---|---|
search |
string | Full-text search across model-specific columns |
sort_by |
string | Column name (must be in model's sortable list) |
sort_direction |
"asc" | "desc" |
Sort direction (default varies by model) |
page |
integer | Page number (paginated endpoints only) |
Query string values "null" and "" are automatically converted to null by middleware.
Pagination
Paginated endpoints (GET .../paginated) wrap results like this:
{
"drivers": [],
"total": 50,
"count": 10,
"per_page": 10,
"current_page": 1,
"total_pages": 5,
"links": {
"first": "http://localhost:8000/api/admin/drivers/paginated?page=1",
"last": "http://localhost:8000/api/admin/drivers/paginated?page=5",
"prev": null,
"next": "http://localhost:8000/api/admin/drivers/paginated?page=2"
}
}
The collection key matches the resource name:
| Endpoint prefix | Collection key |
|---|---|
/admins/paginated |
admins |
/roles/paginated |
roles |
/drivers/paginated |
drivers |
/vehicles/paginated |
vehicles |
/users/paginated |
users |
/driver-unavailabilities/paginated |
driver_unavailabilities |
/shipments/paginated |
shipments |
/shipment-stops/paginated |
shipment_stops |
/routes/paginated |
routes |
Page size is fixed at 10 per page.
Authentication
All routes under /api/admin/* except POST /login require a valid JWT in the Authorization header.
- Guard:
admin - Library: JWT Auth (
tymon/jwt-auth) - Token TTL: Returned as
expires_in(seconds) in login response. Configured viaJWT_TTLenv (minutes); if unset, token may not expire.
POST /api/admin/login
Authenticate an admin and receive a JWT.
Auth required: No
Request body:
| Field | Type | Required | Validation |
|---|---|---|---|
email |
string | Yes | Valid email |
password |
string | Yes | Non-empty string |
fcm_token |
string | No | Push notification device token; stored on admin record |
Example request body:
{
"email": "mousa@example.com",
"password": "SecurePass123!",
"fcm_token": "device-fcm-token-abc123"
}
Success response 200:
{
"status": "Success",
"message": "User successfully signed in",
"data": {
"admin": {
"id": 1,
"name": "mousa",
"email": "mousa@example.com",
"roles": ["operations_manager"],
"permission_groups": [
{
"group": "Driver",
"group_label": "Drivers",
"permissions": [
{
"id": 1,
"name": "ViewAny:Driver",
"display_name": "View Any",
"group": "Driver"
}
]
}
]
},
"token": "eyJ0eXAiOiJKV1QiLCJhbGc...",
"token_type": "bearer",
"expires_in": 3600
},
"statusCode": 200
}
Error responses:
| Status | When |
|---|---|
401 |
Invalid email/password (credentialsError) |
422 |
Validation failed (missing email/password) |
500 |
Token generation failed (couldNotCreateToken) |
POST /api/admin/logout
Invalidate the current JWT session.
Auth required: Yes
Request body: None
Success response 200:
{
"status": "Success",
"message": "User successfully signed out",
"data": null,
"statusCode": 200
}
Error responses:
| Status | When |
|---|---|
403 |
No token provided (Unauthenticated) |
500 |
Logout failed (couldNotLogout) |
Profile
Manage the currently authenticated admin's own profile. Does not require admin-management permissions.
GET /api/admin/profile
Permission required: None (authentication only)
Success response 200: Returns an AdminResource (see Admins) with:
is_current_admin: truepermission_groupsincludedrolesincluded
POST /api/admin/profile
Update the authenticated admin's profile.
Permission required: None (authentication only)
Request body (JSON or multipart):
| Field | Type | Required | Validation |
|---|---|---|---|
name |
string | No | Max 255 chars |
email |
string | No | Valid email, unique among admins |
password |
string | No | Laravel password defaults |
image |
file | No | Image, max 5 MB |
Example request body:
{
"name": "mousa",
"email": "mousa.updated@example.com",
"password": "NewSecurePass123!"
}
Success response 200: Updated AdminResource with message profileUpdatedSuccessfully.
Dashboard Stats
Aggregated KPI statistics for the admin operations dashboard. Both endpoints require the View:Dashboard permission (or super-admin).
GET /api/admin/dashboard/overview
High-level KPI cards for today, this week, and this month. No query parameters.
Permission required: View:Dashboard
Success response 200:
{
"status": "Success",
"message": "...",
"data": {
"shipments_created_today": 12,
"shipments_created_this_week": 48,
"shipments_created_this_month": 190,
"active_routes": 5,
"active_drivers": 18,
"pending_pickups": 9,
"out_for_delivery": 14,
"delivered_today": 22
},
"statusCode": 200
}
| Field | Description |
|---|---|
shipments_created_today |
Shipments created on the current date |
shipments_created_this_week |
Shipments created since Monday of the current week |
shipments_created_this_month |
Shipments created since the 1st of the current month |
active_routes |
Routes with status active |
active_drivers |
Drivers with is_active = true |
pending_pickups |
Shipments in pending or pickup_in_progress |
out_for_delivery |
Shipments in out_for_delivery |
delivered_today |
Shipments delivered today (via POD delivered_at, fallback updated_at) |
GET /api/admin/dashboard/trends
Daily time-series for charts, spanning 7 or 30 days.
Permission required: View:Dashboard
Query parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
days |
integer 7 or 30 |
No | Number of days to include (defaults to 30) |
Success response 200:
{
"data": {
"days": 30,
"from": "2026-06-20",
"to": "2026-07-19",
"series": [
{
"date": "2026-06-20",
"shipments_created": 8,
"shipments_delivered": 6,
"failed_deliveries": 1,
"failure_rate": 0.1429
}
]
}
}
- Every calendar day in the range is present (zeros when no activity).
failure_rate=failed_deliveries / (shipments_delivered + failed_deliveries), or0when both are 0.shipments_delivereduses PODdelivered_atwhen available, falling back to the shipment'supdated_at.
Validation error 422: Returned when days is not 7 or 30.
Admins
Back-office user management.
Permission prefix: Admin (e.g. ViewAny:Admin, Create:Admin)
Hidden admin accounts (configured via HIDDEN_ADMIN env) are excluded from listings.
Endpoints
| Method | Path | Description | Permission |
|---|---|---|---|
GET |
/api/admin/admins |
List all admins | ViewAny:Admin |
GET |
/api/admin/admins/paginated |
List paginated | ViewAny:Admin |
GET |
/api/admin/admins/{id} |
Get single admin | View:Admin |
POST |
/api/admin/admins |
Create admin | Create:Admin |
POST |
/api/admin/admins/{id} |
Update admin | Update:Admin |
DELETE |
/api/admin/admins/{id} |
Delete admin | Delete:Admin |
List Query Parameters
| Parameter | Description |
|---|---|
search |
Searches name, email |
role |
Filter by role name (exact match) |
sort_by |
name, email, created_at |
sort_direction |
asc or desc (default: asc by name) |
Create Request Body
| Field | Type | Required | Validation |
|---|---|---|---|
name |
string | Yes | Max 255 |
email |
string | Yes | Valid email, unique |
password |
string | Yes | Laravel password defaults |
image |
file | No | Image, max 5 MB |
roles |
string[] | No | Array of existing role names |
Example request body:
{
"name": "mousa",
"email": "mousa@example.com",
"password": "SecurePass123!",
"roles": ["operations_manager"]
}
Update Request Body
Same fields as create, all optional (sometimes rules apply). Omit password to keep current password. Send roles: [] to remove all roles.
Example request body:
{
"name": "mousa",
"email": "mousa.admin@example.com",
"roles": ["super_admin"]
}
Admin Resource Shape
{
"id": 1,
"name": "mousa",
"email": "mousa@example.com",
"image": "http://localhost:8000/storage/1/admin_image.jpg",
"roles": ["super_admin"],
"permission_groups": [],
"is_current_admin": false,
"created_at": "2026-01-15T10:00:00.000000Z",
"updated_at": "2026-01-15T10:00:00.000000Z"
}
| Field | Notes |
|---|---|
image |
Full URL to profile image, or empty string if none |
roles |
Array of role name strings |
permission_groups |
Only included on GET /profile and GET /admins/{id} |
is_current_admin |
true if this admin is the one making the request |
Delete Constraints
- Cannot delete your own account →
403with messagecannotDeleteCurrentAdmin
Roles & Permissions
Role-based access control using Spatie Permission (guard: admin).
Permission prefixes: Role, Permission
Endpoints
| Method | Path | Description | Permission |
|---|---|---|---|
GET |
/api/admin/permissions |
All permissions grouped | ViewAny:Permission |
GET |
/api/admin/roles |
List all roles | ViewAny:Role |
GET |
/api/admin/roles/paginated |
List paginated | ViewAny:Role |
GET |
/api/admin/roles/{id} |
Get single role | View:Role |
POST |
/api/admin/roles |
Create role | Create:Role |
POST |
/api/admin/roles/{id} |
Update role | Update:Role |
POST |
/api/admin/roles/{id}/permissions |
Sync permissions only | Update:Role |
DELETE |
/api/admin/roles/{id} |
Delete role | Delete:Role |
List Query Parameters (roles)
| Parameter | Description |
|---|---|
search |
Searches role name |
sort_by |
name, created_at |
sort_direction |
asc or desc (default: asc by name) |
Create Role Request Body
| Field | Type | Required | Validation |
|---|---|---|---|
name |
string | Yes | Unique per guard, max 255 |
permissions |
string[] | No | Array of permission name strings |
Example request body:
{
"name": "warehouse_manager",
"permissions": ["ViewAny:Driver", "Create:Driver", "ViewAny:Vehicle"]
}
Update Role Request Body
| Field | Type | Required | Validation |
|---|---|---|---|
name |
string | No | Unique per guard, max 255 |
permissions |
string[] | No | Replaces all permissions when provided |
Example request body:
{
"name": "warehouse_manager",
"permissions": ["ViewAny:Driver", "Update:Driver"]
}
Sync Permissions Request Body
POST /api/admin/roles/{id}/permissions
| Field | Type | Required | Validation |
|---|---|---|---|
permissions |
string[] | Yes | Array of permission name strings |
Example request body:
{
"permissions": [
"ViewAny:Driver",
"Create:Driver",
"ViewAny:Vehicle",
"Create:Vehicle"
]
}
Success message: permissionsAssignedSuccessfully
Role Resource Shape
{
"id": 2,
"name": "warehouse_manager",
"guard_name": "admin",
"permission_groups": [
{
"group": "Driver",
"group_label": "Drivers",
"permissions": [
{
"id": 1,
"name": "ViewAny:Driver",
"display_name": "View Any",
"group": "Driver"
}
]
}
],
"created_at": "2026-01-15T10:00:00.000000Z",
"updated_at": "2026-01-15T10:00:00.000000Z"
}
Permission Group Shape
Used in login, profile, roles, and the permissions list:
{
"group": "Driver",
"group_label": "Drivers",
"permissions": [
{
"id": 1,
"name": "ViewAny:Driver",
"display_name": "View Any",
"group": "Driver"
}
]
}
Constraints
- Super admin role cannot be modified or deleted →
403(cannotModifySuperAdmin)
Drivers
Delivery driver management.
Permission prefix: Driver
Endpoints
| Method | Path | Description | Permission |
|---|---|---|---|
GET |
/api/admin/drivers |
List all drivers | ViewAny:Driver |
GET |
/api/admin/drivers/paginated |
List paginated | ViewAny:Driver |
GET |
/api/admin/drivers/{id} |
Get single driver | View:Driver |
POST |
/api/admin/drivers |
Create driver | Create:Driver |
POST |
/api/admin/drivers/{id} |
Update driver | Update:Driver |
DELETE |
/api/admin/drivers/{id} |
Delete driver | Delete:Driver |
GET |
/api/admin/drivers/{id}/vehicle-assignments |
Vehicle assignments | ViewAny:DriverVehicleAssignment |
GET |
/api/admin/drivers/{id}/unavailabilities |
Unavailability periods | ViewAny:DriverUnavailability |
POST |
/api/admin/drivers/{driverId}/vehicles/{vehicleId}/assign |
Assign vehicle | Create:DriverVehicleAssignment |
POST |
/api/admin/drivers/{driverId}/vehicles/{vehicleId}/unassign |
Unassign vehicle | Update:DriverVehicleAssignment |
List Query Parameters
| Parameter | Type | Description |
|---|---|---|
search |
string | Searches name, phone, email, driver_number, license_number |
is_active |
boolean | Filter by active status |
lat |
float | Latitude for nearby search (requires lng) |
lng |
float | Longitude for nearby search (requires lat) |
radius |
float | Search radius in km (default: 10) |
work_start_time |
time | Filter drivers whose shift overlaps this start time |
work_end_time |
time | Filter drivers whose shift overlaps this end time |
work_start_time_from |
time | Minimum shift start time |
work_start_time_to |
time | Maximum shift start time |
work_end_time_from |
time | Minimum shift end time |
work_end_time_to |
time | Maximum shift end time |
sort_by |
string | See sortable columns below |
sort_direction |
string | asc or desc |
Sortable columns: name, phone, driver_number, work_start_time, work_end_time, created_at, distance
When lat/lng are provided, results include a distance field (km) and default-sort by nearest first unless sort_by is explicitly set.
Time format: HH:MM:SS or HH:MM (auto-normalized to HH:MM:SS).
Create Request Body
| Field | Type | Required | Validation |
|---|---|---|---|
name |
string | Yes | Max 255 |
phone |
string | Yes | Max 50 |
phone_country |
string | Yes | Max 10 (e.g. +963) |
email |
string | No | Valid email, unique |
password |
string | Yes | Laravel password defaults |
address |
string | Yes | |
license_number |
string | No | Max 255 |
working_days |
string[] | Yes | Min 1 item; see Working Days |
work_start_time |
time | Yes | Must differ from work_end_time |
work_end_time |
time | Yes | Must differ from work_start_time |
lat |
float | No | -90 to 90 |
lng |
float | No | -180 to 180 |
fcm_token |
string | No | Push notification token |
is_active |
boolean | No | Default: true |
image |
file | No | Image, max 5 MB |
vehicle_id |
integer | No | Must exist in vehicles; assigns vehicle on create when provided |
driver_number |
string | No | Unique; auto-generated 6-digit number if omitted |
Example request body:
{
"name": "mousa",
"phone": "944123456",
"phone_country": "+963",
"email": "mousa.driver@example.com",
"password": "SecurePass123!",
"address": "Damascus, Syria",
"license_number": "SY-12345",
"working_days": ["sun", "mon", "tue", "wed", "thu"],
"work_start_time": "08:00:00",
"work_end_time": "17:00:00",
"lat": 33.5138,
"lng": 36.2765,
"is_active": true,
"vehicle_id": 7
}
Vehicle assignment on update: send vehicle_id to assign or switch vehicles; send vehicle_id: null to release the current assignment. Omit the field to leave the assignment unchanged.
Update Request Body
Same fields as create. All fields optional except where sometimes + required applies. Password optional (omit to keep current).
Example request body:
{
"name": "mousa",
"phone": "944654321",
"phone_country": "+963",
"work_start_time": "09:00:00",
"work_end_time": "18:00:00",
"is_active": true
}
Driver Resource Shape
{
"id": 1,
"driver_number": "482910",
"name": "mousa",
"phone": "944123456",
"phone_country": "+963",
"email": "mousa.driver@example.com",
"image": "http://localhost:8000/storage/2/driver_image.jpg",
"address": "Damascus, Syria",
"license_number": "DL-12345",
"working_days": ["sun", "mon", "tue", "wed", "thu"],
"work_start_time": "08:00:00",
"work_end_time": "17:00:00",
"lat": 24.7136,
"lng": 46.6753,
"is_active": true,
"created_at": "2026-01-15T10:00:00.000000Z",
"updated_at": "2026-01-15T10:00:00.000000Z",
"distance": 3.2,
"vehicle": {
"id": 7,
"plate_number": "ABC-1234",
"owner_type": "driver",
"owner_type_label": "سائق",
"owner_id": 1,
"max_weight": 500,
"max_volume": 10,
"is_active": true,
"created_at": "2026-01-15T10:00:00.000000Z",
"updated_at": "2026-01-15T10:00:00.000000Z"
}
}
| Field | Notes |
|---|---|
distance |
Only present when nearby filter (lat/lng) is used |
working_days |
JSON array of day codes |
image |
Full URL or empty string |
vehicle |
Nested VehicleResource for the driver's current active assignment (released_at is null); null when no vehicle is assigned |
Vehicles
Fleet vehicle management.
Permission prefix: Vehicle
Endpoints
| Method | Path | Description | Permission |
|---|---|---|---|
GET |
/api/admin/vehicles |
List all vehicles | ViewAny:Vehicle |
GET |
/api/admin/vehicles/paginated |
List paginated | ViewAny:Vehicle |
GET |
/api/admin/vehicles/{id} |
Get single vehicle | View:Vehicle |
POST |
/api/admin/vehicles |
Create vehicle | Create:Vehicle |
POST |
/api/admin/vehicles/{id} |
Update vehicle | Update:Vehicle |
DELETE |
/api/admin/vehicles/{id} |
Delete vehicle | Delete:Vehicle |
List Query Parameters
| Parameter | Description |
|---|---|
search |
Searches plate_number |
owner_type |
Exact match: company or driver |
is_active |
Boolean filter |
sort_by |
plate_number, owner_type, max_weight, max_volume, created_at |
sort_direction |
asc or desc (default: asc by plate_number) |
Create Request Body
| Field | Type | Required | Validation |
|---|---|---|---|
plate_number |
string | Yes | Unique, max 255 |
owner_type |
string | Yes | company or driver — see OwnerTypes |
owner_id |
integer | Conditional | Required when owner_type is driver; must be a valid driver ID. Must not be sent when owner_type is company |
max_weight |
number | Yes | Min 0 (kg) |
max_volume |
number | Yes | Min 0 (m³) |
Example request body:
{
"plate_number": "DMS-1234",
"owner_type": "company",
"max_weight": 1500,
"max_volume": 12.5
}
Example request body (driver-owned):
{
"plate_number": "DMS-5678",
"owner_type": "driver",
"owner_id": 1,
"max_weight": 2000,
"max_volume": 15
}
Update Request Body
| Field | Type | Required | Validation |
|---|---|---|---|
plate_number |
string | No | Unique, max 255 |
owner_type |
string | No | company or driver |
owner_id |
integer | Conditional | Same rules as create |
max_weight |
number | No | Min 0 |
max_volume |
number | No | Min 0 |
is_active |
boolean | No |
Example request body:
{
"plate_number": "DMS-9999",
"max_weight": 1800,
"max_volume": 14,
"is_active": false
}
Vehicle Resource Shape
{
"id": 1,
"plate_number": "ABC-1234",
"owner_type": "company",
"owner_type_label": "الشركة",
"owner_id": null,
"max_weight": 1500,
"max_volume": 12.5,
"is_active": true,
"created_at": "2026-01-15T10:00:00.000000Z",
"updated_at": "2026-01-15T10:00:00.000000Z"
}
| Field | Notes |
|---|---|
owner_type_label |
Localized Arabic label (API always returns Arabic label regardless of Accept-Language) |
owner_id |
null for company-owned vehicles; driver ID for driver-owned |
Driver Unavailabilities
Time-off / unavailability blocks for drivers.
Permission prefix: DriverUnavailability
Endpoints
| Method | Path | Description | Permission |
|---|---|---|---|
GET |
/api/admin/driver-unavailabilities |
List all | ViewAny:DriverUnavailability |
GET |
/api/admin/driver-unavailabilities/paginated |
List paginated | ViewAny:DriverUnavailability |
GET |
/api/admin/driver-unavailabilities/{id} |
Get single | View:DriverUnavailability |
POST |
/api/admin/driver-unavailabilities |
Create | Create:DriverUnavailability |
POST |
/api/admin/driver-unavailabilities/{id} |
Update | Update:DriverUnavailability |
DELETE |
/api/admin/driver-unavailabilities/{id} |
Delete | Delete:DriverUnavailability |
Also accessible via GET /api/admin/drivers/{id}/unavailabilities.
List Query Parameters
| Parameter | Description |
|---|---|
driver_id |
Filter by driver ID (exact) |
sort_by |
start_date, end_date, created_at |
sort_direction |
Default: desc by start_date |
Create Request Body
| Field | Type | Required | Validation |
|---|---|---|---|
driver_id |
integer | Yes | Must exist in drivers table |
start_date |
date | Yes | ISO date or datetime |
end_date |
date | Yes | Must be after start_date |
reason |
string | No | Free text |
Example request body:
{
"driver_id": 1,
"start_date": "2026-06-01",
"end_date": "2026-06-05",
"reason": "Annual leave"
}
Update Request Body
Same fields, all optional. If both dates are provided on update, end_date must still be after start_date.
Example request body:
{
"start_date": "2026-06-01",
"end_date": "2026-06-10",
"reason": "Extended leave"
}
Driver Unavailability Resource Shape
{
"id": 1,
"driver_id": 5,
"start_date": "2026-06-01T00:00:00.000000Z",
"end_date": "2026-06-05T00:00:00.000000Z",
"reason": "Annual leave",
"created_at": "2026-05-20T10:00:00.000000Z",
"updated_at": "2026-05-20T10:00:00.000000Z"
}
Driver–Vehicle Assignments
Links drivers to vehicles for operational use. A vehicle can only have one active assignment at a time (released_at === null).
Permission prefix: DriverVehicleAssignment
Endpoints
| Method | Path | Description | Permission |
|---|---|---|---|
GET |
/api/admin/drivers/{id}/vehicle-assignments |
List assignments for driver | ViewAny:DriverVehicleAssignment |
POST |
/api/admin/drivers/{driverId}/vehicles/{vehicleId}/assign |
Assign vehicle to driver | Create:DriverVehicleAssignment |
POST |
/api/admin/drivers/{driverId}/vehicles/{vehicleId}/unassign |
Release assignment | Update:DriverVehicleAssignment |
Assign
No request body. Creates a record with:
assigned_at= current timestampreleased_at=null
Error: 422 if vehicle already has an active assignment (vehicleAlreadyAssigned)
Unassign
No request body. Sets released_at to current timestamp on the active assignment.
Error: 404 if no active assignment exists (vehicleAssignmentNotFound)
Driver Vehicle Assignment Resource Shape
{
"id": 1,
"driver_id": 3,
"vehicle_id": 7,
"assigned_at": "2026-06-01T08:00:00.000000Z",
"released_at": null,
"vehicle": {
"id": 7,
"plate_number": "XYZ-5678",
"owner_type": "company",
"owner_type_label": "الشركة",
"owner_id": null,
"max_weight": 2000,
"max_volume": 15,
"is_active": true,
"created_at": "...",
"updated_at": "..."
},
"driver": {
"id": 3,
"driver_number": "482910",
"name": "mousa"
},
"created_at": "2026-06-01T08:00:00.000000Z",
"updated_at": "2026-06-01T08:00:00.000000Z"
}
| Field | Notes |
|---|---|
vehicle |
Nested VehicleResource when relation is loaded |
driver |
Nested DriverResource when relation is loaded |
released_at |
null = currently assigned |
Users (Customers)
End customers who place shipments. These are not admin accounts.
Permission prefix: User
Endpoints
| Method | Path | Description | Permission |
|---|---|---|---|
GET |
/api/admin/users |
List all users | ViewAny:User |
GET |
/api/admin/users/paginated |
List paginated | ViewAny:User |
GET |
/api/admin/users/{id} |
Get single user | View:User |
POST |
/api/admin/users |
Create user | Create:User |
POST |
/api/admin/users/{id} |
Update user | Update:User |
DELETE |
/api/admin/users/{id} |
Delete user | Delete:User |
List Query Parameters
| Parameter | Description |
|---|---|
search |
Searches name, phone, email, company_name |
sort_by |
name, phone, email, company_name, created_at |
sort_direction |
asc or desc (default: asc by name) |
Create Request Body
| Field | Type | Required | Validation |
|---|---|---|---|
name |
string | Yes | Max 255 |
phone |
string | Yes | Max 50; unique per phone_country |
phone_country |
string | Yes | Max 10 |
email |
string | No | Valid email, unique |
company_name |
string | No |
Example request body:
{
"name": "mousa",
"phone": "944123456",
"phone_country": "+963",
"email": "mousa@example.com",
"company_name": "Mousa Trading Co."
}
Update Request Body
Same fields, all optional. Phone uniqueness is re-validated when phone or phone_country changes.
Example request body:
{
"name": "mousa",
"phone": "944654321",
"phone_country": "+963",
"company_name": "Mousa Logistics"
}
User Resource Shape
{
"id": 1,
"name": "mousa",
"phone": "944123456",
"phone_country": "+963",
"email": "mousa@example.com",
"company_name": "Mousa Trading Co.",
"created_at": "2026-01-15T10:00:00.000000Z",
"updated_at": "2026-01-15T10:00:00.000000Z"
}
Shipments
Full shipment lifecycle management — create, update, confirm, cancel, assign to route, and bulk operations.
Permission prefix: Shipment
Endpoints
| Method | Path | Description | Permission |
|---|---|---|---|
GET |
/api/admin/shipments |
List all shipments | ViewAny:Shipment |
GET |
/api/admin/shipments/paginated |
List paginated | ViewAny:Shipment |
GET |
/api/admin/shipments/{id} |
Get single shipment (id or shipment_number) | View:Shipment |
POST |
/api/admin/shipments |
Create shipment | Create:Shipment |
POST |
/api/admin/shipments/create-with-user |
Upsert user + create shipment | Create:Shipment |
POST |
/api/admin/shipments/{id} |
Update shipment | Update:Shipment |
DELETE |
/api/admin/shipments/{id} |
Delete draft shipment | Delete:Shipment |
POST |
/api/admin/shipments/{id}/confirm |
Confirm draft → pending | Update:Shipment |
POST |
/api/admin/shipments/{id}/cancel |
Cancel shipment | Update:Shipment |
POST |
/api/admin/shipments/{id}/status |
Admin status override | Update:Shipment |
POST |
/api/admin/shipments/{id}/assign-route |
Assign to a route | Update:Shipment |
POST |
/api/admin/shipments/{id}/unassign-route |
Remove from route | Update:Shipment |
GET |
/api/admin/shipments/{id}/events |
List shipment events | View:Shipment |
POST |
/api/admin/shipments/bulk/cancel |
Bulk cancel | Create:Shipment |
POST |
/api/admin/shipments/bulk/confirm |
Bulk confirm | Create:Shipment |
POST |
/api/admin/shipments/bulk/assign-route |
Bulk assign route | Create:Shipment |
List Query Parameters
| Parameter | Type | Description |
|---|---|---|
search |
string | Searches shipment_number, user_name, notes |
status |
string | Exact match — see ShipmentStatuses |
type |
string | Exact match — see ShipmentTypes |
user_id |
integer | Filter by customer |
payment_type |
string | Exact match — see PaymentTypes |
priority |
string | Exact match — see ShipmentPriorities |
delivery_date_from |
date | Filter delivery date range start |
delivery_date_to |
date | Filter delivery date range end |
sort_by |
string | shipment_number, delivery_date, status, created_at |
sort_direction |
string | asc or desc (default: desc by created_at) |
Get Single Shipment
GET /api/admin/shipments/{id}
{id} accepts either the numeric primary key or the shipment_number (e.g. SH-AB12CD34).
Create Request Body
| Field | Type | Required | Validation |
|---|---|---|---|
user_id |
integer | Yes | Must exist in users |
delivery_date |
date | Yes | Must be after today |
payment_type |
string | Yes | prepaid or cod |
cod_amount |
number | Conditional | Required when payment_type = cod; min 0 |
notes |
string | No | Optional |
priority |
string | No | low, normal, high, or urgent — see ShipmentPriorities; defaults to normal |
stops |
array | Yes | Min 2 items; exactly 1 pickup, exactly 1 delivery |
stops.*.type |
string | Yes | pickup, delivery, or stop |
stops.*.country |
string | Yes | |
stops.*.city |
string | Yes | |
stops.*.lat |
number | Yes | -90 to 90 |
stops.*.lng |
number | Yes | -180 to 180 |
stops.*.sequence |
integer | Yes | Min 1 |
stops.*.contact_name |
string | No | |
stops.*.phone |
string | No | |
stops.*.phone_country |
string | No | |
stops.*.area |
string | No | |
stops.*.street |
string | No | |
stops.*.building |
string | No | |
stops.*.floor |
string | No | |
stops.*.apartment |
string | No | |
stops.*.full_address |
string | No | |
items |
array | Yes | Min 1 item |
items.*.name |
string | Yes | |
items.*.quantity |
integer | Yes | Min 1 |
items.*.weight |
number | No | kg |
items.*.length |
number | No | cm |
items.*.width |
number | No | cm |
items.*.height |
number | No | cm |
items.*.description |
string | No | |
items.*.declared_value |
number | No |
Example request body:
{
"user_id": 1,
"delivery_date": "2026-07-15",
"payment_type": "cod",
"cod_amount": 150,
"notes": "Handle with care",
"priority": "high",
"stops": [
{
"type": "pickup",
"country": "SA",
"city": "Riyadh",
"lat": 24.7136,
"lng": 46.6753,
"sequence": 1,
"contact_name": "Ali Hassan",
"phone": "0501234567"
},
{
"type": "delivery",
"country": "SA",
"city": "Jeddah",
"lat": 21.4858,
"lng": 39.1925,
"sequence": 2,
"contact_name": "Omar Ahmed",
"phone": "0559876543"
}
],
"items": [
{
"name": "Electronics Box",
"quantity": 1,
"weight": 2.5,
"declared_value": 300
}
]
}
Create With User (Upsert) Request Body
POST /api/admin/shipments/create-with-user
Same shipment fields as create, but replace user_id with a nested user object. Matches an existing customer by phone + phone_country and updates name / email / company_name, or creates a new user when no match exists. Then creates a draft shipment for that user.
| Field | Type | Required | Validation |
|---|---|---|---|
user |
object | Yes | Nested customer payload |
user.name |
string | Yes | Max 255 |
user.phone |
string | Yes | Valid phone for user.phone_country |
user.phone_country |
string | Yes | Max 10 (e.g. SA) |
user.email |
string | No | Unique email (ignored for the matched user on upsert) |
user.company_name |
string | No | |
| (shipment fields) | Same as Create Request Body except user_id |
Example request body:
{
"user": {
"name": "Ahmed Al-Farsi",
"phone": "0501234567",
"phone_country": "SA",
"email": "ahmed@example.com",
"company_name": "Acme"
},
"delivery_date": "2026-07-15",
"payment_type": "cod",
"cod_amount": 150,
"total_weight": 2.5,
"total_volume": 0.0005,
"notes": "Handle with care",
"priority": "high",
"stops": [
{
"type": "pickup",
"country": "SA",
"city": "Riyadh",
"lat": 24.7136,
"lng": 46.6753
},
{
"type": "delivery",
"country": "SA",
"city": "Jeddah",
"lat": 21.4858,
"lng": 39.1925
}
],
"items": [
{
"name": "Electronics Box",
"quantity": 1,
"weight": 2.5
}
]
}
Update Request Body
Same fields as create, all optional (sometimes rules). Only allowed when status is draft or pending. If stops is included, all stops are replaced.
Confirm Request
POST /api/admin/shipments/{id}/confirm
No request body. Moves status draft → pending.
Error: 422 if shipment is not draft (cannotConfirmShipment)
Cancel Request
POST /api/admin/shipments/{id}/cancel
| Field | Type | Required | Validation |
|---|---|---|---|
reason |
string | No | Max 500 chars |
Error: 422 if shipment is already in a terminal status (shipmentAlreadyCancelled)
Change Status Request
POST /api/admin/shipments/{id}/status
| Field | Type | Required | Validation |
|---|---|---|---|
status |
string | Yes | Any valid ShipmentStatuses value |
Admin override — bypasses the normal state machine transitions.
Assign Route Request
POST /api/admin/shipments/{id}/assign-route
| Field | Type | Required | Validation |
|---|---|---|---|
route_id |
integer | Yes | Must exist in routes |
Shipment must be pending. Creates RouteStop records for each stop and sets status to assigned.
Error: 422 if shipment is not pending (shipmentNotPending)
Unassign Route Request
POST /api/admin/shipments/{id}/unassign-route
No request body. Deletes associated RouteStop rows and reverts status to pending.
Error: 422 if shipment is not assigned (shipmentNotAssigned)
Bulk Operations
Bulk Cancel: POST /api/admin/shipments/bulk/cancel
| Field | Type | Required |
|---|---|---|
ids |
integer[] | Yes |
reason |
string | No |
Bulk Confirm: POST /api/admin/shipments/bulk/confirm
| Field | Type | Required |
|---|---|---|
ids |
integer[] | Yes |
Bulk Assign Route: POST /api/admin/shipments/bulk/assign-route
| Field | Type | Required |
|---|---|---|
ids |
integer[] | Yes |
route_id |
integer | Yes |
Delete Constraint
- Only
draftshipments can be deleted →422(cannotDeleteShipment)
Shipment Resource Shape
{
"id": 1,
"shipment_number": "AB3XZ7QR",
"user_id": 5,
"user_name": "Ahmed Al-Farsi",
"user": { "id": 5, "name": "Ahmed Al-Farsi" },
"delivery_date": "2026-07-15T00:00:00.000000Z",
"status": "pending",
"status_label": "في الانتظار",
"type": "domestic",
"type_label": "محلي",
"payment_type": "cod",
"payment_type_label": "عند الاستلام",
"cod_amount": 150,
"total_weight": 2.5,
"total_volume": 0.0005,
"priority": "normal",
"priority_label": "عادية",
"priority_color": "primary",
"notes": "Handle with care",
"stops": [...],
"items": [...],
"events": [...],
"created_at": "2026-06-21T10:00:00.000000Z",
"updated_at": "2026-06-21T10:00:00.000000Z"
}
stops, items, and events are only present when the resource is loaded via GET /shipments/{id}.
ShipmentStop Resource Shape
{
"id": 1,
"shipment_id": 1,
"type": "pickup",
"type_label": "تحصيل",
"contact_name": "Ali Hassan",
"phone": "0501234567",
"phone_country": null,
"country": "SA",
"city": "Riyadh",
"area": null,
"street": null,
"building": null,
"floor": null,
"apartment": null,
"lat": 24.7136,
"lng": 46.6753,
"full_address": null,
"sequence": 1,
"created_at": "2026-06-21T10:00:00.000000Z",
"updated_at": "2026-06-21T10:00:00.000000Z"
}
ShipmentItem Resource Shape
{
"id": 1,
"shipment_id": 1,
"name": "Electronics Box",
"weight": 2.5,
"length": null,
"width": null,
"height": null,
"quantity": 1,
"description": null,
"declared_value": 300,
"created_at": "2026-06-21T10:00:00.000000Z",
"updated_at": "2026-06-21T10:00:00.000000Z"
}
ShipmentEvent Resource Shape
{
"id": 1,
"shipment_id": 1,
"event_type": "status_changed",
"event_type_label": "تم تغيير الحالة",
"data": {
"from_status": "draft",
"to_status": "pending"
},
"causer_type": "App\\Models\\Admin",
"causer_id": 1,
"causer_name": "Admin User",
"notes": null,
"created_at": "2026-06-21T10:05:00.000000Z",
"updated_at": "2026-06-21T10:05:00.000000Z"
}
Shipment Stops
List shipment stops with rich filtering — useful for route planning and operational views. Each stop includes its parent shipment summary.
Permission prefix: ShipmentStop
Endpoints
| Method | Path | Description | Permission |
|---|---|---|---|
GET |
/api/admin/shipment-stops |
List all shipment stops | ViewAny:ShipmentStop |
GET |
/api/admin/shipment-stops/paginated |
List paginated | ViewAny:ShipmentStop |
GET |
/api/admin/shipment-stops/{id} |
Get single stop | View:ShipmentStop |
List Query Parameters
| Parameter | Type | Description |
|---|---|---|
search |
string | Searches contact_name, phone, city, area, street, full_address |
type |
string | Stop type — pickup, delivery, or stop |
shipment_id |
integer | Filter by shipment |
shipment_number |
string | Partial match on parent shipment_number |
shipment_status |
string | Exact match on parent shipment status — see ShipmentStatuses |
shipment_type |
string | Exact match on parent shipment type — see ShipmentTypes |
user_id |
integer | Filter by customer on parent shipment |
payment_type |
string | Exact match on parent shipment payment_type |
priority |
string | Exact match on parent shipment priority |
delivery_date |
date | Parent shipment delivery date (exact day) |
delivery_date_from |
date | Parent shipment delivery date range start |
delivery_date_to |
date | Parent shipment delivery date range end |
country |
string | Exact match on stop country |
city |
string | Exact match on stop city |
unassigned |
boolean | true = not on an assigned or active route |
on_route |
boolean | true = already on an assigned or active route |
sort_by |
string | sequence, type, city, created_at, delivery_date |
sort_direction |
string | asc or desc (default: desc by created_at) |
Shipment Stop Resource Shape
{
"id": 12,
"shipment_id": 4,
"type": "pickup",
"type_label": "تحصيل",
"contact_name": "Warehouse",
"phone": "0501234567",
"phone_country": "SA",
"country": "SA",
"city": "Riyadh",
"area": "Al Olaya",
"street": "King Fahd Rd",
"building": null,
"floor": null,
"apartment": null,
"lat": 24.7136,
"lng": 46.6753,
"full_address": "King Fahd Rd, Al Olaya, Riyadh, SA",
"sequence": 1,
"created_at": "2026-06-21T10:00:00.000000Z",
"updated_at": "2026-06-21T10:00:00.000000Z",
"shipment": {
"id": 4,
"shipment_number": "SH-AB12CD34",
"delivery_date": "2026-07-15T00:00:00.000000Z",
"status": "pending",
"type": "domestic",
"priority": "normal",
"user_id": 5,
"user_name": "Ahmed Al-Farsi",
"payment_type": "prepaid"
}
}
Proof of Deliveries
Delivery confirmation records for shipments.
Permission prefix: ProofOfDelivery
Endpoints
| Method | Path | Description | Permission |
|---|---|---|---|
GET |
/api/admin/shipments/{shipmentId}/proof-of-deliveries |
List PODs for shipment | ViewAny:ProofOfDelivery |
POST |
/api/admin/shipments/{shipmentId}/proof-of-deliveries |
Create POD | Create:ProofOfDelivery |
GET |
/api/admin/proof-of-deliveries/{id} |
Get single POD | View:ProofOfDelivery |
Create Request Body
| Field | Type | Required | Validation |
|---|---|---|---|
receiver_name |
string | No | |
receiver_phone |
string | No | |
receiver_phone_country |
string | No | |
delivered_at |
datetime | Yes | Valid date |
notes |
string | No |
Side effects: Sets shipment status to delivered and logs a pod_uploaded event.
Example request body:
{
"receiver_name": "Omar Ahmed",
"receiver_phone": "0559876543",
"delivered_at": "2026-07-15T14:30:00",
"notes": "Left at door"
}
ProofOfDelivery Resource Shape
{
"id": 1,
"shipment_id": 1,
"receiver_name": "Omar Ahmed",
"receiver_phone": "0559876543",
"receiver_phone_country": null,
"delivered_at": "2026-07-15T14:30:00.000000Z",
"notes": "Left at door",
"created_by_type": "App\\Models\\Admin",
"created_by_id": 1,
"created_by_name": "Admin User",
"shipment": null,
"created_at": "2026-07-15T14:31:00.000000Z",
"updated_at": "2026-07-15T14:31:00.000000Z"
}
shipment is only present when accessed via GET /proof-of-deliveries/{id}.
Failed Deliveries
Records of failed delivery attempts. Created automatically when a RouteStop (of type delivery) is marked failed, or manually via this API.
Permission prefix: FailedDelivery
Endpoints
| Method | Path | Description | Permission |
|---|---|---|---|
GET |
/api/admin/shipments/{shipmentId}/failed-deliveries |
List failed deliveries for shipment | ViewAny:FailedDelivery |
POST |
/api/admin/shipments/{shipmentId}/failed-deliveries |
Manually create | Create:FailedDelivery |
GET |
/api/admin/failed-deliveries/{id} |
Get single record | View:FailedDelivery |
Create Request Body
| Field | Type | Required | Validation |
|---|---|---|---|
reason_code |
string | Yes | See FailedDeliveryReasons |
notes |
string | No |
Side effects: Sets shipment status to delivery_failed (if not already) and logs a delivery_failed event.
Example request body:
{
"reason_code": "customer_refused",
"notes": "Customer refused to accept the package"
}
FailedDelivery Resource Shape
{
"id": 1,
"shipment_id": 1,
"reason_code": "customer_refused",
"reason_code_label": "العميل رفض الاستلام",
"notes": "Customer refused to accept the package",
"created_by_type": "App\\Models\\Admin",
"created_by_id": 1,
"created_by_name": "Admin User",
"shipment": null,
"created_at": "2026-07-15T14:35:00.000000Z",
"updated_at": "2026-07-15T14:35:00.000000Z"
}
shipment is only present when accessed via GET /failed-deliveries/{id}.
Routes
Daily driver routes with ordered stops, distance metrics, and lifecycle actions (start, complete, cancel). Routes can be created manually or generated by the planning job.
Permission prefix: Route
Endpoints
| Method | Path | Description | Permission |
|---|---|---|---|
GET |
/api/admin/routes |
List all routes | ViewAny:Route |
GET |
/api/admin/routes/paginated |
List paginated | ViewAny:Route |
GET |
/api/admin/routes/{id} |
Get single route with stops, driver, vehicle | View:Route |
POST |
/api/admin/routes |
Create route with stops | Create:Route |
POST |
/api/admin/routes/{id} |
Update route | Update:Route |
DELETE |
/api/admin/routes/{id} |
Delete draft or assigned route | Delete:Route |
POST |
/api/admin/routes/{id}/start |
Start route (assigned → active) |
Update:Route |
POST |
/api/admin/routes/{id}/complete |
Complete route (active → completed) |
Update:Route |
POST |
/api/admin/routes/{id}/cancel |
Cancel route | Update:Route |
GET |
/api/admin/routes/{id}/stops |
List ordered route stops | View:Route |
GET |
/api/admin/routes/{id}/assignments |
List driver assignment history for route | ViewAny:RouteAssignment |
POST |
/api/admin/routes/plan |
Dispatch async route planning job | Create:Route |
GET |
/api/admin/routes/plan/result |
Get cached planning summary | ViewAny:Route |
List Query Parameters
| Parameter | Type | Description |
|---|---|---|
status |
string | Exact match — see RouteStatuses |
driver_id |
integer | Filter by driver |
vehicle_id |
integer | Filter by vehicle |
route_date |
date | Filter by route date |
sort_by |
string | route_number, route_date, status, created_at |
sort_direction |
string | asc or desc (default: desc by route_date) |
Create Request Body
| Field | Type | Required | Validation |
|---|---|---|---|
driver_id |
integer | Yes | Must exist in drivers; must have an active vehicle assignment |
route_date |
date | Yes | |
stops |
array | Yes | Min 1 item; ordered list of shipment_stop_id values — sequence is assigned automatically |
stops.* |
integer | Yes | Must exist in shipment_stops; must be unique; must not already be on an active/assigned route; for the same shipment, pickup must appear before delivery |
Not accepted on create: vehicle_id, status, total_stops, optimized_distance, duration — these are resolved or calculated by the server.
Server behaviour:
- Resolves
vehicle_idfrom the driver's latest activeDriverVehicleAssignment - Sets
statustoassigned(driver is assigned but has not started) - Calculates
total_stops,optimized_distance(km), andduration(minutes) from stop coordinates - Creates
RouteStoprecords with estimated arrival/departure times - Sets linked shipments (
pendingordelivery_failed) toassigned
Errors:
422 vehicleAssignmentNotFound— driver has no active vehicle assignment422 shipmentStopAlreadyOnRoute— a stop is already on another active/assigned route422 deliveryStopBeforePickupStop— delivery stop appears before pickup for the same shipment
Example request body:
{
"driver_id": 1,
"route_date": "2026-06-25",
"stops": [10, 11, 15]
}
Update Request Body
| Field | Type | Required | Validation |
|---|---|---|---|
driver_id |
integer | No | Must exist; re-resolves vehicle_id from new driver's assignment |
route_date |
date | No | Recalculates stop metrics when changed |
status |
string | No | See RouteStatuses |
stops |
array | No | Replaces all stops (ordered IDs); recalculates metrics |
Not accepted on update: vehicle_id, total_stops, optimized_distance, duration — always calculated by the server when stops or driver/date change.
Only allowed when route is not in a terminal status (completed, cancelled).
Plan Routes Request
POST /api/admin/routes/plan
| Field | Type | Required | Validation |
|---|---|---|---|
date |
date | Yes | Planning date |
driver_ids |
array | No | Optional filter; each must exist in drivers |
Dispatches an async job. Poll GET /api/admin/routes/plan/result?date=YYYY-MM-DD for the summary.
Route Resource Shape
{
"id": 1,
"route_number": "AB12CD34",
"route_date": "2026-06-25T00:00:00.000000Z",
"status": "assigned",
"status_label": "تم تخصيص السائق",
"total_stops": 3,
"optimized_distance": 42.5,
"duration": 95.0,
"driver": { "...": "DriverResource when loaded" },
"vehicle": { "...": "VehicleResource when loaded" },
"stops": [{ "...": "RouteStopResource when loaded" }],
"created_at": "2026-06-24T20:00:00.000000Z",
"updated_at": "2026-06-24T20:00:00.000000Z"
}
driver, vehicle, and stops are only present when the resource is loaded via GET /routes/{id} or list endpoints that eager-load relations.
Route Assignments
Tracks driver assignment history for a route. A driver can be assigned and later unassigned when the route is reassigned.
Permission prefix: RouteAssignment
| Method | Path | Description | Permission |
|---|---|---|---|
GET |
/api/admin/routes/{id}/assignments |
List assignments for route | ViewAny:RouteAssignment |
Route Assignment Resource Shape
{
"id": 1,
"assigned_at": "2026-06-01T08:00:00.000000Z",
"unassigned_at": null,
"driver": {
"id": 3,
"driver_number": "482910",
"name": "mousa"
},
"created_at": "2026-06-01T08:00:00.000000Z",
"updated_at": "2026-06-01T08:00:00.000000Z"
}
| Field | Notes |
|---|---|
driver |
Nested DriverResource when relation is loaded |
unassigned_at |
null = currently assigned to this route |
Route Stops
Individual ordered stops on a driver route, linked to shipment stops.
Permission prefix: Route
Endpoints
| Method | Path | Description | Permission |
|---|---|---|---|
GET |
/api/admin/route-stops/{id} |
Get single route stop | ViewAny:Route |
POST |
/api/admin/route-stops/{id} |
Update stop status/times | Update:Route |
DELETE |
/api/admin/route-stops/{id} |
Delete stop | Delete:Route |
Route stops are created automatically when a route is created or updated via POST /api/admin/routes. Use the route-level endpoints to manage the full stop list.
Update Request Body
| Field | Type | Required | Validation |
|---|---|---|---|
status |
string | No | See RouteStopStatuses |
arrived_at |
datetime | No | |
completed_at |
datetime | No |
RouteStop Resource Shape
{
"id": 1,
"route_id": 1,
"shipment_stop_id": 10,
"sequence": 1,
"status": "pending",
"status_label": "قيد الانتظار",
"stop_type": "pickup",
"stop_type_label": "تحصيل",
"lat": 24.7136,
"lng": 46.6753,
"full_address": "Riyadh, SA",
"contact_name": "Ali Hassan",
"phone": "0501234567",
"shipment": { "...": "ShipmentResource when loaded" },
"distance_from_previous_km": 5.2,
"estimated_minutes_from_previous": 12.5,
"estimated_arrival_at": "2026-06-25T08:12:00.000000Z",
"estimated_departure_at": "2026-06-25T08:27:00.000000Z",
"arrived_at": null,
"completed_at": null,
"created_at": "2026-06-24T20:00:00.000000Z",
"updated_at": "2026-06-24T20:00:00.000000Z"
}
Domain Models Reference
Currently Exposed via API
| Model | Table | Description | Key Fields |
|---|---|---|---|
| Admin | admins |
Back-office users | name, email, password, fcm_token |
| Role | roles |
Spatie roles (guard: admin) |
name, guard_name |
| Permission | permissions |
Spatie permissions | name, guard_name |
| Driver | drivers |
Delivery drivers | driver_number, name, phone, working_days, work_start_time, work_end_time, lat, lng, is_active |
| Vehicle | vehicles |
Fleet vehicles | plate_number, owner_type, owner_id, max_weight, max_volume, is_active |
| DriverVehicleAssignment | driver_vehicle_assignments |
Driver ↔ vehicle link | driver_id, vehicle_id, assigned_at, released_at |
| DriverUnavailability | driver_unavailabilities |
Driver time-off | driver_id, start_date, end_date, reason |
| User | users |
Customers / shippers | name, phone, phone_country, email, company_name |
| Shipment | shipments |
Delivery orders | shipment_number, user_id, status, type, payment_type, delivery_date |
| ShipmentItem | shipment_items |
Items within a shipment | name, quantity, weight, declared_value |
| ShipmentStop | shipment_stops |
Pickup / delivery stops | type, lat, lng, sequence |
| ShipmentEvent | shipment_events |
Event audit log | event_type, data, causer_type, causer_id |
| ProofOfDelivery | proof_of_deliveries |
Delivery confirmation | receiver_name, delivered_at |
| FailedDelivery | failed_deliveries |
Failed delivery attempts | reason_code, notes |
| Route | routes |
Daily driver routes | route_number, driver_id, vehicle_id, route_date, status, total_stops, optimized_distance, duration |
| RouteStop | route_stops |
Ordered stops on a route | shipment_stop_id, sequence, status, estimated_arrival_at, estimated_departure_at |
| RouteAssignment | route_assignments |
Driver assignment history on a route | route_id, driver_id, assigned_at, unassigned_at |
Planned Domain (Not Yet in API)
These models exist in the database but have no dedicated HTTP endpoints yet.
StaticContent (static_contents)
Key-value CMS / app configuration store (not yet in API).
| Field | Type | Notes |
|---|---|---|
key |
string | Primary key; see StaticContentTypes |
value |
string | Plain text or JSON-encoded value |
Entity Relationship Diagram (Overview)
User ──< Shipment ──< ShipmentItem
──< ShipmentStop ──< RouteStop >── Route >── Driver
──< ShipmentEvent └── Vehicle
──< ProofOfDelivery
──< FailedDelivery
Driver ──< DriverVehicleAssignment >── Vehicle
──< DriverUnavailability
──< RouteAssignment >── Route
Admin ──< Role ──< Permission
Enums Reference
All enum values are snake_case strings. Store them as string constants in the frontend.
OwnerTypes
Used in API today — vehicle ownership.
| Value | Arabic Label | Rules |
|---|---|---|
company |
الشركة | owner_id must be null |
driver |
سائق | owner_id must be a valid driver ID |
API returns both owner_type (value) and owner_type_label (Arabic label).
Suggested TypeScript:
type OwnerType = "company" | "driver";
ShipmentStatuses
Shipment lifecycle state — returned in status and status_label fields. Map UI colors client-side using the table below.
| Value | Arabic Label | Suggested UI Color |
|---|---|---|
draft |
مسودة | secondary |
pending |
في الانتظار | warning |
assigned |
تم التخصيص لسائق | primary |
pickup_in_progress |
قيد التحصيل | warning |
picked_up |
تم التحصيل | success |
at_hub |
في المخزن | info |
out_for_delivery |
قيد التسليم | primary |
delivered |
تم التسليم | success |
delivery_failed |
فشل التسليم | danger |
returned |
مرتجع | warning |
cancelled |
تم الإلغاء | danger |
Typical flow:
draft → pending → assigned → pickup_in_progress → picked_up → at_hub
→ out_for_delivery → delivered
↘ delivery_failed → returned
Any state → cancelled
ShipmentTypes
| Value | Arabic Label |
|---|---|
domestic |
محلي |
international |
دولية |
ShipmentPriorities
Used in API today — shipment urgency for planning and filtering.
| Value | Arabic Label | Suggested UI Color |
|---|---|---|
low |
منخفضة | secondary |
normal |
عادية | primary |
high |
عالية | warning |
urgent |
عاجلة | danger |
Defaults to normal on create when omitted. Higher priority shipments are planned first during route planning.
Suggested TypeScript:
type ShipmentPriority = "low" | "normal" | "high" | "urgent";
ShipmentStopTypes
| Value | Arabic Label | Description |
|---|---|---|
pickup |
تحصيل | Collect goods from sender |
delivery |
تسليم | Deliver to recipient |
stop |
موقف | Intermediate stop |
PaymentTypes
| Value | Arabic Label | Notes |
|---|---|---|
cod |
عند الاستلام | Cash on delivery; requires cod_amount |
prepaid |
مقدما | Paid upfront |
RouteStatuses
| Value | Arabic Label |
|---|---|
draft |
مسودة |
assigned |
تم تخصيص السائق |
active |
فعال |
completed |
مكتمل |
cancelled |
ملغي |
RouteStopStatuses
| Value | Arabic Label |
|---|---|
pending |
قيد الانتظار |
arrived |
تم الوصول |
completed |
مكتمل |
failed |
فشل |
skipped |
تخطي |
ShipmentEvents
Timeline event types logged on shipments. Each event has a structured data JSON payload.
| Value | Arabic Label | data Keys |
|---|---|---|
shipment_created |
تم إنشاء الشحنة | shipment_number, type, payment_type, delivery_date |
status_changed |
تم تغيير الحالة | from_status, to_status |
route_assigned |
تم تعيين المسار | route_id, driver_id, driver_name |
route_unassigned |
تم إلغاء تعيين المسار | route_id, reason |
driver_assigned |
تم تخصيص السائق | driver_id, driver_name |
route_created |
تم إنشاء الرحلة | route_id |
pickup_started |
تم بدء التحصيل | stop_id, driver_id, driver_name |
pickup_completed |
تم إنهاء التحصيل | stop_id, arrived_at, completed_at |
out_for_delivery |
خرجت للتسليم | route_id, driver_id, driver_name |
delivery_failed |
فشل التسليم | stop_id, reason_code, notes |
delivered |
تم التسليم | stop_id, receiver_name, delivered_at |
pod_uploaded |
تم تحميل إثبات التوصيل | receiver_name, delivered_at, notes |
cancelled |
تم الإلغاء | reason, cancelled_by_type, cancelled_by_id |
returned |
تم الإرجاع | reason, returned_at |
FailedDeliveryReasons
| Value | Arabic Label |
|---|---|
customer_unavailable |
العميل غير متاح |
wrong_address |
عنوان خاطئ |
customer_refused |
العميل رفض الاستلام |
unable_to_contact |
تعذر التواصل مع العميل |
damaged_package |
الطرد تالف |
other |
أخرى |
MediaTypes
Internal media collection names (used for image uploads).
| Value | Used For |
|---|---|
admin_image |
Admin profile photos |
driver_image |
Driver profile photos |
user_image |
User profile photos (future) |
StaticContentTypes
CMS / app configuration keys (not yet in API).
| Value | Arabic Label |
|---|---|
privacy_policy |
سياسة الخصوصية |
commission |
عمولة الإدارة |
social_media |
المواقع الاجتماعية |
android_app_version |
نسخة التطبيق للاندرويد |
ios_app_version |
نسخة التطبيق لل IOS |
android_provider_app_version |
نسخة التطبيق للاندرويد للمزود |
ios_provider_app_version |
نسخة التطبيق لل IOS للمزود |
NotificationTypes
Push notification categories (PHP backed enum).
| Value |
|---|
general |
wallet_deposit |
wallet_withdraw |
new_offer |
offer_accepted |
offer_rejected |
order_status_changed |
new_order |
Working Days (not a PHP enum)
Driver schedule days. Accepted as array values in create/update requests.
| Value | Day |
|---|---|
sun |
Sunday |
mon |
Monday |
tue |
Tuesday |
wed |
Wednesday |
thu |
Thursday |
fri |
Friday |
sat |
Saturday |
Suggested TypeScript:
type WorkingDay = "sun" | "mon" | "tue" | "wed" | "thu" | "fri" | "sat";
Permissions Reference
Permissions control what each admin can do. Check them client-side to show/hide UI elements.
Naming Convention
{Action}:{Model}
| Action | Description |
|---|---|
ViewAny |
List/index access |
View |
View single record |
Create |
Create new record |
Update |
Edit existing record |
Delete |
Delete record |
Reorder |
Reorder records (if applicable) |
Examples:
ViewAny:Driver— can list driversCreate:Vehicle— can create vehiclesUpdate:Admin— can edit adminsDelete:Role— can delete roles
Custom Permissions
These do not map to a CRUD model:
| Permission | Description |
|---|---|
Export:Reports |
Export reports |
Send:Notifications |
Send push notifications |
Manage:AppSettings |
Manage app settings |
Assign:Routes |
Assign routes to drivers |
Track:Deliveries |
Track live deliveries |
How to Check Permissions on Frontend
After login, inspect data.admin.permission_groups:
function hasPermission(
permissionGroups: PermissionGroup[],
permissionName: string,
): boolean {
return permissionGroups.some((group) =>
group.permissions.some((p) => p.name === permissionName),
);
}
// Usage
if (hasPermission(admin.permission_groups, "Create:Driver")) {
// Show "Add Driver" button
}
Super admin role has all permissions implicitly.
Error Handling
HTTP Status Codes
| Code | Meaning | When |
|---|---|---|
200 |
Success | Normal operation |
401 |
Unauthorized | Missing/invalid permission, wrong credentials |
403 |
Forbidden | Not authenticated (no/invalid token) |
404 |
Not Found | Route or resource not found |
422 |
Validation Error | Invalid request body/query |
500 |
Server Error | Unexpected failure |
Common Error Messages
| Message Key | English Text | Status |
|---|---|---|
credentialsError |
Wrong Credentials | 401 |
Unauthenticated |
Please login first | 403 |
Unauthorized |
You do not have permissions to perform this action | 401 |
cannotDeleteCurrentAdmin |
You cannot delete your own admin account | 403 |
cannotModifySuperAdmin |
Super admin role cannot be modified | 403 |
vehicleAlreadyAssigned |
This vehicle is already assigned to a driver | 422 |
vehicleAssignmentNotFound |
No active vehicle assignment found for this driver | 404 |
couldNotCreateToken |
Could not create authentication token | 500 |
couldNotLogout |
Could not log out | 500 |
cannotUpdateShipment |
Shipment cannot be updated in its current status | 422 |
cannotConfirmShipment |
Only draft shipments can be confirmed | 422 |
shipmentAlreadyCancelled |
Shipment is already in a terminal status and cannot be cancelled | 422 |
cannotDeleteShipment |
Only draft shipments can be deleted | 422 |
shipmentNotPending |
Shipment must be in pending status to assign a route | 422 |
shipmentNotAssigned |
Shipment must be in assigned status to unassign from a route | 422 |
invalidShipmentStatus |
Invalid shipment status provided | 422 |
Validation errors return the first validation message as message.
Error Response Shape
{
"status": "Error",
"message": "Wrong Credentials",
"data": null,
"statusCode": 401
}
Frontend Integration Checklist
- Store JWT from login response (
data.token) - Send
Authorization: Bearer {token}on every authenticated request - Set
Accept-Language: enorarfor localized messages - Use
POSTfor all updates (not PUT/PATCH) - Use
multipart/form-datawhen uploadingimagefields - Gate UI by checking
permission_groupsfrom login/profile response - Handle the standard
{ status, message, data, statusCode }envelope - Use paginated endpoints with
pagequery param for tables - Pass
search,sort_by,sort_directionfor filterable lists - For vehicles: enforce
owner_idrules based onowner_type - For drivers: send
working_daysas array; times asHH:MMorHH:MM:SS - For shipments: ensure exactly 1
pickupstop and 1deliverystop in thestopsarray - For shipments:
cod_amountis required whenpayment_type=cod - Handle
status_changedevents fromGET /shipments/{id}/eventsfor timeline display - Route assignment: shipment must be
pendingbeforeassign-route;assignedbeforeunassign-route - Route create: send
driver_id+stops[]; do not sendvehicle_id,status, or metrics — server calculates them - Route create requires driver to have an active vehicle assignment
- Route planning: dispatch via
POST /routes/plan, pollGET /routes/plan/result?date=
Route Index (Quick Reference)
| Method | Path |
|---|---|
POST |
/api/admin/login |
POST |
/api/admin/logout |
GET |
/api/admin/profile |
POST |
/api/admin/profile |
GET |
/api/admin/dashboard/overview |
GET |
/api/admin/dashboard/trends |
GET |
/api/admin/permissions |
GET/POST/DELETE |
/api/admin/roles, /api/admin/roles/paginated, /api/admin/roles/{id}, /api/admin/roles/{id}/permissions |
GET/POST/DELETE |
/api/admin/admins, /api/admin/admins/paginated, /api/admin/admins/{id} |
GET/POST/DELETE |
/api/admin/drivers, /api/admin/drivers/paginated, /api/admin/drivers/{id} |
GET/POST |
/api/admin/drivers/{id}/vehicle-assignments, .../assign, .../unassign |
GET |
/api/admin/drivers/{id}/unavailabilities |
GET/POST/DELETE |
/api/admin/vehicles, /api/admin/vehicles/paginated, /api/admin/vehicles/{id} |
GET/POST/DELETE |
/api/admin/driver-unavailabilities, .../paginated, .../{id} |
GET/POST/DELETE |
/api/admin/users, /api/admin/users/paginated, /api/admin/users/{id} |
GET/POST/DELETE |
/api/admin/shipments, /api/admin/shipments/paginated, /api/admin/shipments/{id} |
POST |
/api/admin/shipments/create-with-user |
POST |
/api/admin/shipments/{id}/confirm, /api/admin/shipments/{id}/cancel, /api/admin/shipments/{id}/status |
POST |
/api/admin/shipments/{id}/assign-route, /api/admin/shipments/{id}/unassign-route |
GET |
/api/admin/shipments/{id}/events |
POST |
/api/admin/shipments/bulk/cancel, /api/admin/shipments/bulk/confirm, /api/admin/shipments/bulk/assign-route |
GET |
/api/admin/shipment-stops, /api/admin/shipment-stops/paginated, /api/admin/shipment-stops/{id} |
GET/POST |
/api/admin/shipments/{id}/proof-of-deliveries |
GET |
/api/admin/proof-of-deliveries/{id} |
GET/POST |
/api/admin/shipments/{id}/failed-deliveries |
GET |
/api/admin/failed-deliveries/{id} |
GET/POST/DELETE |
/api/admin/routes, /api/admin/routes/paginated, /api/admin/routes/{id} |
POST |
/api/admin/routes/{id}/start, /api/admin/routes/{id}/complete, /api/admin/routes/{id}/cancel |
GET |
/api/admin/routes/{id}/stops, /api/admin/routes/{id}/assignments |
POST/GET |
/api/admin/routes/plan, /api/admin/routes/plan/result |
GET/POST/DELETE |
/api/admin/route-stops/{id} |
Driver App API
The Driver App API (/api/driver/*) is a separate, mobile-first API for drivers. It uses the same JWT mechanism as the Admin API but with the driver guard. Tokens never expire (expires_in: null).
Driver Authentication
| Method | Endpoint | Auth | Description |
|---|---|---|---|
POST |
/api/driver/login |
Public | Login with phone, phone_country, password. Optionally pass fcm_token. Returns JWT + driver profile. |
POST |
/api/driver/logout |
Required | Invalidate the current token. |
Login request:
{
"phone": "0512345678",
"phone_country": "SA",
"password": "secret",
"fcm_token": null
}
Login response:
{
"data": {
"token": "eyJ...",
"token_type": "bearer",
"expires_in": null,
"driver": { "id": 1, "name": "...", "phone": "...", ... }
}
}
Driver Profile
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/driver/profile |
Get full driver profile including assigned vehicle. |
POST |
/api/driver/profile |
Update name, email, password (all optional, password min 6 chars). |
POST |
/api/driver/fcm-token |
Update push notification token. Body: { "fcm_token": "..." } |
POST |
/api/driver/location |
Update GPS coordinates. Body: { "lat": 24.71, "lng": 46.67 } |
Driver Routes
All route endpoints are automatically scoped to the authenticated driver. A driver has one route per day.
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/driver/routes |
Paginated list (15/page). Filters: ?status=assigned&date=2026-07-02 |
GET |
/api/driver/routes/today |
Today's route. Returns 404 if no route assigned for today. |
GET |
/api/driver/routes/{id} |
Full route with stops. Each stop's shipment includes items so the driver knows what to pick up or deliver. Returns 404 if route does not belong to driver. |
POST |
/api/driver/routes/{id}/start |
Start route: assigned → active. |
POST |
/api/driver/routes/{id}/complete |
Complete route: active → completed. |
Driver Route Stops
All stop endpoints validate that the stop belongs to a route owned by the authenticated driver.
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/driver/routes/{routeId}/stops |
All stops for a route, ordered by sequence. Includes address, items, shipment. |
GET |
/api/driver/routes/{routeId}/stops/{stopId} |
Full stop details with ETA fields. shipment.items lists what to pick up or deliver. |
POST |
/api/driver/routes/{routeId}/stops/{stopId}/arrive |
Mark arrived: pending → arrived. Sets arrived_at. Route must be active (started) first. |
POST |
/api/driver/routes/{routeId}/stops/{stopId}/complete |
Complete stop. See logic below. |
POST |
/api/driver/routes/{routeId}/stops/{stopId}/fail |
Fail a delivery stop. Body: reason_code, notes?. |
POST |
/api/driver/routes/{routeId}/stops/{stopId}/skip |
Skip stop (pending or arrived). |
Complete stop logic by stop_type:
| Stop Type | What happens |
|---|---|
delivery |
Creates ProofOfDelivery, sets shipment → delivered, fires delivered event. Body: receiver_name, receiver_phone, notes? |
pickup |
Sets shipment → picked_up, fires pickup_completed event. No body required. |
Fail stop — reason_code values: customer_unavailable, wrong_address, customer_refused, unable_to_contact, damaged_package, other.
Driver Route Stop Status Flow
pending → arrived → completed (via complete)
→ failed (via fail)
pending → skipped (via skip)
arrived → skipped (via skip)
Driver API Quick Reference
| Method | Path |
|---|---|
POST |
/api/driver/login |
POST |
/api/driver/logout |
GET |
/api/driver/profile |
POST |
/api/driver/profile |
POST |
/api/driver/fcm-token |
POST |
/api/driver/location |
GET |
/api/driver/routes |
GET |
/api/driver/routes/today |
GET |
/api/driver/routes/{id} |
POST |
/api/driver/routes/{id}/start |
POST |
/api/driver/routes/{id}/complete |
GET |
/api/driver/routes/{routeId}/stops |
GET |
/api/driver/routes/{routeId}/stops/{stopId} |
POST |
/api/driver/routes/{routeId}/stops/{stopId}/arrive |
POST |
/api/driver/routes/{routeId}/stops/{stopId}/complete |
POST |
/api/driver/routes/{routeId}/stops/{stopId}/fail |
POST |
/api/driver/routes/{routeId}/stops/{stopId}/skip |
Generated from the shipping_system codebase. Last updated: July 2026.