Bi Express API

Sitenizden Bi Express'e kargo oluşturun, takip edin ve kurye zimmetinden önce iptal edin. Kargo durumu değiştiğinde webhook ile haber alın.

Token yetkileri ve IP kısıtı

Token iki yetkiden biriyle verilir:

Yetki İzin verilen
Okuma + yazma (varsayılan) Tüm uç noktalar
Yalnızca okuma GET uç noktaları ve POST /labels (etiket linki üretir, veri değiştirmez)

Yalnızca okuma yetkisi olan token ile POST /cargoes veya POST /cargoes/{code}/cancel çağrılırsa şu yanıt döner: 403 {"message": "Bu token yalnızca okuma yetkili; bu işlem için yazma yetkisi gerekiyor."}. Bu özellikten önce üretilmiş token'lar tam yetkilidir ve çalışmaya devam eder.

IP kısıtı: Firmanız için izinli IP listesi tanımlanabilir. Her satıra bir IP ya da CIDR yazılır; IPv4 ve IPv6 desteklenir (ör. 203.0.113.10, 198.51.100.0/24, 2001:db8::/32). Liste boşsa her IP'den erişilebilir. Listede olmayan bir IP'den gelen istek şu yanıtı alır: 403 {"message": "Bu IP adresinden erişime izin verilmiyor: <ip>"}. Kontrol, sunucuya doğrudan bağlanan IP'ye göre yapılır; X-Forwarded-For başlığı dikkate alınmaz.

Bölgeler

7 teslimat bölgesi vardır: Lefkoşa, Gazimağusa, Girne, Güzelyurt, İskele, Lefke ve Karpaz. Her bölgenin köy/mahalle listesi /cities yanıtında gelir:

[ { "id": 3, "name": "Girne", "places": [ { "id": 101, "name": "Alsancak" }, { "id": 102, "name": "Çatalköy" } ] } ]

id'leri sabit kodlamayın; listeyi günde bir kez çekip önbelleğe alın.

Uç noktalar

Yöntem Yol Ne yapar
GET /cities Teslimat bölgeleri ve köy/mahalleleri (id, name, places[])
GET /package-types Paket tipleri (id, name, price); price firmanıza uygulanan ücrettir (size özel teklif varsa o)
POST /cargoes Kargo oluşturur
GET /cargoes Kargo listesi (filtre, sayfalama, senkronizasyon)
GET /cargoes/{code} Kargo durumu ve geçmişi
POST /cargoes/{code}/cancel Kargoyu iptal eder (zimmetten önce)
GET /cargoes/{code}/label Etiket yazdırma linki (1 saat geçerli)
POST /labels Toplu etiket yazdırma linki (en fazla 200 kargo)
GET /balance Bakiye: alacak, ödemeye hazır, kuryede, ödenen, son ödemeler
GET /statements?month=YYYY-MM Aylık ekstre (JSON veya CSV)
GET /payouts Size yapılan ödemeler ve her ödemenin kapattığı kargolar

Kargo oluştur: POST /cargoes

Alan Tip Zorunlu Not
full_name string ≤255 evet Alıcı adı
phone string ≤20 evet Alıcı telefonu
address string ≤2000 evet Açık adres
city_id int evet /cities listesinden
place_id int hayır Köy/mahalle; city_id bölgesine ait olmalı (aksi 422)
package_type_id int hayır /package-types listesinden; kargo ücreti buradan hesaplanır
packages_count int 1–100 hayır Koli adedi; her koli için ayrı etiket basılır ("Koli 1/3")
is_payment_on_delivery bool hayır Kapıda ödeme
package_price sayı ≥0 is_payment_on_delivery false değilse evet Kapıda tahsil edilecek tutar
cod_method cash / pos hayır Alıcının kapıda nasıl ödeyeceği (kurye POS cihazı alsın mı). Boşsa cash; kapıda ödeme yoksa yok sayılır ve null döner. Firma kapıda POS hizmeti almıyorsa pos → 422 (cod_method)
is_exchange / is_return bool hayır Değişim / iade gönderisi
desc string ≤1000 hayır Kurye notu
external_ref string ≤100 önerilir Sizin sipariş numaranız

