API Dokümantasyonu

Mobil cihazlardan konum verisi toplayan ve takip eden modern bir RESTful API servisidir. Gerçek zamanlı takip, geçmiş veriler ve kullanıcı yönetimi için kapsamlı endpoint'ler sunar.

📋 Genel Bakış

TraxNavi API, mobil cihazlardan (iOS ve Android) gelen konum verilerini işler ve saklar. API, RESTful prensiplere göre tasarlanmıştır.

Temel Bilgiler

Temel URL https://traxnavi.com/api/v1
Sürüm v1
Format JSON
Kimlik Laravel Sanctum (Bearer Token)
CORS traxops.com, turusatools.com, Mobil Uygulamalar

Veri Formatı

Tüm endpoint'ler artık standart bir JSON yapısı kullanır.

Standart Başarılı Yanıt:

{
    "success": true,
    "message": "Optional message",
    "data": { ... },
    "meta": { ... } // Sayfalama varsa
}

Standart Hata Yanıtı:
{
    "success": false,
    "message": "Error description",
    "errors": { ... } // Validation errors
}

Content-Type: application/json

⚠️ Validasyon ve Hata Kodları

İstek yükü veya parametreleri geçersiz olduğunda döndürülür. :field alanı beklenen kuralı karşılamıyor. Tam şema için doğrulama bölümüne bakın.

HTTP Durum Kodları

Kod Anlam Kullanım
200OKBaşarılı (GET/PUT/POST)
207Multi-StatusKonum toplu gönderim: kısmi başarı (bazı kayıtlar geçersiz)
401Kimlik doğrulanmadıEksik/geçersiz token veya hatalı giriş bilgisi
403YasaklandıYetkisiz (örn. company_id eşleşmiyor)
404BulunamadıKaynak veya endpoint bulunamadı (örn. user_id)
405Metoda İzin VerilmiyorYanlış HTTP metodu
422Doğrulama BaşarısızValidasyon hatası; detaylar errors alanında
429Çok Fazla İstekRate limit aşıldı
500Sunucu HatasıBeklenmeyen sunucu hatası

Standart Yanıt Yapısı

Tüm API endpoint'leri aynı JSON zarfını kullanır. success alanı boolean'dır, message açıklama metni içerir ve data (başarılı durumda) veya errors (başarısız) yük detaylarını taşır.

Başarılı Yanıt (200 OK)

{
  "success": true,
  "message": "Operation description",
  "data": { ... }
}

Sayfalı Koleksiyon Yanıtı (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"
  }
}

Hata Yanıt Örnekleri

401 Kimlik Doğrulanmadı

Token gönderilmedi veya süresi doldu.

{
  "success": false,
  "message": "Unauthenticated"
}

403 Yasak

Yetkisiz erişim (örn. kayıt başka şirkete ait, sürücü başka kullanıcıya erişmeye çalışıyor).

{
  "success": false,
  "message": "You do not have access to this resource."
}

404 Bulunamadı

Kaynak bulunamadı (model veya endpoint).

{
  "success": false,
  "message": "Location not found."
}

405 Metoda İzin Verilmiyor

Yanlış HTTP metodu kullanıldı (örn. GET yerine POST).

{
  "success": false,
  "message": "HTTP method not allowed for this endpoint."
}

422 Doğrulama Başarısız

Gönderilen veri doğrulama kurallarını karşılamıyor. Alan bazlı hata mesajları errors nesnesinde döner.

{
  "success": false,
  "message": "Validation Failed",
  "errors": {
    "email": [
      "The email field must be a valid email address."
    ],
    "password": [
      "The password field is required."
    ]
  }
}

429 Çok Fazla İstek

Rate limit aşıldı. Yanıt başlıklarında Retry-After.

{
  "success": false,
  "message": "Too many requests. Please slow down."
}

207 Multi-Status (Batch Kısmi Başarı)

Toplu konum gönderiminde bazı kayıtlar başarılı, bazıları hatalı olduğunda döner.

{
  "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 Sunucu Hatası

Beklenmeyen sunucu hatası. Debug modunda debug nesnesi eklenir.

{
  "success": false,
  "message": "A database error occurred."
}

💡 İpucu: Kısmi başarıda konum batch endpoint'i 207. Tüm kayıtlar geçersizse 422 ve errors her öğe index, errors (alan -> mesajlar) ve input_data.

🔐 Kimlik Doğrulama

💡 Not: API access token + refresh token çifti kullanır. Access token aldıktan sonra tüm API isteklerinde şunu ekleyin: Authorization: Bearer {token} başlığı.

🎫

Access Token

API isteklerinde kullanılır.

Süre: 365 gün
🔄

Token Yenile

Access token yenilemek için kullanılır.

Süre: 730 gün
🔁

Token Yenileme:

Her yenilemede yeni bir token çifti üretilir ve eski refresh token iptal edilir.

Token Alma

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

Token Kullanımı

Başlık
Authorization: Bearer your-token-here
Content-Type: application/json

⚠️ Token Yaşam Döngüsü: Access token 365 gün geçerlidir; süresi dolduğunda POST /api/v1/refresh yukarıdaki endpoint'e refresh token göndererek yeni bir çift alabilirsiniz. Normal kullanımda refresh nadiren gerekir. Login, logout ve şifre sıfırlama işlemlerinde tüm token'lar otomatik iptal edilir.

İstek Sınırlama

Endpoint Grubu Limit Açıklama
Giriş 5 / dk Brute-force koruması
Konumlar (POST) Driver: 60/dk
Company: 6000/dk
Rol bazlı ingest limiti
Diğer Endpoint'ler Driver: 1000/dk
Company: 10000/dk
Genel API kullanımı

⚠️ Dikkat: Rate limit aşıldığında 429 Too Many Requests hatası alırsınız.

🔑 Kimlik Doğrulama Endpoint'leri

POST

/api/v1/login

HERKESE AÇIK

E-posta ve şifreyi Sanctum erişim belirteci ve yenileme belirteci ile değiştirir. Yenileme belirteci tek kullanımlıktır — her oturum açma (veya şifre değişikliği) bir öncekini iptal eder. Kullanıcının FCM belirteçleri de yeni bir oturum açmada iptal edilir, böylece anlık bildirimler yeni cihazı izler.

İstek Gövdesi

Parametre Tür Gerekli Açıklama
email string Kullanıcı email adresi (geçerli email formatı)
password string Kullanıcı şifresi
client string Hayır "mobile" veya "web". Mobil uygulama mutlaka "mobile"; göndermeli; sadece driver giriş yapabilir.

Validasyon Kuralları

Alan Kural
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"
  }'

Başarılı 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
  }
}
Alan Açıklama
tokenAccess token (Bearer olarak kullanılır, 365 gün geçerli)
refresh_tokenRefresh token (access token yenilemek için, 730 gün geçerli)
expires_inAccess token ömrü (saniye cinsinden, 31536000 = 365 gün)
country_codeISO 3166-1 alpha-2 ülke kodu (ör. TR, US, DE)

Hatalı Response (401)

{
  "success": false,
  "message": "Invalid credentials."
}

Validasyon Hatası (422)

{
  "success": false,
  "message": "Validation Failed",
  "errors": {
    "email": ["The email field must be a valid email address."],
    "password": ["The password field is required."]
  }
}

⚠️ Not: Login işlemi mevcut tüm access ve refresh token'ları iptal eder, yeni bir çift (access + refresh) oluşturur. api_docs.store_the refresh_token değerini güvenli bir şekilde saklayın.

POST

/api/v1/forgot-password

HERKESE AÇIK

Şifre sıfırlama bağlantısını e-posta adresine gönderin.

İstek Gövdesi

Parametre Tür Gerekli Açıklama
email string Geçerli email formatı, zorunlu

Başarılı Yanıt (200)

{
  "success": true,
  "message": "We have emailed your password reset link.",
  "data": null
}

Validasyon Hatası (422)

{
  "success": false,
  "message": "Validation Failed",
  "errors": {
    "email": ["The email field is required."]
  }
}

Kullanıcı Bulunamadı (400)

{
  "success": false,
  "message": "We can't find a user with that email address."
}
POST

/api/v1/reset-password

HERKESE AÇIK

Token'ı kullanarak şifreyi sıfırlayın.

İstek Gövdesi

Parametre Tür Gerekli Açıklama
token string E-postadaki reset token
email string Kullanıcı e-posta adresi
password string Yeni şifre (min. 8 kar.)
password_confirmation string Şifre tekrarı

Başarılı Yanıt (200)

{
  "success": true,
  "message": "Your password has been reset.",
  "data": null
}

Validasyon Hatası (422)

{
  "success": false,
  "message": "Validation Failed",
  "errors": {
    "password": ["The password field must be at least 8 characters."],
    "token": ["The token field is required."]
  }
}

