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
}

Content-Type: application/json

⚠️ 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
200OKSuccessful (GET/PUT/POST)
207Multi-StatusLocation batch: partially successful (some records invalid)
401UnauthenticatedMissing/invalid token or invalid login credentials
403ForbiddenUnauthorized (e.g. company_id mismatch)
404Not FoundResource or endpoint not found (e.g. user_id)
405Method Not AllowedWrong HTTP method
422Validation FailedValidation error; details in the errors field
429Too Many RequestsRate limit exceeded
500Server ErrorUnexpected 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.

Duration: 365 days
🔄

Refresh Token

Used to refresh the access token.

Duration: 730 days
🔁

Token Rotation:

On each refresh, a new token pair is issued and the old refresh token is revoked.

Get Token

POST /api/v1/login
{
  "email": "user@example.com",
  "password": "your-password"
}

Token Usage

Header
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

POST

/api/v1/login

PUBLIC

Exchanges 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
email 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
emailrequired, email
passwordrequired, string
clientnullable, 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
tokenAccess token (used as Bearer, valid for 365 days)
refresh_tokenRefresh token (used to renew access token, valid for 730 days)
expires_inAccess token lifetime (in seconds, 31536000 = 365 days)
country_codeISO 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.

POST

/api/v1/forgot-password

PUBLIC

Send a password reset link to the email address.

Request Body

Parameter Type Required Description
email 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."
}
POST

/api/v1/reset-password

PUBLIC

Reset the password using the token.

Request Body

Parameter Type Required Description
token string Reset token from email
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."
}
POST

/api/v1/refresh

PUBLIC

Exchanges 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.

POST

/api/v1/logout

AUTH REQUIRED

Logout 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

POST

/api/v1/locations

AUTH REQUIRED

Send 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 locationTemplate configuration
  • Partial Success: Valid records are processed, invalid ones are reported.
  • Latest location is automatically saved into locations table
  • Timestamp Protection: The locations table 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; only location_histories is updated.
  • All locations are appended to location_histories table
  • Single format: If root timestamp exists, it is used as locations.updated_at and location_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_atrequired (biri), date (ISO 8601)
coords.latituderequired, numeric, -90 .. 90
coords.longituderequired, numeric, -180 .. 180
coords.accuracynullable, 0 .. 999999.99
coords.speednullable, -1 .. 999.99 (-1 = bilinmiyor)
coords.headingnullable, -1 .. 360 (-1 = bilinmiyor)
coords.altitudenullable, -999999.99 .. 999999.99
coords.altitude_accuracynullable, -1 .. 999999.99 (-1 = bilinmiyor)
coords.speed_accuracynullable, -1 .. 999999.99 (-1 = bilinmiyor)
odometry | odometernullable, 0 .. 9999999999.99 (both field names are accepted)
activity.typenullable, string max 191
activity.confidencenullable, integer 0 .. 100
battery.levelnullable, -1 .. 1 (-1 = bilinmiyor, double/float kabul)
battery.is_chargingnullable, boolean
uuid, event, typenullable, string max 191
agenullable, numeric >= 0 (float/int, e.g. 0.227)
extrasnullable, object (inside location row)
extras.power_savenullable, 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."
}
GET

/api/v1/locations

AUTH REQUIRED

Drivers 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": "..."
  }
}
GET

/api/v1/locations/{location}

AUTH REQUIRED

View 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.

GET

/api/v1/location-histories

AUTH REQUIRED

List 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": { ... }
}
GET

/api/v1/location-histories/{id}

AUTH REQUIRED

View 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

GET

/api/v1/user

AUTH REQUIRED

View 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"
  }
}
PUT

/api/v1/user

AUTH REQUIRED

Update 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
namestringOptionalcommon.messages.max_255
emailstringOptionalvalid email, max 255, unique (except own record)
passwordstringOptionalcommon.messages.min_8 confirmed required
password_confirmationstringrequired if password providedmust match password
phonestringOptionalnullable, max 50
vehiclestringOptionalnullable, max 255
job_statusstringOptionalnullable, 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

GET

/api/v1/revgeo

PUBLIC

Converts 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.

GET

List Conversations

GET /api/v1/conversations?page=1

User conversation list with last message and unread count (paginated).

