Storefront Oluşturma

Uçtan uca kılavuz

API anahtarından kataloğa, sepete, checkout'a, siparişlere, B2B tekliflerine, müşteri hesaplarına ve webhook'lara kadar Borkol API üzerinde eksiksiz bir özel storefront oluşturun.

Markdown olarak görüntüle

Bu sayfa, Borkol API'nin tüm storefront yeteneklerini bir storefront'u inşa edeceğiniz sırayla ele alır: katalog gezinme, ürün yapılandırma, sepetler, indirimler, checkout, sipariş takibi, B2B teklif akışı, müşteri hesapları ve webhook'lar. Her adım size çalışan kod verir ve her alanı belgeleyen referans sayfasına bağlantı sunar. Bu sayfa ile referans bölümleri elinizdeyken, başka hiçbir şey okumadan eksiksiz bir storefront oluşturabilirsiniz.

Tüm request'ler için temel URL: https://api.borkol.com. Her storefront route'u /v1/api altında yaşar ve Authorization: Bearer {api_key} olarak gönderilen bir tenant API anahtarıyla doğrulanır. Anahtar mağazayı tanımlar, dolayısıyla herhangi bir tenant header'ına gerek yoktur. Tüm para değerleri, ayrı bir ISO 4217 para birimi alanıyla birlikte minör birim (kuruş/cent) cinsinden tam sayılardır. Tüm kaynak ID'leri UUID string'leridir (sipariş numaraları düz tam sayılardır). Zaman damgaları ISO 8601 biçimindedir.

Herhangi bir koddan önce tek bir kural: API anahtarı sunucu tarafında tutulması gereken bir sırdır. Tüm mağaza için sepet yazma, checkout ve sipariş okuma yetkisi verir. Onu asla bir tarayıcıya veya mobil uygulamaya göndermeyin; storefront arayüzü ile Borkol API arasına ince bir sunucu katmanı koyun. Mimari sayfası bu deseni ayrıntılı olarak ele alır.

Adım 1: API anahtarı ve scope'ları edinin

Satıcı, API anahtarlarını client dashboard'da (app.borkol.com) Settings > API keys altında oluşturur. Oluşturma sırasında bir ad ve bir scope kümesi seçer; düz metin anahtar tam olarak bir kez gösterilir ve tekrar alınamaz. Anahtarların süresi asla dolmaz; iptal edilene kadar çalışırlar.

Scope'lar modül başına okuma/yazma çiftleridir. Bir anahtar yalnızca mağazada etkin olan modüllerin scope'larını taşıyabilir; her zaman verilebilen tenant:read bunun dışındadır. Eksiksiz bir storefront için şu scope'lara sahip bir anahtar isteyin:

ScopeGerekli olduğu yerler
commerce:readÜrünler, kategoriler, configurator şeması ve fiyat teklifleri, sepet okuma, kargo yöntemleri, siparişler, token ile teklif okuma
commerce:writeSepet oluşturma ve değiştirme, checkout, teklif talepleri, teklif kabul/ret
customers:readMüşteri profili ve adres okuma (yalnızca hesap özelliği geliştiriyorsanız)
customers:writeKayıt, giriş, çıkış, profil ve adres yazma (yalnızca hesap özelliği geliştiriyorsanız)

stock:read ve stock:write isteğe bağlıdır: ürün response'ları zaten bir stock_status alanı içerir, bu yüzden çoğu storefront stok modülünü doğrudan hiç çağırmaz. objects:read yalnızca mağaza CMS tarzı içerik için Objects modülünü kullanıyorsa gereklidir.

Gerekli scope'a sahip olmayan bir anahtarla yapılan request 403 {"message": "Invalid ability provided."} ile başarısız olur. Mağazada etkin olmayan bir modüle yapılan request 403 {"error": "Module not enabled: commerce"} ile başarısız olur (bu tek envelope'ta error anahtarına dikkat edin). Tüm ayrıntılar: Kimlik doğrulama ve Hatalar ve rate limit'ler.

Rate limit, o mağazanın tüm anahtarları genelinde, mağaza başına dakikada 120 request'tir. Aşıldığında Retry-After header'ıyla birlikte 429 {"message": "Too Many Attempts."} döner.

Adım 2: Sağlık kontrolü ve kimlik

