Siparişler ve Teklifler

Teklif token'ları

Teklif e-postasındaki token'lı bağlantıyla teklif verilmiş bir teklifi getirme, kabul etme veya reddetme.

Markdown olarak görüntüle

Satıcı bir teklif gönderdiğinde, alıcı düz 48 karakterlik bir kabul token'ı içeren bir e-posta alır. Sunucu tarafında bu token'ın yalnızca SHA-256 hash'i saklanır; dolayısıyla e-postadaki bağlantı tek kimlik bilgisidir ve token'ı yeniden türetmenin bir yolu yoktur. Storefront, bağlantıdaki token'ı alıp bu üç endpoint'i süren bir teklif sayfası barındırır. X-Customer-Token işin içinde değildir: erişim kanıtı kabul token'ının kendisidir.

Bir teklif hem bir storefront sepetinden hem de doğrudan satıcı tarafından yazılarak ortaya çıkabilir (aşağıdaki origin'e bakın) — bu üç endpoint her iki durumu da aynı şekilde sunar. Her endpoint iki özdeş yolda kullanılabilir: /v1/api/sales/quotes/by-token/{token} ve /v1/api/commerce/quotes/by-token/{token}. İkisi de sales modülünün etkin olmasını gerektirir ve bir API anahtarının ya sales:read/sales:write yetenek ailesini ya da eski commerce:read/commerce:write ailesini taşımasını kabul eder — bkz. aşağıdaki Scopes. Bu sayfadaki örnekler sales yolunu kullanır; her biri commerce yolunda da aynı şekilde çalışır.

Token sayfasının davranışı, mağazanın kabul modasına (acceptance_mode, GET yanıtına bakın) bağlıdır: click modunda kabul için ek bir girdi gerekmez; signature modunda kabul için çizilmiş bir imza gerekir. POST .../accept ve POST .../decline, çağıranın IP'siyle anahtarlanarak dakikada 30 istek ile sınırlandırılmıştır.

Üç endpoint de önce tembel (lazy) süre dolumu uygular: teklif offered durumundaysa ve valid_until geçmişse, request işlenmeden önce durumu expired olur. Bu nedenle süre dolumundan sonraki bir okuma "status": "expired" döner; kabul veya ret QUOTE_EXPIRED ile başarısız olur.

Mağazanın hiçbir teklifiyle eşleşmeyen bir token 404 döner.

GET /v1/api/sales/quotes/by-token/{token}

Alias: /v1/api/commerce/quotes/by-token/{token}

Modül ayrımından önce oluşturulmuş entegrasyonlar için kalıcı olarak korunan, özdeş rota, request ve response.

Bu token'a ait teklifi getirir. sales:read (veya commerce:read) gerektirir. Müşteriye dönük teklif serileştirmesini döner (merchant_note, cart_id, customer_id, contact_id, document_type_id, template_id, billing_address_id, work_address_id ve invoice_id kaldırılır; token veya hash'i asla body'de yer almaz). Teklif tutarları money nesnesi değil, minör birim cinsinden düz tamsayılardır.

Parametreler

Path

AdTürZorunluAçıklama
tokenstringevetTeklif e-postasındaki düz 48 karakterlik token.
Request
curl https://api.borkol.com/v1/api/sales/quotes/by-token/{token} \
  -H "Authorization: Bearer {api_key}"
Response
{
  "data": {
    "id": "6e7f8a9b-0c1d-4e2f-a3b4-c5d6e7f8a9b0",
    "number": "Q-17",
    "status": "offered",
    "email": "buyer@corp.example",
    "name": "Bob Buyer",
    "phone": "+31 6 12345678",
    "billing_address": {
      "name": "Corp BV",
      "street": "Main St 1",
      "postal_code": "1000 AA",
      "city": "Amsterdam",
      "country": "NL"
    },
    "shipping_address": null,
    "currency": "EUR",
    "customer_note": "Need delivery before October.",
    "message_to_customer": "Volume price applied.",
    "valid_until": "2026-08-31T23:59:59+00:00",
    "subtotal_amount": 44421,
    "tax_total_amount": 9329,
    "shipping_total_amount": 0,
    "total_amount": 53750,
    "tax_breakdown": [{ "rate": 21, "base": 44421, "tax": 9329 }],
    "converted_order_id": null,
    "submitted_at": "2026-08-14T10:15:30+00:00",
    "offered_at": "2026-08-15T09:00:00+00:00",
    "accepted_at": null,
    "declined_at": null,
    "cancelled_at": null,
    "created_at": "2026-08-14T10:15:30+00:00",
    "updated_at": "2026-08-15T09:00:00+00:00",
    "origin": "merchant",
    "intro": "Thanks for the opportunity to quote this project.",
    "terms": "Prices valid for 30 days. 50% deposit on acceptance.",
    "acceptance_mode": "click",
    "pdf_url": "https://storage.borkol.com/private/....pdf?signature=...",
    "signed_at": null,
    "signed_pdf_url": null,
    "scope_items": [
      {
        "id": "b1c2d3e4-5f6a-4b7c-8d9e-0f1a2b3c4d5e",
        "category": "included",
        "label": "Delivery within 10 business days",
        "included": true,
        "sort_order": 0
      }
    ],
    "media_ids": ["a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"],
    "lines": [
      {
        "id": "9b0c1d2e-3f4a-4b5c-d6e7-f8a9b0c1d2e3",
        "product_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
        "variant_id": null,
        "is_custom": false,
        "title": "Garden Chair",
        "sku": "CHAIR-GRN",
        "quantity": 25,
        "indicative_unit_price": 2250,
        "quoted_unit_price": 2150,
        "unit_price": 2150,
        "tax_class_id": null,
        "attributes": null,
        "sort_order": 0,
        "kind": "product",
        "unit": "pcs",
        "description": null,
        "tax_rate": "21.00"
      }
    ]
  }
}

lines[].kind, product, service, material veya other olabilir. lines[].unit bir birim etiketidir: ürün satırlarında ürünün birimi (adet olarak satılan ürün için pcs, aksi halde örneğin kg veya m2), diğer satırlarda serbest metin (örneğin hour). Birimler kaydedilmeye başlamadan önce yazılmış satırlarda null olabilir. lines[].description, satır düzeyinde isteğe bağlı bir nottur. lines[].tax_rate, gönderim anında anlık görüntüsü alınan ondalık vergi oranıdır (bir string olarak, örneğin "21.00"), tax_class_id'den bağımsızdır. quantity bir sayıdır ve kesirli olabilir (örneğin 1.5), en fazla üç ondalık basamakla; adet olarak satılan bir ürünün satırı her zaman tam sayıdır.

scope_items[], teklife eklenen "dahil olan / olmayan" maddelerini listeler; her biri serbest metin bir category (örneğin included, excluded) ve bir label içerir. intro ve terms, teklif gönderildiği andaki giriş ve şartlar metnidir (mağaza veya teklif bunları hiç ayarlamadıysa null). origin, doğrudan satıcının yazdığı bir teklif için "merchant", sepet tabanlı bir teklif talebi olarak başlayan bir teklif için "storefront"'tur. acceptance_mode, mağazanın güncel ayarıdır ("click" veya "signature") ve POST .../accept'in bir signature gerektirip gerektirmediğini belirler. pdf_url, teklif belgesine 15 dakika geçerli geçici bir URL'dir; bu teklif için henüz bir PDF oluşturulmadıysa null'dır. signed_at, teklif kabul edildiği anda (her iki kabul modunda da) ayarlanır ve o ana kadar null kalır. signed_pdf_url yanıtta her zaman bulunur ancak teklif kabul edilene kadar null'dır; kabul edildikten sonra karşı imzalı belgeye 15 dakika geçerli geçici bir URL'dir.

Hatalar

DurumGövdeNe zaman
404{"message": "..."}Bilinmeyen token veya başka bir mağazaya ait token.
403scope, modül veya askıya alma gövdesiAnahtar sales:read (veya commerce:read) scope'undan yoksun, modül devre dışı veya mağaza askıya alınmış.
429{"message": "Too Many Attempts."}Rate limit aşıldı.

POST /v1/api/sales/quotes/by-token/{token}/accept

Alias: /v1/api/commerce/quotes/by-token/{token}/accept

Modül ayrımından önce oluşturulmuş entegrasyonlar için kalıcı olarak korunan, özdeş rota, request ve response.

Teklif verilmiş bir teklifi kabul eder. Sonrasında ne olacağı, tenant'ın on_accept ayarına bağlıdır: teklif ya pending_payment durumunda bir siparişe (sipariş modu) ya da bir faturaya (fatura modu) dönüşür. sales:write (veya commerce:write) gerektirir. İşlem satır kilitli (row-locked) ve idempotent'tir: sipariş modunda zaten kabul edilmiş bir teklifi kabul etmek aynı siparişi yeniden döner.

Sipariş modunda, siparişi oluşturmadan önce toplamlar yeniden hesaplanır ve gönderim anında dondurulan rakamlarla artık eşleşmiyorlarsa (örneğin satıcının yeniden düzenlemesi veya bir vergi değişikliği sonrasında) OFFER_CHANGED ile reddedilir; ayrıca her ürün satırı için ürünün line handler'ı üzerinden teklif edilen fiyattan stok rezerve edilir (made-to-measure bir satır bileşenlerini düşer) ve yalnızca gerçek bir yetersizlikte OUT_OF_STOCK ile reddedilir. Ürünü artık satılamayan bir satır (geri çekilmiş, capability kapatılmış, yapılandırma artık geçerli değil) yine de stok hareketi olmadan dönüştürülür ve siparişte satıcı için işaretlenir. Kesirli satır miktarları siparişe olduğu gibi aktarılır.

Response, GET .../by-token/{token} ile aynı müşteriye dönük teklif şeklini data içine sarılmış olarak taşır; order_id ve order_number ise data'nın içinde değil, envelope'un kökünde data'nın yanına eklenir. Tenant'ın on_accept ayarı sipariş yerine fatura oluşturduğunda ikisi de null'dır. order_number düz bir tamsayıdır. Siparişin tamamına ihtiyacınız varsa oluşturulan siparişi GET /v1/api/commerce/orders/{order_id} ile getirin.

Parametreler

Path

AdTürZorunluAçıklama
tokenstringevetTeklif e-postasındaki düz token.

Body

AdTürZorunluAçıklama
signaturestring veya nullmağazanın acceptance_mode'u signature olduğunda zorunluÇizilmiş imzanın en fazla 2 MB'lık bir data:image/png;base64, veri URL'si. Çözülen baytlar gerçek bir PNG olarak yeniden doğrulanır.

acceptance_mode click iken gönderilen bir signature yok sayılır. Click modunda kabul, çizilmiş bir imza bloğu olmadan da kabul edilen belge için bir signed_pdf_url üretir — karşı imzalı bir belgeye sahip olmak için imza gerekmez.

Request
curl -X POST https://api.borkol.com/v1/api/sales/quotes/by-token/{token}/accept \
  -H "Authorization: Bearer {api_key}" \
  -H "Content-Type: application/json" \
  -d '{"signature": "data:image/png;base64,iVBORw0KGgo..."}'
Request (JS)
const res = await fetch(
  `https://api.borkol.com/v1/api/sales/quotes/by-token/${token}/accept`,
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${apiKey}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ signature: null }),
  }
);
if (res.status === 409) {
  const err = await res.json();
  // err.code: "QUOTE_NOT_OFFERED" | "OFFER_CHANGED" | "OUT_OF_STOCK" | "ALREADY_CONVERTED"
}
if (res.status === 422) {
  const err = await res.json();
  // err.code: "SIGNATURE_REQUIRED" | "SIGNATURE_INVALID"
}
const { data: quote, order_id, order_number } = await res.json();
Response
{
  "data": {
    "id": "6e7f8a9b-0c1d-4e2f-a3b4-c5d6e7f8a9b0",
    "number": "Q-17",
    "status": "accepted",
    "...": "same customer-facing quote shape as GET /v1/api/sales/quotes/by-token/{token}"
  },
  "order_id": "7f8a9b0c-1d2e-4f3a-b4c5-d6e7f8a9b0c1",
  "order_number": 1043
}