{
  "success": true,
  "data": [
    {
      "id": 1,
      "company_id": 1,
      "participants": [
        { "id": 2, "name": "Admin", "role": "admin" },
        { "id": 6, "name": "Driver 4", "role": "driver" }
      ],
      "last_message": {
        "id": 42,
        "body": "Route has changed",
        "type": "text",
        "sender_id": 2,
        "created_at": "2026-03-03T10:30:00+00:00"
      },
      "unread_count": 3,
      "created_at": "2026-03-01T08:00:00+00:00",
      "updated_at": "2026-03-03T10:30:00+00:00"
    }
  ],
  "meta": { "current_page": 1, "last_page": 1, "per_page": 20, "total": 1 }
}

Start / Find Conversation

POST /api/v1/conversations

// Request
{ "recipient_id": 6 }

// Response (201 Created)
{
  "success": true,
  "message": "Conversation ready.",
  "data": { "id": 1, "company_id": 1, "participants": [...], ... }
}

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

{ "success": true, "data": { "count": 5 } }
GET

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.

// Response (200)
{
  "success": true,
  "message": "Contacts retrieved.",
  "data": [
    {
      "id": 4,
      "name": "John Driver",
      "role": "driver",
      "email": "john@example.com",
      "phone": "+1234567890",
      "vehicle": "34 ABC 123"
    },
    {
      "id": 7,
      "name": "Jane Driver",
      "role": "driver",
      "email": "jane@example.com",
      "phone": null,
      "vehicle": null
    }
  ]
}

Note: Use this list to start a new conversation. Existing conversations are fetched with GET /conversations.

POST

Send Message

POST /api/v1/conversations/{id}/messages

// Request
{
  "body": "Hello, route has changed.",
  "type": "text"   // optional: text (default), alert, system
}

// Response (201 Created)
{
  "success": true,
  "message": "Message sent.",
  "data": {
    "id": 43,
    "conversation_id": 1,
    "sender_id": 2,
    "sender_name": "Admin",
    "body": "Hello, route has changed.",
    "type": "text",
    "created_at": "2026-03-03T11:00:00+00:00"
  }
}

When a message is sent, automatic FCM push notifications are sent to all recipient devices. Web panel updates in real-time via WebSocket.

DELETE

Delete message

DELETE /api/v1/conversations/{id}/messages/{messageId}

Deletes a message sent by the current user. Only sender can delete the message.

// Response (200)
{
  "success": true,
  "message": "Message deleted."
}

Error Cases

CodeStatus
403Message does not belong to this user or user is not a participant of this conversation
404Message not found in this conversation
DELETE

Clear Chat History

DELETE /api/v1/conversations/{id}/messages

Permanently deletes all messages in a conversation. Any conversation participant can perform this action.

// Response (200)
{
  "success": true,
  "message": "All messages cleared."
}

Warning: This action cannot be undone. All conversation messages are permanently deleted for both sides. Confirming with user before action is recommended.

POST

Device Tokens (FCM)

Register/remove device FCM token for push notifications.

Register Token

POST /api/v1/device-tokens

// Request
{
  "token": "cXB1c2hfdG9rZW5fMTIzNDU2Nzg5...",
  "platform": "android"   // android | ios | web
}

// Response (201)
{ "success": true, "message": "Device token registered." }

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.

{ "success": true, "message": "Device token removed." }
POST

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.

How it works: The permission booleans and device metadata are stored as columns on the users table (not a separate table). When you call this endpoint, the following columns are updated for the authenticated user:
  • perm_locationlocation
  • perm_background_locationbackground_location
  • perm_motionmotion
  • perm_notificationsnotifications
  • perm_battery_optimizationbattery_optimization
  • device_platformplatform
  • device_branddevice_brand
  • device_modeldevice_model
  • os_versionos_version
  • app_versionapp_version
  • device_info_updated_at ← auto-set to current timestamp
  • background_zero_since_at ← set when background permission turns off; cleared when it turns on again
  • background_zero_last_notified_at ← server-managed reminder timestamp for background permission alerts
  • background_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
locationbooleanYesperm_locationWhether location permission is granted
background_locationbooleanYesperm_background_locationWhether background location permission is granted
motionbooleanYesperm_motionWhether motion/activity permission is granted
notificationsbooleanYesperm_notificationsWhether notification permission is granted
battery_optimizationbooleanYesperm_battery_optimizationWhether battery optimization is disabled (true = disabled = good)
platformstringYesdevice_platform"android" or "ios"
device_brandstring (max 80)Yesdevice_branddevice.brand
device_modelstring (max 80)Yesdevice_modeldevice.model
os_versionstring (max 80)Noos_versiondevice.os_version
app_versionstring (max 80)Noapp_versiondevice.app_version
// Request
POST /api/v1/device-info
Authorization: Bearer {token}
Content-Type: application/json