GET /v1/ping kimlik doğrulaması gerektirmez; bağlantıyı ve temel URL'yi doğrular. GET /v1/api/me kimlik doğrulamalı ilk çağrı için standart yoldur: hiçbir scope gerektirmez, anahtarın ait olduğu mağazayı döner ve anahtarın taşıdığı scope'ları listeler. Bozuk veya iptal edilmiş bir anahtarda erken hata almak için uygulamanız açılırken çağırın.

Request (JS)
const ping = await fetch("https://api.borkol.com/v1/ping");
// { "status": "ok", "version": "v1" }

const me = await fetch("https://api.borkol.com/v1/api/me", {
  headers: { Authorization: `Bearer ${process.env.BORKOL_API_KEY}` },
});
const identity = await me.json();
Response
{
  "tenant": {
    "id": "9c4e7f0a-2b31-4d6e-8f0a-1d2c3b4a5e6f",
    "name": "Four Seasons Garden",
    "slug": "four-seasons-garden"
  },
  "scopes": ["tenant:read", "commerce:read", "commerce:write", "customers:read", "customers:write"]
}

401 {"message": "Unauthenticated."} anahtarın eksik, hatalı biçimli veya iptal edilmiş olduğu anlamına gelir. 403 {"message": "Tenant suspended.", "code": "tenant_suspended"} mağazanın askıya alındığı anlamına gelir; askı süresi boyunca her /v1/api route'u bunu döner, bu yüzden bunu storefront genelinde bir kesinti durumu olarak ele alın.

Adım 3: Kategorileri ve ürünleri çekin, bir katalog oluşturun

Tüm kataloğu iki endpoint yönetir. GET /v1/api/commerce/categories aktif kategori ağacının tamamını iç içe, pozisyona göre sıralı ve sayfalamasız döner. GET /v1/api/commerce/products aktif ürünleri en yeniden eskiye, sayfa başına 20 kayıt olarak döner (sabittir; per_page, sıralama veya arama parametresi yoktur) ve isteğe bağlı olarak filter[category]={slug} ile bir kategori alt ağacına filtrelenebilir. Bilinmeyen veya pasif bir slug 404 değil, boş bir sayfa döner.

Request (JS)
const headers = { Authorization: `Bearer ${apiKey}` };

const cats = await fetch("https://api.borkol.com/v1/api/commerce/categories", { headers });
const { data: tree } = await cats.json();

const prods = await fetch(
  "https://api.borkol.com/v1/api/commerce/products?filter[category]=verandas&page=1",
  { headers }
);
const { data: products, meta } = await prods.json();
// meta.current_page, meta.last_page, meta.total drive your pager
Response (products, abridged)
{
  "data": [
    {
      "id": "9c2f1e6a-8b4d-4f2a-9e11-3d5a7b9c0e21",
      "capability": "standard",
      "capability_active": true,
      "purchasability": "direct",
      "unit": "piece",
      "sku": "TABLE-OAK",
      "slug": "oak-table",
      "name": "Oak Table",
      "description": "Solid oak table.",
      "status": "active",
      "price": { "amount": 54900, "currency": "EUR" },
      "price_display": { "mode": "inc" },
      "categories": [
        { "id": "7a1b3c5d-2e4f-4a6b-8c0d-1e2f3a4b5c6d", "name": "Tables", "slug": "tables", "is_active": true }
      ],
      "variants": [],
      "has_variants": false,
      "price_from": { "amount": 54900, "currency": "EUR" },
      "image": {
        "url": "https://cdn.borkol.com/t/9c2f/table.jpg",
        "variants": { "thumb": "https://wsrv.nl/?url=...&w=160&we=1&output=webp&q=80", "card": "https://wsrv.nl/?url=...&w=480&we=1&output=webp&q=80", "detail": "https://wsrv.nl/?url=...&w=1200&we=1&output=webp&q=80" },
        "alt": "Oak table"
      },
      "stock_status": "in_stock",
      "created_at": "2026-07-02T09:14:33.000000Z",
      "updated_at": "2026-08-01T15:20:11.000000Z"
    }
  ],
  "links": { "first": "...", "last": "...", "prev": null, "next": "..." },
  "meta": { "current_page": 1, "last_page": 3, "per_page": 20, "total": 47 }
}