Hatalar

DurumGövdeNe zaman
422{"code": "SIGNATURE_REQUIRED", "message": "A signature is required to accept this quote."}Mağaza çizilmiş bir imza gerektiriyor ve hiçbiri gönderilmedi.
422{"code": "SIGNATURE_INVALID", "message": "..."}İmza bir PNG veri URL'si değil, 2 MB'dan büyük veya gerçek bir PNG'ye çözülmüyor.
409{"code": "QUOTE_NOT_OFFERED", "message": "Quote is not awaiting a response (status: declined)."}Teklif şu anda offered değil (submitted, declined, expired veya cancelled; mesajdaki durum gerçek durumu yansıtır).
409{"code": "OFFER_CHANGED", "message": "This offer has changed since it was sent. Ask the merchant to re-send it."}Yalnızca sipariş modu: yeniden hesaplanan toplamlar dondurulmuş teklif rakamlarıyla artık eşleşmiyor.
409{"code": "OUT_OF_STOCK", "message": "Insufficient stock.", "out_of_stock": [{"product_id": "...", "variant_id": null, "requested": 25, "available": 10}]}Yalnızca sipariş modu: bir veya daha fazla ürün satırı (ya da made-to-measure bir satırın bileşenleri) için yetersiz stok. requested ve available JSON sayılarıdır ve ondalıklı olabilir.
409{"code": "ALREADY_CONVERTED", "message": "This quote was already converted to a document."}Yalnızca sipariş modu ve yalnızca bir yarış durumunda: teklif, arkasında henüz bir sipariş olmadan başka bir request tarafından zaten kabul edildi.
410{"code": "QUOTE_EXPIRED", "message": "This quote is no longer valid."}Teklifin geçerlilik süresi doldu; durumu expired olarak değişti.
404{"message": "..."}Bilinmeyen token.
403scope, modül veya askıya alma gövdesiAnahtar sales:write (veya commerce:write) scope'undan yoksun, modül devre dışı veya mağaza askıya alınmış.
429{"message": "Too Many Attempts."}Çağıranın IP'siyle anahtarlanarak dakikada 30'dan fazla accept veya decline isteği.