{
  "location": true,
  "background_location": true,
  "motion": true,
  "notifications": true,
  "battery_optimization": true,
  "platform": "android",
  "device_brand": "Samsung",
  "device_model": "SM-G998B",
  "os_version": "14",
  "app_version": "1.2.0"
}

// Response (200)
{
  "success": true,
  "message": "Device info updated successfully.",
  "data": {
    "id": 1,
    "name": "Ahmet",
    "email": "ahmet@example.com",
    "permissions": {
      "location": true,
      "background_location": true,
      "motion": true,
      "notifications": true,
      "battery_optimization": true
    },
    "device": {
      "platform": "android",
      "brand": "Samsung",
      "model": "SM-G998B",
      "os_version": "14",
      "app_version": "1.2.0"
    },
    "device_info_updated_at": "2026-03-05T16:30:00+00:00"
  }
}

// Validation Error (422)
{
  "message": "The location field is required.",
  "errors": {
    "location": ["The location field is required."],
    "platform": ["The selected platform is invalid."]
  }
}
When to call:
  • 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)
Important notes:
  • All permission booleans are overwritten on each call — always send all 5 permission values.
  • The device_info_updated_at timestamp 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.
Background permission reminders (FCM, messages channel):
  • Applies only to authenticated users with role driver.
  • When background_location is 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_step after 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).

GET

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).

// Response (200)
{
  "success": true,
  "message": "Loads retrieved.",
  "data": [
    {
      "id": 1,
      "load_no": "LD-001",
      "rate": 1500.00,
      "status": "assigned",
      "status_label": "Assigned",
      "pickup_date": "2026-03-10",
      "delivery_date": "2026-03-12",
      "notes": null,
      "distance_unit": "km",
      "mileage": 724,
      "dead_head": 129,
      "driver": { "id": 5, "name": "Ahmet", "email": "ahmet@example.com", "phone": "+90..." },
      "pickups": [
        { "id": 10, "sequence": 0, "name": "Warehouse A", "address": "...", "city": "Istanbul", "state": null, "country": "TR", "phone": null, "lat": 41.02, "lng": 28.97 }
      ],
      "drops": [
        { "id": 11, "sequence": 1, "name": "Store B", "address": "...", "city": "Ankara", "state": null, "country": "TR", "phone": null, "lat": 39.92, "lng": 32.85 }
      ],
      "documents": [
        { "id": 1, "load_id": 1, "type": "pickup", "load_stop_id": 10, "original_name": "bol.pdf", "size_bytes": 12345, "mime": "application/pdf", "uploaded_by": 5, "created_at": "...", "updated_at": "..." }
      ],
      "document_quota": { "max": 3, "used": 1, "remaining": 2, "pickup_count": 1, "drop_count": 1, "slots": [...] }
      // no "route" / "route_data" on list
    }
  ],
  "meta": { "current_page": 1, "last_page": 1, "per_page": 20, "total": 1 },
  "links": { "first": "...", "last": "...", "prev": null, "next": null }
}
GET

Load Detail

