BetakuryeGeliştirici

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/webhooks gövdesi: url (https zorunlu), isteğe bağlı event_types ve description.
  • event_types verilmezse delivery.courier_location dışındaki bütün delivery.* olayları gelir. Konum olayını isterseniz listede açıkça yazın.
  • Yanıttaki secret (whsec_…) yalnız bir kez gösterilir. Kaybederseniz POST /v1/webhooks/{id}/rotate ile 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ürNe zaman
delivery.createdTeslimat açıldı
delivery.queuedKurye aranıyor (açılışta ya da teklif düşünce)
delivery.offeredKuryeye teklif gitti
delivery.assignedKurye atandı
delivery.at_pickupKurye restoranda
delivery.picked_upPaket alındı
delivery.en_routeYolda
delivery.arrivedKapıda
delivery.deliveredTeslim edildi
delivery.failedTeslim edilemedi
delivery.cancelledİptal
delivery.returningPaket restorana geri götürülüyor
delivery.returnedPaket restorana bırakıldı
delivery.updatedAdres, hazır olma saati, not ya da ileri tarih değişti
delivery.courier_locationKurye konumu (yalnız açıkça abone olursanız; sipariş başına en çok 30 sn'de bir)
webhook.testDeneme 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ı reddedin
  • webhook-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:
DenemeNe zaman
1hemen
25 sn sonra
35 dk sonra
430 dk sonra
52 sa sonra
65 sa sonra
710 sa sonra
810 sa sonra
  • 410 Gone dö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/webhooks ile 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-id ile tekrarı, data.version ile 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