Tekrar güvenliği: Aynı external_ref ile tekrar istek atarsanız yeni kargo açılmaz; mevcut kargo 200 ile döner (ilk oluşturmada 201). Ağ hatasında isteği güvenle tekrarlayabilirsiniz.

curl -X POST https://bi.express/api/v1/cargoes \
  -H "Authorization: Bearer $TOKEN" -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{"full_name":"Ayşe Yılmaz","phone":"05331234567","address":"Ortaköy, Lefkoşa",
       "city_id":1,"package_type_id":2,"is_payment_on_delivery":true,
       "package_price":450,"external_ref":"WC-1042"}'
{
  "code": "K7Q2M9XA",
  "external_ref": "WC-1042",
  "delivery_code": "4821",
  "tracking_url": "https://bi.express/takip?kod=K7Q2M9XA",
  "status": "Pending",
  "status_label": "Kargo oluşturuldu",
  "cancelable": true,
  "courier": null,
  "cod_method": "cash"
}

delivery_code, alıcının teslimatta kuryeye söylediği 4 haneli koddur. Alıcıya iletin.

Her kargo yanıtında bir de finance nesnesi gelir:

"finance": { "fee": 250, "cod_amount": 450, "pos_bisiparis": 0, "havale_bisiparis": 0, "pos_commission": 0, "net": 200, "cod_collected_at": null, "paid_out_at": null }

fee kargo ücretidir. cod_amount kapıda tahsil edilecek tutardır; kapıda ödeme yoksa 0 olur. pos_bisiparis, kapıda kendi POS cihazınıza ya da hesabınıza (havale) Bi-siparis adına giren tutardır (havale_bisiparis bunun havale kısmı); bu para Bi Express'e girmediği için ödemenizden düşülür. pos_commission, firmanız Bi Express POS'unu kullanıyorsa POS ile çekilen tutarın %4'üdür ve ödemenizden kesilir; kendi POS'unuzu kullanıyorsanız 0'dır (POS sahibi firma ayarlarında belirlenir). net, size ödenecek tutarı gösterir (cod_amount - fee - pos_bisiparis - pos_commission); negatifse kargo ücreti size ödenecek tutardan düşülür. cod_collected_at kuryenin parayı tahsil ettiği, paid_out_at tutarın size aktarıldığı zamandır.

cod_method: kapıda ödeme şekli (cash / pos); kapıda ödeme yoksa null.

courier: kargo bir kuryeye zimmetlenince {"name": "Ali Veli", "phone": "05331112233"}, zimmetlenmemişse null. Telefon kayıtlı değilse phone alanı null olur. cancelable: şu an POST /cancel ile iptal edilebilir mi.

Kargo sorgula: GET /cargoes/{code}

Yukarıdaki alanlara ek olarak city, place (yoksa null) ve history döner. history en yeni olay başta olacak şekilde sıralıdır:

{ "...": "...", "city": "Lefkoşa", "place": "Gönyeli",
  "history": [ { "status": "Received", "status_label": "Teslim alındı", "note": null, "at": "2026-11-04T10:12:00+02:00" } ] }

İptal: POST /cargoes/{code}/cancel

Kargo yalnızca "Kargo oluşturuldu" durumundaysa ve henüz bir kuryeye zimmetlenmediyse iptal edilebilir. Aksi halde 409 döner; bu durumda iptal için Bi Express ile iletişime geçin.

Kargo listesi: GET /cargoes

Yalnızca kendi firmanızın kargolarını en yeniden eskiye doğru listeler. Her öğe, tekil kargo yanıtıyla aynı yapıdadır (code, status, courier, finance…).