NEW

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
idloads.idPrimary key of the load.
load_noloads.load_noHuman-readable load number set on the web panel.
rateloads.rateLoad rate / pay amount (float).
statusloads.statusMachine status code (draft, available, assigned, accepted, in_progress, completed, …).
status_labelComputed in API (not a DB column)Localized label for status (for UI display).
pickup_dateloads.pickup_datePlanned pickup date (Y-m-d) or null.
delivery_dateloads.delivery_datePlanned delivery date (Y-m-d) or null.
notesloads.notesFree-text notes from dispatch.
distance_unitcompanies.license_region → CountryProfile`km` (TR) or `mi` (US). Applies to mileage, dead_head, and route.summary.distance — not to lengthInMeters.
mileageloads.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_headloads.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.
driverusers via loads.driver_idAssigned driver snapshot (id, name, email, phone) or null.
pickups / dropsload_stops (type)Stop rows sorted by sequence: name, address, city, state, country, phone, lat, lng.
documentsload_documentsPDF metadata only (no file bytes). Use /documents/{id}/preview or /download for the binary.
document_quotaComputed in API (not a DB column)Upload slots: max = pickup_count + drop_count + 1 additional; used/remaining and per-slot filled flags.
routeloads.route_data JSON → overlayDetail-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.polylineroute_data.routes[].geometry[[lat, lng], …] for drawing the path (capped ~500 points).
route.summary.lengthInMetersroute_data.totalDistance (SI m)Canonical engine length in meters (SI). Use when you need precision; not converted by distance_unit.
route.summary.travelTimeInSecondsroute_data.totalDuration (s)Canonical travel time in seconds from OSRM/ORS (via totalDuration).
route.summary.distanceComputed 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_unitCountryProfile`km` (TR) or `mi` (US). Applies to mileage, dead_head, and route.summary.distance — not to lengthInMeters.
route.summary.duration_secondsalias of travelTimeInSecondsSame value as travelTimeInSeconds — convenience alias for mobile.
// Response (200) — list fields + route (detail only; null if no saved route)
{
  "success": true,
  "data": {
    "id": 1,
    "load_no": "LD-001",
    "rate": 1500.00,
    "status": "assigned",
    "status_label": "Assigned",
    "pickup_date": "2026-03-10",
    "delivery_date": "2026-03-12",
    "notes": null,
    "distance_unit": "km",
    "mileage": 724,
    "dead_head": 129,
    "driver": { "id": 5, "name": "Ahmet", "email": "...", "phone": "..." },
    "pickups": [ { "id": 10, "sequence": 0, "name": "...", "lat": 41.02, "lng": 28.97, ... } ],
    "drops": [ { "id": 11, "sequence": 1, "name": "...", "lat": 39.92, "lng": 32.85, ... } ],
    "documents": [ ... ],
    "document_quota": { "max": 3, "used": 1, "remaining": 2, "slots": [...] },
    "route": {
      "polyline": [[41.02, 28.97], [39.92, 32.85]],
      "summary": {
        "lengthInMeters": 724205,
        "travelTimeInSeconds": 28800,
        "distance": 724.2,
        "distance_unit": "km",
        "duration_seconds": 28800
      }
    }
  }
}
POST

Accept Load

POST /api/v1/loads/{id}/accept

Driver accepts load; driver_id is assigned, status -> accepted. Only role=driver can call this endpoint.

// Response (200) - updated load object (data)
POST

Decline Load

POST /api/v1/loads/{id}/decline

Driver declines load; driver_id is cleared, status -> available. Driver role only.

// Response (200) - updated load object (data)
PATCH

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.

FieldTypeRequiredDescription
statusstringYesAllowed values: draft, available, assigned, accepted, in_pickup_location, in_progress, in_drop_location, completed, cancelled
// Request
{
  "status": "in_progress"
}

// Response (200)
{
  "success": true,
  "message": "Status updated.",
  "data": { ... updated load object (id, load_no, status, status_label, pickups, drops, driver, route, documents, document_quota, etc.) ... }
}

403 - Load does not belong to this user (driver cannot update someone else's load). 404 - Load not found or different company.

PDF

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.

GET

List Documents

GET /api/v1/loads/{id}/documents

// Response (200)
{
  "success": true,
  "message": "Documents retrieved.",
  "data": {
    "documents": [
      {
        "id": 1,
        "load_id": 42,
        "type": "pickup",
        "load_stop_id": 10,
        "original_name": "bol.pdf",
        "size_bytes": 12345,
        "mime": "application/pdf",
        "uploaded_by": 5,
        "created_at": "2026-07-10T08:00:00+00:00",
        "updated_at": "2026-07-10T08:00:00+00:00"
      }
    ],
    "document_quota": {
      "max": 3,
      "used": 1,
      "remaining": 2,
      "pickup_count": 1,
      "drop_count": 1,
      "slots": [
        { "type": "pickup", "load_stop_id": 10, "stop_name": "Warehouse A", "sequence": 0, "filled": true, "document_id": 1 },
        { "type": "drop", "load_stop_id": 11, "stop_name": "Store B", "sequence": 1, "filled": false, "document_id": null },
        { "type": "additional", "load_stop_id": null, "stop_name": null, "sequence": null, "filled": false, "document_id": null }
      ]
    }
  }
}
POST

Upload Document

POST /api/v1/loads/{id}/documentsmultipart/form-data

FieldNotes
filerequired, PDF, max 10 MB
typepickup | drop | additional
load_stop_idnullable integer; required for pickup/drop when 2+ stops of that type; auto-inferred when exactly one; omit for additional
// Response (201)
{
  "success": true,
  "message": "Document uploaded.",
  "data": {
    "id": 1,
    "load_id": 42,
    "type": "pickup",
    "load_stop_id": 10,
    "original_name": "bol.pdf",
    "size_bytes": 12345,
    "mime": "application/pdf",
    "uploaded_by": 5,
    "created_at": "...",
    "updated_at": "..."
  }
}

422 — validation / quota exceeded / stop already has a document / type mismatch. 403 — not authorized to manage documents.

POST

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.

// Response (200)
{ "success": true, "message": "Document replaced.", "data": { "id": 1, "type": "pickup", "original_name": "bol-v2.pdf", ... } }

403 / 404 — same access rules as upload; document must belong to the load.

DELETE

Delete Document

DELETE /api/v1/loads/{id}/documents/{documentId}

// Response (200)
{ "success": true, "message": "Document deleted.", "data": null }

403 — driver cannot delete when load status is completed.

GET

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.

GET

Preview Document

NEW

GET /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.

GET

Driver Stats

NEW

GET /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).