Geçersiz Token (400)

{
  "success": false,
  "message": "This password reset token is invalid."
}
POST

/api/v1/refresh

HERKESE AÇIK

Geçerli (iptal edilmemiş, süresi dolmamış) bir yenileme belirtecini yeni bir erişim + yenileme belirteci çiftiyle değiştirir. Sağlanan yenileme belirteci atomik olarak iptal edilir — tekrar kullanılması 401 döndürür. Bu uç nokta için Authorization başlığı GEREKMEZ; yenileme belirteci yalnızca istek gövdesinde gönderilir.

İstek Gövdesi

Parametre Tür Gerekli Açıklama
refresh_token string Login'den alınan 64 karakterlik refresh token
client string Hayır "mobile" veya "web". Mobil uygulama "mobile".
curl -X POST https://traxnavi.com/api/v1/refresh \
  -H "Content-Type: application/json" \
  -d '{
    "refresh_token": "a1b2c3d4e5f6...64karakter",
    "client": "mobile"
  }'

Not: Bu endpoint Authorization header gerektirmez. Sadece request body'de refresh token gönderin.

Başarılı Yanıt (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
  }
}

Geçersiz/Expired Token (401)

{
  "success": false,
  "message": "Invalid or expired refresh token."
}

🔄 Token Yenileme: Her başarılı refresh'te eski refresh token iptal edilir ve yeni bir çift döner. Mobil uygulamada her zaman en son alınan refresh_token değerini güvenli bir şekilde saklayın.

POST

/api/v1/logout

KİMLİK DOĞRULAMA GEREKLİ

Mevcut access token'ı ve tüm aktif refresh token'ları iptal ederek çıkış yapar. Sunucu ayrıca kullanıcının tüm FCM cihaz tokenlarını siler; bu nedenle yeni bir token kaydedilene kadar tüm cihazlarda push bildirimleri durur. Çıkış sonrası hiçbir token kullanılamaz; yeniden giriş gerekir.

Örnek İstek

curl -X POST https://traxnavi.com/api/v1/logout \
  -H "Authorization: Bearer YOUR_TOKEN_HERE"

Not: Body gerekmez, sadece Authorization header yeterlidir.

Başarılı Yanıt (200)

{
  "success": true,
  "message": "Logged out successfully.",
  "data": null
}

Token Yoksa (401)

{
  "success": false,
  "message": "Unauthenticated"
}

📍 Konum Endpoint'leri

POST

/api/v1/locations

KİMLİK DOĞRULAMA GEREKLİ

Mobil cihazdan konum verisi gönderin. Üç farklı format desteklenir: location location wrapper ile (dizi veya tekil nesne), veya wrapper olmadan doğrudan raw array/obje olarak (flutter_background_geolocation v5 uyumlu).

Özellikler

  • 3 Format Desteği: {"location": [...]} wrapper, {"location": {...}} common.messages.batch_max_100
  • flutter_background_geolocation v5 Uyumlu: Plugin'in locationTemplate ayarı gerekmeden varsayılan raw formatı doğrudan kabul edilir
  • Kısmi Başarı: Geçerli veriler işlenir, hatalılar raporlanır.
  • En güncel konum otomatik locations tablosuna kaydedilir
  • Timestamp Koruması: api_docs.the locations locations tablosu sadece gelen veri mevcut kayıttan daha yeni veya eşitse güncellenir. Hem tekil hem batch gönderimlerde geçerlidir. Eski offline/gecikmeli veri gönderimlerinde mevcut güncel konum korunur; yalnızca location_histories güncellenir.
  • Tüm konumlar location_histories tablosuna kaydedilir
  • Tekil format: Root timestamp varsa locations.updated_at ve location_histories.created_at

Lisans bölgesi koordinat kısıtı

Her konum noktası WGS84 doğrulamasından sonra şirket lisans bölgesine göre kontrol edilir. İzinli alan dışındaki noktalar reddedilir (satır bazlı doğrulama hatası) ve locations / location_histories tablolarına yazılmaz. TR lisansları: Türkiye ile listelenen Avrupa, Kafkasya, Orta Asya ve Orta Doğu kara taşıma ülkeleri (ülke AABB sınırları). US lisansları: yalnızca ABD anakarası (Alaska, Hawaii ve Porto Riko kabul edilmez). Reddedilen noktalar sunucuda license_geo.reject olarak loglanır. İstemciler kısmi başarıyı işlemelidir: aynı batch içindeki geçerli satırlar yine de kaydedilir.

Lisans bölgesi dışındaki koordinatlar için örnek satır hatası:

{
  "processed_count": 0,
  "error_count": 1,
  "errors": [
    {
      "index": 0,
      "errors": {
        "coords.latitude": ["Koordinatlar TR lisansınız için Türkiye veya izin verilen kara taşımacılığı ülkeleri içinde olmalıdır."],
        "coords.longitude": ["Koordinatlar TR lisansınız için Türkiye veya izin verilen kara taşımacılığı ülkeleri içinde olmalıdır."]
      }
    }
  ]
}

TR: Koordinatlar TR lisansınız için Türkiye veya izin verilen kara taşımacılığı ülkeleri içinde olmalıdır. · US: Koordinatlar lisans bölgeniz için ABD anakarası (contiguous) içinde olmalıdır (Alaska, Hawaii ve Porto Riko dahil değildir).

Request Body Parametreleri

Parametre Tür Gerekli Açıklama
location array | object * Konum dizisi [{...}] veya tekil nesne {...} (Max: 100). Wrapper olmadan da gönderilebilir - aşağıdaki "Raw Format" örneğine bakın.
timestamp string Opsiyonel Tekil format için: updated_at ve created_at olarak kullanılır (ISO 8601)
user_id numeric Opsiyonel Sayısal kullanıcı ID; yoksa auth kullanıcısı kullanılır.
company_id int Opsiyonel Hesabınızdaki şirket ID ile eşleşmeli; aksi halde 403.
app_version string Opsiyonel Uygulama versiyonu ( extras.app_version)
extras object Opsiyonel İstek kökünde: örn. {"power_save": true}. Konum satırında extras.power_save, yoksa bu değer kullanılır (batch/tekil aynı mantık).
extras.power_save boolean Opsiyonel Sadece kök extras; içinde; nullable boolean, doğrulanır.

Konum objesi validasyon kuralları (DB limitleri)

Hem batch hem tekil format aynı konum objesi validasyon kurallarına tabidir. Her konum elemanı aşağıdaki kurallara uymalıdır. Uymayan kayıtlar errors dizisinde index ile raporlanır (tekil format için index: 0).

Alan Kural / 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 (her iki alan adı da kabul edilir)
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, ör. 0.227)
extrasnullable, object (konum satırı içinde)
extras.power_savenullable, boolean - önce bu alan; yoksa kök extras.power_save; her ikisinde de yoksa veya açıkça null, o zaman şu şekilde kaydedilir: power_save: null

Location Object Yapısı (Dizi Elemanı)

Alan Tür Gerekli Açıklama
uuid string Opsiyonel Benzersiz ID (max 191)
timestamp | recorded_at string ✓ (biri) ISO 8601 (2026-01-06T14:30:00Z)
coords object latitude ve longitude zorunlu; diğerleri yukarıdaki limitlere uymalı
is_moving boolean Opsiyonel Hareket durumu
odometry | odometer float Opsiyonel Sayaç metre (0 .. 9999999999.99). Her iki alan adı da kabul edilir (SDK v5: odometer)
activity object Opsiyonel {type: string max 191, confidence: 0-100}
battery object Opsiyonel {level: -1..1 (-1=bilinmiyor, double kabul), is_charging: bool}
extras object Opsiyonel Örn. {"power_save": true}. Kök ile aynı anahtarlar extras; satır değeri önceliklidir.

Örnek Request (Batch / Tekil / Raw)

Batch (Dizi)
{
  "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
  }
}
Tekil Nesne

Kök timestamp -> locations.updated_at ve 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 uyumlu)

Doğrudan raw array veya tek obje, location wrapper'ı olmadan gönderilebilir. API otomatik olarak coords alanını algılayıp formatı normalize eder. Plugin tarafında locationTemplate ayarına gerek yoktur. Tam gövde yalnızca konum dizisi olduğunda kök extras kullanılamaz; şu gibi alanları power_save ilgili konum objesinin içindeki extras ile gönderin veya şu şekilde sarmalayın: {"location": [...], "extras": {...}}.

Raw Array (birden fazla konum)
[
  {
    "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 Tek Obje

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}
}

Yanıt Senaryoları

Tümü Başarılı (200 OK)

Tüm konum kayıtları validasyondan geçti ve işlendi.

{
  "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"
  }
}

Kısmi Başarı (207 Multi-Status)

