Kavramlar
Webhook
Her durum değişikliği kayıtlı adresinize imzalı HTTP POST olarak gelir. İmza Standard Webhooks biçiminde (HMAC-SHA256); hazır kütüphanelerle de doğrulanır.
Adres kaydetmek
POST /v1/webhooksgövdesi:url(https zorunlu), isteğe bağlıevent_typesvedescription.event_typesverilmezsedelivery.courier_locationdışındaki bütündelivery.*olayları gelir. Konum olayını isterseniz listede açıkça yazın.- Yanıttaki
secret(whsec_…) yalnız bir kez gösterilir. KaybedersenizPOST /v1/webhooks/{id}/rotateile yenisini alın. - Kimlik başına en çok 5 aktif adres.
DELETE /v1/webhooks/{id}adresi kapatır ve bekleyen gönderimleri iptal eder.
POST /v1/webhooks · 201
{
"id": "0f8c2d1e-5b6a-4c3d-8e9f-a1b2c3d4e5f6",
"url": "https://pos.ornek.com.tr/betakurye/webhook",
"description": "Sipariş durumları",
"event_types": [
"delivery.created",
"delivery.queued",
"delivery.offered",
"delivery.assigned",
"delivery.at_pickup",
"delivery.picked_up",
"delivery.en_route",
"delivery.arrived",
"delivery.delivered",
"delivery.failed",
"delivery.cancelled",
"delivery.returning",
"delivery.returned",
"delivery.updated"
],
"status": "ACTIVE",
"failing_since": null,
"created_at": "2026-10-11T13:20:55.200Z",
"secret": "whsec_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX="
}Olaylar
| Tür | Ne zaman |
|---|---|
delivery.created | Teslimat açıldı |
delivery.queued | Kurye aranıyor (açılışta ya da teklif düşünce) |
delivery.offered | Kuryeye teklif gitti |
delivery.assigned | Kurye atandı |
delivery.at_pickup | Kurye restoranda |
delivery.picked_up | Paket alındı |
delivery.en_route | Yolda |
delivery.arrived | Kapıda |
delivery.delivered | Teslim edildi |
delivery.failed | Teslim edilemedi |
delivery.cancelled | İptal |
delivery.returning | Paket restorana geri götürülüyor |
delivery.returned | Paket restorana bırakıldı |
delivery.updated | Adres, hazır olma saati, not ya da ileri tarih değişti |
delivery.courier_location | Kurye konumu (yalnız açıkça abone olursanız; sipariş başına en çok 30 sn'de bir) |
webhook.test | Deneme olayı (POST /v1/webhooks/{id}/test); data: null |
Gövde her olayda aynı biçimdedir: data teslimatın o anki tam hâli (GET /v1/deliveries/{id} yanıtıyla aynı).
Örnek: delivery.assigned
{
"id": "b3a8f2c1-6d4e-4f5a-9b8c-7d6e5f4a3b2c",
"type": "delivery.assigned",
"timestamp": "2026-10-11T13:22:04.118Z",
"data": {
"id": "c06218cb-638d-488d-a5d2-e368e9c3bd74",
"external_ref": "SM-48213",
"store_id": "6ef5f915-05c4-4900-9f70-ccf86232d580",
"status": "ASSIGNED",
"version": 3,
"courier": {
"name": "Emre Yılmaz",
"phone_masked": "+90532***4567",
"phone": "+905321114567",
"plate": "35 BTK 35",
"lat": 38.4655,
"lng": 27.096,
"location_at": "2026-10-11T13:22:01.000Z"
},
"dropoff": {
"text": "Şehitler Cad. No:12 D:4 · Eczanenin üstü",
"lat": 38.4701,
"lng": 27.1015,
"source": "pos",
"ref": null,
"candidates": null,
"district": "Karşıyaka",
"neighborhood": "Bostanlı",
"city": "İzmir"
},
"payment": {
"type": "NAKIT",
"amount_due": 380,
"collected_type": null,
"subtotal": 420,
"discount": 40
},
"customer": {
"name": "Ayşe Demir",
"phone": "+905321234567",
"phone_extension": null,
"note": "Zili çalmayın"
},
"items": [
{
"name": "Adana Dürüm",
"quantity": 2,
"total": 360
},
{
"name": "Ayran",
"quantity": 2,
"total": 60
}
],
"scheduled_for": null,
"notes": null,
"origin_channel": "pos",
"package_count": 1,
"prepaid": false,
"test": false,
"fee": null,
"distance_m": 1412,
"road_km": 1.9,
"tracking_url": "https://betakurye.com/t/NwnC4m6V8KVOUJNbXW9CUA",
"eta_pickup_at": "2026-10-11T13:30:49.068Z",
"eta_dropoff_at": "2026-10-11T13:38:23.068Z",
"ready_at": null,
"assigned_at": "2026-10-11T13:22:04.090Z",
"picked_up_at": null,
"delivered_at": null,
"cancelled_at": null,
"returned_at": null,
"delivery_pin": "2870",
"pin_required": true,
"failure_reason": null,
"cancel_reason": null,
"unassigned_alert_at": null,
"created_at": "2026-10-11T13:20:49.044Z",
"updated_at": "2026-10-11T13:20:49.044Z"
}
}İmzayı doğrulama
Her istekte üç başlık gelir:
webhook-id: olay kimliği; aynı olayın yeniden denemelerinde değişmez (tekrar işleme önlemi için kullanın)webhook-timestamp: Unix saniye; 5 dakikadan eski ya da ileri olanı reddedinwebhook-signature:v1,<base64>; boşlukla ayrılmış birden çok imza olabilir, biri tutması yeter
İmzalanan metin {webhook-id}.{webhook-timestamp}.{ham gövde}; anahtar, secret'ın whsec_ öneki atılıp base64 çözülmüş hâli; algoritma HMAC-SHA256.
import { createHmac, timingSafeEqual } from 'node:crypto'
import express from 'express'
const SIR = process.env.BETAKURYE_WEBHOOK_SECRET // whsec_…
const anahtar = Buffer.from(SIR.replace(/^whsec_/, ''), 'base64')
const app = express()
// İmza ham gövde üzerinden: JSON'a çevirip yeniden yazmayın.
app.post('/betakurye/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const id = req.header('webhook-id')
const zaman = req.header('webhook-timestamp')
const imzalar = (req.header('webhook-signature') ?? '').split(' ')
if (!id || !zaman || Math.abs(Date.now() / 1000 - Number(zaman)) > 300) return res.sendStatus(400)
const beklenen = createHmac('sha256', anahtar).update(`${id}.${zaman}.${req.body}`).digest()
const gecerli = imzalar.some((s) => {
const [surum, deger] = s.split(',')
const gelen = Buffer.from(deger ?? '', 'base64')
return surum === 'v1' && gelen.length === beklenen.length && timingSafeEqual(gelen, beklenen)
})
if (!gecerli) return res.sendStatus(401)
const olay = JSON.parse(req.body.toString('utf8'))
// webhook-id aynı olayın yeniden denemelerinde sabittir: daha önce işlediyseniz 200 dönüp geçin.
isle(olay).catch(console.error) // ağır işi kuyruğa alın; hızlıca 2xx dönün
res.sendStatus(200)
})Yanıt ve yeniden deneme
- 2xx dışındaki her yanıt başarısız sayılır; 15 sn içinde yanıt gelmezse ve yönlendirme (3xx) dönerse de (yönlendirme izlenmez). Hızlıca 200 dönün, ağır işi kendi kuyruğunuza alın.
- Başarısız olay en çok 8 kez gönderilir:
| Deneme | Ne zaman |
|---|---|
| 1 | hemen |
| 2 | 5 sn sonra |
| 3 | 5 dk sonra |
| 4 | 30 dk sonra |
| 5 | 2 sa sonra |
| 6 | 5 sa sonra |
| 7 | 10 sa sonra |
| 8 | 10 sa sonra |
410 Gonedönerseniz adres hemen kapatılır (artık istemiyorum demek).- Bir adres 5 gün aralıksız başarısız olursa kapatılır; yeniden
POST /v1/webhooksile açarsınız. Kaçırdıklarınızı imleçli listeden toplayın (GET /v1/deliveries?since=…). - Olaylar sırasız ve tekrarlı gelebilir:
webhook-idile tekrarı,data.versionile sırayı yönetin.
Doğrulamanızı denemek
curl
curl -X POST https://betakurye.com/v1/webhooks/<id>/test -H "Authorization: Bearer $TOKEN"
# 202 {"event_id":"…"} — birkaç saniye içinde adresinize "webhook.test" olayı gelir