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üleWebhook'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:
| Alan | Tür | Açıklama |
|---|---|---|
id | string (UUID) | Endpoint tanımlayıcısı. |
url | string | Hedef URL, en fazla 2048 karakter. Yerel geliştirme host'ları (localhost, 127.0.0.1, *.test, *.localhost) dışında https olmak zorundadır. |
events | array of strings | Bu endpoint'in abone olduğu olaylar; aşağıdaki olay kataloğunun herhangi bir alt kümesi. |
is_active | boolean | Pasif endpoint'lere teslimat yapılmaz. |
secret | string | whsec_ 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:
| Header | Değer |
|---|---|
Content-Type | application/json |
X-Borkol-Event | Olay adı (örneğin order.created). |
X-Borkol-Signature | Ham 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):
| Ad | Tür | Zorunlu | Açıklama |
|---|---|---|---|
event | string | her 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_at | string (ISO 8601) | her zaman | Teslimat kaydının oluşturulma zaman damgası (olayın kaydedildiği an, bu deneme özelinde gönderim anı değil). |
data | object | her zaman | Olaya özgü payload; aşağıdaki kataloğa bakın. |
{
"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ı:
| Ad | Tür | Zorunlu | Açıklama |
|---|---|---|---|
id | string (UUID) | her zaman | Sipariş ID'si. |
number | integer | her zaman | Mağaza başına ardışık sipariş numarası. |
status | string | her zaman | Oluşturma anındaki sipariş durumu; şunlardan biri: pending_payment, paid, shipped, completed, cancelled, refunded. |
total_amount | integer | her zaman | Küçük birim (kuruş/cent) cinsinden sipariş toplamı. |
currency | string | her zaman | ISO 4217 para birimi kodu. |
email | string | her zaman | Alıcının e-posta adresi. |
{
"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_payment | paid, cancelled |
paid | shipped, cancelled, refunded |
shipped | completed, refunded |
completed | refunded |
cancelled | yok (uç durum) |
refunded | yok (uç durum) |
{
"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ı:
| Ad | Tür | Zorunlu | Açıklama |
|---|---|---|---|
product_id | string (UUID) | her zaman | Stoku değişen ürün. |
variant_id | string (UUID) or null | her zaman | Stoku değişen varyant; ürünün varyantı yoksa null. |
quantity | number | her zaman | Yeni 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). |
{
"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):
| Ad | Tür | Zorunlu | Açıklama |
|---|---|---|---|
id | string (UUID) | her zaman | Teklif kimliği. |
number | string | her zaman | Mağazanın numara serisinden teklif numarası, örneğin Q-17. |
status | string | her zaman | Olay anındaki teklif durumu; draft, submitted, offered, accepted, declined, expired, cancelled değerlerinden biri. |
origin | string | her zaman | Sepet tabanlı bir istek olarak başlayan teklif için storefront, merchant'ın doğrudan yazdığı teklif için merchant. |
total_amount | integer | her zaman | En küçük para biriminde (kuruş) teklif toplamı. |
currency | string | her zaman | ISO 4217 para birimi kodu. |
email | string | her zaman | Alıcının e-posta adresi. |
valid_until | string (ISO 8601) veya null | her zaman | Önerinin geçerlilik sonu; önerinin süresi dolmuyorsa null. |
signed_at | string (ISO 8601) veya null | her zaman | Alıcının kabul ettiği an (her iki kabul modunda); o ana kadar null. |
{
"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):
| Ad | Tür | Zorunlu | Açıklama |
|---|---|---|---|
id | string (UUID) | her zaman | Fatura kimliği. |
number | string | her zaman | Mağazanın numara serisinden belge numarası. |
kind | string | her zaman | invoice veya credit_note. |
status | string | her zaman | draft, sent, paid, overdue, cancelled değerlerinden biri. |
total_amount | integer | her zaman | En küçük para biriminde (kuruş) belge toplamı. |
paid_amount | integer | her zaman | Kaydedilen ödemelerin en küçük para birimindeki toplamı. |
open_amount | integer | her zaman | En küçük para biriminde hâlâ açık olan tutar (total_amount eksi paid_amount, asla negatif değil). |
currency | string | her zaman | ISO 4217 para birimi kodu. |
email | string | her zaman | Alıcının e-posta adresi. |
issue_date | string (YYYY-MM-DD) veya null | her zaman | Düzenleme tarihi. |
due_date | string (YYYY-MM-DD) veya null | her zaman | Vade tarihi; alacak dekontunda null. |
{
"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):
| Ad | Tür | Zorunlu | Açıklama |
|---|---|---|---|
id | string (UUID) | her zaman | Kayıt ID'si. |
type_key | string | her zaman | Nesne türü anahtarı, örneğin service. |
slugs | object | her zaman | Dil 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_at | string (ISO 8601) or null | her zaman | Kaydı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.
{
"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ı:
| Ad | Tür | Zorunlu | Açıklama |
|---|---|---|---|
key | string | her zaman | GET /v1/api/forms/{key} içinde kullanılan form anahtarı. |
version | integer | her zaman | Az önce yayınlanan sürüm numarası. |
{
"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ı:
| Ad | Tür | Zorunlu | Açıklama |
|---|---|---|---|
id | string (UUID) | her zaman | Başvuru ID'si. |
number | string | her zaman | Başvuru numarası, örneğin INQ-0042. |
form_key | string | her zaman | Gönderilen formun anahtarı. |
status | string | her zaman | Oluşturulduğu andaki gelen kutusu durumu, her zaman new. Sonraki durumlar (open, done, spam) dashboard'da belirlenir ve olay göndermez. |
locale | string | her zaman | Başvurunun dili: sitenin gönderdiği locale mağaza için etkinse o, değilse mağazanın varsayılan dili. |
name | string veya null | her zaman | name anahtarlı alanın yanıtı veya null. |
email | string veya null | her zaman | Yanıtlanmış ilk email alanının yanıtı veya null. |
created_at | string (ISO 8601) | her zaman | Başvurunun saklandığı zaman. |
{
"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ı:
| Ad | Tür | Zorunlu | Açıklama |
|---|---|---|---|
message | string | her zaman | Her zaman "Borkol webhook test". |
{
"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.
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) ...
});$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:
| Deneme | Ne zaman |
|---|---|
| 1 | Olaydan hemen sonra. |
| 2 | 1. deneme başarısız olduktan 60 saniye sonra. |
| 3 | 2. 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.