Bazı kayıtlar hatalı ama en az biri başarılı. Hatalı kayıtlar errors dizisinde raporlanır.

{
  "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"
  }
}

Tümü Hatalı (422)

Hiçbir kayıt validasyondan geçemedi veya location alanı eksik/yanlış tipte. Tekil format için errors[0] döner.

{
  "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."]
        }
      }
    ]
  }
}

Tekil Format Validasyon Hatası (422)

Tekil nesne gönderiminde location (örn. eksik koordinatlar), aynı hata yapısı döndürülür 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."]
        }
      }
    ]
  }
}

user_id Geçersiz (422)

user_id sayısal değil.

{
  "success": false,
  "message": "user_id must be a numeric value."
}

Kullanıcı Bulunamadı (404)

Belirtilen user_id veritabanında yok.

{
  "success": false,
  "message": "User with id '999' not found."
}

Şirket Eşleşmiyor (403)

company_id kullanıcının şirketi ile uyuşmuyor.

{
  "success": false,
  "message": "company_id does not match your account."
}
GET

/api/v1/locations

KİMLİK DOĞRULAMA GEREKLİ

Sürücüler ve İzleyiciler yalnızca kendi kayıtlarını görür; Yöneticiler ve Süper-yöneticiler şirket içindeki herhangi bir kaydı listeleyebilir. Zaman aralığını sınırlamak için :from ve :to (ISO 8601) kullanın. Seyrek segmentler boş bir liste döndürebilir — daha geniş bir pencere deneyin.

{
  "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}

KİMLİK DOĞRULAMA GEREKLİ

Belirli bir konumun detaylarını görüntüleyin. Sürücü sadece kendisine ait kaydı, Admin/Viewer şirketindeki herhangi bir kaydı görebilir.

Başarılı Yanıt (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"
  }
}

Kayıt Bulunamadı (404)

{
  "success": false,
  "message": "Location not found."
}

Yetkisiz Erişim (403)

Başka şirkete ait kayıt veya driver başka kullanıcının kaydına erişim.

{
  "success": false,
  "message": "You do not have access to this resource."
}

📊 Konum Geçmişi Endpoint'leri

💡 Not: Konum geçmişi sadece okunabilir (read-only).

GET

/api/v1/location-histories

KİMLİK DOĞRULAMA GEREKLİ

Rolünüze göre konum geçmişini listeleyin. Sürücü sadece kendi, Admin/Viewer tüm geçmişi görür.

{
  "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}

KİMLİK DOĞRULAMA GEREKLİ

Belirli bir geçmiş kaydının detaylarını görüntüleyin. Sürücü sadece kendisine ait kaydı, Admin/Viewer şirketindeki herhangi bir kaydı görebilir.

Başarılı Yanıt (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"
  }
}

Kayıt Bulunamadı (404)

{
  "success": false,
  "message": "LocationHistory not found."
}

Yetkisiz Erişim (403)

{
  "success": false,
  "message": "You do not have access to this resource."
}

👤 Kullanıcı Endpoint'leri

GET

/api/v1/user

KİMLİK DOĞRULAMA GEREKLİ

Giriş yapmış kullanıcının profil bilgilerini görüntüleyin.

{
  "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

KİMLİK DOĞRULAMA GEREKLİ

Kullanıcının profil bilgilerini güncelleyin. Sadece gönderilen alanlar güncellenir (partial update). Şifre güncellemesi için password_confirmation da zorunludur.

Parametreler ve Validasyon

Parametre Tür Gerekli Kural
namestringOpsiyonelcommon.messages.max_255
emailstringOpsiyonelgeçerli email, max 255, unique (kendi kaydı hariç)
passwordstringOpsiyonelcommon.messages.min_8 confirmed zorunlu
password_confirmationstringpassword varsa ✓password ile aynı olmalı
phonestringOpsiyonelnullable, max 50
vehiclestringOpsiyonelnullable, max 255
job_statusstringOpsiyonelnullable, max 255

Örnek İstek

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"
  }'

Başarılı Yanıt (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"
  }
}

Validasyon Hatası (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."
    ]
  }
}

🌍 Genel Servisler

GET

/api/v1/revgeo

HERKESE AÇIK

Koordinatları açık adrese çevirir (Reverse Geocoding). Önbellek (cache) mekanizması kullanır.

URL Parametreleri

Parametre Tür Gerekli Açıklama
lat float Enlem (-90 .. 90)
lng float Boylam (-180 .. 180)
lang string Opsiyonel Dil (örn: tr, en)

Örnek İstek

GET /api/v1/revgeo?lat=41.0082&lng=28.9784&lang=tr

Başarılı Yanıt (200)

data içinde kısa adres, tam adres ve ülke kodu (ISO 3166) döner. data. Sonuçlar 10 dk önbelleğe alınır.

{
  "success": true,
  "message": null,
  "data": {
    "short": "Istanbul, Marmara Region, Turkiye",
    "full": "Alemdar Street, Cankurtaran District, Istanbul, Marmara Region, 34110, Turkiye",
    "cc": "TR"
  }
}

Parametre Eksik (422)

{
  "success": false,
  "message": "lat and lng parameters are required."
}

Sayısal Değil (422)

{
  "success": false,
  "message": "lat and lng must be numeric values."
}

Aralık Dışı (422)

{
  "success": false,
  "message": "lat must be between -90 and 90, lng between -180 and 180."
}

Mesajlaşma API

Admin-driver arası 1-on-1 mesajlaşma. Tüm endpoint'ler auth:sanctum.

GET

Konuşmaları Listele

GET /api/v1/conversations?page=1

Kullanıcının tüm sohbet listesi, son mesaj ve okunmamış sayısı ile birlikte (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 }
}

Sohbet Başlat / Bul

POST /api/v1/conversations

// Request
{ "recipient_id": 6 }

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

Sohbet Detayı + Mesajlar

GET /api/v1/conversations/{id}?page=1

Sohbet detayı ve paginated mesaj listesi (en yenisi önce).

Okundu İşaretle

PUT /api/v1/conversations/{id}/read

Sohbet mesajlarını okundu olarak işaretler (last_read_at = now).

Okunmamış Sayısı

GET /api/v1/conversations/unread-count

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

Kişiler

GET /api/v1/conversations/contacts

Kullanıcının mesaj gönderebileceği kişilerin listesi. Aynı şirketteki uygun roldeki (admin<->driver) aktif kullanıcıları döner. Silinmiş kullanıcılar listelenmez.

// 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
    }
  ]
}

Not: Bu liste yeni sohbet başlatmak için kullanılır. Mevcut sohbetler GET /conversations.

POST

Mesaj Gönder

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

Mesaj gönderildiğinde, karşı tarafın tüm cihazlarına otomatik FCM push notification gönderilir. Web panelde WebSocket ile anlık güncellenir.

DELETE

Mesajı sil

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

Kullanıcının kendi gönderdiği bir mesajı siler. Sadece mesajı gönderen kişi silebilir.

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

Hata Durumları

KodDurum
403Mesaj bu kullanıcıya ait değil veya sohbete katılımcı değil
404Mesaj bu sohbette bulunamadı
DELETE

Sohbet Geçmişini Temizle

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

Bir sohbetteki tüm mesajları kalıcı olarak siler. Sohbetin herhangi bir katılımcısı bu işlemi yapabilir.

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

Dikkat: Bu işlem geri alınamaz. Sohbetteki tüm mesajlar her iki taraf için de kalıcı olarak silinir. İşlem öncesi kullanıcıdan onay alınması önerilir.

POST

Cihaz Tokenları (FCM)

Push notification almak için cihazın FCM token'ını kaydetme/silme.

Token Kaydet

POST /api/v1/device-tokens

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

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