Görselleştirme notları:

  • Duyarlı (responsive) görseller için image.variants URL'lerini kullanın (thumb 160w, card 480w, detail 1200w); url orijinaldir.
  • price.amount null olabilir: mağazalar fiyatları misafirlerden gizleyebilir. Bu durumda "fiyatlar için giriş yapın" gösterin.
  • stock_status şu değerlerden biridir: in_stock, low_stock, out_of_stock, backorder, untracked ve yalnızca stock modülü kayıtlıysa mevcuttur. untracked (ürünün track_stock: false değeri var veya stoğu sayılmıyor) ürünün asla tükenmeyeceği anlamına gelir.
  • unit adet girişini belirler: piece ürün tam sayı kabul eder, diğer tüm birimler (kg, m2, hour, ...) en fazla üç ondalık basamak. capability ürünü sepete nasıl ekleyeceğinizi söyler (adım 5); capability_active: false ise ürünün şu anda satın alınamayacağı anlamına gelir: satın al düğmesini gizleyin.
  • Storefront'unuzdaki bir ürün veya kategori URL'si artık çözümlenmiyorsa, 404 döndürmeden önce yeni adresini GET /v1/api/redirects?path=... ile sorun ve sitemap.xml dosyasını GET /v1/api/sitemap üzerinden oluşturun. Bkz. Yönlendirmeler ve site haritası.
  • Liste response'u options alanını ve tam images galerisini içermez; ürün detay sayfası için GET /v1/api/commerce/products/{id} çağırın; bu ikisine ek olarak SEO için meta_title/meta_description alanlarını da ekler.
  • has_variants: true olan ürünler, options (adlar ve değerler) ile variants[].option_values eşleştirilerek oluşturulan bir varyant seçici gerektirir.
  • Giriş yapmış bir müşterinin oturum token'ını ürün request'lerinde X-Customer-Token olarak iletirseniz o müşteriye özel fiyatlandırma alırsınız (grup fiyatları, B2B KDV hariç gösterim, quantity_tiers). Token olmadan fiyatlandırma misafir için hesaplanır.

Alan alan referans: Ürünler ve Kategoriler.

Adım 4: Yapılandırılabilir ürünler: şema ve fiyat teklifleri

capability: "made_to_measure" olan ürünler (bir veranda, ölçüye göre üretilen bir tente) sepete düz bir ürün ID'siyle eklenmez. Önce yapılandırma şemasını çekin, ondan bir configurator arayüzü oluşturun, sonra ziyaretçinin seçimini quote endpoint'iyle fiyatlandırın. Her ikisi de yalnızca catalog:read veya commerce:read gerektirir ve mağazada configurator modülü etkin olmalıdır (aksi halde 403 {"error": "Module not enabled: configurator"}).

GET /v1/api/commerce/configurator/products/{id}/schema üç blok döner: dimensions (min/max değerleri ve seçime bağlı kısıt daraltması olan, anahtarlı sayısal girdiler), price_matrices (her birinin hangi seçim koşullarında geçerli olduğunu belirten id, ad ve koşullar) ve option_groups (tekli veya çoklu seçim, zorunluluk bayrağı, fiyat farklı seçenekler, requires/excludes kuralları, görseller ve requires_quote bayrakları). Ürün aktif ve ölçüye göre üretilen (made to measure) bir ürün değilse 404 döner.

POST /v1/api/commerce/configurator/products/{id}/quote somut bir seçimi fiyatlandırır. Saf bir hesaplamadır: hiçbir şey oluşturulmaz, hiçbir şey yazılmaz.

Request (JS)
const schemaRes = await fetch(
  `https://api.borkol.com/v1/api/commerce/configurator/products/${productId}/schema`,
  { headers }
);
const { data: schema } = await schemaRes.json();

const quoteRes = await fetch(
  `https://api.borkol.com/v1/api/commerce/configurator/products/${productId}/quote`,
  {
    method: "POST",
    headers: { ...headers, "Content-Type": "application/json" },
    body: JSON.stringify({
      dimensions: { width: 420, depth: 300 },
      choices: ["c1a2b3c4-d5e6-4f7a-8b9c-0d1e2f3a4b5c", "f6a7b8c9-d0e1-4f2a-b3c4-d5e6f7a8b9c0"],
    }),
  }
);
const { data: quote } = await quoteRes.json();
Response
{
  "data": {
    "total": 561500,
    "currency": "EUR",
    "breakdown": [
      { "label": "Base", "amount": 530000 },
      { "label": "LED lighting: LED set (6)", "amount": 31500 }
    ],
    "matrix": { "id": "d4e5f6a7-b8c9-4d0e-9f1a-2b3c4d5e6f7a", "name": "Glass roof" }
  }
}

