API Documentation
A modern RESTful API service that collects and tracks location data from mobile devices. Comprehensive endpoints for real-time tracking, historical data, and user management.
📋 Overview
TraxNavi API processes and stores location data from mobile devices (iOS and Android). The API is designed according to RESTful principles.
Basic Information
| Base URL | https://traxnavi.com/api/v1 |
|---|---|
| Version | v1 |
| Format | JSON |
| Auth | Laravel Sanctum (Bearer Token) |
| CORS | traxops.com, turusatools.com, Mobile Applications |
Data Format
All endpoints now use a standard JSON structure.
Standard Success Response:
{
"success": true,
"message": "Optional message",
"data": { ... },
"meta": { ... } // Sayfalama varsa
}
Standard Error Response:
{
"success": false,
"message": "Error description",
"errors": { ... } // Validation errors
}
⚠️ Validation and Error Codes
Returned when the request payload or parameters are invalid. The :field field does not meet the expected rule. See the validation section for the full schema.
HTTP Status Codes
| Code | Meaning | Usage |
|---|---|---|
| 200 | OK | Successful (GET/PUT/POST) |
| 207 | Multi-Status | Location batch: partially successful (some records invalid) |
| 401 | Unauthenticated | Missing/invalid token or invalid login credentials |
| 403 | Forbidden | Unauthorized (e.g. company_id mismatch) |
| 404 | Not Found | Resource or endpoint not found (e.g. user_id) |
| 405 | Method Not Allowed | Wrong HTTP method |
| 422 | Validation Failed | Validation error; details in the errors field |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Server Error | Unexpected server error |
Standard Response Structure
All API endpoints use the same JSON envelope. The success field is boolean, message contains explanatory text, and data (success) or errors (failure) carries payload details.
Success Response (200 OK)
{
"success": true,
"message": "Operation description",
"data": { ... }
}
Paginated Collection Response (200 OK)
{
"success": true,
"message": "...",
"data": [ ... ],
"meta": {
"current_page": 1,
"last_page": 5,
"per_page": 50,
"total": 234,
"from": 1,
"to": 50
},
"links": {
"first": "...?page=1",
"last": "...?page=5",
"prev": null,
"next": "...?page=2"
}
}
Error Response Examples
401 Unauthenticated
Token was not sent or has expired.
{
"success": false,
"message": "Unauthenticated"
}
403 Forbidden
Unauthorized access (e.g. record belongs to another company, driver tries to access another user).
{
"success": false,
"message": "You do not have access to this resource."
}
404 Not Found
Resource not found (model or endpoint).
{
"success": false,
"message": "Location not found."
}
405 Method Not Allowed
Wrong HTTP method used (e.g. POST instead of GET).
{
"success": false,
"message": "HTTP method not allowed for this endpoint."
}
422 Validation Failed
Submitted data does not satisfy validation rules. Field-level error messages are returned in the errors object.
{
"success": false,
"message": "Validation Failed",
"errors": {
"email": [
"The email field must be a valid email address."
],
"password": [
"The password field is required."
]
}
}
429 Too Many Requests
Rate limit exceeded. The response headers include Retry-After.
{
"success": false,
"message": "Too many requests. Please slow down."
}
207 Multi-Status (Partial Batch Success)
Returned when some records succeed and some fail in batch location ingestion.
{
"success": true,
"message": "Processed with some errors",
"data": {
"processed_count": 2,
"stored_history_count": 2,
"error_count": 1,
"errors": [
{
"index": 1,
"input_data": { "timestamp": "invalid", ... },
"errors": {
"timestamp": ["The timestamp field must be a valid date."]
}
}
],
"latest_timestamp": "2026-01-06T14:31:40.000Z"
}
}
500 Server Error
Unexpected server error. In debug mode, a debug object is included.
{
"success": false,
"message": "A database error occurred."
}
💡 Tip: In partial success, the location batch endpoint returns 207. If all records are invalid, it returns 422 and each item in errors contains index, errors (field -> messages), and input_data.
🔐 Authentication
💡 Note: The API uses an access token + refresh token pair.
After obtaining an access token, include the following in all API requests:
Authorization: Bearer {token}
header.
Access Token
Used in API requests.
Refresh Token
Used to refresh the access token.
Token Rotation:
On each refresh, a new token pair is issued and the old refresh token is revoked.
Get Token
{
"email": "user@example.com",
"password": "your-password"
}
Token Usage
Authorization: Bearer your-token-here
Content-Type: application/json
⚠️ Token Lifecycle: Access token is valid for 365 days; when it expires,
POST /api/v1/refresh
send the refresh token to the endpoint above to receive a new pair.
In normal usage refresh is rarely needed. During login, logout and password reset operations, all tokens are automatically revoked.
⚡ Rate Limiting
| Endpoint Group | Limit | Description |
|---|---|---|
| Login | 5 / min | Brute-force protection |
| Locations (POST) |
Driver: 60/min Company: 6000/min |
Role-based ingest limit |
| Other Endpoints |
Driver: 1000/min Company: 10000/min |
General API usage |
⚠️ Warning: When the rate limit is exceeded, you receive a 429 Too Many Requests
error.
🔑 Authentication Endpoints
/api/v1/login
PUBLICExchanges email and password for a Sanctum access token and a refresh token. The refresh token is single-use — every login (or password change) revokes the previous one. FCM tokens for the user are also revoked on a new login so push notifications follow the new device.
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
| string | ✓ | User email address (valid email format) | |
| password | string | ✓ | User password |
| client | string | No | "mobile" or "web". The mobile app must send "mobile"; only driver can login. |
Validation Rules
| Field | Rule |
|---|---|
| required, email | |
| password | required, string |
| client | nullable, in:mobile,web |
curl -X POST https://traxnavi.com/api/v1/login \
-H "Content-Type: application/json" \
-d '{
"email": "driver@example.com",
"password": "secret123",
"client": "mobile"
}'
Successful Response (200 OK)
{
"success": true,
"message": "Login successful.",
"data": {
"user": {
"id": 1,
"name": "Alex Yilmaz",
"email": "driver@example.com",
"phone": "+905551234567",
"country_code": "TR",
"role": "driver",
"company_id": 1,
"vehicle": "34 ABC 123",
"job_status": "active",
"created_at": "2025-01-01T12:00:00+00:00",
"updated_at": "2025-01-01T12:00:00+00:00"
},
"token": "1|SampleTokenStringHere123456789",
"refresh_token": "a1b2c3d4e5f6...64 karakter random string",
"expires_in": 31536000
}
}
| Field | Description |
|---|---|
| token | Access token (used as Bearer, valid for 365 days) |
| refresh_token | Refresh token (used to renew access token, valid for 730 days) |
| expires_in | Access token lifetime (in seconds, 31536000 = 365 days) |
| country_code | ISO 3166-1 alpha-2 country code (e.g. TR, US, DE) |
Error Response (401)
{
"success": false,
"message": "Invalid credentials."
}
Validation Error (422)
{
"success": false,
"message": "Validation Failed",
"errors": {
"email": ["The email field must be a valid email address."],
"password": ["The password field is required."]
}
}
⚠️ Note: Login revokes all existing access and refresh tokens, then creates a new pair (access + refresh).
Store the refresh_token value securely.
/api/v1/forgot-password
PUBLICSend a password reset link to the email address.
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
| string | ✓ | Valid email format, required |
Success Response (200)
{
"success": true,
"message": "We have emailed your password reset link.",
"data": null
}
Validation Error (422)
{
"success": false,
"message": "Validation Failed",
"errors": {
"email": ["The email field is required."]
}
}
User Not Found (400)
{
"success": false,
"message": "We can't find a user with that email address."
}
/api/v1/reset-password
PUBLICReset the password using the token.
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
| token | string | ✓ | Reset token from email |
| string | ✓ | User email address | |
| password | string | ✓ | New password (min. 8 chars) |
| password_confirmation | string | ✓ | Password confirmation |
Success Response (200)
{
"success": true,
"message": "Your password has been reset.",
"data": null
}
Validation Error (422)
{
"success": false,
"message": "Validation Failed",
"errors": {
"password": ["The password field must be at least 8 characters."],
"token": ["The token field is required."]
}
}
Invalid Token (400)
{
"success": false,
"message": "This password reset token is invalid."
}
/api/v1/refresh
PUBLICExchanges a valid (non-revoked, non-expired) refresh token for a new access + refresh token pair. The supplied refresh token is revoked atomically — replaying it returns 401. The Authorization header is NOT required for this endpoint; the refresh token travels in the request body only.
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
| refresh_token | string | ✓ | 64-character refresh token returned from login |
| client | string | No | "mobile" or "web". The mobile app should send "mobile". |
curl -X POST https://traxnavi.com/api/v1/refresh \
-H "Content-Type: application/json" \
-d '{
"refresh_token": "a1b2c3d4e5f6...64karakter",
"client": "mobile"
}'
Note: This endpoint does not require the Authorization header. Send refresh token only in the request body.
Success Response (200)
{
"success": true,
"message": "Token refreshed successfully.",
"data": {
"user": {
"id": 1,
"name": "Alex Yilmaz",
"email": "driver@example.com",
"phone": "+905551234567",
"country_code": "TR",
"role": "driver",
"company_id": 1,
"vehicle": "34 ABC 123",
"job_status": "active",
"created_at": "2025-01-01T12:00:00+00:00",
"updated_at": "2025-01-01T12:00:00+00:00"
},
"token": "2|NewAccessTokenHere...",
"refresh_token": "x9y8z7w6...yeni 64 karakter",
"expires_in": 31536000
}
}
Invalid/Expired Token (401)
{
"success": false,
"message": "Invalid or expired refresh token."
}
🔄 Token Rotation: On each successful refresh, the old refresh token is revoked and a new pair is returned.
In mobile apps, always store the latest refresh_token value.
/api/v1/logout
AUTH REQUIREDLogout by revoking the current access token and all active refresh tokens. The server also removes every FCM device token of the user, so push notifications stop on every device until a new token is registered. After logout, no token can be used; login is required again.
Sample Request
curl -X POST https://traxnavi.com/api/v1/logout \ -H "Authorization: Bearer YOUR_TOKEN_HERE"
Note: Body is not required; only the Authorization header is enough.
Success Response (200)
{
"success": true,
"message": "Logged out successfully.",
"data": null
}
Missing Token (401)
{
"success": false,
"message": "Unauthenticated"
}
📍 Location Endpoints
/api/v1/locations
AUTH REQUIREDSend location data from a mobile device. Three formats are supported: with the
location wrapper (array or single object), or directly as raw array/object without wrapper (compatible with flutter_background_geolocation v5).
Features
- 3 Format Support:
{"location": [...]}wrapper,{"location": {...}}common.messages.batch_max_100 - Compatible with flutter_background_geolocation v5: Default raw format is accepted directly without requiring plugin
locationTemplateconfiguration - Partial Success: Valid records are processed, invalid ones are reported.
- Latest location is automatically saved into
locationstable - Timestamp Protection: The
locationstable is updated only when incoming data is newer than or equal to the current record. This applies to both single and batch submissions. For delayed/offline submissions, current latest location is preserved; onlylocation_historiesis updated. - All locations are appended to
location_historiestable - Single format: If root
timestampexists, it is used aslocations.updated_atandlocation_histories.created_at
Request Body Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| location | array | object | * | Location array [{...}] or single object {...} (Max: 100). It can also be sent without wrapper - see the "Raw Format" example below. |
| timestamp | string | Optional | For single format: used as updated_at and created_at (ISO 8601) |
| user_id | numeric | Optional | Numeric user ID; if missing, authenticated user is used. |
| company_id | int | Optional | Must match your account company ID; otherwise 403. |
| app_version | string | Optional | Application version (persisted together with extras.app_version) |
| extras | object | Optional | At request root: e.g. {"power_save": true}. If location row does not include extras.power_save, this value is used (same logic for batch/single). |
| extras.power_save | boolean | Optional | Only inside root extras; nullable boolean, validated. |
Location object validation rules (DB limits)
Both batch and single format follow the same location object validation rules. Every location item must satisfy the rules below. Invalid records are reported with index in errors array (for single format index is 0).
| Field | Rule / Limit |
|---|---|
| timestamp | recorded_at | required (biri), date (ISO 8601) |
| coords.latitude | required, numeric, -90 .. 90 |
| coords.longitude | required, numeric, -180 .. 180 |
| coords.accuracy | nullable, 0 .. 999999.99 |
| coords.speed | nullable, -1 .. 999.99 (-1 = bilinmiyor) |
| coords.heading | nullable, -1 .. 360 (-1 = bilinmiyor) |
| coords.altitude | nullable, -999999.99 .. 999999.99 |
| coords.altitude_accuracy | nullable, -1 .. 999999.99 (-1 = bilinmiyor) |
| coords.speed_accuracy | nullable, -1 .. 999999.99 (-1 = bilinmiyor) |
| odometry | odometer | nullable, 0 .. 9999999999.99 (both field names are accepted) |
| activity.type | nullable, string max 191 |
| activity.confidence | nullable, integer 0 .. 100 |
| battery.level | nullable, -1 .. 1 (-1 = bilinmiyor, double/float kabul) |
| battery.is_charging | nullable, boolean |
| uuid, event, type | nullable, string max 191 |
| age | nullable, numeric >= 0 (float/int, e.g. 0.227) |
| extras | nullable, object (inside location row) |
| extras.power_save | nullable, boolean - this field first; otherwise root extras.power_save; if missing in both or explicitly null, then stored as power_save: null |
Location Object Structure (Array Item)
| Field | Type | Required | Description |
|---|---|---|---|
| uuid | string | Optional | Unique ID (max 191) |
| timestamp | recorded_at | string | ✓ (biri) | ISO 8601 (2026-01-06T14:30:00Z) |
| coords | object | ✓ | latitude and longitude are required; others must satisfy limits above |
| is_moving | boolean | Optional | Movement status |
| odometry | odometer | float | Optional | Odometer meters (0 .. 9999999999.99). Both field names are accepted (SDK v5: odometer) |
| activity | object | Optional | {type: string max 191, confidence: 0-100} |
| battery | object | Optional | {level: -1..1 (-1=unknown, double accepted), is_charging: bool} |
| extras | object | Optional | e.g. {"power_save": true}. Same keys as root extras; row value has priority. |
Sample Request (Batch / Single / Raw)
Batch (Array)
{
"location": [
{
"uuid": "A1-B2-C3-D4",
"timestamp": "2026-01-06T14:30:00.000Z",
"coords": {
"latitude": 41.0082,
"longitude": 28.9784,
"accuracy": 5.4,
"speed": 12.5,
"heading": 90.0,
"altitude": 50.0
},
"is_moving": true,
"odometry": 15000.0,
"activity": {"type": "in_vehicle", "confidence": 100},
"battery": {"level": 0.95, "is_charging": false}
},
{
"uuid": "E5-F6-G7-H8",
"timestamp": "2026-01-06T14:30:50.000Z",
"coords": {
"latitude": 41.0090,
"longitude": 28.9790,
"accuracy": 5.2,
"speed": 18.0,
"heading": 92.0,
"altitude": 51.0
},
"is_moving": true,
"odometry": 15200.0,
"activity": {"type": "in_vehicle", "confidence": 100},
"battery": {"level": 0.94, "is_charging": false}
}
],
"user_id": "555",
"company_id": "12",
"app_version": "1.0.0",
"extras": {
"power_save": true
}
}
Single Object
Root timestamp -> locations.updated_at and location_histories.created_at
{
"timestamp": "2026-02-18T10:20:35.238Z",
"app_version": "1.0.0",
"user_id": "4",
"company_id": "1",
"extras": {
"power_save": true
},
"location": {
"mock": true,
"coords": {
"speed_accuracy": 0,
"speed": 33.41,
"longitude": -122.07997503,
"latitude": 37.33492184,
"accuracy": 5,
"altitude": 0,
"heading": 307.97
},
"is_moving": true,
"age": 0.006,
"odometer": 1718302.37,
"uuid": "737D3103-B489-4828-B22F-664136C4E63D",
"activity": {"type": "unknown", "confidence": 0},
"battery": {"level": -1, "is_charging": false},
"recorded_at": "2026-02-18T10:20:35.224Z"
}
}
Raw Format (flutter_background_geolocation v5 compatible)
You can send raw array or single object directly without location wrapper.
API automatically detects coords and normalizes format. Plugin-side locationTemplate configuration is not required.
When payload is only location array, root extras cannot be used; send fields like power_save inside each location object extras or wrap as {"location": [...], "extras": {...}}.
Raw Array (multiple locations)
[
{
"uuid": "A1-B2-C3-D4",
"timestamp": "2026-01-06T14:30:00.000Z",
"coords": {
"latitude": 41.0082,
"longitude": 28.9784,
"accuracy": 5.4,
"speed": 12.5,
"heading": 90.0,
"altitude": 50.0
},
"is_moving": true,
"odometer": 15000.0,
"activity": {"type": "in_vehicle", "confidence": 100},
"battery": {"level": 0.95, "is_charging": false},
"extras": {"power_save": true}
},
{
"uuid": "E5-F6-G7-H8",
"timestamp": "2026-01-06T14:30:50.000Z",
"coords": {
"latitude": 41.0090,
"longitude": 28.9790,
"accuracy": 5.2,
"speed": 18.0,
"heading": 92.0
},
"is_moving": true,
"odometer": 15200.0,
"activity": {"type": "in_vehicle", "confidence": 100},
"battery": {"level": 0.94, "is_charging": false}
}
]
Raw Single Object
common.messages.flutter_v5_format
{
"uuid": "737D3103-B489-4828-B22F-664136C4E63D",
"timestamp": "2026-02-18T10:20:35.238Z",
"coords": {
"speed_accuracy": 0,
"speed": 33.41,
"longitude": -122.07997503,
"latitude": 37.33492184,
"accuracy": 5,
"altitude": 0,
"heading": 307.97
},
"is_moving": true,
"age": 0.006,
"odometer": 1718302.37,
"activity": {"type": "unknown", "confidence": 0},
"battery": {"level": -1, "is_charging": false},
"event": "motionchange",
"extras": {"power_save": true}
}
Response Scenarios
All Successful (200 OK)
All location records passed validation and were processed.
{
"success": true,
"message": "Locations processed successfully",
"data": {
"processed_count": 2,
"stored_history_count": 2,
"error_count": 0,
"errors": [],
"latest_timestamp": "2026-01-06T14:30:50.000Z"
}
}
Partial Success (207 Multi-Status)
Some records are invalid but at least one succeeded. Invalid records are reported in the errors array.
{
"success": true,
"message": "Processed with some errors",
"data": {
"processed_count": 1,
"stored_history_count": 1,
"error_count": 1,
"errors": [
{
"index": 1,
"input_data": {
"timestamp": "not-a-date",
"coords": {"latitude": 41.0}
},
"errors": {
"timestamp": [
"The timestamp field must be a valid date."
],
"coords.longitude": [
"The coords.longitude field is required."
]
}
}
],
"latest_timestamp": "2026-01-06T14:30:00.000Z"
}
}
All Failed (422)
No record passed validation or location field is missing/wrong type. For single format, errors[0] is returned.
{
"success": false,
"message": "All location records failed validation",
"errors": {
"processed_count": 0,
"error_count": 2,
"errors": [
{
"index": 0,
"errors": {
"coords.latitude": [
"The coords.latitude field must be between -90 and 90."
]
}
},
{
"index": 1,
"errors": {
"timestamp": ["The timestamp field is required."]
}
}
]
}
}
Single Format Validation Error (422)
If single object payload contains invalid location (e.g. missing coords), same error structure is returned with index: 0.
{
"success": false,
"message": "All location records failed validation",
"errors": {
"processed_count": 0,
"error_count": 1,
"errors": [
{
"index": 0,
"errors": {
"coords": ["The coords field is required."],
"coords.latitude": ["The coords.latitude field is required."],
"coords.longitude": ["The coords.longitude field is required."]
}
}
]
}
}
Invalid user_id (422)
user_id is not numeric.
{
"success": false,
"message": "user_id must be a numeric value."
}
User Not Found (404)
Specified user_id does not exist in database.
{
"success": false,
"message": "User with id '999' not found."
}
Company Mismatch (403)
company_id does not match user company.
{
"success": false,
"message": "company_id does not match your account."
}
/api/v1/locations
AUTH REQUIREDDrivers and Viewers see only their own records; Admins and Super-admins can list any record in the company. Use :from and :to (ISO 8601) to bound the time window. Sparse segments may return an empty list — try a wider window.
{
"success": true,
"message": "Locations retrieved successfully.",
"data": [
{
"id": 1,
"user_id": 1,
"company_id": 1,
"coords": {
"latitude": 41.015137,
"longitude": 28.97953,
"accuracy": 4.8,
"altitude": 52.0,
"heading": 95.0,
"speed": 22.5
},
"activity": {
"type": "in_vehicle",
"confidence": 100
},
"battery": {
"level": 0.94,
"is_charging": false
},
"odometer": 15500.0,
"is_moving": true,
"event": "location",
"uuid": "I9-J0-K1-L2",
"extras": {
"device_id": null,
"app_version": "1.0.0",
"raw_uuid": "I9-J0-K1-L2",
"power_save": true
},
"timestamp": "2026-01-06T14:31:40+00:00",
"updated_at": "2026-01-06T14:31:40+00:00"
}
],
"meta": {
"current_page": 1,
"from": 1,
"last_page": 5,
"total": 234,
"per_page": 50
},
"links": {
"first": "...",
"last": "...",
"prev": null,
"next": "..."
}
}
/api/v1/locations/{location}
AUTH REQUIREDView details of a specific location. Driver can view only own record, while Admin/Viewer can view any record in company.
Success Response (200)
{
"success": true,
"message": "Location retrieved successfully.",
"data": {
"id": 1,
"user_id": 1,
"company_id": 1,
"coords": {
"latitude": 41.015137,
"longitude": 28.97953,
"accuracy": 4.8,
"altitude": 52.0,
"heading": 95.0,
"speed": 22.5
},
"activity": {
"type": "in_vehicle",
"confidence": 100
},
"battery": {
"level": 0.94,
"is_charging": false
},
"odometer": 15500.0,
"is_moving": true,
"event": "location",
"uuid": "I9-J0-K1-L2",
"extras": {
"device_id": null,
"app_version": "1.0.0",
"raw_uuid": "I9-J0-K1-L2",
"power_save": true
},
"timestamp": "2026-01-06T14:31:40+00:00",
"updated_at": "2026-01-06T14:31:40+00:00"
}
}
Record Not Found (404)
{
"success": false,
"message": "Location not found."
}
Unauthorized Access (403)
Record belongs to another company or driver accesses another user record.
{
"success": false,
"message": "You do not have access to this resource."
}
📊 Location History Endpoints
💡 Note: Location history is read-only.
/api/v1/location-histories
AUTH REQUIREDList location history by role. Driver sees only own history, Admin/Viewer sees all history.
{
"success": true,
"message": "Location histories retrieved successfully.",
"data": [
{
"id": 1,
"user_id": 1,
"company_id": 1,
"coords": {
"latitude": 41.015137,
"longitude": 28.97953,
...
},
"timestamp": "2026-01-06T14:30:50+00:00"
}
],
"meta": {
"current_page": 1,
"last_page": 10,
"total": 500,
"per_page": 50
...
},
"links": { ... }
}
/api/v1/location-histories/{id}
AUTH REQUIREDView details of a specific history record. Driver can view only own record, while Admin/Viewer can view any record in company.
Success Response (200)
{
"success": true,
"message": "Location history retrieved successfully.",
"data": {
"id": 42,
"user_id": 1,
"company_id": 1,
"coords": {
"latitude": 41.0082,
"longitude": 28.9784,
"accuracy": 5.4,
"altitude": 50.0,
"heading": 90.0,
"speed": 12.5
},
"activity": {
"type": "in_vehicle",
"confidence": 100
},
"battery": {
"level": 0.95,
"is_charging": false
},
"odometer": 15000.0,
"is_moving": true,
"event": "location",
"uuid": "A1-B2-C3-D4",
"extras": null,
"timestamp": "2026-01-06T14:30:00+00:00"
}
}
Record Not Found (404)
{
"success": false,
"message": "LocationHistory not found."
}
Unauthorized Access (403)
{
"success": false,
"message": "You do not have access to this resource."
}
👤 User Endpoints
/api/v1/user
AUTH REQUIREDView profile information of authenticated user.
{
"success": true,
"message": "User profile retrieved successfully.",
"data": {
"id": 1,
"name": "Alex Yilmaz",
"email": "driver@example.com",
"phone": "+905551234567",
"country_code": "TR",
"vehicle": "34 ABC 123",
"job_status": "active",
"role": "driver",
"company_id": 1,
"created_at": "2025-01-01T10:00:00+00:00",
"updated_at": "2025-01-01T10:00:00+00:00"
}
}
/api/v1/user
AUTH REQUIREDUpdate user profile information. Only submitted fields are updated (partial update). For password update, password_confirmation is also required.
Parameters and Validation
| Parameter | Type | Required | Rule |
|---|---|---|---|
| name | string | Optional | common.messages.max_255 |
| string | Optional | valid email, max 255, unique (except own record) | |
| password | string | Optional | common.messages.min_8 confirmed required |
| password_confirmation | string | required if password provided | must match password |
| phone | string | Optional | nullable, max 50 |
| vehicle | string | Optional | nullable, max 255 |
| job_status | string | Optional | nullable, max 255 |
Sample Request
curl -X PUT https://traxnavi.com/api/v1/user \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Ahmet Yilmaz",
"phone": "+905551234567",
"vehicle": "34 XYZ 789"
}'
Success Response (200)
{
"success": true,
"message": "Profile updated successfully.",
"data": {
"id": 1,
"name": "Ahmet Yilmaz",
"email": "driver@example.com",
"phone": "+905551234567",
"country_code": "TR",
"vehicle": "34 XYZ 789",
"job_status": "active",
"company_id": 1,
"role": "driver",
"created_at": "2025-01-01T10:00:00+00:00",
"updated_at": "2026-02-17T15:30:00+00:00"
}
}
Validation Error (422)
{
"success": false,
"message": "Validation Failed",
"errors": {
"email": [
"The email has already been taken."
],
"password": [
"The password field must be at least 8 characters.",
"The password field confirmation does not match."
]
}
}
🌍 General Services
/api/v1/revgeo
PUBLICConverts coordinates to readable address (Reverse Geocoding). Uses caching mechanism.
URL Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| lat | float | ✓ | Latitude (-90 .. 90) |
| lng | float | ✓ | Longitude (-180 .. 180) |
| lang | string | Optional | Language (e.g. tr, en) |
Sample Request
GET /api/v1/revgeo?lat=41.0082&lng=28.9784&lang=tr
Success Response (200)
Returns short address, full address and country code (ISO 3166) in data. Results are cached for 10 minutes.
{
"success": true,
"message": null,
"data": {
"short": "Istanbul, Marmara Region, Turkiye",
"full": "Alemdar Street, Cankurtaran District, Istanbul, Marmara Region, 34110, Turkiye",
"cc": "TR"
}
}
Missing Parameter (422)
{
"success": false,
"message": "lat and lng parameters are required."
}
Not Numeric (422)
{
"success": false,
"message": "lat and lng must be numeric values."
}
Out of Range (422)
{
"success": false,
"message": "lat must be between -90 and 90, lng between -180 and 180."
}
Messaging API
1-on-1 messaging between admin and driver. All endpoints are protected by auth:sanctum.
List Conversations
GET /api/v1/conversations?page=1
User conversation list with last message and unread count (paginated).
Start / Find Conversation
POST /api/v1/conversations
Conversation Detail + Messages
GET /api/v1/conversations/{id}?page=1
Conversation detail and paginated message list (newest first).
Mark as Read
PUT /api/v1/conversations/{id}/read
Marks conversation messages as read (last_read_at = now).
Unread Count
GET /api/v1/conversations/unread-count
Contacts
GET /api/v1/conversations/contacts
List of users that current user can message. Returns active users with compatible roles (admin<->driver) in the same company. Deleted users are excluded.
Note: Use this list to start a new conversation. Existing conversations are fetched with GET /conversations.
Send Message
POST /api/v1/conversations/{id}/messages
When a message is sent, automatic FCM push notifications are sent to all recipient devices. Web panel updates in real-time via WebSocket.
Delete message
DELETE /api/v1/conversations/{id}/messages/{messageId}
Deletes a message sent by the current user. Only sender can delete the message.
Error Cases
| Code | Status |
|---|---|
| 403 | Message does not belong to this user or user is not a participant of this conversation |
| 404 | Message not found in this conversation |
Clear Chat History
DELETE /api/v1/conversations/{id}/messages
Permanently deletes all messages in a conversation. Any conversation participant can perform this action.
Warning: This action cannot be undone. All conversation messages are permanently deleted for both sides. Confirming with user before action is recommended.
Device Tokens (FCM)
Register/remove device FCM token for push notifications.
Register Token
POST /api/v1/device-tokens
Single active device: TraxNavi enforces one active device per user. When a token is registered, every other token for the same user is removed; on POST /api/v1/login, POST /api/v1/logout and password reset, all of the user's FCM tokens are deleted on the server. The mobile client must call POST /api/v1/device-tokens right after login (and whenever Firebase rotates the token) so that push notifications are routed to the correct device.
Token validity: Tokens reported by Firebase as invalidTokens or unknownTokens after a delivery attempt are removed automatically. Re-registering the latest FCM token will restore push delivery.
Remove Token (Logout)
DELETE /api/v1/device-tokens/{token}
Optional. Useful when the app wants to stop receiving push without logging out. The logout endpoint already removes every FCM token of the user, so this call is redundant after POST /api/v1/logout.
Device Info
Sends device information and app permission states to server. This endpoint updates the authenticated user's record directly in the users table. Should be called on each app launch (after login) and whenever any permission status changes on the device.
users table (not a separate table). When you call this endpoint, the following columns are updated for the authenticated user:
perm_location←locationperm_background_location←background_locationperm_motion←motionperm_notifications←notificationsperm_battery_optimization←battery_optimizationdevice_platform←platformdevice_brand←device_branddevice_model←device_modelos_version←os_versionapp_version←app_versiondevice_info_updated_at← auto-set to current timestampbackground_zero_since_at← set when background permission turns off; cleared when it turns on againbackground_zero_last_notified_at← server-managed reminder timestamp for background permission alertsbackground_zero_notify_step← server-managed escalating reminder step counter
Update Device Info
POST /api/v1/device-info
Auth: Bearer token required. The user is identified from the token — no user ID parameter needed.
| Field | Type | Required | DB Column | Description |
|---|---|---|---|---|
location | boolean | Yes | perm_location | Whether location permission is granted |
background_location | boolean | Yes | perm_background_location | Whether background location permission is granted |
motion | boolean | Yes | perm_motion | Whether motion/activity permission is granted |
notifications | boolean | Yes | perm_notifications | Whether notification permission is granted |
battery_optimization | boolean | Yes | perm_battery_optimization | Whether battery optimization is disabled (true = disabled = good) |
platform | string | Yes | device_platform | "android" or "ios" |
device_brand | string (max 80) | Yes | device_brand | device.brand |
device_model | string (max 80) | Yes | device_model | device.model |
os_version | string (max 80) | No | os_version | device.os_version |
app_version | string (max 80) | No | app_version | device.app_version |
- Immediately after successful login (first launch)
- When any device permission is granted or revoked by the user
- When the app returns to foreground (resume from background)
- After an app update (to report new app_version)
- All permission booleans are overwritten on each call — always send all 5 permission values.
- The
device_info_updated_attimestamp is set automatically by the server. - This endpoint only updates the currently authenticated user (identified via Bearer token). You cannot update another user's permissions.
- The response returns the full user object via UserResource, reflecting the updated state.
- Applies only to authenticated users with role
driver. - When
background_locationis false, the server tracks how long background permission has been off. - Reminder checks run hourly on the server and send only when the user reaches the next due hour.
- First push reminder is sent after 2 hours, then at escalating intervals: 6h, 12h, 20h, 30h, 42h, 56h, 72h, 90h, 110h, 132h, 156h.
- Reminders stop after 1 week (168 hours) with background permission still off.
- If background permission is granted again, reminder state is reset automatically on the next device-info call.
- If permission stays off continuously, the server increments
background_zero_notify_stepafter each successful reminder send. - If no active device token exists, no push can be sent for that user until a token is registered again.
- Reminder message language is selected from user locale (
users.locale); fallback is application default locale. - Tracking starts when this endpoint is called with
background_location=false(not retroactive for older records that never reported this state). - Push uses the same Android notification channel as messaging:
messages.
// FCM notification
title: "Background permission off"
body: "Your background location permission is off. Location cannot be sent."
// FCM data payload
{
"type": "background_permission_off",
"user_id": "123"
}📦 Load Management (Loads)
Load listing, detail, driver accept/decline and token-based opening for carrier and broker licensed companies. All load endpoints (except by-token) require Authorization: Bearer <token> and check.license.api. GET /loads/by-token/{token} does not require authentication (for share/deep link).
List Loads
GET /api/v1/loads?page=1&per_page=20&status=...&driver_id=...&start_date=...&end_date=...&search=...
Driver: assigned loads only. Admin (or other roles): all company loads. Filters: status, driver_id, start_date (pickup >=), end_date (pickup <=), search (load_no, rate, notes). Pagination: page, per_page (1-100). route_data is also returned in list (simplified geometry).
Load Detail
GET /api/v1/loads/{id}
Single load detail; pickup/drop list (including phone), and route_data (including map geometry). Driver can only view assigned load.
Why both mileage and route.summary.distance?
They are not duplicates of one field — two different sources. The JSON key stays mileage (legacy DB/web name for US trucking); values are already converted for the company region.
mileage/dead_head— Dispatch summary on the load row (list + detail). Web saves statute miles in loads.mileage; API returns that number in distance_unit (km or mi). dead_head is empty miles only on this level — not inside route.route.summary.distance(+lengthInMeters) — Geometry from the saved map route (detail only). OSRM/ORS meters → route.summary.distance in distance_unit; lengthInMeters stays SI. Null/absent when no route was saved. Can differ slightly from mileage (rounding, route edits, missing route).distance_unit— Single display unit for the company (TR=km, US=mi). Use for mileage, dead_head, and route.summary.distance.
List card / pay summary → mileage (+ dead_head). Map polyline screen → route.polyline + route.summary. Prefer route.summary.duration_seconds for ETA; do not sum mileage with route.summary.distance.
Load object fields (source → meaning)
| Field | Source | Description |
|---|---|---|
| id | loads.id | Primary key of the load. |
| load_no | loads.load_no | Human-readable load number set on the web panel. |
| rate | loads.rate | Load rate / pay amount (float). |
| status | loads.status | Machine status code (draft, available, assigned, accepted, in_progress, completed, …). |
| status_label | Computed in API (not a DB column) | Localized label for status (for UI display). |
| pickup_date | loads.pickup_date | Planned pickup date (Y-m-d) or null. |
| delivery_date | loads.delivery_date | Planned delivery date (Y-m-d) or null. |
| notes | loads.notes | Free-text notes from dispatch. |
| distance_unit | companies.license_region → CountryProfile | `km` (TR) or `mi` (US). Applies to mileage, dead_head, and route.summary.distance — not to lengthInMeters. |
| mileage | loads.mileage (DB always statute miles from web map) | Legacy key name (not renamed to distance). Loaded-trip distance from loads.mileage for list/detail cards. DB stores miles; API converts when distance_unit=km. Independent of route — present even if route is null. |
| dead_head | loads.dead_head (DB always statute miles from web map) | Empty/deadhead distance from loads.dead_head (same unit as mileage). Manual on web; not part of route.summary. |
| driver | users via loads.driver_id | Assigned driver snapshot (id, name, email, phone) or null. |
| pickups / drops | load_stops (type) | Stop rows sorted by sequence: name, address, city, state, country, phone, lat, lng. |
| documents | load_documents | PDF metadata only (no file bytes). Use /documents/{id}/preview or /download for the binary. |
| document_quota | Computed in API (not a DB column) | Upload slots: max = pickup_count + drop_count + 1 additional; used/remaining and per-slot filled flags. |
| route | loads.route_data JSON → overlay | Detail-only map payload from loads.route_data. null if no route saved. Omitted on GET /loads list. Do not treat as a second copy of mileage. |
| route.polyline | route_data.routes[].geometry | [[lat, lng], …] for drawing the path (capped ~500 points). |
| route.summary.lengthInMeters | route_data.totalDistance (SI m) | Canonical engine length in meters (SI). Use when you need precision; not converted by distance_unit. |
| route.summary.travelTimeInSeconds | route_data.totalDuration (s) | Canonical travel time in seconds from OSRM/ORS (via totalDuration). |
| route.summary.distance | Computed in API (not a DB column) | Same path as lengthInMeters, converted to distance_unit for UI. Map/ETA context — not a replacement for the load-level mileage field. |
| route.summary.distance_unit | CountryProfile | `km` (TR) or `mi` (US). Applies to mileage, dead_head, and route.summary.distance — not to lengthInMeters. |
| route.summary.duration_seconds | alias of travelTimeInSeconds | Same value as travelTimeInSeconds — convenience alias for mobile. |
Accept Load
POST /api/v1/loads/{id}/accept
Driver accepts load; driver_id is assigned, status -> accepted. Only role=driver can call this endpoint.
Decline Load
POST /api/v1/loads/{id}/decline
Driver declines load; driver_id is cleared, status -> available. Driver role only.
Update Load Status
PATCH /api/v1/loads/{id}/status
Updates load status field. Driver can update only assigned load status. Admin (or other roles) can update any load status in company.
| Field | Type | Required | Description |
|---|---|---|---|
| status | string | Yes | Allowed values: draft, available, assigned, accepted, in_pickup_location, in_progress, in_drop_location, completed, cancelled |
403 - Load does not belong to this user (driver cannot update someone else's load). 404 - Load not found or different company.
Load Documents
PDF-only documents per load. Quota = pickup_count + drop_count + 1 additional. Types: pickup, drop, additional. Max size 10 MB. Multipart form-data for upload/replace.
Who can manage: admin; viewer with can_manage_load_documents=true; assigned driver (upload/replace; delete blocked when status is completed).
Who can list/download: Same company users who can view the load (admin/viewer; assigned driver).
load_stop_id: Required for pickup/drop when that type has 2+ stops. If exactly one stop of that type exists, the server auto-selects it when the field is omitted. Always omit for additional.
Requires Authorization: Bearer <token> and check.license.api.
List Documents
GET /api/v1/loads/{id}/documents
Upload Document
POST /api/v1/loads/{id}/documents — multipart/form-data
| Field | Notes |
|---|---|
| file | required, PDF, max 10 MB |
| type | pickup | drop | additional |
| load_stop_id | nullable integer; required for pickup/drop when 2+ stops of that type; auto-inferred when exactly one; omit for additional |
422 — validation / quota exceeded / stop already has a document / type mismatch. 403 — not authorized to manage documents.
Replace Document
POST /api/v1/loads/{id}/documents/{documentId} — multipart/form-data, field file (PDF). Keeps the same document id, type and stop link; replaces the stored file.
403 / 404 — same access rules as upload; document must belong to the load.
Delete Document
DELETE /api/v1/loads/{id}/documents/{documentId}
403 — driver cannot delete when load status is completed.
Download Document
GET /api/v1/loads/{id}/documents/{documentId}/download
Returns the raw PDF binary (Content-Type: application/pdf) as a file download — not a JSON envelope.
404 — document or stored file missing. 403 — driver can only access documents on their assigned load.
Preview Document
NEWGET /api/v1/loads/{id}/documents/{documentId}/preview
Streams the PDF for in-app viewing (Content-Disposition: inline). Same auth as download; not a JSON envelope.
404 — document or stored file missing. 403 — driver can only access documents on their assigned load.
Load list and load detail (GET /loads, GET /loads/{id}) also include documents and document_quota.
Driver Stats
NEWGET /api/v1/drivers/stats — ?driver_id= required for admin/viewer; drivers omit it (self only)
Calendar day / week (Mon–Sun) / month totals in the company timezone: distance (km or mi), moving_seconds and stopped_seconds (idle folded into stopped). Completed days come from nightly pre-aggregation; today is computed live from hot location history (replay-style displacement + IFTA segment filters).
403 — driver cannot request another driver_id; or package not carrier/broker. 404 — driver not in company. 422 — admin/viewer missing driver_id.
Load (Share Token)
GET /api/v1/loads/by-token/{token} - No Auth
When opened via share link (/l/{token}) or deep link (traxnavi://load/{token}), mobile app fetches load from this endpoint. Token must be valid and expires_at must not be expired.
Load Push Notifications
Drivers receive automatic FCM push notifications on load lifecycle events. No polling needed; tie your screens to the payload below.
Prerequisite: The device must register its FCM token via POST /api/v1/device-tokens (see Device Tokens section) right after login. The server enforces a single active device, so logging out, logging in again or resetting the password automatically removes every FCM token of the user. DELETE /api/v1/device-tokens/{token} is optional and only needed if the app wants to stop receiving push without ending the session.
Events
| Event Type | Recipient | Trigger |
|---|---|---|
load_created |
Company admins/viewers (excluding actor) | A load is created on the web panel |
load_assigned |
Assigned driver | Admin assigns a driver via web (create/edit load) |
load_status_updated |
Assigned driver + admins/viewers | Status changes via any endpoint (web or API). The actor who triggered the change is excluded. |
load_deleted |
Assigned driver and company admins/viewers (excluding actor) | A load is deleted on the web panel |
new_message |
Conversation participants (except sender) | A message is sent to a conversation |
load_created
Sent to company admins/viewers (excluding the creator) when a load is created on the web. Uses the generic default.
load_assigned
Sent when an admin assigns a driver from the web panel (load create or edit). Android uses the loads channel; iOS uses a standard alert payload with badge: 1, mutable-content: 1 and content-available: 1.
load_status_updated
Sent to the assigned driver when status changes via web or API by anyone other than the driver themselves, and to admins/viewers (excluding the actor) on every status change. Uses the generic system channel; payload type is load_status_updated. Title/body are localized to the recipient locale.
load_deleted
Sent when a load is deleted on the web. The assigned driver receives a loads channel payload with load_id so the app can remove the load from lists. Other admins/viewers get a generic default push (type/title/body).
Mobile Integration Guide
Required Android Channels
Create these notification channels once at app startup so push sound/importance behave correctly:
| Channel ID | Used For | Importance |
|---|---|---|
loads |
load_assigned, load_deleted |
HIGH |
messages |
new_message |
HIGH |
default |
load_status_updated, system alerts |
HIGH |
Note: If a driver is reassigned from one driver to another via edit, only the new driver receives load_assigned. Invalid, unregistered or unknown tokens are auto-removed from the database after the first failed send.
In-App Notification Record
In addition to the push, every load_assigned / load_status_updated / load_deleted event is also stored in the notifications table for the recipient (including drivers). This lets future app versions show a bell-icon history without a separate endpoint change.