Tek aktif cihaz: TraxNavi her kullanıcı için tek aktif cihaz kuralını uygular. Yeni bir token kaydedildiğinde aynı kullanıcıya ait diğer tüm tokenlar silinir; POST /api/v1/login, POST /api/v1/logout ve şifre sıfırlama işlemlerinde kullanıcının tüm FCM token'ları sunucuda silinir. Mobil uygulama şu endpoint'i çağırmalıdır: POST /api/v1/device-tokens (ve Firebase token'ı yenilediğinde de tekrar) böylece push bildirimleri doğru cihaza yönlenir.

Token geçerliliği: Firebase tarafından invalidTokens veya unknownTokens olarak işaretlenen tokenlar gönderim sonrasında otomatik silinir. Güncel FCM tokenını tekrar kaydetmek push iletimini geri açar.

Token Kaldır (Çıkış)

DELETE /api/v1/device-tokens/{token}

Opsiyoneldir. Uygulama çıkış yapmadan push almayı durdurmak istediğinde kullanışlıdır. Logout uç noktası kullanıcının tüm FCM tokenlarını zaten sildiği için bu çağrı şu uçtan sonra gereksizdir: POST /api/v1/logout.

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

Cihaz Bilgisi

Sunucuya cihaz bilgilerini ve uygulama izin durumlarını gönderir. Bu endpoint, kimliği doğrulanmış kullanıcının kaydını doğrudan users tablosunda günceller. Uygulama her açılışta (giriş sonrası) ve cihazda herhangi bir izin durumu değiştiğinde çağrılmalıdır.

Nasıl çalışır: İzin boolean değerleri ve cihaz metadata bilgileri users tablosunda kolon olarak saklanır (ayrı bir tablo değil). Bu endpoint çağrıldığında kimliği doğrulanmış kullanıcı için şu kolonlar güncellenir:
  • 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 ← otomatik olarak geçerli zaman damgasına ayarlanır
  • background_zero_since_at ← arkaplan izni kapandığında set edilir; tekrar açıldığında temizlenir
  • background_zero_last_notified_at ← arkaplan izin uyarıları için sunucu tarafından yönetilen hatırlatma zaman damgası
  • background_zero_notify_step ← sunucu tarafından yönetilen artan hatırlatma adım sayacı

Cihaz Bilgisini Güncelle

POST /api/v1/device-info

Kimlik: Bearer token gerekli. Kullanıcı tokendan belirlenir — user ID parametresi gerekmez.

Alan Tür Gerekli DB Kolonu Açıklama
locationbooleanEvetperm_locationKonum izni verilmiş mi
background_locationbooleanEvetperm_background_locationArka plan konum izni verilmiş mi
motionbooleanEvetperm_motionHareket/aktivite izni verilmiş mi
notificationsbooleanEvetperm_notificationsBildirim izni verilmiş mi
battery_optimizationbooleanEvetperm_battery_optimizationPil optimizasyonunun devre dışı olup olmadığı (true = devre dışı = iyi)
platformstringEvetdevice_platform"android" veya "ios"
device_brandstring (max 80)Evetdevice_branddevice.brand
device_modelstring (max 80)Evetdevice_modeldevice.model
os_versionstring (max 80)Hayıros_versiondevice.os_version
app_versionstring (max 80)Hayırapp_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."]
  }
}
Ne zaman çağrılmalı:
  • Başarılı girişten hemen sonra (ilk açılış)
  • Kullanıcı herhangi bir cihaz iznini verdiğinde veya iptal ettiğinde
  • Uygulama ön plana döndüğünde (arka plandan devam)
  • Uygulama güncellemesinden sonra (yeni app_version bildirmek için)
Önemli notlar:
  • Tüm izin boolean değerleri her çağrıda üzerine yazılır — her zaman 5 izin değerinin tamamını gönderin.
  • api_docs.the device_info_updated_at zaman damgası sunucu tarafından otomatik ayarlanır.
  • Bu endpoint yalnızca kimliği doğrulanmış kullanıcıyı (Bearer token ile tanımlanan) günceller. Başka bir kullanıcının izinlerini güncelleyemezsiniz.
  • Yanıt, güncellenmiş durumu yansıtan UserResource üzerinden tam kullanıcı objesini döner.
Arkaplan izin hatırlatmaları (FCM, mesaj kanalı):
  • Sadece rolü şu olan giriş yapmış kullanıcılar için geçerlidir driver.
  • Tarih background_location değeri false olduğunda sunucu, arkaplan izninin ne kadar süredir kapalı olduğunu takip eder.
  • Hatırlatma kontrolleri sunucuda saatlik çalışır ve sadece kullanıcı bir sonraki eşik saatine geldiğinde gönderim yapılır.
  • İlk push hatırlatma 2 saat sonra gönderilir; ardından artan aralıklarla devam eder: 6s, 12s, 20s, 30s, 42s, 56s, 72s, 90s, 110s, 132s, 156s.
  • Arkaplan izni kapalı kalırsa hatırlatmalar 1 haftadan (168 saat) sonra durur.
  • Arkaplan izni tekrar verilirse hatırlatma durumu bir sonraki device-info çağrısında otomatik sıfırlanır.
  • İzin kesintisiz kapalı kalırsa sunucu background_zero_notify_step alanını her başarılı hatırlatma gönderiminden sonra artırır.
  • Aktif cihaz tokenı yoksa, yeni token kaydedilene kadar o kullanıcıya push gönderilemez.
  • Hatırlatma mesaj dili kullanıcı locale alanından seçilir ( users.locale); geri dönüş varsayılan uygulama yerel ayarıdır.
  • Takip, bu endpoint background_location=false (bu durumu hiç bildirmemiş eski kayıtlar için geriye dönük geçerli değildir).
  • Push, mesajlaşma ile aynı Android bildirim kanalını kullanır: 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"
}

📦 Yük Yönetimi (Loads)

Carrier ve broker lisanslı şirketler için yük (load) listeleme, detay, sürücü kabul/red ve paylaşım linki ile yük açma. Tüm load endpoint'leri (by-token hariç) Authorization: Bearer <token> ve check.license.api. GET /loads/by-token/{token} kimlik doğrulama gerektirmez (paylaşım linki / deep link için).

GET

Yükleri Listele

GET /api/v1/loads?page=1&per_page=20&status=...&driver_id=...&start_date=...&end_date=...&search=...

