Webhook'lar

Webhook'lar

Borkol'un sunucunuza POST ettiği olay bildirimleri; olay kataloğu, teslimat semantiği, HMAC imza doğrulaması ve yeniden deneme davranışıyla birlikte.

Markdown olarak görüntüle

Webhook'lar, platform olaylarını gerçekleştikleri anda Borkol'dan sunucunuza iletir; böylece sürekli sorgulamanız (poll) gerekmez. Bir sipariş oluşturulduğunda, bir siparişin durumu değiştiğinde, bir stok seviyesi ayarlandığında, bir teklif önerildiğinde, kabul edildiğinde, reddedildiğinde veya süresi dolduğunda, bir fatura veya alacak dekontu gönderildiğinde, bir fatura ödendiğinde veya vadesi geçtiğinde, bir içerik kaydı yayınlandığında, bir form yayınlandığında veya bir ziyaretçi bir form gönderdiğinde Borkol, mağazanın o olaya abone olan her aktif endpoint'ine bir HTTP POST gönderir. Her teslimat, endpoint'e özel bir secret ile imzalanır; böylece sunucunuz isteğin gerçekten Borkol'dan geldiğini doğrulayabilir.

Endpoint'leri yapılandırma

Webhook endpoint'leri mağaza başına client dashboard'da (app.borkol.com) Settings > Webhooks altında yapılandırılır. Endpoint yapılandırması herkese açık storefront API'sinin parçası değildir. Her endpoint şunlara sahiptir:

AlanTürAçıklama
idstring (UUID)Endpoint tanımlayıcısı.
urlstringHedef URL, en fazla 2048 karakter. Yerel geliştirme host'ları (localhost, 127.0.0.1, *.test, *.localhost) dışında https olmak zorundadır.
eventsarray of stringsBu endpoint'in abone olduğu olaylar; aşağıdaki olay kataloğunun herhangi bir alt kümesi.
is_activebooleanPasif endpoint'lere teslimat yapılmaz.
secretstringwhsec_ ile başlayıp 40 rastgele alfasayısal karakterle devam eden formatta imzalama secret'ı. Yalnızca oluşturma anında bir kez gösterilir, sonrasında gizlenir; hemen saklayın.

Bir olay, mağazanın o olaya abone olan her aktif endpoint'ine dağıtılır. Dashboard ayrıca, aboneliklerinden bağımsız olarak tek bir endpoint'e ping olayı ileten bir "Send test" işlemi de sunar.

Teslimat formatı

Her teslimat, endpoint URL'ine şu header'larla yapılan bir HTTP POST'tur:

HeaderDeğer
Content-Typeapplication/json
X-Borkol-EventOlay adı (örneğin order.created).
X-Borkol-SignatureHam request body'nin tamamı üzerinden, endpoint'in whsec_ secret'ıyla hesaplanan küçük harfli hex HMAC-SHA256.