Frontend'inizin uygulaması gereken configurator arayüz kuralları (API bunların tümünü quote sırasında yeniden doğrular ve ihlal edilen her alan için bir 422 mesajı döner):

  • Şemadaki her boyut zorunludur ve kendi min/max aralığında bir tam sayı olmalıdır. Bir boyutun constraints listesinde yer alan bir seçeneğin seçilmesi o aralığı daraltır.
  • single grup başına en fazla bir seçenek; her required grup karşılanmalıdır; requires listeleri hedef grup içinde OR, gruplar arasında AND olarak değerlendirilir; excludes kesin çakışmalardır.
  • Görüntülenen fiyatın her zaman güncel olması için her seçim değişikliğinde yeniden quote alın. Debounce uygulayın ama agresif önbelleklemeyin: quote response'ları tam seçime bağlıdır.
  • requires_quote: true olan bir seçenek, bu yapılandırmanın doğrudan checkout yerine B2B teklif talebi akışından (adım 9) geçmesi gerektiği anlamına gelir; quote endpoint'i görüntüleme amacıyla yine de fiyatlandırır.
  • errors.configuration = ["not_priceable"] içeren bir 422, satıcının yapılandırma uzayının bu bölgesini fiyatlandırmadığı anlamına gelir. "Teklif isteyin" şeklinde bir geri dönüş gösterin.

Tam algoritma (matris seçimi, delta geçişleri, yuvarlama) ve tüm 422 mesajları: Configurator nasıl çalışır, Yapılandırma şeması, Fiyat teklifi.

Adım 5: Sepet oluşturun ve satır ekleyin

Sepetler sunucu tarafında tutulan nesnelerdir. POST /v1/api/commerce/carts ile bir tane oluşturun (body yok; commerce:write gerektirir), dönen id değerini ziyaretçi için saklayın (kendi oturumunuza bağlı bir cookie veya local storage) ve satır endpoint'leriyle değiştirin. Sepetler EUR para birimiyle, active durumunda ve her yazma işleminin uzattığı 30 günlük bir geçerlilik süresiyle oluşturulur.

Request (JS)
const cartRes = await fetch("https://api.borkol.com/v1/api/commerce/carts", {
  method: "POST",
  headers,
});
const { data: cart } = await cartRes.json(); // cart.id is the visitor's handle

// Standard product: product_id (+ variant_id when has_variants)
await fetch(`https://api.borkol.com/v1/api/commerce/carts/${cart.id}/lines`, {
  method: "POST",
  headers: { ...headers, "Content-Type": "application/json" },
  body: JSON.stringify({
    product_id: "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
    variant_id: "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
    quantity: 2,
  }),
});

// Made-to-measure product: configuration instead of variant_id
await fetch(`https://api.borkol.com/v1/api/commerce/carts/${cart.id}/lines`, {
  method: "POST",
  headers: { ...headers, "Content-Type": "application/json" },
  body: JSON.stringify({
    product_id: "9c2f1e6a-8b4d-4f2a-9e11-3d5a7b9c0e21",
    quantity: 1,
    configuration: {
      dimensions: { width: 420, depth: 300 },
      choices: ["c1a2b3c4-d5e6-4f7a-8b9c-0d1e2f3a4b5c"],
    },
  }),
});

Her sepet mutasyonu tam sepeti döner (GET /v1/api/commerce/carts/{id} ile aynı şekil): anlık görüntüler ve para nesneleri içeren satırlar, tax_breakdown içeren totals, tenant'ın policy bloğu, requires_quote ile birlikte quote_reasons ve indirim durumu. Sepet arayüzünüzü her response'tan yeniden oluşturun; toplamları asla istemci tarafında hesaplamayın.