Sürücü: kendine atanmış yükler. Admin (veya diğer roller): şirketin tüm yükleri. Filtreler: status, driver_id, start_date (pickup >=), end_date (pickup <=), search (load_no, rate, notes). Sayfalama: page, per_page (1-100). route_data listede de döner (sadeleştirilmiş 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,
      "mileage_unit": "km",
      "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": 5, "used": 1, "remaining": 4, "pickup_count": 1, "drop_count": 1, "max_per_stop": 2, "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

Yük Detayı

YENİ

GET /api/v1/loads/{id}

Tek yük detayı; pickup/drop listesi (phone dahil), route_data (harita geometry dahil). Sürücü sadece kendine atanmış yükü görebilir.

Neden hem mileage hem route.summary.distance var?

Aynı alanın kopyası değil — iki farklı kaynak. JSON anahtarı geriye uyumluluk için mileage kalır; sayısal değer loads.distance’tan gelir ve şirket bölgesi biriminde saklanmıştır — API yeniden dönüştürmez.

  • mileage / dead_head — Yük satırındaki sevkiyat özeti (liste + detay). Web loads.distance (ve dead_head) değerini şirket biriminde (km veya mi) yazar ve loads.distance_unit set eder. API bunu mileage + mileage_unit olarak döner. dead_head tamsayı özet; harita geometrisi/ETA route.deadhead altında (yalnızca detay).
  • route.summary / route.deadhead.summary (+ lengthInMeters) — Kayıtlı harita rotasının geometrisi (yalnızca detay). Yüklü mil: route.polyline + route.summary (route_data.routes / totalDistance). Deadhead: route.deadhead (route_data.deadhead; yoksa null). OSRM metre → summary.distance (distance_unit); lengthInMeters SI kalır.
  • distance_unit — distance_unit lisans bölgesinden şirket gösterim birimidir (TR=km, US=mi). mileage_unit yük satırında saklanan birimdir (loads.distance_unit). UI için distance_unit tercih edin; mileage/dead_head değerlerini mileage_unit (yoksa distance_unit) ile etiketleyin.

Liste / ücret özeti → mileage (+ dead_head), etiket için mileage_unit (yoksa distance_unit). Şirket gösterim birimi → distance_unit. Harita → route.polyline + route.summary (yüklü sefer) ve route.deadhead.polyline + route.deadhead.summary (sürücü→ilk pickup). ETA için *.summary.duration_seconds. mileage ile route mesafelerini veya dead_head ile route.deadhead.summary.distance’ı toplamayın.

Yük nesnesi alanları (kaynak → anlam)

Alan Kaynak Açıklama
idloads.idYük birincil anahtarı.
load_noloads.load_noWeb panelinde girilen yük numarası.
rateloads.rateYük ücreti / rate (float).
statusloads.statusMakine durumu kodu (draft, available, assigned, accepted, in_progress, completed, …).
status_labelAPI’de üretilir (DB kolonu değil)status için yerelleştirilmiş etiket (UI).
pickup_dateloads.pickup_datePlanlanan yükleme tarihi (Y-m-d) veya null.
delivery_dateloads.delivery_datePlanlanan teslim tarihi (Y-m-d) veya null.
notesloads.notesSevkiyat notları (serbest metin).
distance_unitcompanies.license_region → CountryProfile`km` (TR) veya `mi` (US) — companies.license_region → CountryProfile. mileage, dead_head ve route.*.summary.distance için şirket gösterim birimi; lengthInMeters’a uygulanmaz.
mileageloads.distance — value in mileage_unitEski JSON anahtarı (DB/web kolonu loads.distance). Liste/detay kartı mesafesi; şirket bölgesi biriminde saklanır. API dönüştürmez. route’tan bağımsız — route null olsa da gelebilir.
mileage_unitloads.distance_unit`km` veya `mi` — loads.distance_unit; mileage ve dead_head sayılarının birimi. Normalde distance_unit ile aynıdır; saklanan değeri etiketlerken bunu kullanın.
dead_headloads.dead_head — same unit as mileageloads.dead_head boş mesafe (mileage / mileage_unit ile aynı birim). Web’de rota veya elle. Harita overlay: route.deadhead (detay); route.deadhead.summary.distance ile toplamayın.
driverusers via loads.driver_idAtanan sürücü özeti (id, name, email, phone) veya null.
pickups / dropsload_stops (type)sequence’e göre duraklar: name, address, city, state, country, phone, lat, lng.
documentsload_documentsYalnızca PDF metadata (byte yok). Binary için /documents/{id}/preview veya /download.
document_quotaAPI’de üretilir (DB kolonu değil)Yükleme slotları: max = pickup + drop + 1 additional; used/remaining ve slot dolulukları.
routeloads.route_data JSON → overlayYalnızca detay — loads.route_data’dan harita paketi: yüklü polyline/summary ve isteğe bağlı deadhead overlay. Rota yoksa null. GET /loads listesinde yok. mileage’ın ikinci kopyası değildir.
route.polylineroute_data.routes[].geometryYüklü pickup→drop yolu için [[lat, lng], …] (yaklaşık 500 noktaya kısaltılabilir). Deadhead içermez.
route.summary.lengthInMetersroute_data.totalDistance (SI m)Motorun kanonik uzunluğu (metre, SI). Hassasiyet için; distance_unit ile dönüşmez.
route.summary.travelTimeInSecondsroute_data.totalDuration (s)OSRM/ORS’tan gelen kanonik süre (saniye, totalDuration).
route.summary.distanceAPI’de üretilir (DB kolonu değil)lengthInMeters’ın distance_unit’e çevrilmiş hali (harita/ETA). Üst seviye mileage alanının yerine geçmez.
route.summary.distance_unitCountryProfile`km` (TR) veya `mi` (US) — companies.license_region → CountryProfile. mileage, dead_head ve route.*.summary.distance için şirket gösterim birimi; lengthInMeters’a uygulanmaz.
route.summary.duration_secondsalias of travelTimeInSecondstravelTimeInSeconds ile aynı — mobil için kısayol alan.
route.deadheadroute_data.deadhead → overlaySürücü→ilk pickup overlay (route_data.deadhead). summary şekli route.summary ile aynı. Hesaplanmadıysa/kaydedilmediyse null. Ayrı katman — route.polyline’a karıştırılmaz.
route.deadhead.polylineroute_data.deadhead.geometryDeadhead yolu için [[lat, lng], …] (yaklaşık 300 noktaya kısaltılabilir).
route.deadhead.summary.*deadhead.distance / duration (SI)lengthInMeters’ın distance_unit’e çevrilmiş hali (harita/ETA). Üst seviye mileage alanının yerine geçmez. travelTimeInSeconds ile aynı — mobil için kısayol alan.
route.deadhead.origindeadhead.coordinates + driver_idDeadhead başlangıç anlık görüntüsü: web rota hesabı sırasındaki atanan sürücü cihazının son konumu (lat/lng + driver_id). source her zaman driver_device (canlı yeniden çekilmez).
// 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,
    "mileage_unit": "km",
    "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": 5, "used": 1, "remaining": 4, "max_per_stop": 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
      },
      "deadhead": {
        "polyline": [[41.01, 28.96], [41.02, 28.97]],
        "summary": {
          "lengthInMeters": 129000,
          "travelTimeInSeconds": 5400,
          "distance": 129.0,
          "distance_unit": "km",
          "duration_seconds": 5400
        },
        "origin": { "lat": 41.01, "lng": 28.96, "driver_id": 5, "source": "driver_device" }
      }
    }
  }
}
POST

Yükü Kabul Et

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

Sürücü yükü kabul eder; driver_id atanır, status -> accepted. Yalnızca role=driver kullanıcılar çağırabilir.

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

Yükü Reddet

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

Sürücü yükü reddeder; driver_id temizlenir, status -> available. Sadece sürücü rolü.

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

Yük Durumunu Güncelle

PATCH /api/v1/loads/{id}/status

Yükün status alanını günceller. Sürücü: sadece kendine atanmış yükün durumunu güncelleyebilir. Admin (veya diğer roller): şirketteki herhangi bir yükün durumunu güncelleyebilir.

AlanTürGerekliAçıklama
statusstringEvetGeçerli değerler: 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.

POST

Geofence Olayları

POST /api/v1/loads/{id}/geofence-events

Pickup/drop noktası için mobil OS geofence enter/exit olayını bildirir. Şirket admin ve viewer kullanıcılarına uygulama içi (ve tercihe bağlı e-posta/FCM) bildirim oluşturur. Yük durumunu değiştirmez — gerekirse ayrı olarak PATCH /status kullanın.

Kimlik: Yüke atanmış sürücü veya şirket admini. Viewer olay raporlayamaz.

Bu uç nokta yük durumunu güncellemez ve geofence yarıçapını doğrulamaz (yarıçap/dwell mobil istemcide kalır).

AlanTürGerekliAçıklama
stop_idintegerEvetload_stops.id for this load
eventstringEvetenter | exit
latnumberHayır-90 … 90
lngnumberHayır-180 … 180
accuracynumberHayırmeters, >= 0
occurred_atdatetimeHayırISO 8601; defaults to now; within last 7 days / +5 minutes
// Request
{
  "stop_id": 42,
  "event": "enter",
  "lat": 41.015137,
  "lng": 28.979530,
  "accuracy": 12.5,
  "occurred_at": "2026-07-23T14:02:11Z"
}

// Response (200) — new event
{
  "success": true,
  "message": "Geofence event recorded.",
  "data": {
    "id": 1001,
    "load_id": 55,
    "stop_id": 42,
    "event": "enter",
    "deduped": false,
    "occurred_at": "2026-07-23T14:02:11+00:00"
  }
}

// Response (200) — duplicate within 120s
{
  "success": true,
  "message": "Geofence event already recorded recently.",
  "data": {
    "id": 1001,
    "load_id": 55,
    "stop_id": 42,
    "event": "enter",
    "deduped": true,
    "occurred_at": "2026-07-23T14:02:11+00:00"
  }
}

Aynı yük + stop + event + raporlayan 120 saniye içinde tekrarlanırsa önceki kayıt deduped: true ile döner ve yeni bildirim gönderilmez.

403 - Not assigned driver / not admin. 404 - Load not found or different company. 422 - Validation (e.g. stop_id not on this load).

Nasıl çalışır:

  • Load detail → use stops[].id, lat, lng, type
  • OS geofence identifier (recommended): load:{loadId}:stop:{stopId}
  • On enter/exit callback → call this endpoint (retry / offline queue recommended)
PDF+OCR

Yük Belgeleri

Yük başına belgeler. Kota = pickup_count + drop_count + 1 ek (pickup/drop durağı başına tek belge slotu). Türler: pickup, drop, additional. Yükleme/değiştirme bağımsız isteğe bağlı parçalar kabul eder: PDF (file veya processed), ham görüntü(ler) ve/veya ocr_text — en az bir parça zorunlu. Belge başına en fazla 10 ham görüntü; sonradan POST .../raws ile eklenebilir. PDF download/preview ile; ham download/raw, preview/raw veya raws/{rawId} ile; OCR metni liste/detay JSON ve GET ocr-text ile sunulur. Dosya başına maks. 10 MB. Multipart form-data. Web panel PDF yükler veya görselleri PDF’e tarar; ham önizleme ve OCR metni varsa gösterir.

Kimler yönetebilir: admin; can_manage_load_documents=true olan viewer; atanan sürücü (yükleme/değiştirme; durum completed ise silme engellenir).

Kimler listeleyebilir/indirebilir: Yükü görüntüleyebilen aynı şirket kullanıcıları (admin/viewer; atanan sürücü).

load_stop_id: pickup/drop için, o türde 2+ durak varsa zorunludur. Tam olarak bir durak varsa, alan atlandığında sunucu otomatik seçer. additional için her zaman atlayın.

raw ve ocr_text için şirketin ocr_enabled lisans bayrağı gerekir. Yalnızca PDF yüklemeleri (file veya processed tek başına) için gerekmez.

Gerektirir Authorization: Bearer <token> ve check.license.api.

FLOW

Çoklu durak iş akışı