Parametre Açıklama
status Durum değeri (Pending, Received, OutForDelivery, Delivered…)
from, to Oluşturma tarihi aralığı (YYYY-MM-DD, iki uç dahil)
updated_since ISO 8601 zaman (ör. 2026-11-02T09:30:00+03:00). Bu andan sonra değişen kargolar döner. Senkronizasyon için son çekiş zamanınızı gönderin; webhook kaçırdığınız değişiklikleri böyle yakalarsınız
external_ref Sipariş numaranız
per_page Sayfa boyu: varsayılan 50, en fazla 100. Sayfalar arasında page ile gezilir

Yanıt: data (kargolar), current_page, last_page, per_page, total, next_page_url.

Etiket yazdırma

{ "url": "https://bi.express/etiketler/imzali?ids=...&signature=...", "expires_at": "2026-11-04T15:00:00+02:00" }

url 1 saat geçerlidir ve oturum ya da token gerektirmez. Tarayıcıda açılınca 100×100 mm etiket sayfası ve yazdırma penceresi gelir. Süresi dolmuş veya değiştirilmiş link 403 döner. Kargonun packages_count değeri kaç ise o kadar etiket basılır; her etikette "Koli 1/3" gibi sıra yazar, barkod hepsinde kargo kodudur.

Teslim edilemedi

Kurye teslim edemezse kargo Dağıtımda → Transferde durumuna döner ve bir sebep kaydedilir. Kargo daha sonra tekrar dağıtıma çıkabilir; bunun için ayrı bir durum yoktur.

Her kargo yanıtında şu alanlar da gelir:

Alan Açıklama
delivery_attempts Başarısız teslim denemesi sayısı
last_failure Son başarısız deneme: {"reason": "adreste_yok", "reason_label": "Alıcı adreste yok", "at": "..."}. Hiç başarısız deneme yoksa null

history[] kayıtlarında failure_reason ve failure_reason_label alanları vardır; başarısız deneme olmayan kayıtlarda null olur.

Sebep kodu Açıklama
adreste_yok Alıcı adreste yok
telefon_kapali Telefon kapalı / ulaşılamadı
reddetti Alıcı teslim almadı
adres_hatali Adres hatalı / bulunamadı
erteleme Alıcı ileri tarihe erteledi
diger Diğer

Herkese açık takip sayfasında yalnızca "Teslim edilemedi — " görünür; kuryenin notu gösterilmez.

Aylık ekstre: GET /statements?month=YYYY-MM

O ay içinde teslim edilen kargoları listeler. Ay, teslim tarihine göre belirlenir; böylece her kargo yalnızca bir ayın ekstresinde yer alır. İade ve iptal edilen kargolar ekstreye girmez. Bir kargonun size ödenip ödenmediğini satırdaki paid_out_at alanından görürsünüz.

{
  "month": "2026-10", "currency": "TRY",
  "rows": [{ "code": "AB12CD34", "external_ref": "WC-1001", "status": "Delivered",
             "delivered_at": "2026-10-15T10:00:00+03:00", "cod_amount": 1000, "fee": 250, "net": 750,
             "cod_collected_at": "2026-10-15T10:00:00+03:00", "paid_out_at": null }],
  "totals": { "count": 1, "cod_amount": 1000, "fee": 250, "net": 750, "paid_net": 0, "unpaid_net": 750 }
}

?format=csv eklerseniz Excel'de doğrudan açılan bir CSV dosyası iner (ekstre-2026-10.csv). Ayraç ;, ondalıklar virgüllüdür ve en altta TOPLAM satırı vardır.

Bakiye: GET /balance

Firmanızın Bi Express nezdindeki hesap özetini döner. Tutarlar TL cinsindendir ve kargo ücretleri düşülmüş net değerlerdir. Rakamlar, Bi Express'in tahsilat ekranındakiyle birebir aynı hesaplanır.