Body her zaman şu zarftır (slash'ları escape edilmeden JSON kodlanır):

AdTürZorunluAçıklama
eventstringher zamanŞunlardan biri: order.created, order.status_changed, stock.adjusted, quote.offered, quote.accepted, quote.declined, quote.expired, invoice.sent, invoice.paid, invoice.overdue, credit_note.sent, object_record.published, object_record.updated, object_record.unpublished, object_record.deleted, form.published, form_submission.created, ping.
created_atstring (ISO 8601)her zamanTeslimat kaydının oluşturulma zaman damgası (olayın kaydedildiği an, bu deneme özelinde gönderim anı değil).
dataobjecther zamanOlaya özgü payload; aşağıdaki kataloğa bakın.
Request
{
  "event": "order.created",
  "created_at": "2026-08-14T09:12:44+00:00",
  "data": {
    "id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
    "number": 1042,
    "status": "pending_payment",
    "total_amount": 15900,
    "currency": "EUR",
    "email": "buyer@example.com"
  }
}

Sunucunuz 10 saniye içinde herhangi bir 2xx durumuyla yanıt vermelidir. Response body dikkate alınmaz. 2xx dışı herhangi bir durum, bağlantı hatası veya zaman aşımı başarısız bir deneme sayılır ve yeniden denemeyi tetikler.

Olay kataloğu

Abone olunabilecek olaylar yalnızca bunlardır. Yeniden denemeler aynı zarfı tekrar iletir; işlemeniz idempotent değilse kendi tarafınızda tekilleştirin (event artı created_at artı payload kimliği pratik bir anahtardır).

order.created

Bir sipariş oluşturulduğunda gönderilir. data alanları:

AdTürZorunluAçıklama
idstring (UUID)her zamanSipariş ID'si.
numberintegerher zamanMağaza başına ardışık sipariş numarası.
statusstringher zamanOluşturma anındaki sipariş durumu; şunlardan biri: pending_payment, paid, shipped, completed, cancelled, refunded.
total_amountintegerher zamanKüçük birim (kuruş/cent) cinsinden sipariş toplamı.
currencystringher zamanISO 4217 para birimi kodu.
emailstringher zamanAlıcının e-posta adresi.
Payload
{
  "event": "order.created",
  "created_at": "2026-08-14T09:12:44+00:00",
  "data": {
    "id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
    "number": 1042,
    "status": "pending_payment",
    "total_amount": 15900,
    "currency": "EUR",
    "email": "buyer@example.com"
  }
}

order.status_changed

Bir sipariş yeni bir duruma geçtiğinde gönderilir. data, order.created ile aynı şekle sahiptir; status yeni durumdur.

Sipariş durumları şu durum makinesini izler:

Kaynak durumİzin verilen geçişler
pending_paymentpaid, cancelled
paidshipped, cancelled, refunded
shippedcompleted, refunded
completedrefunded
cancelledyok (uç durum)
refundedyok (uç durum)
Payload
{
  "event": "order.status_changed",
  "created_at": "2026-08-14T10:03:01+00:00",
  "data": {
    "id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
    "number": 1042,
    "status": "paid",
    "total_amount": 15900,
    "currency": "EUR",
    "email": "buyer@example.com"
  }
}

stock.adjusted

Bir stok seviyesi değiştiğinde gönderilir. data alanları:

AdTürZorunluAçıklama
product_idstring (UUID)her zamanStoku değişen ürün.
variant_idstring (UUID) or nullher zamanStoku değişen varyant; ürünün varyantı yoksa null.
quantitynumberher zamanYeni mutlak eldeki miktar (fark değil), ürünün unit biriminde. Ağırlık, uzunluk, alan veya hacimle satılan ürünlerde ondalıklı olabilir (örneğin 47.84).
Payload
{
  "event": "stock.adjusted",
  "created_at": "2026-08-14T11:27:19+00:00",
  "data": {
    "product_id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e",
    "variant_id": null,
    "quantity": 42
  }
}

quote.offered

Bir merchant bir teklif üzerinde bir öneri gönderdiğinde gönderilir (durum offered olur). Aynı öneriyi yeniden göndermek olay göndermez. Payload hiçbir zaman kabul token'ını içermez: alıcının bağlantısı yalnızca e-posta ile gönderilir. data alanları (dört quote.* olayı için aynıdır):

AdTürZorunluAçıklama
idstring (UUID)her zamanTeklif kimliği.
numberstringher zamanMağazanın numara serisinden teklif numarası, örneğin Q-17.
statusstringher zamanOlay anındaki teklif durumu; draft, submitted, offered, accepted, declined, expired, cancelled değerlerinden biri.
originstringher zamanSepet tabanlı bir istek olarak başlayan teklif için storefront, merchant'ın doğrudan yazdığı teklif için merchant.
total_amountintegerher zamanEn küçük para biriminde (kuruş) teklif toplamı.
currencystringher zamanISO 4217 para birimi kodu.
emailstringher zamanAlıcının e-posta adresi.
valid_untilstring (ISO 8601) veya nullher zamanÖnerinin geçerlilik sonu; önerinin süresi dolmuyorsa null.
signed_atstring (ISO 8601) veya nullher zamanAlıcının kabul ettiği an (her iki kabul modunda); o ana kadar null.
Payload
{
  "event": "quote.offered",
  "created_at": "2026-10-24T09:12:44+00:00",
  "data": {
    "id": "6e7f8a9b-0c1d-4e2f-a3b4-c5d6e7f8a9b0",
    "number": "Q-17",
    "status": "offered",
    "origin": "storefront",
    "total_amount": 248900,
    "currency": "EUR",
    "email": "buyer@corp.example",
    "valid_until": "2026-11-07T23:59:59+00:00",
    "signed_at": null
  }
}

quote.accepted

Bir teklif accepted olduğunda gönderilir: alıcı POST .../accept ile kabul eder (tıklama veya imza modu) ya da merchant dashboard'da dönüştürür. signed_at ayarlanır. Mağazanın ayarına göre teklif bir siparişe (bu durumda order.created de gönderilir) veya bir faturaya dönüşmüştür. data, quote.offered ile aynıdır.

quote.declined

Alıcı, önerilen bir teklifi POST .../decline ile reddettiğinde gönderilir. Reddedilen bir teklif nihai değildir: sonraki yeni bir öneri yeniden quote.offered gönderir. data, quote.offered ile aynıdır.

quote.expired

offered durumundaki bir teklif valid_until tarihini geçtiğinde gönderilir: süre dolumu taraması onu bulur ya da o andan sonraki ilk token tabanlı istek durumu değiştirir. Her süre dolumu için bir kez gönderilir. data, quote.offered ile aynıdır; status expired olur.

invoice.sent

Bir merchant bir fatura gönderdiğinde (kind invoice) gönderilir: numara o anda tahsis edilir ve PDF kilitlenir. Yeniden göndermek olay göndermez. data alanları (invoice.sent, invoice.paid, invoice.overdue ve credit_note.sent için aynıdır):

AdTürZorunluAçıklama
idstring (UUID)her zamanFatura kimliği.
numberstringher zamanMağazanın numara serisinden belge numarası.
kindstringher zamaninvoice veya credit_note.
statusstringher zamandraft, sent, paid, overdue, cancelled değerlerinden biri.
total_amountintegerher zamanEn küçük para biriminde (kuruş) belge toplamı.
paid_amountintegerher zamanKaydedilen ödemelerin en küçük para birimindeki toplamı.
open_amountintegerher zamanEn küçük para biriminde hâlâ açık olan tutar (total_amount eksi paid_amount, asla negatif değil).
currencystringher zamanISO 4217 para birimi kodu.
emailstringher zamanAlıcının e-posta adresi.
issue_datestring (YYYY-MM-DD) veya nullher zamanDüzenleme tarihi.
due_datestring (YYYY-MM-DD) veya nullher zamanVade tarihi; alacak dekontunda null.
Payload
{
  "event": "invoice.sent",
  "created_at": "2026-10-24T10:03:01+00:00",
  "data": {
    "id": "0f1e2d3c-4b5a-4968-8776-655443322110",
    "number": "F2026-0042",
    "kind": "invoice",
    "status": "sent",
    "total_amount": 248900,
    "paid_amount": 0,
    "open_amount": 248900,
    "currency": "EUR",
    "email": "buyer@corp.example",
    "issue_date": "2026-10-24",
    "due_date": "2026-11-07"
  }
}

invoice.paid

Bir fatura paid durumuna geçtiğinde bir kez gönderilir: kaydedilen bir ödeme açık tutarı karşılar ya da merchant faturayı ödendi olarak işaretler. Zaten ödenmiş bir faturaya yapılan ek bir ödeme veya ikinci bir işaretleme hiçbir şey göndermez. data, invoice.sent ile aynıdır; status paid, open_amount 0 olur.

invoice.overdue

Vade taraması, due_date tarihi geçmiş sent durumundaki bir faturayı overdue durumuna taşıdığında gönderilir. Her fatura için bir kez gönderilir. data, invoice.sent ile aynıdır; status overdue olur.

credit_note.sent

Bir merchant bir alacak dekontu gönderdiğinde (kind credit_note) gönderilir; alacak dekontunun kendi numarası o anda tahsis edilir. data, invoice.sent ile aynıdır; kind credit_note, due_date null olur.

object_record.published

Yayınlanabilir bir nesne türünün taslak kaydı yayınlandığında gönderilir. Satıcı yayınladığı anda gönderilir, kayıt gelecekteki bir published_at için zamanlanmış olsa bile; zamanlamanın dolduğu sonraki an hiçbir şey göndermez. Yayınlanabilir olmayan türlerin kayıtlarının taslak adımı yoktur, bu yüzden yayınlama onlar için bu olayı asla göndermez; böyle bir türde panelde veya API üzerinden kayıt oluşturmak da hiçbir olay göndermez. İstisna objects:import komutudur: yayınlanmış olarak oluşturduğu her kayıt, yayınlanabilir olmayan bir türde de object_record.published gönderir.

data alanları (dört object_record.* olayı için de aynıdır):

AdTürZorunluAçıklama
idstring (UUID)her zamanKayıt ID'si.
type_keystringher zamanNesne türü anahtarı, örneğin service.
slugsobjecther zamanDil etiketinden kaydın o dildeki adresine, örneğin {"en": "kitchen-fitting"}. Türün adresi yoksa veya kaydın henüz adresi yoksa boş nesne {}. Public API'deki kaydın slugs alanında olduğu gibi yalnızca içerik dili olarak etkin olan dilleri içerir.
published_atstring (ISO 8601) or nullher zamanKaydın yayına gireceği veya girdiği an, public API'deki kayıtla aynı biçimde (2026-10-12T09:30:00.000000Z). Zamanlanmış bir kayıt için gelecekte.

Payload hiçbir zaman kayıt değerlerini içermez. Kaydı GET /v1/api/objects/records/{id} ile veya adresiyle getirin; bunlar yalnızca kayıt görünür olduğunda yanıt verir.

Payload
{
  "event": "object_record.published",
  "created_at": "2026-10-12T09:30:00+00:00",
  "data": {
    "id": "7a8b9c0d-1e2f-4a3b-84c5-d6e7f8a9b0c1",
    "type_key": "service",
    "slugs": { "en": "kitchen-fitting", "nl": "keukenmontage" },
    "published_at": "2026-10-12T09:30:00.000000Z"
  }
}

object_record.updated

Yayınlanmış bir kayıt değiştiğinde gönderilir: değerleri, adresleri veya published_at değeri (yayınlanmış bir kaydı yeniden zamanlama ya da tarihini değiştirme). Hiçbir şeyi değiştirmeden kaydetmek bir şey göndermez, bir taslağı düzenlemek de göndermez. Yayınlanmış bir kayıt yine de gelecek için zamanlanmış olabileceğinden, henüz görünür olmayan bir kayıt için gönderilebilir. data, object_record.published ile aynıdır.

object_record.unpublished

Yayınlanmış bir kayıt taslağa geri alındığında gönderilir. Yalnızca gerçek bir geçişte gönderilir: zaten taslak olan bir kaydı yayından kaldırmak bir şey göndermez. Sayfayı önbelleğinizden veya sitenizden kaldırın. data, object_record.published ile aynıdır.

object_record.deleted

Yayınlanmış bir kayıt silindiğinde gönderilir; toplu silme dahil. Bir taslağı silmek bir şey göndermez, çünkü public API onu hiç sunmadı. slugs, kaydın sahip olduğu adresleri taşır; böylece o sayfaları kaldırabilirsiniz. data, object_record.published ile aynıdır.

form.published

Bir satıcı bir formun yeni bir sürümünü her yayınladığında gönderilir. Önbellekteki bir form tanımını yenilemek için kullanın. Formun durumundan bağımsız olarak gönderilir; bu nedenle henüz taslak olan ve yüklenemeyen bir form için de gelebilir. Bir formu etkinleştirmek veya arşivlemek ve ayarlarını kaydetmek hiçbir şey göndermez. data alanları:

AdTürZorunluAçıklama
keystringher zamanGET /v1/api/forms/{key} içinde kullanılan form anahtarı.
versionintegerher zamanAz önce yayınlanan sürüm numarası.
Payload
{
  "event": "form.published",
  "created_at": "2026-10-13T09:30:00+00:00",
  "data": {
    "key": "quote-request",
    "version": 3
  }
}

form_submission.created

Bir ziyaretçinin başvurusu POST /v1/api/forms/{key}/submissions üzerinden saklandıktan sonra gönderilir. Honeypot durumunda hiçbir şey saklanmaz ve hiçbir şey gönderilmez. Payload asla yanıtları, dosyaları veya ziyaretçinin adresine dair herhangi bir şeyi içermez; satıcı bunları dashboard'daki gelen kutusunda okur ve public API'de bir başvuruyu getirmek için bir endpoint yoktur. data alanları:

AdTürZorunluAçıklama
idstring (UUID)her zamanBaşvuru ID'si.
numberstringher zamanBaşvuru numarası, örneğin INQ-0042.
form_keystringher zamanGönderilen formun anahtarı.
statusstringher zamanOluşturulduğu andaki gelen kutusu durumu, her zaman new. Sonraki durumlar (open, done, spam) dashboard'da belirlenir ve olay göndermez.
localestringher zamanBaşvurunun dili: sitenin gönderdiği locale mağaza için etkinse o, değilse mağazanın varsayılan dili.
namestring veya nullher zamanname anahtarlı alanın yanıtı veya null.
emailstring veya nullher zamanYanıtlanmış ilk email alanının yanıtı veya null.
created_atstring (ISO 8601)her zamanBaşvurunun saklandığı zaman.
Payload
{
  "event": "form_submission.created",
  "created_at": "2026-10-13T09:41:12+00:00",
  "data": {
    "id": "4d5e6f7a-8b9c-4d0e-9f1a-2b3c4d5e6f7a",
    "number": "INQ-0042",
    "form_key": "quote-request",
    "status": "new",
    "locale": "en",
    "name": "Ada Lovelace",
    "email": "ada@example.com",
    "created_at": "2026-10-13T09:41:12+00:00"
  }
}

ping (yalnızca test)

Abone olunamaz. Bir merchant dashboard'da "Send test" düğmesine tıkladığında, endpoint'in aboneliklerinden bağımsız olarak tek bir endpoint'e gönderilir. data alanları:

AdTürZorunluAçıklama
messagestringher zamanHer zaman "Borkol webhook test".
Payload
{
  "event": "ping",
  "created_at": "2026-08-14T12:00:00+00:00",
  "data": {
    "message": "Borkol webhook test"
  }
}

İmzaları doğrulama

Endpoint'in whsec_ secret'ını kullanarak, tam ham request body üzerinden (herhangi bir JSON ayrıştırma veya yeniden serileştirmeden önce) HMAC-SHA256 hesaplayın, sonucu hex olarak kodlayın ve X-Borkol-Signature header'ıyla sabit zamanlı bir karşılaştırma kullanarak karşılaştırın. Farklıysalar isteği reddedin.

Node.js (Express) verification
import crypto from "node:crypto";
import express from "express";

const app = express();

// Capture the raw body; parsing and re-stringifying breaks the signature.
app.post("/webhooks/borkol", express.raw({ type: "application/json" }), (req, res) => {
  const secret = process.env.BORKOL_WEBHOOK_SECRET; // "whsec_..."
  const signature = req.get("X-Borkol-Signature") ?? "";

  const expected = crypto.createHmac("sha256", secret).update(req.body).digest("hex");

  const valid =
    signature.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));

  if (!valid) {
    return res.status(401).send("invalid signature");
  }

  const { event, created_at, data } = JSON.parse(req.body.toString("utf8"));
  // Acknowledge fast; do real work asynchronously (the 10 second timeout is strict).
  res.status(200).end();

  // handle(event, data) ...
});
PHP verification
$secret = getenv('BORKOL_WEBHOOK_SECRET'); // "whsec_..."
$rawBody = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_BORKOL_SIGNATURE'] ?? '';