Satır endpoint'lerine gömülü kurallar:

  • variant_id, ürünün aktif varyantları olduğunda zorunlu, aksi halde yasaktır. configuration, made_to_measure ürünler için zorunlu, aksi halde yasaktır. 422 mesajları açıktır ("Choose a variant for this product.", "Configure this product before adding it.") ve dallanmak için en üst düzeyde bir code taşır (CAPABILITY_INACTIVE, INVALID_CONFIGURATION, NOT_PURCHASABLE).
  • quantity bir sayıdır: piece ürün için tam sayı, diğer birimler için en fazla üç ondalık basamak (0.75 kg). Sepet, sipariş ve stok adetleri JSON sayısı olarak döner.
  • Aynı ürün + varyant + yapılandırma mevcut satırla birleştirilir: miktar toplanır ve birim fiyat yeni miktar kademesinde yeniden hesaplanır.
  • {"quantity": n} body'si ile PATCH .../lines/{lineId} miktarı değiştirir; DELETE .../lines/{lineId} satırı kaldırır.
  • policy bloğu mağazanın limitlerini taşır: max_lines (varsayılan 100), max_quantity_per_line (varsayılan 100), min_order_value_amount, identity_mode (guest_allowed, login_required, guest_only) ve audience. Limit ihlalleri 422 {"message": "...", "code": "cart_constraint"} döner.
  • PATCH /v1/api/commerce/carts/{id} ile email ve/veya metadata (en fazla 16 skaler anahtar) ayarlanır. E-posta ayarlamak, sepeti bir misafir müşteri kaydına bağlar ve satıcı tarafında terk edilmiş sepet kurtarmayı etkinleştirir.
  • Yazma endpoint'leri yalnızca active durumundaki sepetleri kabul eder; dönüştürülmüş veya birleştirilmiş bir sepet yazma işlemlerinde 404 döner (tek istisna checkout'tur, bkz. adım 7).
  • Bir sepet gerçek bir müşteri hesabına bağlıysa, her sepet request'i o müşterinin X-Customer-Token değerini de taşımalıdır; aksi halde 403 {"message": "This cart belongs to a customer."} ile başarısız olur.

Referans: Sepetler, Sepet satırları, Sepet sahiplenme.

Adım 6: İndirim kodları ve kargo yöntemleri

Bir sepet en fazla bir indirim kodu taşır. {"code": "WELCOME10"} body'si ile PUT /v1/api/commerce/carts/{id}/discount-code kodu olduğu gibi saklar ve kullanılamaz bir kod için asla hata dönmez: bunun yerine her sepet response'u kodu değerlendirir ve sonucu bildirir. discount_code_error alanını kontrol edin (null veya invalid, expired, min_order, not_eligible, usage_limit değerlerinden biri) ve discounts[] dizisini görüntüleyin (her biri {id, name, code, kind, amount}; kind şunlardan biridir: order, products, shipping, bogo). Aynı URL'ye DELETE kodu kaldırır.

Checkout'tan önce, bu sepete özel geçerli fiyatlarıyla kargo seçeneklerini listeleyin:

Request (JS)
const shipRes = await fetch(
  `https://api.borkol.com/v1/api/commerce/carts/${cart.id}/shipping-methods`,
  { headers }
);
const { data: methods } = await shipRes.json();
Response
{
  "data": [
    { "id": "3c4d5e6f-7a8b-4c9d-0e1f-2a3b4c5d6e7f", "name": "Express", "amount": 995, "free_over_amount": null, "effective_amount": 995 },
    { "id": "4d5e6f7a-8b9c-4d0e-1f2a-3b4c5d6e7f8a", "name": "Standard", "amount": 495, "free_over_amount": 5000, "effective_amount": 0 }
  ]
}

effective_amount, bu sepetin gerçekte ödeyeceği tutardır: sepetin ürün toplamı yöntemin free_over_amount değerine ulaştığında 0'a düşer. Bu tutarlar para nesnesi değil, minör birim cinsinden düz tam sayılardır. Referans: İndirim kodları, Kargo yöntemleri.

Adım 7: Checkout ile siparişe dönüştürme

POST /v1/api/commerce/carts/{id}/checkout sepeti pending_payment durumunda bir siparişe dönüştürür. Her satırı yeniden doğrular, her fiyatı yeniden hesaplar, stoğu yeniden kontrol eder, mağazanın sepet politikasını uygular, B2B teklif tetikleyicilerini değerlendirir ve indirim kullanımını kilitler. shipping_method_id, sepette kargolanabilir satırlar varsa (requires_shipping: true olan ürünler) ve mağazanın en az bir aktif kargo yöntemi varsa tam olarak o durumda zorunludur; yalnızca kargo gerektirmeyen ürünler içeren sepetler bunu atlar.