{
  "currency": "TRY",
  "receivable":   { "amount": 750, "count": 3 },
  "ready_to_pay": { "amount": 500, "count": 2 },
  "in_courier":   { "amount": 250, "count": 1 },
  "paid":         { "amount": 50,  "count": 1, "last_paid_at": "2026-11-03T12:00:00+02:00" },
  "payouts": [ { "paid_at": "2026-11-03T12:00:00+02:00", "count": 1, "amount": 50 } ]
}
Alan Anlamı
receivable Toplam alacağınız: tahsil edilmiş ama size henüz ödenmemiş tutar. Kapıda ödemesiz teslimatların ücreti düşülmüştür
ready_to_pay receivable'ın kasaya girmiş, ödemeye hazır kısmı
in_courier receivable'ın hâlâ kuryede olan kısmı
paid Bugüne kadar size ödenen toplam
payouts Son 20 ödeme, en yeni başta (id, paid_at, count, amount, method)
advance Avans: size yapılan ödemelerden henüz hiçbir kargoya mahsup edilmemiş kısım (≥ 0)
due Kalan borç = ready_to_pay − advance. Avans fazlaysa negatif olabilir

Ödemeler: GET /payouts

Firmanıza yapılan ödemeleri listeler; en yeni ödeme başta gelir. Sayfa boyu per_page ile ayarlanır (1–100, varsayılan 20).

{ "data": [ { "id": 12, "paid_at": "2026-11-04T15:00:00+03:00", "amount": 5000, "method": "Havale", "note": "Ekim", "count": 2,
  "cargoes": [ { "code": "GJQ5RENX", "external_ref": "BX1384655", "net": 4210.5 } ] } ],
  "current_page": 1, "last_page": 1, "total": 1 }

Ödeme nasıl kargolara dağıtılır: Ödemeler tutar olarak yapılır ve parçalı olabilir. Tutar önce ücret kesintilerine (net ≤ 0 olan kargolar) mahsup edilir. Sonra en eski tahsilattan başlanarak kargolar sırayla kapanır. Tutarın yetmediği ilk kargoda durulur ve artan kısım avans olarak sonraki ödemeye aktarılır. Bi Express seçtiği kargoları doğrudan "ödendi" olarak da işaretleyebilir; bu durumda ödeme tutarı tam olarak o kargoların net toplamıdır. Ekstre satırlarındaki sender_payout_id alanı, kargoyu hangi ödemenin kapattığını gösterir.

Durumlar

status Anlamı
Pending Kargo oluşturuldu
Received Teslim alındı (depoda)
InTransfer Transferde
OutForDelivery Dağıtımda
Delivered Teslim edildi (son durum)
Returned İade edildi (son durum)
Canceled İptal edildi (son durum)

Hatalar

Kod Ne zaman
401 Token yok ya da geçersiz
403 Token bir gönderici firmaya ait değil, token yalnızca okuma yetkili, IP izinli değil ya da etiket linkinin süresi dolmuş
404 Kargo yok ya da başka bir firmaya ait
409 İptal artık mümkün değil
422 Doğrulama hatası: {"message": "...", "errors": {"alan": ["..."]}}
429 İstek limiti aşıldı; Retry-After başlığındaki süre kadar bekleyin

Webhook

Bi Express yöneticisi firmanız için bir webhook URL (yalnızca https) ve bir imza anahtarı (secret) tanımlar. Şu durumlarda bu adrese POST atılır:

event Ne zaman
cargo.status_changed Kargo oluşturulduğunda ve durumu her değiştiğinde
cargo.courier_assigned Kargo bir kuryeye zimmetlendiğinde (durum değişmeden). Bu andan itibaren iptal edilemez
cargo.delivery_failed Teslim denemesi başarısız olduğunda. status = InTransfer; failure_reason ve failure_reason_label dolu gelir
payout.created Size bir ödeme kaydedildiğinde: {"payout_id", "amount", "paid_at", "method", "note", "cargoes": [{"code", "external_ref", "net"}], "advance_after", "currency"}. Yalnızca ücret mahsubu yapılan kapatmalarda amount 0 veya negatif olabilir
payout.reverted Hatalı girilen son ödeme geri alındığında: {"payout_id", "amount", "cargoes": [{"code", "external_ref"}], "advance_after", "currency"}. Listelenen kargolar yeniden "ödenecek" durumuna döner
ping Panelden "Deneme bildirimi gönder" ile atılan test isteği: {"event": "ping", "event_id": "...", "at": "..."}. 2xx dönmeniz yeterli