Her pickup/drop durağında tek belge slotu vardır (slot_index 0). Kota = pickup_count + drop_count + 1 ek. legacy: true işaretli slotlar, tek-slot sınırından önce yüklenmiş fazla belgelerdir ve salt okunurdur. Bir türde 2+ durak varsa load_stop_id zorunludur — sunucu tahmin etmez.

  1. Slotlardaki load_stop_id için GET yük detayı veya GET documents çağırın.
  2. type=pickup|drop ve eşleşen load_stop_id ile POST upload (o türde 2+ durak varsa load_stop_id zorunlu). İlk partial ham set yüklenebilir (ör. 10’dan 6).
  3. Kalan her durak için tekrarlayın; isteğe bağlı olarak bir kez type=additional yükleyin (load_stop_id göndermeyin).
  4. Document id ile replace / download / preview / ocr-text kullanın. Ham sayfalar için: POST .../raws ile ekle, GET .../raws/{rawId}/preview ile önizle, POST .../raws/{rawId} ile tekini değiştir, DELETE .../raws/{rawId} ile sil.
  5. Belge üzerindeki DELETE satırı ve hem işlenmiş PDF hem tüm ham görüntü dosyalarını siler.

Örnek: 2 pickup + 2 drop için slot listesi

// GET /api/v1/loads/42/documents  — load with 2 pickups + 2 drops
// Quota max = 2 + 2 + 1 = 5  (one doc per stop + additional)
{
  "document_quota": {
    "max": 5,
    "used": 0,
    "remaining": 5,
    "pickup_count": 2,
    "drop_count": 2,
    "max_per_stop": 1,
    "slots": [
      { "type": "pickup", "load_stop_id": 10, "slot_index": 0, "stop_name": "Warehouse A", "sequence": 0, "filled": false, "document_id": null, "legacy": false },
      { "type": "pickup", "load_stop_id": 11, "slot_index": 0, "stop_name": "Warehouse B", "sequence": 1, "filled": false, "document_id": null, "legacy": false },
      { "type": "drop",   "load_stop_id": 12, "slot_index": 0, "stop_name": "Store C",     "sequence": 2, "filled": false, "document_id": null, "legacy": false },
      { "type": "drop",   "load_stop_id": 13, "slot_index": 0, "stop_name": "Store D",     "sequence": 3, "filled": false, "document_id": null, "legacy": false },
      { "type": "additional", "load_stop_id": null, "slot_index": 0, "stop_name": null, "sequence": null, "filled": false, "document_id": null, "legacy": false }
    ]
  }
}

Örnek: her durağa bir belge (ve isteğe bağlı additional)

// 1) Pickup #1 (stop 10) — full parts
POST /api/v1/loads/42/documents
raw=@pickup1.jpg
processed=@pickup1.pdf
ocr_text=BOL pickup Warehouse A...
type=pickup
load_stop_id=10

// 2) Pickup #2 (stop 11) — PDF only
POST /api/v1/loads/42/documents
file=@pickup2.pdf
type=pickup
load_stop_id=11

// 3) Drop #1 (stop 12) — raw only
POST /api/v1/loads/42/documents
raw=@drop1.jpg
type=drop
load_stop_id=12

// 4) Drop #2 (stop 13) — OCR text only
POST /api/v1/loads/42/documents
ocr_text=Delivery receipt notes...
type=drop
load_stop_id=13

// 5) Additional (no load_stop_id)
POST /api/v1/loads/42/documents
file=@extra.pdf
type=additional

// WRONG when 2+ pickups — omit load_stop_id → 422
POST /api/v1/loads/42/documents
file=@bol.pdf
type=pickup

Örnek: değiştir, indir, önizle, preview/raw, ocr-text, sil

// List / quota
GET /api/v1/loads/42/documents

// Partial replace — PDF only (keeps existing raw/OCR)
POST /api/v1/loads/42/documents/7
file=@bol-v2.pdf

// Partial replace — OCR text only
POST /api/v1/loads/42/documents/7
ocr_text=Updated OCR text...

// Partial replace — raw pages only replaces ENTIRE raw set (keeps PDF/OCR)
POST /api/v1/loads/42/documents/7
raw[]=@page1.jpg
raw[]=@page2.jpg

// Append raw pages without wiping existing (current + new ≤ 10)
POST /api/v1/loads/42/documents/7/raws
raw[]=@page3.jpg
raw[]=@page4.jpg

// Replace / delete one raw by id
POST /api/v1/loads/42/documents/7/raws/101
raw=@page1-v2.jpg
DELETE /api/v1/loads/42/documents/7/raws/101

// Download / preview binaries
GET /api/v1/loads/42/documents/7/download                 → PDF (attachment; 404 if has_pdf=false)
GET /api/v1/loads/42/documents/7/preview                  → PDF (inline; 404 if has_pdf=false)
GET /api/v1/loads/42/documents/7/download/raw             → primary raw (sort 0, attachment)
GET /api/v1/loads/42/documents/7/preview/raw              → primary raw (sort 0, inline)
GET /api/v1/loads/42/documents/7/raws/{rawId}/download    → specific raw (attachment)
GET /api/v1/loads/42/documents/7/raws/{rawId}/preview     → specific raw (inline)
GET /api/v1/loads/42/documents/7/ocr-text                 → JSON { ocr_text, has_ocr_text }

// Delete document (removes PDF + all raw files when present)
DELETE /api/v1/loads/42/documents/7

// Tip: stop ids also come from load detail
GET /api/v1/loads/42  →  data.stops[].id / type / sequence
// Tip: documents[] on load detail include has_pdf / has_raw / ocr_text

Durağa tek belge (aynı load_stop_id). Yük başına bir additional. Dolu durakta ikinci store 422 döner. Dolu slota yeniden yükleme için store değil, o belgenin id’si ile replace kullanın. Ham sayfalar: belge başına en fazla 10 — eklemek için POST .../raws, tekini değiştirmek için POST .../raws/{rawId}, silmek için DELETE .../raws/{rawId}.

GET

Belgeleri Listele

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

Liste/detaydaki her belge nesnesi hangi parçaların saklandığını bayraklarla gösterir:

AlanNotlar
id, load_id, type, load_stop_idDocument identity + slot link
has_pdftrue when a processed PDF is stored
original_name, size_bytes, mimePDF metadata when has_pdf=true; otherwise null / display name from raw
has_rawtrue when at least one raw image is stored
raw_size_bytes, raw_mime, raw_original_namePrimary raw (sort_order 0) metadata; null when has_raw=false
raw_count, raws[]Additive: count + list of {id, sort_order, size_bytes, mime, original_name}
has_ocr_texttrue when non-empty ocr_text is stored
ocr_textFull client-supplied OCR string (or null). Max 500000 chars. One combined string per document.
// 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,
        "has_pdf": true,
        "has_raw": true,
        "raw_size_bytes": 204800,
        "raw_mime": "image/jpeg",
        "raw_original_name": "page1.jpg",
        "raw_count": 2,
        "raws": [
          { "id": 101, "sort_order": 0, "size_bytes": 204800, "mime": "image/jpeg", "original_name": "page1.jpg" },
          { "id": 102, "sort_order": 1, "size_bytes": 198000, "mime": "image/jpeg", "original_name": "page2.jpg" }
        ],
        "has_ocr_text": true,
        "ocr_text": "BOL #12345 ...",
        "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,
      "max_per_stop": 1,
      "slots": [
        { "type": "pickup", "load_stop_id": 10, "slot_index": 0, "stop_name": "Warehouse A", "sequence": 0, "filled": true, "document_id": 1, "legacy": false },
        { "type": "drop", "load_stop_id": 11, "slot_index": 0, "stop_name": "Store B", "sequence": 1, "filled": false, "document_id": null, "legacy": false },
        { "type": "additional", "load_stop_id": null, "slot_index": 0, "stop_name": null, "sequence": null, "filled": false, "document_id": null, "legacy": false }
      ]
    }
  }
}
POST

Belge Yükle

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

Bağımsız isteğe bağlı parçalar — şu kombinasyonlardan herhangi biri: PDF (file VEYA processed), raw/raw[], ocr_text. En az bir parça zorunlu. file ile processed’ı birlikte göndermeyin. raw ve ocr_text için ocr_enabled gerekir.

AlanNotlar
fileİsteğe bağlı PDF (maks. 10 MB). processed ile aynı — file VEYA processed gönderin, ikisini birden değil. raw ve/veya ocr_text ile birlikte kullanılabilir.
rawİsteğe bağlı: tek görüntü veya 1..10 elemanlı raw[] (JPEG/PNG/HEIC/HEIF, her biri maks. 10 MB). Tek başına veya PDF/ocr_text ile gönderilebilir. ocr_enabled gerekir. Tekil raw tam desteklenmeye devam eder.
processedİsteğe bağlı PDF (maks. 10 MB). file ile aynı — file VEYA processed gönderin, ikisini birden değil. İndirme/önizleme bunu sunar. raw ve/veya ocr_text ile birlikte kullanılabilir.
ocr_textİsteğe bağlı düz metin (maks. 500000 karakter). Tek başına veya PDF/raw ile gönderilebilir. ocr_enabled gerekir. Değiştirmede alanı göndermezseniz mevcut metin korunur; boş string göndererek temizleyebilirsiniz.
typepickup | drop | additional
load_stop_idnullable integer; pickup/drop için, o türde 2+ durak varsa zorunlu; tam bir tane varsa otomatik; additional için atla