POST /v1/api/sales/quotes/by-token/{token}/decline

Alias: /v1/api/commerce/quotes/by-token/{token}/decline

Modül ayrımından önce oluşturulmuş entegrasyonlar için kalıcı olarak korunan, özdeş rota, request ve response.

Teklif verilmiş bir teklifi, satıcının dahili notuna eklenen isteğe bağlı bir nedenle reddeder. sales:write (veya commerce:write) gerektirir. Eşzamanlı yarışan bir kabule karşı satır kilitlidir. Güncellenmiş müşteriye dönük teklifi (durum declined, declined_at ayarlı) 200 durum koduyla döner. Reddedilmiş bir teklif terminal değildir: satıcı yine de yeniden teklif verebilir veya iptal edebilir. Accept ile aynı şekilde sınırlandırılmıştır: çağıranın IP'siyle anahtarlanarak dakikada 30 istek.

Parametreler

Path

AdTürZorunluAçıklama
tokenstringevetTeklif e-postasındaki düz token.

Body

AdTürZorunluAçıklama
reasonstring veya nullhayırAlıcının neden reddettiği. Satıcının dahili notuna eklenir; müşteriye dönük teklifte asla geri gösterilmez.
Request
curl -X POST https://api.borkol.com/v1/api/sales/quotes/by-token/{token}/decline \
  -H "Authorization: Bearer {api_key}" \
  -H "Content-Type: application/json" \
  -d '{"reason": "Found a better price elsewhere."}'
