obivan.
Geliştirici Dokümanı

Obivan Entegrasyon API — Sipariş (Sefer) Yönetimi

Dış sistemlerin (ERP, WMS, kendi iç sisteminiz) Obivan üzerinde sefer açması, izlemesi ve yönetmesi için REST API referansı. Bu doküman canlı API davranışından birebir çıkarılmıştır — üretilen/tahmin edilen değildir.

Taban URL: https://api.obivan.co Format: JSON (Content-Type: application/json) Versiyon: Endpoint'ler versiyonsuz (/api/...) — kırıcı değişiklik olursa ayrıca bildirilir.

İçindekiler

  1. Hızlı Başlangıç
  2. Kimlik Doğrulama
  3. Sipariş (Sefer) Oluşturma
  4. Sefer Yönetimi Endpoint'leri
  5. Hata Yönetimi Rehberi
  6. Sık Sorulan Sorular

Hızlı Başlangıç

1) Kimlik bilgisi alın — Obivan ekibinden bir X-WA-Service-Key (servis anahtarı) isteyin. Bu, sisteminizin API'ye kimlik kanıtlamasını sağlar.

2) İlk isteğinizi atın — bir test seferi oluşturun:

curl -X POST https://api.obivan.co/api/trips/ \
  -H "X-WA-Service-Key: $OBIVAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "driver_phone": "905551234567",
    "driver_first_name": "Test",
    "driver_last_name": "Sürücü",
    "plate": "34 TEST 01",
    "source": "API",
    "yukleme_no": "TEST-0001",
    "stops": [
      { "order": 1, "stop_type": "PICKUP", "location_name": "Test Depo", "expected_document_count": 1 },
      { "order": 2, "stop_type": "DELIVERY", "location_name": "Test Müşteri", "expected_document_count": 1 }
    ]
  }'