cargo.status_changed ve status = Delivered olduğunda gövdeye ek olarak payments (kapıda alınan ödeme satırları: method cash/pos/havale, amount, owner, pos_owner, pos_ref, card_last4, installments, has_receipt; dekont fotoğrafı gönderilmez) ve finance (yukarıdaki yapı) eklenir.

Bütün bildirimlerde failure_reason ve failure_reason_label alanları bulunur; cargo.delivery_failed dışındaki olaylarda null gelir.

{
  "event": "cargo.status_changed",
  "event_id": "9b2f4c1e-6a7d-4e3b-8f0a-2c5d1e7b9a40",
  "code": "K7Q2M9XA",
  "external_ref": "WC-1042",
  "status": "Delivered",
  "status_label": "Teslim edildi",
  "note": null,
  "cancelable": false,
  "courier": { "name": "Ali Veli", "phone": "05331112233" },
  "tracking_url": "https://bi.express/takip?kod=K7Q2M9XA",
  "at": "2026-11-04T15:40:00+02:00"
}

event_id her bildirim için benzersiz bir metindir (UUID); tekrar denemelerde aynı kalır.

Başlıklar: X-BiExpress-Event, X-BiExpress-Timestamp (unix saniye), X-BiExpress-Signature.

İmza doğrulama: hex(HMAC-SHA256(timestamp + "." + ham_gövde, secret)) değeri X-BiExpress-Signature başlığına eşit olmalı. Gövdeyi JSON'a çevirmeden önce ham haliyle imzalayın. 5 dakikadan eski timestamp'leri reddedin.

$body = file_get_contents('php://input');
$ts   = $_SERVER['HTTP_X_BIEXPRESS_TIMESTAMP'] ?? '';
$sig  = $_SERVER['HTTP_X_BIEXPRESS_SIGNATURE'] ?? '';
$ok   = abs(time() - (int) $ts) < 300
     && hash_equals(hash_hmac('sha256', $ts.'.'.$body, BIEXPRESS_WEBHOOK_SECRET), $sig);
if (! $ok) { http_response_code(401); exit; }
$event = json_decode($body, true);
// event_id ile tekrarları ayıklayın, sonra siparişi güncelleyin
http_response_code(200);

Teslim garantisi: 10 saniye içinde 2xx dönmezseniz istek tekrar denenir. Yaklaşık 30 sn, 2 dk, 10 dk, 30 dk ve 1 saat aralıklarla toplam 6 deneme yapılır. Yönlendirmeler (3xx) izlenmez. Aynı olay birden fazla kez gelebilir; event_id ile tekrarları ayıklayın.

Kayıt ve tekrar gönderim: Her bildirim denemesi Bi Express tarafında 30 gün saklanır; HTTP kodu, yanıtın ilk 1000 karakteri ve hata mesajı kaydedilir. Size ulaşmayan bir bildirimi destek ekibimiz panelden "Tekrar gönder" ile yeniden iletebilir. Tekrar gönderilen bildirim yeni bir event_id taşır ve içeriği ilk gönderildiği anki haliyle aynıdır. Kargonun güncel durumu için GET /cargoes/{code} kullanın.

Not: cancelable ve courier alanları bildirimin gönderildiği andaki durumu gösterir. İptal edip edemeyeceğinizi kesin olarak POST /cancel yanıtı belirler.

Ops (kurye) API — iç kullanım