Yalnızca PDF (file veya processed)

// multipart fields
file=@bol.pdf
type=pickup
load_stop_id=10

// same PDF via processed alias
processed=@bol.pdf
type=pickup
load_stop_id=10

Yalnızca ham görüntü(ler)

raw=@bol-scan.jpg
type=pickup
load_stop_id=10

// or multiple pages
raw[]=@page1.jpg
raw[]=@page2.jpg
type=pickup
load_stop_id=10

Yalnızca OCR metni

ocr_text=BOL #12345 shipper...
type=pickup
load_stop_id=10

Tüm parçalar (ham + işlenmiş PDF + OCR metni)

// multipart — singular raw + PDF + OCR
raw=@bol-scan.jpg
processed=@bol.pdf
ocr_text=BOL #12345 shipper...
type=pickup
load_stop_id=10

// multipart — multiple raw pages (1..10) + PDF + OCR
raw[]=@page1.jpg
raw[]=@page2.jpg
raw[]=@page3.jpg
processed=@bol.pdf
ocr_text=Combined OCR text from all pages...
type=pickup
load_stop_id=10
// 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,
    "has_pdf": true,
    "has_raw": true,
    "raw_size_bytes": 204800,
    "raw_mime": "image/jpeg",
    "raw_original_name": "bol-scan.jpg",
    "raw_count": 1,
    "raws": [
      { "id": 101, "sort_order": 0, "size_bytes": 204800, "mime": "image/jpeg", "original_name": "bol-scan.jpg" }
    ],
    "has_ocr_text": true,
    "ocr_text": "BOL #12345 shipper...",
    "created_at": "...",
    "updated_at": "..."
  }
}

422 — validasyon / boş yük / file+processed çakışması / kota aşıldı / durağın zaten belgesi var / tür uyuşmazlığı. 403 — belgeleri yönetme yetkiniz yok veya raw/ocr_text gönderirken OCR etkin değil.

POST

Belge Değiştir

POST /api/v1/loads/{id}/documents/{documentId}multipart/form-data

Aynı belge kimliğini, türünü ve durak bağlantısını korur. Yalnızca gönderdiğiniz parçalar güncellenir; gönderilmeyen PDF, ham ve OCR korunur. file/processed, raw veya ocr_text’ten en az biri zorunlu. ocr_text yalnızca alan gönderildiğinde güncellenir (boş string temizler). raw/raw[] gönderildiğinde tüm ham set değişir (birleştirme değil). Mevcut sayfaları yeniden yüklemeden eklemek için POST .../documents/{id}/raws kullanın.

Bağımsız isteğe bağlı parçalar — şu kombinasyonlardan herhangi biri: PDF (file VEYA processed), raw/raw[], ocr_text. En az bir parça zorunlu. file ile processed’ı birlikte göndermeyin. raw ve ocr_text için ocr_enabled gerekir.

AlanNotlar
fileİsteğe bağlı PDF (maks. 10 MB). processed ile aynı — file VEYA processed gönderin, ikisini birden değil. raw ve/veya ocr_text ile birlikte kullanılabilir.
rawİsteğe bağlı: tek görüntü veya 1..10 elemanlı raw[] (JPEG/PNG/HEIC/HEIF, her biri maks. 10 MB). Tek başına veya PDF/ocr_text ile gönderilebilir. ocr_enabled gerekir. Tekil raw tam desteklenmeye devam eder.
processedİsteğe bağlı PDF (maks. 10 MB). file ile aynı — file VEYA processed gönderin, ikisini birden değil. İndirme/önizleme bunu sunar. raw ve/veya ocr_text ile birlikte kullanılabilir.
ocr_textİsteğe bağlı düz metin (maks. 500000 karakter). Tek başına veya PDF/raw ile gönderilebilir. ocr_enabled gerekir. Değiştirmede alanı göndermezseniz mevcut metin korunur; boş string göndererek temizleyebilirsiniz.

Kısmi değiştirme (yalnızca gönderilen parçalar güncellenir; diğerleri korunur)

// OCR text only
ocr_text=Updated OCR text...

// Raw pages only (full-set replace — existing raws are removed)
raw[]=@page1.jpg
raw[]=@page2.jpg

// PDF only
file=@bol-v2.pdf
// Response (200)
{ "success": true, "message": "Document replaced.", "data": { "id": 1, "type": "pickup", "original_name": "bol-v2.pdf", "has_pdf": true, "has_raw": true, "has_ocr_text": true, ... } }

403 / 404 — yükleme ile aynı erişim kuralları; belge yüke ait olmalıdır; raw veya ocr_text gönderirken OCR lisansı gerekir.

POST

Ham Görüntü Ekle

YENİ

POST /api/v1/loads/{id}/documents/{documentId}/rawsmultipart/form-data

Mevcut sayfaları silmeden belgeye bir veya daha fazla ham görüntü ekler. raw veya raw[] (JPEG/PNG/HEIC/HEIF, her biri maks. 10 MB). mevcut + yeni ≤ 10 olmalı. ocr_enabled gerekir. Store ile aynı yükleme yetkisi. Güncellenmiş belgeyi döner (raws[], raw_count).

// Append 2 pages to a document that already has 6 (total 8 ≤ 10)
raw[]=@page7.jpg
raw[]=@page8.jpg

// or singular
raw=@extra.jpg
// Response (200)
{ "success": true, "message": "Raw images added.", "data": { "id": 7, "raw_count": 8, "raws": [ ... ], "has_raw": true, ... } }

422 / 403 / 404 — mevcut + yeni 10’u aşarsa 422; yetkisiz veya OCR kapalıysa 403; belge yoksa 404.

POST

Tek Ham Görüntüyü Değiştir

YENİ

POST /api/v1/loads/{id}/documents/{documentId}/raws/{rawId}multipart/form-data

Id ile tek bir ham görüntüyü değiştirir. Multipart alanı raw (tek dosya). Aynı raw id ve sort_order korunur; kardeşlere dokunulmaz. ocr_enabled gerekir. Güncellenmiş belgeyi döner.

raw=@page1-v2.jpg
// Response (200)
{ "success": true, "message": "Raw image replaced.", "data": { "id": 7, "raw_count": 6, "raws": [{ "id": 101, "sort_order": 0, "original_name": "page1-v2.jpg", ... }, ...], ... } }

403 / 404 — append ile aynı erişim kuralları; raw belgeye ait olmalı.

DELETE

Tek Ham Görüntüyü Sil

YENİ

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

Id ile tek bir ham görüntüyü siler ve sort_order’ı 0..n-1 olacak şekilde yeniden sıkıştırır. PDF ve ocr_text korunur. Son raw silinince belge PDF/OCR-only kalır (has_raw=false). Güncellenmiş belgeyi döner. Yük tamamlandığında sürücü silemez.

// Response (200)
{ "success": true, "message": "Raw image deleted.", "data": { "id": 7, "raw_count": 5, "has_raw": true, "raws": [ ... ], ... } }

403 / 404 — belge silme ile aynı erişim kuralları; raw belgeye ait olmalı.

DELETE

Belge Sil

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

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

403 — yük durumu tamamlandığında sürücü silemez.

GET

Belge İndir

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

İşlenmiş PDF ikilisini (Content-Type: application/pdf) dosya indirme olarak döndürür — JSON zarfı değildir. has_pdf false ise (PDF yok) 404.

404 — belge yok veya PDF saklanmamış (has_pdf=false). 403 — sürücü yalnızca kendine atanmış yükün belgelerine erişebilir.

GET

Ham Görüntüyü İndir

GET /api/v1/loads/{id}/documents/{documentId}/download/raw

Ham çekim görüntüsünü (JPEG/PNG/HEIC) dosya indirme olarak döndürür. Belgede ham dosya yoksa 404. İndirme ile aynı yetki; JSON zarfı değildir.

Content-Type: image/jpeg | image/png | image/heic | image/heif.

404 — bu belge için ham görüntü yok. 403 — sürücü yalnızca kendine atanmış yükün belgelerine erişebilir.

GET

Belge Önizleme

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

İşlenmiş PDF’yi uygulama içi görüntüleme için stream eder (Content-Disposition: inline). İndirme ile aynı yetki; JSON zarfı değildir. has_pdf false ise 404.