3) Yanıtı doğrulayın201 Created + trip_code (örn. DL-260701-001) dönerse sefer oluşmuştur. ⚠️ Bu istek gerçek bir WhatsApp mesajı tetikler (driver_phone'a) — test yaparken gerçek bir şoför numarası kullanmayın.

Sonraki adım: seferi izlemek için GET /api/trips/{trip_code}/ ile durum sorgulayın (webhook yok, polling gerekir — bkz. SSS).


Kimlik Doğrulama

İki yoldan biri kabul edilir (AllowWAOrCompanyIsWAService OR IsCompanyUser). Dış entegrasyonlar için önerilen: Servis Anahtarı.

Yöntem A — Servis Anahtarı (önerilen, server-to-server)

X-WA-Service-Key: <sizin-anahtarınız>

Statik, paylaşılan bir sır — token değişimi/yenileme gerekmez. Her isteğe bu header'ı eklemeniz yeterli. Obivan ekibinden alırsınız.

⚠️ Anahtarı asla client-side kodda (tarayıcı, mobil app) saklamayın — yalnızca sunucu-sunucu (backend-to-backend) çağrılarda kullanın.

Yöntem B — JWT (kullanıcı hesabı üzerinden)

Sisteminize özel bir panel kullanıcı hesabı açılır. Giriş akışı:

# 1) Giriş yap — access + refresh token al
curl -X POST https://api.obivan.co/api/token/ \
  -H "Content-Type: application/json" \
  -d '{"email": "entegrasyon@sizinsirket.com", "password": "..."}'
# → { "access": "eyJ...", "refresh": "eyJ..." }

# 2) Sonraki isteklerde access token'ı kullan
curl https://api.obivan.co/api/trips/ \
  -H "Authorization: Bearer eyJ..."

# 3) Access token süresi dolunca (1 saat) yenile
curl -X POST https://api.obivan.co/api/token/refresh/ \
  -H "Content-Type: application/json" \
  -d '{"refresh": "eyJ..."}'
# → { "access": "eyJ (yeni)" }

refresh token 7 gün geçerli. Süresi dolarsa /api/token/ ile tekrar giriş yapmanız gerekir.

Hangisini seçmeliyim?

Senaryo Yöntem
Sunucu-sunucu entegrasyon (ERP, WMS, cron job) Servis Anahtarı
Kullanıcı adına işlem (kim yaptı izlenmeli) JWT
Basitlik, token yenileme istemiyorum Servis Anahtarı

Sipariş (Sefer) Oluşturma

POST /api/trips/
Content-Type: application/json

Request Body

{
  "company_id": null,
  "driver_phone": "905551234567",
  "driver_first_name": "Ahmet",
  "driver_last_name": "Yılmaz",
  "plate": "34 ABC 123",
  "trailer_plate": "34 XYZ 456",
  "notes": "",
  "flow_mode": "AUTO",
  "yukleme_no": "",
  "source": "API",
  "stops": [
    {
      "order": 1,
      "stop_type": "PICKUP",
      "location_name": "Fabrika - Adana",
      "location_code": "",
      "location_address": "...",
      "location_latitude": null,
      "location_longitude": null,
      "contact_name": "",
      "contact_phone": "",
      "expected_document_count": 1,
      "waybill_numbers": [],
      "planned_arrival": null,
      "notes": ""
    }
  ]
}

Alan Referansı

Sefer (trip) düzeyiTripCreateSerializer:

Alan Tip Zorunlu Default Açıklama
company_id UUID null Boş bırakılırsa: istekteki kullanıcının şirketi (varsa), yoksa ilk aktif şirket. Servis anahtarıyla entegre oluyorsanız bunu her zaman açıkça gönderin — aksi halde hangi şirkete sefer açıldığı belirsiz olur.
driver_phone string (≤20) Şoför telefonu = benzersiz kimlik. Yoksa otomatik oluşturulur (get_or_create).
driver_first_name string (≤150) "" Şoför yeni oluşturuluyorsa/isim güncelleniyorsa kullanılır.
driver_last_name string (≤150) "" "
plate string (≤20) Araç plakası.
trailer_plate string (≤20) "" Dorse plakası.
notes string "" Serbest not.
flow_mode enum "AUTO" AUTO | LEGACY | V2. V2 sadece staff kullanıcı için geçerli, aksi halde LEGACY'e düşer. Dış entegrasyonlar AUTO bırakmalı.
yukleme_no string (≤100) "" Sizin sisteminizdeki sipariş/yükleme kodu. Doluysa mükerrer kontrolü tetiklenir — bkz. idempotency.
source enum "MANUAL" MANUAL | EXCEL | API. Dış entegrasyon her zaman "API" göndermeli — panelde kaynağa göre filtrelenip ayırt edilebilsin diye.
stops array ✓ (≥1) Aşağıdaki durak nesnelerinin listesi. order alanları tekrarsız olmalı.

Durak (stop) düzeyiTripStopCreateSerializer, her stops[] elemanı:

Alan Tip Zorunlu Açıklama
order int Durak sırası, sefer içinde benzersiz (1'den başlayın).
stop_type enum PICKUP (Yükleme) | DELIVERY (Teslimat) | BOTH (Yükleme+Teslimat aynı durakta).
location_name string ✓ (pratikte) Durak adı — panelde ve şoför mesajlarında görünür. Otomatik .strip() uygulanır.
location_code string Sizin sisteminizdeki dahili konum kodu (round-trip için saklanır, Obivan bunu kullanmaz).
location_address string Açık adres — şoföre WhatsApp'ta gösterilir.
location_latitude / location_longitude float Koordinat — varsa harita/mesafe hesaplarında kullanılır.
contact_name / contact_phone string Durak yetkilisi (teslim alan kişi bilgisi).
expected_document_count int Bu durakta beklenen irsaliye sayısı (varsayılan 1). Şoför bu sayıya ulaşana kadar durağı kapatamaz (bkz. MISSING_DOCS).
waybill_numbers array[string] Önceden bilinen irsaliye no'ları (opsiyonel, bilgi amaçlı — doğrulama şoförün yüklediği fotoğraftan yapılır).
planned_arrival datetime (ISO 8601) Planlanan varış zamanı — hatırlatma/gecikme hesaplarında referans alınır.
notes string Durağa özel not (şoföre iletilir).

Sunucu Tarafı Davranış

  1. Şoför driver_phone ile bulunur/oluşturulur; Wallet yoksa oluşturulur. Yeni şoför için ayrıca kayıt işlemi gerekmez — ilk sefer isteğinde otomatik açılır.
  2. Cüzdan borcu hard-block: company.billing_mode == ACTIVE VE şoförün wallet.debt_amount > 0 ise → 402 Payment Required, sefer açılmaz.
  3. Mükerrer sefer kontrolü (yukleme_no doluysa, şirket-bazlı): aynı yukleme_no ile aktif veya son 3 günde açılmış (iptal hariç) bir sefer varsa → 409 Conflict.
  4. Şoförün başka aktif seferi varsa engellenmez, sadece response'a warning alanı eklenir.
  5. Şoförün eski, unutulmuş CREATED durumundaki seferleri otomatik CANCELLED'a çekilir.
  6. trip_code otomatik üretilir: DL-YYMMDD-XXX formatında.
  7. Sefer + duraklar oluşturulur, şoföre otomatik WhatsApp bildirimi gönderilir (hata olursa sefer oluşturma etkilenmez, sadece loglanır).

Başarılı Yanıt — 201 Created

{
  "id": "8f3e2c10-4a9b-4e77-9c1a-2b6d5e8f0a11",
  "trip_code": "DL-260701-001",
  "share_token": "b7a1c9e0-...",
  "status": "CREATED",
  "last_action": null,
  "notes": "",
  "yukleme_no": "ERP-2026-0001",
  "created_via": "API",
  "flow_mode": "LEGACY",
  "company": { "id": "...", "name": "Örnek Üretici A.Ş." },
  "company_name": "Örnek Üretici A.Ş.",
  "driver": { "id": "...", "phone": "905551234567", "name": "Ahmet Yılmaz" },
  "driver_name": "Ahmet Yılmaz",
  "driver_phone": "905551234567",
  "plate": "34 ABC 123",
  "trailer_plate": "34 XYZ 456",
  "current_latitude": null,
  "current_longitude": null,
  "last_location_update": null,
  "last_eta_update": null,
  "created_by": null,
  "created_at": "2026-07-01T09:15:32Z",
  "completed_at": null,
  "deleted_at": null,
  "trip_position_v2": null,
  "issues": [],
  "open_issue_count": 0,
  "stops": [
    {
      "id": "b1a2c3d4-...",
      "order": 1,
      "stop_type": "PICKUP",
      "location_name": "Fabrika - Adana",
      "status": "PENDING",
      "expected_document_count": 1,
      "waybill_numbers": [],
      "planned_arrival": null,
      "actual_arrival": null,
      "actual_departure": null,
      "documents": []
    },
    {
      "id": "c2b3d4e5-...",
      "order": 2,
      "stop_type": "DELIVERY",
      "location_name": "Depo - Ankara",
      "status": "PENDING",
      "expected_document_count": 1,
      "waybill_numbers": [],
      "planned_arrival": null,
      "actual_arrival": null,
      "actual_departure": null,
      "documents": []
    }
  ]
}

Şoförün zaten aktif bir seferi varsa, aynı body'ye ek olarak:

{ "warning": "Bu sürücünün aktif seferi var: DL-260630-004" }

Hata Durumları

HTTP Durum Body
400 Validasyon hatası (DRF standart) { "driver_phone": ["This field is required."] }
402 Şoför cüzdan borcu (billing_mode=ACTIVE) { "error": "wallet_debt", "message": "Şoförün (Ahmet Yılmaz) cüzdanında ₺50.00 borç var. Yeni sefer atanması için borç kapatılmalı.", "debt_amount": "50.00", "driver_phone": "905551234567" }
404 Şirket bulunamadı (company_id yanlış/pasif) { "error": "Şirket bulunamadı." }
409 Mükerrer yukleme_no { "error": "duplicate_yukleme_no", "message": "Bu Yükleme No (ERP-2026-0001) ile zaten bir sefer var: DL-260701-001 (01.07.2026 · CREATED). Mükerrer sefer engellendi.", "existing_trip_code": "DL-260701-001", "yukleme_no": "ERP-2026-0001" }

⚠️ Sayaç (trip_code üretimi) count()+1 ile hesaplanır, atomik değildir — çok yoğun eşzamanlı istekte teorik çakışma riski var (DB'de trip_code unique=True olduğu için çakışma sessizce yutulmaz, IntegrityError/500 fırlatır — bkz. Hata Yönetimi).


Diğer Entegrasyon Endpoint'leri

Aynı auth (JWT veya X-WA-Service-Key), aynı taban URL.

Sefer Listeleme — GET /api/trips/

Query param'lar (sonuçlar şirket-kapsamlı):

Param Açıklama
status CREATED, DRIVER_NOTIFIED, IN_PROGRESS, ALL_STOPS_DONE, COMPLETED, FAILED, CANCELLED
search trip_code, plaka, şoför adı/telefonunda arama
source MANUAL | EXCEL | API
waybill İrsaliye no / ETTN içeren seferler
date_from, date_to YYYY-MM-DD, created_at aralığı
loading_point Yükleme noktası adına göre (PICKUP/BOTH durak location_name)
page Sayfa no, sayfa başına 20 kayıt
curl "https://api.obivan.co/api/trips/?status=IN_PROGRESS&page=1" \
  -H "X-WA-Service-Key: $OBIVAN_API_KEY"
{
  "count": 47,
  "next": "https://api.obivan.co/api/trips/?page=2",
  "previous": null,
  "results": [ { "trip_code": "DL-260701-001", "...": "TripSerializer (yukarıdaki gibi)" } ],
  "stats": { "active": 12, "completed": 340, "total": 352 }
}

stats, status/search filtresinden önce hesaplanır — şirket geneli toplam, filtrelenmiş sayfayı değil.

Sefer Detayı — GET /api/trips/{trip_code}/

Tek seferin tam TripSerializer çıktısı (durak + belge listesi dahil, nested — bkz. yukarıdaki 201 örneği aynı şekli döner).

{ "error": "Sefer bulunamadı." }

404, sefer yoksa veya başka şirkete aitse.

Sefer Güncelleme — PATCH /api/trips/{trip_code}/

Body: TripUpdateSerializer alanları (opsiyonel, sadece gönderilenler güncellenir) — driver_phone, driver_first_name/driver_last_name (şoför değiştirmek için hepsi birlikte gönderilmeli, yoksa upsert tetiklenmez), plate, trailer_plate, notes.

curl -X PATCH https://api.obivan.co/api/trips/DL-260701-001/ \
  -H "X-WA-Service-Key: $OBIVAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"notes": "Müşteri saat 14:00 sonrası teslim istiyor"}'

Sefer Durakları — GET /api/trips/{trip_code}/stops/

O sefere ait tüm durakların listesi (TripStopSerializer, belgeler dahil) — results array'i olmadan doğrudan bir dizi döner.

Sefer Tamamlama — POST /api/trips/{trip_code}/complete/

Body yok. Sefer COMPLETED yapılır, completed_at set edilir, şoför cüzdanına asenkron ücret yansıtılır. 200 + güncel TripSerializer.

Sefer İptali — POST /api/trips/{trip_code}/cancel/

Body yok. Zaten COMPLETED/CANCELLED ise 400 ({"error": "Sefer zaten Tamamlandı durumunda."}). Başarılıysa status=CANCELLED, bekleyen/yoldaki duraklar SKIPPED'a çekilir.

Konum Güncelleme — POST /api/trips/{trip_code}/location/

{ "latitude": 39.9208, "longitude": 32.8541 }

{"status": "ok", "last_location_update": "2026-07-01T10:22:00Z"}. Genelde saha uygulaması/wa-agent kullanır ama kendi GPS entegrasyonunuz varsa siz de gönderebilirsiniz.

Sorun Bildirimi — POST /api/trips/{trip_code}/issue/

{ "issue_type": "DAMAGE", "description": "Palet hasarlı geldi", "stop_id": "b1a2c3d4-..." }

201 + oluşturulan sorun kaydı.

Teslim Kanıtı / Mutabakat — GET /api/trips/{trip_code}/proof/

Sefer kapandıktan sonra teslim edilmiş irsaliye görselleri + karşılaştırma sonuçlarını (POD — proof of delivery) çekmek için kullanılır.

Durak Belgeleri — GET /api/stops/{stop_id}/documents/

Bir durağa ait tüm irsaliye belgelerini listeler. Sefer detayındaki nested documents alanı zaten aynı veriyi taşıdığı için, çoğu senaryoda ayrı çağrıya gerek yoktur — belge yalnızca değiştiyse/yenisi geldiyse tekrar sorgulayın.


Hata Yönetimi Rehberi

Genel prensip: 4xx = isteğinizde düzeltilmesi gereken bir şey var, tekrar denemeden önce body'yi düzeltin. 5xx = sunucu tarafı geçici sorun, kısa bir bekleme sonrası tekrar deneyebilirsiniz (idempotency notlarına dikkat ederek — aşağıda).

Kod Anlamı Ne yapmalı
400 Eksik/hatalı alan response.json()'daki alan-bazlı hata mesajlarını okuyup body'yi düzeltin.
401 Kimlik doğrulanamadı X-WA-Service-Key yanlış/eksik ya da JWT süresi dolmuş — refresh deneyin, olmazsa yeniden giriş yapın.
402 Şoför cüzdan borcu Sefer o şoförle açılamaz; müşterinizle (nakliyeci) iletişime geçilmesi gerekir — retry işe yaramaz.
403 Yetki yok Bu kaynağa erişim izniniz yok (örn. başka şirketin verisi) — kimlik bilgilerinizi kontrol edin.
404 Bulunamadı trip_code/id yanlış ya da kaydınız yok — retry işe yaramaz.
409 Mükerrer kayıt Bu bir hata değil — existing_trip_code'u kullanın, aynı isteği tekrar göndermeyin.
429 Çok fazla istek (bazı endpoint'lerde throttle var, örn. /api/register/) Retry-After varsa bekleyin.
5xx Sunucu tarafı sorun Üstel geri çekilme (exponential backoff) ile birkaç kez deneyin; sürerse bize bildirin.

Idempotency — Tekrar-Gönderim Güvenliği

API'de native bir idempotency-key mekanizması yok. Tekrar-gönderim koruması dolaylı olarak yukleme_no üzerinden sağlanır:

  1. Her siparişe kendi sisteminizde benzersiz bir yukleme_no verin.
  2. 409 duplicate_yukleme_no aldığınızda bunu hata değil, başarı sayın — existing_trip_code zaten oluşturulmuş sefer demektir.
  3. yukleme_no göndermezseniz (boş bırakırsanız) mükerrer kontrolü çalışmaz — ağ zaman aşımında retry yaparsanız aynı sefer 2 kez oluşabilir. Bu yüzden dış entegrasyonlarda yukleme_no göndermeniz şiddetle önerilir.

Sık Sorulan Sorular

API'ye dışarıdan (internet üzerinden) erişilebiliyor mu? Evet. api.obivan.co genel internete açık (bu, panelin tarayıcıdan çalışabilmesi için zaten zorunlu). Doğru kimlik bilgisi (servis anahtarı veya JWT) olmadan hiçbir endpoint veri döndürmez — tüm auth-gerekli endpoint'ler kimliksiz istekte 401 döner. Kimliğiniz varsa, evet, dışarıdan tam olarak erişebilirsiniz.

Test/sandbox ortamı var mı? Hayır, ayrı bir staging API yok — tek ortam api.obivan.co (prod). Test isteklerinde gerçek olmayan telefon numarası kullanın (gerçek bir şoföre WhatsApp mesajı gitmesin diye) ve düşük hacimli/test amaçlı bir şirket hesabıyla çalışın.

Rate limit var mı? Genel endpoint'lerde özel bir throttle tanımlı değil. (/api/register/ gibi bazı public/anon endpoint'lerde IP-bazlı throttle var, ama bunlar entegrasyon akışının dışında.) Toplu gönderimde makul bir hızda (örn. saniyede birkaç istek) gitmeniz önerilir.

Sefer durumu değiştiğinde bana otomatik bildirim (webhook) gelir mi? Hayır, webhook mekanizması yok. Durum takibi için periyodik GET /api/trips/{trip_code}/ (tekil) veya GET /api/trips/?date_from=... (toplu senkronizasyon) ile polling yapmanız gerekir.

Sipariş oluşturunca ne oluyor, şoför nasıl haberdar oluyor? POST /api/trips/ başarılı olduğunda şoföre otomatik bir WhatsApp mesajı gider (sefer özeti + duraklar). Şoför uygulama kurmaz — tamamen WhatsApp üzerinden yönlendirilir, irsaliye fotoğraflarını da oradan yükler.

Bir siparişi ikinci kez göndersem ne olur? yukleme_no gönderdiyseniz ve aynı değeri tekrar gönderirseniz 409 alırsınız (mükerrer koruma) — bkz. Idempotency. yukleme_no göndermediyseniz koruma yok, iki ayrı sefer açılır.