$expected = hash_hmac('sha256', $rawBody, $secret);

if (! hash_equals($expected, $signature)) {
    http_response_code(401);
    exit;
}

$payload = json_decode($rawBody, true);
http_response_code(200);
// handle($payload['event'], $payload['data']) ...

Pratikte iki kural önemlidir: imzayı ayrıştırılmış JSON'ın yeniden kodlanmış bir kopyasına karşı değil, aldığınız ham byte'lara karşı doğrulayın; ve == yerine sabit zamanlı bir karşılaştırma (crypto.timingSafeEqual, hash_equals) kullanın.

Yeniden denemeler ve teslimat kayıtları

Teslimatlar kuyruğa alınır ve asenkron gönderilir. Her teslimat toplamda en fazla 3 kez denenir:

DenemeNe zaman
1Olaydan hemen sonra.
21. deneme başarısız olduktan 60 saniye sonra.
32. deneme başarısız olduktan 300 saniye sonra.

Bir deneme, 2xx dışı herhangi bir yanıtta, bağlantı hatasında veya zaman aşımında (10 saniye) başarısız olur. Üçüncü başarısız denemeden sonra teslimat failed olarak işaretlenir ve bir daha denenmez. Bir teslimatın durumu pending'den success veya failed'e geçer.

Her teslimat id, event, status (pending, success veya failed), attempts, response_status (sunucunuzun döndürdüğü HTTP durumu, bağlantı hatasında null), last_attempt_at ve created_at ile kaydedilir. Endpoint başına son 25 teslimat client dashboard'da görüntülenebilir; bir alıcıda hata ayıklarken ilk bakılacak yer burasıdır.

Yeniden denemeler var olduğu için alıcınız idempotent olmalıdır: aynı olay birden fazla kez gelebilir (örneğin sunucunuz olayı işlediği halde 2xx yanıtı kaybolduğunda). Hızlıca 2xx yanıtlayın, sonra işleyin; yanıt vermeden önce yavaş iş yapan bir handler zaman aşımına uğrar ve mükerrer teslimatlara neden olur.