404 — belge yok veya PDF saklanmamış (has_pdf=false). 403 — sürücü yalnızca kendine atanmış yükün belgelerine erişebilir.

GET

Ham Görüntü Önizleme

GET /api/v1/loads/{id}/documents/{documentId}/preview/raw

GET /api/v1/loads/{id}/documents/{documentId}/raws/{rawId}/preview · .../raws/{rawId}/download

Ham çekim görüntüsünü uygulama içi görüntüleme için stream eder (Content-Disposition: inline + raw_mime). Image/WebView önizlemesi için kullanın. Dosyayı kaydetmek için download/raw tercih edin. Belgede ham dosya yoksa 404. İndirme ile aynı yetki; JSON zarfı değildir.

Belirli bir ham görüntüyü id ile stream eder (Content-Disposition: inline veya attachment). raw_count > 1 iken bunu tercih edin. Eski GET .../preview/raw ve .../download/raw birincil hamı (sort_order 0) sunmaya devam eder.

JPEG/PNG çoğu tarayıcıda açılır; HEIC/HEIF için cihazda yerel görüntüleyici gerekebilir.

// Response (200) — binary image stream (NOT a JSON envelope)
HTTP/1.1 200 OK
Content-Type: image/jpeg
Content-Disposition: inline; filename="bol-scan.jpg"

<raw image bytes>

// Response (404) — legacy PDF-only document (no raw)
{
  "success": false,
  "message": "Raw image not found."
}

Content-Type: image/jpeg | image/png | image/heic | image/heif.

404 — bu belge için ham görüntü yok. 403 — sürücü yalnızca kendine atanmış yükün belgelerine erişebilir.

GET

OCR Metnini Al

GET /api/v1/loads/{id}/documents/{documentId}/ocr-text

Tek bir belgenin saklanan OCR metnini JSON zarfı olarak döndürür. Yalnızca metin gerektiğinde tercih edin (tüm belge listesini yeniden çekmez). İndirme ile aynı yetki. OCR yüklenmemişse ocr_text null, has_ocr_text false olur.

Tam ocr_text ayrıca GET documents ve GET yük detayı içinde (documents[].ocr_text) gömülüdür. Bu uç tek belge ekranları içindir.

// Response (200) — OCR present
{
  "success": true,
  "message": "OCR text retrieved.",
  "data": {
    "ocr_text": "BOL #12345 shipper ACME\n...",
    "has_ocr_text": true
  }
}

// Response (200) — no OCR (legacy PDF-only upload)
{
  "success": true,
  "message": "OCR text retrieved.",
  "data": {
    "ocr_text": null,
    "has_ocr_text": false
  }
}

403 — sürücü yalnızca kendine atanmış yükün belgelerine erişebilir. 404 — belge yok veya PDF saklanmamış (has_pdf=false).

Yük listesi ve yük detayı (GET /loads, GET /loads/{id}) ayrıca içerir documents ve document_quota (Bağımsız isteğe bağlı parçalar — şu kombinasyonlardan herhangi biri: PDF (file VEYA processed), raw/raw[], ocr_text. En az bir parça zorunlu. file ile processed’ı birlikte göndermeyin. raw ve ocr_text için ocr_enabled gerekir.).

GET

Sürücü İstatistikleri

YENİ

GET /api/v1/drivers/stats?driver_id= admin/viewer için zorunlu; sürücü göndermez (yalnızca kendi)

Şirket saat diliminde takvim günü / hafta (Pzt–Paz) / ay toplamları: mesafe (km veya mi), moving_seconds ve stopped_seconds (kısa idle stopped’a dahil). Tamamlanan günler gece pre-aggregation’dan; bugün sıcak location history’den canlı hesaplanır (replay yer değişimi + IFTA segment filtreleri).

// 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 — sürücü başka driver_id isteyemez; veya paket carrier/broker değil. 404 — sürücü şirkette yok. 422 — admin/viewer için driver_id eksik.

GET

Yük (Paylaşım Tokenı)

GET /api/v1/loads/by-token/{token} - Auth yok

Paylaşım linki ile (/l/{token}) veya deep link ile (traxnavi://load/{token}), açıldığında mobil uygulama bu endpoint ile yükü çeker. Token geçerli ve expires_at dolmamış olmalı.

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

Yük Push Bildirimleri

Sürücüler, yük yaşam döngüsü olaylarında otomatik FCM push bildirimleri alır. Polling gerekmez; ekranlarınızı aşağıdaki payload'a göre bağlayın.

Ön Koşul: Cihaz, FCM token'ını şu uç ile kaydetmelidir: POST /api/v1/device-tokens (Cihaz Tokenları bölümüne bakın) girişten hemen sonra. Sunucu, kullanıcı başına tek bir aktif cihaz zorunlu kılar; çıkış yapmak, tekrar giriş yapmak veya şifre sıfırlamak, kullanıcının tüm FCM token'larını otomatik olarak kaldırır. DELETE /api/v1/device-tokens/{token} opsiyoneldir; yalnızca uygulama oturumu kapatmadan push almayı durdurmak istediğinde kullanılır.

Olaylar

Olay Tipi Alıcı Tetikleyici
load_created Yöneticiler/izleyiciler (işlemi yapan hariç) Web panelinde bir yük oluşturulur
load_assigned Atanan sürücü Admin web üzerinden sürücü atar (yük oluştur/düzenle)
load_status_updated Atanan sürücü + admin/viewer'lar Herhangi bir uçta (web veya API) durum değişir. Değişikliği tetikleyen kullanıcı hariç tutulur.
load_deleted Atanan sürücü ve yöneticiler/izleyiciler (işlemi yapan hariç) Web panelinde bir yük silinir
geofence_entered / geofence_exited Yöneticiler/izleyiciler (işlemi yapan hariç) Mobil uygulama POST /loads/{id}/geofence-events ile enter/exit bildirir
new_message Sohbet katılımcıları (gönderen hariç) Bir sohbete mesaj gönderilir

load_created

Web'de bir yük oluşturulduğunda yöneticilere/izleyicilere (oluşturan hariç) gönderilir. Varsayılan Android kanalını kullanır. Başlık/gövde alıcının yerel ayarına göre yerelleştirilir. 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

Admin web panelden bir sürücü atadığında gönderilir (yük oluştur veya düzenle). Android şu kanalı kullanır: loads kanalı; iOS standart alert payload'ı kullanır: badge: 1, mutable-content: 1 ve 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

Durum; sürücünün kendisi dışındaki biri tarafından web veya API üzerinden değiştirildiğinde atanan sürücüye, her durum değişikliğinde de admin/viewer'lara (değişikliği yapan hariç) gönderilir. Genel sistem kanalını kullanır; payload tipi: load_status_updated. Başlık/gövde alıcının diline göre yerelleştirilir.

// 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."
}

geofence_entered / geofence_exited

Mobil uygulama geofence enter/exit bildirdiğinde şirket admin/viewer kullanıcılarına (raporlayan hariç) gönderilir. Default kanalı kullanır. Ayrıca uygulama içi bildirim kaydı oluşur; e-posta yalnızca alıcının mail bildirimi açıksa gider.

// FCM notification
{
  "title": "Geofence entered",
  "body":  "John Doe entered Pickup (Warehouse A) for load LD-2026-0042."
}

// FCM data payload
{
  "type":  "geofence_entered",
  "title": "Geofence entered",
  "body":  "John Doe entered Pickup (Warehouse A) for load LD-2026-0042."
}

load_deleted

Web'de bir yük silindiğinde gönderilir. Atanan sürücü, loads kanalında load_id alanlı bir payload alır; uygulama yükü listelerden kaldırabilir. Diğer yöneticiler/izleyiciler genel default push alır (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."
}

Mobil Entegrasyon Rehberi

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

Gerekli Android Kanalları

Push ses/öncelik davranışının doğru çalışması için bu bildirim kanallarını uygulama açılışında bir kez oluşturun:

Kanal ID Kullanım Amacı Önem
loads load_assigned, load_deleted HIGH
messages new_message HIGH
default load_status_updated, geofence_entered, geofence_exited, sistem uyarıları HIGH

Not: Düzenleme ile bir sürücüden diğerine yeniden atama yapılırsa yalnızca yeni sürücü şunu alır: load_assigned. Geçersiz, kayıtsız veya bilinmeyen tokenlar ilk başarısız gönderimden sonra veritabanından otomatik silinir.

Uygulama İçi Bildirim Kaydı

Push'a ek olarak her load_assigned / load_status_updated / load_deleted / geofence_entered / geofence_exited olayı ayrıca şu tabloda saklanır: notifications alıcı için (sürücüler dahil). Böylece gelecekteki uygulama sürümleri, ayrı bir uç değişikliğine gerek kalmadan zil simgesi geçmişini gösterebilir.