Request (JS)
const res = await fetch(`https://api.borkol.com/v1/api/commerce/carts/${cart.id}/checkout`, {
  method: "POST",
  headers: { ...headers, "Content-Type": "application/json" },
  body: JSON.stringify({
    email: "jane@example.com",
    billing_address: {
      name: "Jane Doe",
      street: "Keizersgracht 1",
      postal_code: "1015 CC",
      city: "Amsterdam",
      country: "NL",
    },
    shipping_address: null,
    shipping_method_id: "4d5e6f7a-8b9c-4d0e-1f2a-3b4c5d6e7f8a",
  }),
});

if (res.status === 201 || res.status === 200) {
  const { data: order } = await res.json(); // order.id, order.number, order.status
} else if (res.status === 409) {
  const err = await res.json();
  if (err.code === "QUOTE_REQUIRED") {
    // route the buyer to the quote request flow (step 9); err.reasons explains why
  } else {
    // "Cart contents changed.": err.cart is the refreshed cart (lines carry
    // "unavailable": true where relevant), err.out_of_stock and
    // err.discount_unavailable list the specifics when applicable.
    // Re-render the cart from err.cart and let the buyer retry.
  }
} else if (res.status === 422) {
  const err = await res.json();
  // err.code (when present): login_required | guest_only | b2b_approval_required
  // | b2c_only | min_order_value | cart_constraint. Otherwise field validation
  // in err.errors ("Cart is empty.", "Choose a valid shipping method.", addresses).
}

Body gereksinimleri: email (zorunlu, geçerli e-posta), billing_address (zorunlu nesne; name, street, postal_code, city zorunlu ve country tam 2 karakterlik ISO kodu), shipping_address (isteğe bağlı, null olabilir, serbest biçimli), shipping_method_id (UUID, yalnızca yukarıdaki koşul altında zorunlu). Response tam siparişi içerir: tam sayı number, para nesnesi toplamlar, tax_breakdown, satır başına vergi, kargo ücreti ve adı.

Checkout idempotent'tir: zaten dönüştürülmüş bir sepet için tekrar POST atmak mevcut siparişi 200 ile döner (ilk başarı 201'dir), dolayısıyla bir ağ yeniden denemesi asla yinelenen sipariş oluşturamaz. Checkout'a geçerli bir X-Customer-Token eşlik ederse sipariş o müşteriye bağlanır ve sipariş geçmişinde görünür; geçersiz bir token kesin bir 401 {"message": "Invalid customer token."} hatasıdır.

Her hata şekli ve tam sipariş alan referansı: Checkout ve Siparişler.

Adım 8: Sipariş durumu ve sipariş geçmişi

Siparişler, tamamen satıcı tarafından yönetilen sabit bir durum makinesinde ilerler: pending_payment durumundan paid veya cancelled durumuna; paid durumundan shipped, cancelled veya refunded durumuna; shipped durumundan completed veya refunded durumuna; completed durumundan refunded durumuna; cancelled ve refunded son durumlardır. Storefront durumu yalnızca okur.

İki okuma yolu vardır:

  • GET /v1/api/commerce/orders/{id}, commerce:read ile bir siparişi UUID üzerinden çeker. Müşteri sahipliği kontrolü yoktur; API anahtarı tek başına yetkilendirir. Storefront sunucunuz, bu çağrıyı proxy'lemeden önce isteği yapanın o siparişi görmeye yetkili olduğunu (örneğin siparişin kendi oturumunun checkout'undan geldiğini) doğrulamalıdır.
  • GET /v1/api/commerce/my/orders güvenli "siparişlerim" sayfasıdır: X-Customer-Token gerektirir ve yalnızca o müşterinin siparişlerini, en yeniden eskiye, sayfalanmış olarak döner (per_page 1 ile 100 arası, varsayılan 25).
Request (JS)
// Confirmation page polling (your server verified ownership)
const orderRes = await fetch(`https://api.borkol.com/v1/api/commerce/orders/${orderId}`, { headers });
const { data: order } = await orderRes.json();
// order.status, order.tracking_carrier, order.tracking_number

// Account order history
const myOrders = await fetch("https://api.borkol.com/v1/api/commerce/my/orders?per_page=25", {
  headers: { ...headers, "X-Customer-Token": customerToken },
});
const { data: orders, meta } = await myOrders.json();

Bir sipariş durumu sayfasını birkaç saniyede bir sorgulamak, dakikada 120 request bütçesine karşı savurganlıktır; sayfa yüklendiğinde ve kullanıcı eyleminden sonra sorgulayın, gerçek zamanlı olması gereken her şeyi webhook'larla (adım 11) yönetin. Referans: Siparişler.