Response
{
  "data": {
    "id": "6e7f8a9b-0c1d-4e2f-a3b4-c5d6e7f8a9b0",
    "number": "Q-17",
    "status": "declined",
    "declined_at": "2026-08-16T08:30:00+00:00",
    "...": "same customer-facing quote shape as GET /v1/api/sales/quotes/by-token/{token}"
  }
}

Hatalar

DurumGövdeNe zaman
409{"code": "QUOTE_NOT_OFFERED", "message": "Quote is not awaiting a response (status: accepted)."}Teklif şu anda offered değil.
422{"message": "...", "errors": {"reason": ["..."]}}reason mevcut ancak string değil.
404{"message": "..."}Bilinmeyen token.
403scope, modül veya askıya alma gövdesiAnahtar sales:write (veya commerce:write) scope'undan yoksun, modül devre dışı veya mağaza askıya alınmış.
429{"message": "Too Many Attempts."}Çağıranın IP'siyle anahtarlanarak dakikada 30'dan fazla accept veya decline isteği.

Scope'lar

GET, her iki yolda da sales:read gerektirir; accept/decline ise sales:write gerektirir. Modül ayrımından önce oluşturulmuş anahtarlar için commerce:read / commerce:write, sales:read / sales:write'ın kabul edildiği her yerde kabul edilir — mevcut bir anahtarın yeniden oluşturulmasına gerek yoktur. Bu yüzeyde sales.view, sales.manage veya sales.settings diye bir scope yoktur: bu adlar mevcut değildir.