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
- Hızlı Başlangıç
- Kimlik Doğrulama
- Sipariş (Sefer) Oluşturma
- Sefer Yönetimi Endpoint'leri
- Hata Yönetimi Rehberi
- 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ın — 201 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 (AllowWAOrCompany → IsWAService 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üzeyi — TripCreateSerializer:
| 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üzeyi — TripStopCreateSerializer, 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ış
- Şoför
driver_phoneile bulunur/oluşturulur;Walletyoksa oluşturulur. Yeni şoför için ayrıca kayıt işlemi gerekmez — ilk sefer isteğinde otomatik açılır. - Cüzdan borcu hard-block:
company.billing_mode == ACTIVEVE şoförünwallet.debt_amount > 0ise →402 Payment Required, sefer açılmaz. - Mükerrer sefer kontrolü (
yukleme_nodoluysa, şirket-bazlı): aynıyukleme_noile aktif veya son 3 günde açılmış (iptal hariç) bir sefer varsa →409 Conflict. - Şoförün başka aktif seferi varsa engellenmez, sadece response'a
warningalanı eklenir. - Şoförün eski, unutulmuş
CREATEDdurumundaki seferleri otomatikCANCELLED'a çekilir. trip_codeotomatik üretilir:DL-YYMMDD-XXXformatında.- 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:
- Her siparişe kendi sisteminizde benzersiz bir
yukleme_noverin. 409 duplicate_yukleme_noaldığınızda bunu hata değil, başarı sayın —existing_trip_codezaten oluşturulmuş sefer demektir.yukleme_nogö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ış entegrasyonlardayukleme_nogö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.