Adım 9: B2B yolu: teklif talebi, token'lı kabul ve ret

Sales modülü etkin ve teklif talepleri açık olan mağazalar bir RFQ (teklif talebi) yolunu zorunlu kılabilir veya sunabilir. Bunu sepet size söyler: requires_quote: true ile birlikte makine tarafından okunabilir quote_reasons (cart_threshold, configurator_choice, customer_group, product_purchasability). Böyle bir sepette checkout 409 {"code": "QUOTE_REQUIRED", "reasons": [...]} ile reddedilir; bu yüzden alıcıyı ödeme adımı yerine bir teklif talebi formuna yönlendirin. purchasability: "quote" olan ürünler ve requires_quote bayraklı configurator seçenekleri, doğrudan ürün sayfasından oraya yönlendirilmelidir.

POST /v1/api/commerce/carts/{id}/quote-request sepeti bir teklif olarak gönderir (durum submitted, Q-17 gibi bir numara) ve sepeti dönüştürür. Geçerli bir X-Customer-Token bunları müşteri profilinden sağlamıyorsa email ve name zorunludur. Mağazada teklif talepleri kapalıysa veya sales modülü devre dışıysa endpoint 404 döner.

Request (JS)
const rfq = await fetch(`https://api.borkol.com/v1/api/commerce/carts/${cart.id}/quote-request`, {
  method: "POST",
  headers: { ...headers, "Content-Type": "application/json" },
  body: JSON.stringify({
    email: "buyer@corp.example",
    name: "Bob Buyer",
    customer_note: "Need delivery before October.",
  }),
});
const { data: quote } = await rfq.json(); // quote.number: "Q-17", quote.status: "submitted"

Ardından satıcı teklifi fiyatlandırır ve bir öneri gönderir. Alıcı, 48 karakterlik düz bir kabul token'ı içeren bir bağlantıyla e-posta alır (sunucu tarafında yalnızca SHA-256 hash'i saklanır). Bu bağlantının işaret ettiği sayfayı storefront'unuz barındırır ve üç token endpoint'ini yönetir; token'ın kendisi kimlik bilgisidir, X-Customer-Token devrede değildir:

  • GET /v1/api/commerce/quotes/by-token/{token} müşteriye dönük teklifi çeker (commerce:read): düz tam sayı tutarlar olarak dondurulmuş toplamlar, valid_until, quoted_unit_price içeren satırlar.
  • POST /v1/api/commerce/quotes/by-token/{token}/accept (commerce:write, body yok) teklifi teklif edilen fiyatlarla bir siparişe dönüştürür ve yalın bir {"order_id": "...", "order_number": 1043} döner. Idempotent'tir: tekrar kabul etmek aynı siparişi döner. Şu 409 kodlarıyla reddedebilir: QUOTE_NOT_OFFERED (şu anda teklif aşamasında değil; valid_until geçtikten sonra tembel süre aşımı dahil), OFFER_CHANGED (yeniden hesaplanan toplamlar dondurulmuş teklifle artık eşleşmiyor) veya OUT_OF_STOCK (bir out_of_stock dizisiyle).
  • POST /v1/api/commerce/quotes/by-token/{token}/decline isteğe bağlı bir {"reason": "..."} ile teklifi reddedilmiş olarak işaretler.

Teklif yaşam döngüsü: submitted durumundan offered durumuna (satıcı gönderir, toplamlar dondurulur), oradan accepted veya declined durumuna; valid_until süresi geçmiş bir teklif bir sonraki erişimde expired durumuna geçer; satıcı cancel edebilir. Referans: Teklif talepleri ve Teklif token'ları.

Adım 10: Müşteri hesapları

Customers modülü kayıt, giriş, profiller ve adres defterleri ekler. API anahtarınızın yanına ikinci bir kimlik bilgisi getirir: register ve login tarafından dönen ve X-Customer-Token header'ı olarak gönderilen müşteri oturum token'ı. API anahtarı storefront sunucunuzu mağazaya karşı doğrular; müşteri token'ı request'in hangi alışverişçi adına yapıldığını belirler.

Request (JS)
const login = await fetch("https://api.borkol.com/v1/api/customers/login", {
  method: "POST",
  headers: { ...headers, "Content-Type": "application/json" },
  body: JSON.stringify({ email: "jane@example.com", password: "s3cret-pass" }),
});
const { data: customer, token: customerToken } = await login.json();