Bi-siparis personelinin Bi Express kargolarını (her firmanın) dağıtması içindir; firma token'larıyla çağrılamaz. Token: sunucuda php artisan ops:token <email> (yalnız ops yetkisi, panele giremeyen ops rollü kullanıcı; her çalıştırma eski token'ı iptal eder). Önek /api/v1/ops, limit 120 istek/dk. Sunucu ortamında OPS_ALLOWED_IPS (virgüllü IP/CIDR) doluysa yalnız bu IP'lerden erişilir, diğerleri 403 ip_not_allowed; boşsa kısıt yok. Kurye, isteğin courier_phone alanıyla (her biçim +90XXXXXXXXXX'e çevrilir) kurye rolündeki kullanıcıdan bulunur; bulunamazsa 403 + "code": "courier_not_found". Kargo yanıtında cod_method (cash / pos / kapıda ödeme yoksa null) alıcının kapıda nasıl ödeyeceğini gösterir: pos ise kurye POS cihazı almalıdır. Hatalar diğer uçlarla aynı {"message", "errors"} biçimindedir; iş kuralı hatalarında ayrıca code gelir: courier_not_found, assigned_to_other (+ courier), not_assigned, closed, invalid_status, amount_mismatch (+ cash_in_hand), courier_inactive, phone_taken.

Yöntem Yol Gövde
GET /cargoes/{code}?courier_phone= —
POST /cargoes/{code}/assign courier_phone, force? (başka kuryedeyse devralır)
POST /cargoes/{code}/out-for-delivery courier_phone
POST /cargoes/{code}/deliver courier_phone, delivery_code, payments[], amount_diff_reason?, proof_photo (base64), note?
POST /cargoes/{code}/fail courier_phone, reason, note?
POST /cargoes/{code}/price package_price (yalnız düşürülür), reason, courier_phone?
GET /couriers/{phone}/cargoes — açık zimmetler
GET /couriers/{phone}/cash — kuryedeki nakit
POST /couriers/{phone}/settle amount (kuryedeki nakde eşit olmalı)
POST /couriers phone, name, external_id — kurye tanımla/güncelle (upsert), active=true; telefon başka aktif kuryedeyse 409 phone_taken
POST /couriers/{phone}/deactivate — pasif kurye zimmet alamaz, dağıtıma çıkamaz, teslim edemez (403 courier_inactive); listeler, kasa ve devir çalışır

payments[] satırı: method (cash/pos/havale), amount; pos için pos_ref zorunlu; pos_owner gönderilebilir ama yok sayılır (POS sahibini firmanın admin ayarı belirler, yanıttaki pos_owner bu değeri gösterir: firmanın kendi POS'u → bisiparis, Bi Express POS'u → biexpress ve %4 komisyon), card_last4, installments (1–12) isteğe bağlı; havale için owner (bisiparis/biexpress, paranın gönderildiği hesap) ve receipt_photo (base64 JPEG/PNG/HEIC, en fazla 6 MB) zorunlu. Kapıda ödemede satır toplamı tahsil tutarına eşit değilse amount_diff_reason zorunludur ve tahsil tutarı toplanan tutara çekilir. Firma kapıda POS hizmeti almıyorsa (kargo yanıtında customer.pos_allowed = false) pos satırı 422 payments hatası verir. Kasaya yalnız cash satırları girer; nakit yoksa kargo teslimde kasaya alınmış sayılır.

delivery_code iade gönderisinde (kargo yanıtında is_return = true) zorunlu değildir; gönderilirse yine doğrulanır. Diğer kargolarda zorunludur. proof_photo her durumda zorunludur.

deliver ve fail isteğe bağlı location alır: {"lat", "lng", "accuracy"?, "at"?} (lat −90..90, lng −180..180, accuracy ≥ 0). Olay kaydına yazılır; kargo yanıtında son konum last_location olarak döner. Mesafe kontrolü yapılmaz.