// Response (200)
{
  "success": true,
  "message": "Driver stats retrieved.",
  "data": {
    "driver_id": 5,
    "distance_unit": "km",
    "timezone": "Europe/Istanbul",
    "day":   { "distance": 128.4, "moving_seconds": 7200, "stopped_seconds": 3600 },
    "week":  { "distance": 812.0, "moving_seconds": 45000, "stopped_seconds": 20000 },
    "month": { "distance": 3100.5, "moving_seconds": 180000, "stopped_seconds": 90000 }
  }
}

403 — driver cannot request another driver_id; or package not carrier/broker. 404 — driver not in company. 422 — admin/viewer missing driver_id.

GET

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.

// 200 – load object (pickups, drops, route_data, …)
// 404 – unknown token, revoked row, or missing load (error_code: load_share_link_invalid)
// 410 – link past expires_at (error_code: load_share_link_expired)
// 409 – load already has a driver (another driver accepted or dispatcher assigned on web; error_code: load_taken_by_other_driver)
FCM

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.

// FCM notification
{
  "title": "New load created",
  "body":  "Load LD-2026-0042 has been created."
}

// FCM data payload
{
  "type":  "load_created",
  "title": "New load created",
  "body":  "Load LD-2026-0042 has been created."
}

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.

// FCM notification
{
  "title": "Yeni yuk atandi",
  "body":  "LD-2026-0042 nolu yuk size atandi."
}

// FCM data payload
{
  "type":          "load_assigned",
  "load_id":       "123",
  "load_no":       "LD-2026-0042",
  "pickup_date":   "2026-05-01",
  "delivery_date": "2026-05-03"
}

// iOS APNS aps payload
{
  "alert": {
    "title": "Yeni yuk atandi",
    "body":  "LD-2026-0042 nolu yuk size atandi."
  },
  "sound": "default",
  "badge": 1,
  "mutable-content": 1,
  "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.

// FCM notification
{
  "title": "Yuk durumu guncellendi",
  "body":  "LD-2026-0042 nolu yukun durumu Yolda olarak guncellendi."
}

// FCM data payload (may vary)
{
  "type":  "load_status_updated",
  "title": "Yuk durumu guncellendi",
  "body":  "LD-2026-0042 nolu yukun durumu Yolda olarak guncellendi."
}

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).

// FCM notification (driver)
{
  "title": "Yuk silindi",
  "body":  "LD-2026-0042 nolu yuk silindi."
}

// FCM data payload (driver — loads channel)
{
  "type":    "load_deleted",
  "load_id": "123",
  "load_no": "LD-2026-0042"
}

// FCM data payload (admins/viewers — default channel)
{
  "type":  "load_deleted",
  "title": "Yuk silindi",
  "body":  "LD-2026-0042 nolu yuk silindi."
}

Mobile Integration Guide

// 1) Register device token right after login
POST /api/v1/device-tokens
Authorization: Bearer <access_token>
{
  "token": "<FCM registration token from FirebaseMessaging.getToken()>",
  "platform": "android" // or "ios"
}

// 2) Handle foreground/background messages
// Flutter (firebase_messaging) example:
FirebaseMessaging.onMessage.listen((msg) {
  final type = msg.data['type'];
  if (type == 'load_assigned') {
    final loadId = msg.data['load_id'];
    // open /loads/:loadId (load_id is present in this payload)
  } else if (type == 'load_deleted') {
    final loadId = msg.data['load_id'];
    // remove loadId from local lists / close detail if open
  } else if (type == 'load_status_updated' || type == 'load_created') {
    // refresh loads list (FCM data has type/title/body only)
  }
});

FirebaseMessaging.onMessageOpenedApp.listen((msg) {
  // same routing when user taps notification
});

// 3) Logout (server clears every FCM token of the user automatically)
POST /api/v1/logout
Authorization: Bearer <access_token>

// Optional: stop push without logging out
DELETE /api/v1/device-tokens/<fcm_token>

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.