// Attach the anonymous cart to the account (merges any older active cart in)
await fetch(`https://api.borkol.com/v1/api/commerce/carts/${cart.id}/claim`, {
  method: "POST",
  headers: { ...headers, "X-Customer-Token": customerToken },
});

Müşteri token'ının platform genelinde yaptıkları:

  • Ürün ve sepet okumaları müşterinin grubuna göre fiyatlanır (kademeli fiyatlar, B2B KDV hariç gösterim).
  • POST /v1/api/commerce/carts/{id}/claim anonim bir sepeti hesaba bağlar ve müşterinin diğer tüm aktif sepetlerini içine birleştirir.
  • Token'lı checkout siparişi hesaba bağlar; GET /v1/api/commerce/my/orders sayfasını dolduran budur.
  • Profil ve adresler GET/PATCH /v1/api/customers/me ve GET/POST/PATCH/DELETE /v1/api/customers/me/addresses altında yaşar; checkout'u önceden doldurmak için adres defterini kullanın.

Kayıt, misafir kayıtlarını yerinde yükseltir: e-posta zaten bir sepette yakalanmışsa (adım 5), yeni hesap aynı müşteri id'sini ve sipariş geçmişini korur. register ve login, mağaza genelindeki limite ek olarak IP başına dakikada 10 request'lik ekstra bir limit taşır ve her ikisi de customers:write gerektirir. Müşteri token'ını kendi oturumunuzda sunucu tarafında saklayın; bir bearer kimlik bilgisidir.

Referans: Müşteri hesapları, Kayıt ve oturumlar, Profil, Adresler, Parola sıfırlama.

Adım 11: Sipariş olayları için webhook'lar

Polling neyin değiştiğini siz sorduğunuzda söyler; webhook'lar olay olduğu anda söyler. Satıcı, client dashboard'da (Settings > Webhooks) bir URL, abone olunan olay listesi ve oluşturma sırasında bir kez gösterilen whsec_ önekli bir secret ile endpoint'leri yapılandırır. Borkol, endpoint'inize şu envelope ile POST atar:

Delivery body
{
  "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"
  }
}

Abone olunabilir olaylar: order.created ve order.status_changed (her ikisi de yukarıdaki sipariş veri şekliyle) ve stock.adjusted (yeni mutlak miktarla {product_id, variant_id, quantity}). Dashboard'daki "Send test" düğmesi için bir ping olayı mevcuttur.

Her teslimat X-Borkol-Event (olay adı) ve X-Borkol-Signature taşır; ikincisi, endpoint secret'ı ile ham request body'sinin tam hali üzerinden hesaplanan hex bir HMAC-SHA256 imzasıdır. Güvenmeden önce doğrulayın:

Signature verification (Node)
import { createHmac, timingSafeEqual } from "node:crypto";

function verify(rawBody, signatureHeader, secret) {
  const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
  const a = Buffer.from(expected, "hex");
  const b = Buffer.from(signatureHeader, "hex");
  return a.length === b.length && timingSafeEqual(a, b);
}

10 saniye içinde herhangi bir 2xx ile yanıt verin. Bunun dışındaki her şey (veya bir timeout) yeniden denenir: 60 saniye, ardından 300 saniye geri çekilme ile toplam 3 deneme; sonrasında teslimat başarısız olarak işaretlenir. Asıl işi (önbellek geçersiz kılma, e-posta, sipariş karşılama senkronizasyonu) yanıt verdikten sonra, bir kuyrukta yapın. Tipik storefront kullanımları: order.status_changed ile sipariş onay sayfasını "ödendi" durumuna geçirmek ve stock.adjusted ile önbelleğe alınmış bir ürün sayfasını geçersiz kılmak.

Tam payload ve teslimat referansı: Webhook'lar.

Sıradaki adımlar

  • Storefront mimarisi: sunucu tarafı proxy deseni, önbellekleme, idempotency ve ortam kuralları.
  • Hatalar ve rate limit'ler: proxy'nizin eşlemesi gereken eksiksiz hata kataloğu.
  • Stok durumu: ürünlerdeki stock_status yeterli değilse toplu stok sorguları.
  • Objects: satıcı tarafından yönetilen sayfalar için CMS tarzı içerik tipleri ve kayıtları.