Помощ Каса Собствен сайт
За разработчици и за търговци с ръчно направен сайт

Интеграция за собствен сайт

Ако сайтът ти не е Shopify, WooCommerce или CloudCart — а просто HTML страници, които някой ти е направил — Sendaro пак работи. Слагаш един ред и казваш на кой бутон да се закача. Тази страница е пълната референция: всеки атрибут, всяко събитие, връзката сървър-към-сървър (webhooks и подписана количка), тестовият режим, точните мерни единици и капаните, в които се пада най-често.

Съдържание
  1. Едно поставяне — какво прави
  2. 1. Минимална страница
  3. 2. Кой бутон отваря касата
  4. 3. Откъде идва продуктът
  5. ⚠ Мерни единици (цена и тегло)
  6. 4. Пълна референция
  7. 5. Събития към твоята страница
  8. 6. Чистене на количката
  9. 7. Thank-you страница
  10. 8. Записване на поръчката при теб
  11. 9. Готов CSP блок
  12. 10. Чести грешки
  13. 11. Бърза проверка преди пускане
  14. 12. Webhooks — Sendaro звъни на теб
  15. 13. Подписана количка (цените от твоя сървър)
  16. 14. Тестов режим

Едно поставяне — какво прави

Редът със скрипта прави четири неща, без да пипа нищо друго по сайта ти:

Нищо не е задължително освен два неща: редът със скрипта (с твоето data-store) и по един атрибут на бутоните ти. Всичко останало на тази страница е за когато искаш повече.

1. Минимална страница, която работи

Копирай това в празен файл, смени ТВОЕТО-ID с ID-то на магазина си от дашборда и отвори файла в браузър. Кликът върху „Поръчай“ отваря касата.

html
<!doctype html>
<html lang="bg">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Моят магазин</title>
</head>
<body>

  <h1>Кожен портфейл</h1>
  <p>129,90 €</p>

  <button type="button"
          data-vsx-checkout
          data-vsx-title="Кожен портфейл"
          data-vsx-price="129.90"
          data-vsx-weight="0.25"
          data-vsx-sku="WALLET-BRN">
    Поръчай
  </button>

  <!-- ЕДИНСТВЕНИЯТ ред, който Sendaro иска. Сложи го точно преди </body>. -->
  <script src="https://app.sendaro.bg/checkout.js" data-store="ТВОЕТО-ID" defer></script>
</body>
</html>
Къде е ID-то на магазина? В дашборда, на екрана за свързване на магазин — там е и готовият ред за поставяне. Копирай само ID-то, без адреси и без параметри след &.

2. Кой бутон отваря касата

На собствен сайт правилото е просто: Sendaro отваря касата само на бутоните, които сам си маркирал. Маркировката е един празен атрибут — без стойност, без стил, без нищо друго.

html
<!-- 1) Обикновен бутон -->
<button type="button" data-vsx-checkout>Поръчай</button>

<!-- 2) Линк (Sendaro спира навигацията сам) -->
<a href="#" data-vsx-checkout>Купи сега</a>

<!-- 3) Дълго име, ако „vsx" ти изглежда странно — прави същото -->
<button type="button" data-sendaro-checkout>Поръчай</button>

<!-- 4) „Не пипай нищо тук" — важи за елемента И за всичко вътре в него -->
<div data-sendaro-ignore>
  <button type="button" id="checkout">Вътрешен бутон, който Sendaro пропуска</button>
</div>

3. Откъде идва продуктът

Касата трябва да знае какво поръчва купувачът. Има три начина да ѝ кажеш — избери един. Ако сложиш повече от един, Sendaro ги пробва точно в този ред:

Има и четвърти, най-строг начин: количка, подписана от твоя сървър (data-sendaro-cart). Тогава цените изобщо не минават през браузъра и никой не може да ги пипне. Той бие и трите по-долу — виж раздел 13.

А. От дашборда — най-сигурното

Ти пазиш продуктите си в Sendaro; страницата подава само SKU. Цената идва от дашборда, значи никой не може да я подправи от браузъра.

html
<!-- Заглавието, цената, снимката и теглото идват от продуктовия дашборд на Sendaro. -->
<!-- Страницата НЕ може да ги подправи. Изисква data-store на скрипта.               -->
<button type="button" data-vsx-checkout data-vsx-product="WALLET-BRN">
  Поръчай
</button>

<!-- Няколко реда наведнъж: до 10 SKU / външни ID-та, разделени със запетая -->
<button type="button" data-vsx-checkout data-vsx-product="WALLET-BRN,BELT-40,BOX-01">
  Поръчай комплекта
</button>
Резервен вариант вграден: ако продуктът не се намери в дашборда, Sendaro пада към инлайн атрибутите на същия бутон (ако си ги сложил). Слагането и на двете е напълно нормално.

Б. Инлайн, направо на бутона — най-бързото

Нула настройка. Заглавието и цената са задължителни; останалите са по желание.

html
<button type="button"
        data-vsx-checkout
        data-vsx-title="Кожен портфейл"
        data-vsx-variant="Кафяв"
        data-vsx-sku="WALLET-BRN"
        data-vsx-price="129.90"      
        data-vsx-weight="0.25"       
        data-vsx-image="https://tvoiat-sait.bg/img/portfeil.jpg"
        data-vsx-qty="1">
  Поръчай
</button>

В. Количка с няколко реда — window.VSX_PRODUCT

Когато имаш истинска количка (сесия, localStorage) и клиентът поръчва няколко неща наведнъж.

html
<button type="button" data-vsx-checkout id="poracha">Поръчай количката</button>

<script>
// Твоята количка (сесия, localStorage, каквото ползваш) → формата, която Sendaro разбира.
// Sendaro чете window.VSX_PRODUCT В МОМЕНТА НА КЛИКА, затова я обновявай при всяка промяна.
function sendaroSyncCart(cart) {
  window.VSX_PRODUCT = cart.lines.map(function (l) {
    return {
      sku:     l.sku,
      title:   l.title,
      variant: l.variant || "",
      image:   l.image || "",
      price:   (l.priceCents / 100).toFixed(2),  // ← ЕВРО с точка, НЕ центове
      weight:  l.weightGrams / 1000,             // ← КИЛОГРАМИ, НЕ грамове
      qty:     l.qty                             // 1..99
    };
  });
}

sendaroSyncCart(myCart);            // при зареждане
// …и пак след „добави", „премахни", „смени количеството"
</script>

<script src="https://app.sendaro.bg/checkout.js" data-store="ТВОЕТО-ID" defer></script>
Едно правило: Sendaro чете window.VSX_PRODUCT в момента на клика, не при зареждане. Обнови я при всяка промяна в количката, иначе купувачът ще поръча старото съдържание.

⚠ Мерни единици — тук се греши най-много

Цената е в ЕВРО, с десетична точка. НЕ в центове.
data-vsx-price="129.90" → 129,90 €
data-vsx-price="12990"12 990,00 € — сто пъти повече, и наложеният платеж тръгва такъв.
ПолеМерна единицаПримерКакво значи
priceевро, десетична точка"129.90"129,90 €. Приема се и запетая: "129,90". Интервалите се махат. Нула или отрицателна стойност → артикулът се пропуска.
weightкилограми, за един брой"0.25"250 грама. Sendaro умножава по количеството. Празно → теглото по подразбиране на магазина ти.
qtyцели бройки"2"Ограничава се до 1–99.
totalEur
в събитието
евро, число141.64Обща сума на поръчката, вече с доставката. Число, не низ.

Магазинът работи в евро. Ако твоята система пази цени в центове/стотинки, дели на 100 преди да ги подадеш; ако пази теглата в грамове, дели на 1000. Две деления, които спестяват най-скъпата грешка в цялата интеграция.

4. Пълна референция

Атрибути на <script> тага

АтрибутНужен ли еКакво прави
srcзадължителноВинаги https://app.sendaro.bg/checkout.js. Не си го копирай при теб — виж „Чести грешки".
data-storeзадължителноID на магазина ти от дашборда. Без него касата се отваря, но БЕЗ твоята тема, твоите куриери и цени, а поръчката не влиза в хъба. Работи и като ?store=… в адреса на скрипта.
data-cart-clear-urlпо изборАдрес на ТВОЯ сайт. Sendaro прави POST там веднага след успешна поръчка, със същите бисквитки (same-origin) → изпразваш количката си.
data-vsx-success-urlпо изборТвоята thank-you страница. Sendaro пренасочва там с ?order=… (номерът на поръчката). Псевдоним: data-success-url.
data-buyпо изборТекст на плаващ бутон „Поръчай", който Sendaro сам рисува върху страницата. Работи само ако на скрипта има и продукт (data-product или data-title + data-price).
data-positionпо изборКъде стои плаващият бутон: br (по подразбиране, долу вдясно), bl, tr, tl.
data-product
data-title data-price
data-image data-weight
data-variant data-sku
data-qty data-qty-lock
по изборПродуктът за плаващия бутон. Sendaro ги пренася върху него като data-vsx-* — значението им е същото като в таблицата по-долу.
?sendaro_debug=1в адресаНе е атрибут, а параметър в адреса на страницата. Включва подробен лог в конзолата на браузъра: коя тема е заредена, разпознат ли е магазинът, откъде е разчетена количката и с колко реда. Без него касата не пише нищо излишно. Ползвай го, когато цената или продуктът излизат различни от очакваното.
data-testсамо при пробаСтойност "1"тестов режим: поръчката минава целия път, но куриер не се вика и купувачът не получава SMS. Виж раздел 14. Работи и като ?sendaro_test=1 в адреса на страницата. Махни го, преди да пуснеш сайта на живо.
data-langпо изборЕзик на видимия текст в касата: bg или en. Без него Sendaro следва &lt;html lang&gt; на страницата ти; всичко различно от en е български.
data-consentпо изборПикселите чакат съгласие по подразбиране. Meta/Google/Klaviyo и сигналът за изоставени кошници тръгват едва когато твоят cookie банер извика Sendaro.consent({ marketing: true }). Ако банерът ти се изпълнява ПРЕДИ касата (типично за CMP в &lt;head&gt;), сложи window.SENDARO_CONSENT = { marketing: true } — четем и него. Имаш друго основание и не искаш да чакаме? data-consent="off" (или window.SENDARO_CONSENT_NOT_REQUIRED = true). Стойността "required" продължава да работи и значи същото като подразбирането.
nonceсамо при строг CSPАко сайтът ти работи с 'nonce-…', сложи същия nonce и на този таг. Sendaro го пренася върху всеки стил и скрипт, който създава → не ти трябва 'unsafe-inline'.
data-proxyсамо ShopifyПодпътят на App Proxy. По подразбиране sendaro. На собствен сайт не се пипа.

Атрибути на бутона

Атрибут на бутонаСтойностКакво прави
data-vsx-checkoutпразенТози бутон отваря касата на Sendaro. Слага се на всеки бутон „Купи / Поръчай / Към плащане".
data-sendaro-checkoutпразенТочно същото — по-дългото име, ако предпочиташ него.
data-sendaro-cartподписан токенКоличка, подписана от твоя сървър (раздел 13). Бие всички останали източници: цените се четат от токена, не от страницата. Псевдоним: data-vsx-cart.
data-vsx-productSKU или външно IDПродуктът се тегли от твоя продуктов дашборд (заглавие, цена, снимка, тегло). До 10, разделени със запетая. Изисква data-store. Страницата не може да подправи цената.
data-vsx-titleтекстЗаглавие на артикула. Задължително за инлайн продукт.
data-vsx-priceчисло в евро129.90 = 129,90 €. Приема се и запетая (129,90). Интервалите се махат. Задължително за инлайн продукт. Трябва да е по-голямо от нула.
data-vsx-weightчисло в килограмиТегло на ЕДИН брой. 0.25 = 250 грама. Sendaro сам умножава по количеството. Празно → магазинът ползва теглото по подразбиране.
data-vsx-imageадрес на снимкаПоказва се в списъка с артикули в касата.
data-vsx-variantтекстВариант („Кафяв", „XL", „42") — влиза в името на артикула в поръчката.
data-vsx-skuтекстТвоят идентификатор на артикула. Влиза в поръчката.
data-vsx-qtyцяло числоКоличество. Ограничава се до 1–99. По подразбиране 1.
data-vsx-qty-lock"1"Купувачът НЕ може да променя количеството в касата.
data-sendaro-ignoreпразенSendaro не пипа този елемент, нито каквото и да е вътре в него. Слага се на бутони и контейнери, които не са „поръчай".
data-sendaro-hideпразенОбратната посока: елементът се скрива автоматично, щом касата на Sendaro е на сайта. За стар текст, който вече не важи („ще ви потърсим по телефона, за да потвърдим" и подобни) — маркираш го и той изчезва сам, без да триеш код. Махнеш ли скрипта, текстът просто се показва отново.
data-sendaro-successпразенСлага се на контейнер на thank-you страницата (тази от data-vsx-success-url). Пристигне ли купувач с ?order=… в адреса, Sendaro сам изписва вътре „Благодарим! Поръчка №… е приета" на езика на касата — не пишеш и не поддържаш свой текст. Без ?order= не пипа нищо: каквото е в контейнера, остава резервен текст за директно отваряне.
data-vsx-ordersпразенОтваря панела „Моите поръчки" (клиентът влиза с код на имейл). Псевдоним: data-sendaro-orders.

Глобални променливи

Глобална променливаСтойностКакво прави
window.VSX_PRODUCTобект или масив от обектиКоличката, когато няма как да сложиш атрибути по бутона. Полета: title price weight qty image variant sku id. Същите мерни единици: цената в евро, теглото в кг. Чете се в момента на клика.
window.SENDARO_PRODUCTсъщотоПсевдоним на горното.
window.VSX_STOREID на магазинаРезерва за data-store, ако системата ти не дава да сложиш атрибут на скрипта. Задава се преди реда с checkout.js.
window.SENDARO_STOREсъщотоПсевдоним на горното.
window.SENDARO_CART_URLадресИстинският адрес на твоята количка. Ако купувачът затвори касата, без да поръча, се връща там. Без него Sendaro пробва /cart/.
window.SENDARO_NO_TRACKtrueСпира сигнала за изоставени кошници (напр. докато клиентът не е приел бисквитките ти). Всичко останало работи както обикновено.

5. Събития към твоята страница

Sendaro съобщава на страницата ти какво се случва. Събитията се пускат върху document и се качват нагоре, така че можеш да ги слушаш и на window. Никога не хвърлят грешка към твоя код.

СъбитиеКогаe.detail
sendaro:readyСкриптът се е зареден и бутоните са закачени.{ store } — ID на магазина или null.
sendaro:openКасата се отвори пред купувача.{ store }
sendaro:orderПоръчката е приета от Sendaro.{ orderNumber, totalEur, waybill } — номерът е низ (напр. "#1410"), сумата е число в евро, товарителницата е низ. Всяко от трите може да е null.
html (пейстни го в страницата си)
<script>
// Виджетът е зареден и бутоните са закачени.
document.addEventListener("sendaro:ready", function (e) {
  console.log("Sendaro е готов, магазин:", e.detail.store);
});

// Купувачът отвори касата (добро място за „InitiateCheckout" в собствената ти статистика).
document.addEventListener("sendaro:open", function (e) {
  console.log("касата се отвори", e.detail.store);
});

// Поръчката е приета от Sendaro.
document.addEventListener("sendaro:order", function (e) {
  var d = e.detail || {};
  console.log(d.orderNumber);  // "#1410"          низ или null
  console.log(d.totalEur);     // 141.64           число (евро) или null
  console.log(d.waybill);      // "1055183955219"  низ или null
});
</script>

6. Чистене на количката

След успешна поръчка твоята количка трябва да се изпразни — иначе купувачът се връща и вижда същите артикули. Дай на Sendaro адрес и той ще го извика вместо теб.

html
<script src="https://app.sendaro.bg/checkout.js"
        data-store="ТВОЕТО-ID"
        data-cart-clear-url="/api/cart-clear.php"
        defer></script>
php
<?php
// /api/cart-clear.php — Sendaro прави POST тук веднага след успешна поръчка.
// Адресът е ОТНОСИТЕЛЕН → същият домейн → сесийната бисквитка пътува с заявката.
session_start();
$_SESSION["cart"] = [];
header("Content-Type: application/json; charset=utf-8");
echo json_encode(["ok" => true]);
Заявката е POST, без тяло, с твоите бисквитки (same-origin). Затова адресът трябва да е на твоя домейн — най-добре относителен. Sendaro не чака отговор: ако нещо се обърка при теб, поръчката пак минава.

7. Thank-you страница

По подразбиране купувачът вижда екрана „поръчката е приета“ вътре в касата. Ако искаш своя страница — кажи адреса ѝ.

html
<script src="https://app.sendaro.bg/checkout.js"
        data-store="ТВОЕТО-ID"
        data-cart-clear-url="/api/cart-clear.php"
        data-vsx-success-url="/blagodarim.php"
        defer></script>
php
<?php
// /blagodarim.php — Sendaro отваря този адрес като /blagodarim.php?order=%231410
// ⚠ Стойността идва от адресната лента. Показвай я ЕКРАНИРАНА и никога не я приемай
//   като доказателство за платена поръчка.
$order = isset($_GET["order"]) ? htmlspecialchars($_GET["order"], ENT_QUOTES, "UTF-8") : "";
?>
<h1>Благодарим!</h1>
<?php if ($order !== ""): ?>
  <p>Поръчка <strong><?= $order ?></strong> е приета. Ще получиш SMS с номера на пратката.</p>
<?php else: ?>
  <p>Поръчката е приета.</p>
<?php endif; ?>
Кога точно става пренасочването: когато купувачът затвори екрана „поръчката е приета“, не в секундата на поръчката. Това е нарочно — човекът първо вижда номера си. Без data-vsx-success-url Sendaro отива на началната страница (ако адресът съдържа /cart или /checkout) или просто презарежда текущата.

8. Записване на поръчката при теб

Поръчката вече е в Sendaro — товарителницата е направена, SMS-ът тръгва, куриерът я вижда. Този раздел е само ако искаш копие и в собствената си база (за твоя списък „моите поръчки“, за статистика, за счетоводството ти).

html (пейстни го в страницата си)
<script>
document.addEventListener("sendaro:order", function (e) {
  var d = e.detail || {};
  fetch("/api/sendaro-order.php", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    credentials: "same-origin",           // сесийната ти бисквитка пътува
    keepalive: true,                      // заявката оцелява при бърз redirect
    body: JSON.stringify({
      orderNumber: d.orderNumber,
      totalEur:    d.totalEur,
      waybill:     d.waybill
    })
  }).catch(function () {});               // никога не чупи касата
});
</script>
php
<?php
// /api/sendaro-order.php — записва номера на поръчката при теб и изпразва количката.
header("Content-Type: application/json; charset=utf-8");
session_start();

$raw  = file_get_contents("php://input");
$data = json_decode($raw, true);

if (!is_array($data) || empty($data["orderNumber"])) {
    http_response_code(400);
    echo json_encode(["ok" => false, "error" => "няма номер на поръчка"]);
    exit;
}

$order = [
    "number"  => substr((string) $data["orderNumber"], 0, 40),
    "total"   => isset($data["totalEur"]) ? (float) $data["totalEur"] : null,
    "waybill" => isset($data["waybill"]) ? substr((string) $data["waybill"], 0, 40) : null,
    "items"   => isset($_SESSION["cart"]) ? $_SESSION["cart"] : [],
    "created" => date("c"),
];

file_put_contents(
    __DIR__ . "/../data/orders.jsonl",
    json_encode($order, JSON_UNESCAPED_UNICODE) . "\n",
    FILE_APPEND | LOCK_EX
);

$_SESSION["cart"] = [];   // количката е поръчана
echo json_encode(["ok" => true]);
Бъди честен със себе си за този сигнал. Той идва от браузъра на купувача — значи всеки може да извика адреса ти с измислен номер. Ползвай го за удобство (изпразване на количка, вътрешна статистика, показване на потвърждение). Истината за поръчките е в хъба на Sendaro — не пускай стока и не отчитай пари само въз основа на този запис. Трябва ли ти потвърждение, на което да стъпят склад и фактури, ползвай webhook-ите (раздел 12): те идват от нашия сървър към твоя и носят подпис.

9. Готов CSP блок

Ако сайтът ти има Content-Security-Policy (а ако няма — този раздел не ти трябва), ето кое трябва да е разрешено. Всичко на касата се зарежда от един чужд домейн — този на Sendaro.

http header
Content-Security-Policy:
  default-src 'self';
  script-src  'self' https://app.sendaro.bg;
  style-src   'self' 'unsafe-inline' https://app.sendaro.bg;
  img-src     'self' data: https://app.sendaro.bg;
  connect-src 'self' https://app.sendaro.bg;
  font-src    'self' data:;
  form-action 'self';
  base-uri    'self';
ДомейнКога се зареждаЗа какво
app.sendaro.bgвинагиСамият скрипт, стиловете на картата, знамената за телефонния код, логата на куриерите, заявките към Sendaro.
connect.facebook.net
www.googletagmanager.com
static.klaviyo.com
само ако си въвел съответния пиксел в дашбордаМаркетинг пиксели. Без въведен пиксел не се зарежда нищо от тях.
http header
# ДОБАВИ САМО РЕДОВЕТЕ ЗА ПИКСЕЛА, КОЙТО НАИСТИНА СИ ВКЛЮЧИЛ В ДАШБОРДА.
# Изключен пиксел = нула заявки → не отваряй домейни „за всеки случай".

# Meta / Facebook пиксел
  script-src  … https://connect.facebook.net;
  img-src     … https://www.facebook.com;
  connect-src … https://www.facebook.com;

# Google Ads / GA4 (gtag)
  script-src  … https://www.googletagmanager.com;
  img-src     … https://www.google.com https://googleads.g.doubleclick.net;
  connect-src … https://www.google-analytics.com https://*.analytics.google.com https://*.googletagmanager.com;

# Klaviyo
  script-src  … https://static.klaviyo.com;
  connect-src … https://a.klaviyo.com;
'unsafe-inline' в style-src е нужен, защото касата слага стиловете си като вграден блок и ползва inline style атрибути. data: в img-src е за малките иконки в самата каса.
Не искаш да отваряш 'unsafe-inline'? Не се налага. Сложи своя nonce на реда със скрипта — Sendaro го пренася върху всеки стил и скрипт, който създава в страницата ти.
html
<!-- Строг CSP със стойност вместо 'unsafe-inline': -->
<!--   style-src 'self' 'nonce-r4nd0m'; script-src 'self' 'nonce-r4nd0m' https://app.sendaro.bg; -->

<script src="https://app.sendaro.bg/checkout.js"
        data-store="ТВОЕТО-ID"
        nonce="r4nd0m"
        defer></script>

<!-- Стойността на nonce трябва да е НОВА при всяко зареждане на страницата -->
<!-- и да съвпада с тази в хедъра. Сървърът ти я генерира.                 -->
http header
# Бутонът „Локализирай ме" (най-близък офис) ползва геолокацията на браузъра.
# Ако сайтът ти забранява геолокация, бутонът просто няма да работи.
Permissions-Policy: geolocation=(self)

10. Чести грешки

Всяка от тези е била истински случай при истинска интеграция. Първите две струват пари.

Цената е сложена в центове

Слагаш data-vsx-price="12990", защото системата ти пази цените в центове. Sendaro чете това като 12 990 € — 100 пъти повече. Купувачът вижда абсурдна сума, а наложеният платеж тръгва грешен.

Поправката: Дели на 100 преди да я подадеш: data-vsx-price="129.90". Същото важи за теглото — data-vsx-weight="750" означава 750 килограма, не 750 грама. Правилното е 0.75.

Липсва data-store

Касата се отваря и изглежда наред, затова грешката се вижда чак когато търговецът каже „поръчах, а в дашборда няма нищо". Без data-store няма твоята тема, няма твоите куриери и цени, data-vsx-product мълчи, а поръчката не влиза в хъба ти.

Поправката: Сложи data-store="ТВОЕТО-ID" на реда със скрипта (или window.VSX_STORE преди него). Копирай само ID-то от дашборда — не целия адрес и не ред с параметри след &.

Бутонът не отваря касата

Кликът просто не прави нищо или отвежда на стария адрес. Четирите обичайни причини: (1) на бутона липсва data-vsx-checkout; (2) бутонът е вътре в елемент с data-sendaro-ignore; (3) бутонът живее в <iframe> — Sendaro не вижда вътре в чужди рамки; (4) твой собствен onclick спира събитието преди Sendaro.

Поправката: Провери с реда от „Бърза проверка" по-долу колко бутона намира Sendaro. Ако е 0 — атрибутът липсва или е в рамка. Бутони, които се рисуват от JS по-късно, се хващат автоматично, не се притеснявай за тях.

Кеширан стар скрипт

Копирал си checkout.js при себе си или си го сложил в свой bundle → застива на версията от онзи ден и подминава всяка поправка. Или CDN-ът ти (Cloudflare и подобни) кешира HTML-а и сервира стария data-store.

Поправката: Зареждай скрипта винаги от https://app.sendaro.bg/checkout.js. Прочисти кеша на CDN-а, презареди с Ctrl+F5 и виж в „Network" (F12) че файлът наистина се тегли от app.sendaro.bg.

Редът е сложен два пъти

Веднъж ръчно в шаблона и веднъж от плъгин/билдър. Sendaro се пази сам и вторият път не се изпълнява, но настройките се четат от тага, който е тръгнал пръв — а той може да е „голият", без твоите атрибути.

Поправката: Остави един ред с checkout.js в целия сайт и всички атрибути на него. Търси го в шаблона на футъра, в хедъра и в настройките на темата си.

„Thank-you страницата не се отваря"

Поръчката минава, но купувачът си остава на екрана „поръчката е приета" и никъде не отива.

Поправката: Така и трябва: пренасочването към data-vsx-success-url става, когато купувачът затвори този екран. Без атрибута Sendaro отива на началната страница (ако си на /cart или /checkout) или презарежда текущата.

Количката не се изпразва

Купувачът поръчва, връща се на сайта и количката още е пълна.

Поправката: Провери, че data-cart-clear-url сочи адрес на твоя домейн (най-добре относителен: /api/cart-clear.php). Към чужд домейн бисквитките ти не пътуват и сесията не се намира.

11. Бърза проверка преди пускане

Три реда в конзолата на браузъра (F12 → Console) отговарят на 90% от въпросите „защо не работи“:

javascript (конзолата на браузъра)
// Пусни това в конзолата на браузъра (F12 → Console) на страницата с бутона.

document.querySelectorAll("[data-vsx-checkout],[data-sendaro-checkout]").length
// 0 → Sendaro няма какво да хване: атрибутът липсва или бутонът е в <iframe>.

document.querySelector("script[src*='checkout.js']").getAttribute("data-store")
// null или "" → липсва data-store. Поръчките няма да влязат в твоя магазин.

document.addEventListener("sendaro:ready", function (e) { console.log("OK", e.detail); });
// Презареди страницата. Няма ли „OK" → скриптът не се е заредил изобщо.

И списъкът, който минаваш преди да пуснеш сайта на живо:

12. Webhooks — Sendaro звъни на теб

Събитията от раздел 5 идват от браузъра на купувача: удобни за интерфейса, но могат и да не се случат (човекът затваря раздела) и всеки може да ги подправи. Webhook-ът е другата посока — нашият сървър звъни на твоя, с подпис. Това е сигналът, на който може да стъпи склад, фактура и счетоводство.

Каталогът от твоя сървър

Сървърното препотвърждаване на цените (по-горе) работи само за продукти, които ние познаваме. За да ги познаваме, подай ги веднъж — и после при всяка промяна на цена или наличност:

php
$telo = json_encode(["products" => [
  ["sku" => "KAT-100", "title" => "Палетна количка 2500 кг", "priceEur" => 207.00,
   "weightKg" => 78.5, "stockQty" => 4, "imageUrl" => "https://tvoiat-sait.bg/img/kat100.jpg"],
]], JSON_UNESCAPED_UNICODE);

$chas = time();
$podpis = "sha256=" . hash_hmac("sha256", $chas . "." . $telo, SENDARO_INT_KEY);

$ch = curl_init("https://app.sendaro.bg/api/catalog");
curl_setopt_array($ch, [
  CURLOPT_POST => true, CURLOPT_POSTFIELDS => $telo, CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => [
    "Content-Type: application/json",
    "X-Sendaro-Store: " . SENDARO_STORE,
    "X-Sendaro-Timestamp: " . $chas,
    "X-Sendaro-Signature: " . $podpis,
  ],
]);
$otgovor = json_decode(curl_exec($ch), true);   // created / updated / skipped
ПолеЗадължителноКакво е
skuдаТвоят код на продукта. Той е ключът — по него намираме реда и следващия път го обновяваме, вместо да създаваме втори. Ред без sku се пропуска.
titleдаИме, каквото искаш да види купувачът в касата.
priceEurдаЦена в евро — приема и точка, и запетая. Тя става авторитетната: по-ниска цена, подадена от браузъра, вече не минава.
stockQtyпо изборНаличност в брой. Не подадеш ли полето — не се пипа. Подадеш ли празно, значи „не следя наличност". Работи само при магазини без собствен склад (Друг сайт, Simvoly) — при Shopify/WooCommerce складът е техен.
weightKg · lengthCm · widthCm · heightCmпо изборТегло и размери за куриера. Без тегло доставката се смята по фиксиран 1 кг и при едра стока доплащаш разликата.
imageUrlпо изборСнимка — само https://.
barcodeпо изборБаркод за пакетиране и печат.
Отговорът казва какво е станало с ВСЕКИ ред: created, updated и skipped — с причина за всеки пропуснат. Няма мълчаливо изгубени продукти. До 500 реда на заявка; повече качвай на партиди.
Подписът е същият като при подписаната количка: HMAC-SHA256 върху <час>.<сурово тяло> с ключа за интеграция. Кодът, който вече си написал за количката, върши работа и тук.

Версии и заключване (SRI)

По подразбиране скриптът е /checkout.js — обновява се сам, включително с поправките по сигурността. За повечето сайтове това е правилният избор.

Ако правилата ти изискват Subresource Integrity или искаш сам да решаваш кога се обновява касата, ползвай замразена версия. Заключване и авто-обновяване се изключват взаимно — не можеш да имаш и двете:

Кои версии съществуват и с какъв хеш: /v/versions.json. Оттам вземи стойността sri и я сложи така:

html
<script src="https://app.sendaro.bg/v/1.0.3/checkout.js"
        integrity="sha384-JiTEqfl2+HUlK4ZIn/H6IkROFELpK7hpQSr6qC6Jm7It9eilj3BVIyjF0XR8k1ir"
        crossorigin="anonymous"
        data-store="ТВОЯТ_КЛЮЧ" defer></script>
Замразената версия НЕ получава поправки. Пинеш ли, поеми ангажимент да следиш /v/versions.json — иначе оставаш на стар код, включително при поправка по сигурността.

Най-краткият път: готовият файл

Долното в тази глава описва как се проверява подпис на ръка — прочети го, за да знаеш какво става. Но не е нужно да го пишеш сам: дашбордът дава файла готов, с твоите ключове вътре.

Твоят код влиза на едно място във файла — функцията sendaro_obrabotka($sabitie, $poracka, $test) най-долу. Дотогава всяко известие се записва ред по ред в sendaro-porachki.jsonl до файла, така че виждаш какво пристига още преди да си написал един ред.

Файлът носи тайната ти. Затова: не го препращай по имейл и по чат, не го качвай в публична папка за изтегляне и не му сменяй разширението на .txt — PHP файл не издава кода си на браузъра, но текстов файл го издава. Всяко сваляне прави нова тайна: старият файл спира да важи в мига, в който качиш новия.

Какво пристига при теб

http заявка
POST /sendaro-hook HTTP/1.1
Host: moiat-sait.bg
Content-Type: application/json
X-Sendaro-Event: order.created
X-Sendaro-Delivery: cm3f8k0a20001x9y7q2v1b8kd
X-Sendaro-Timestamp: 1755780000
X-Sendaro-Signature: sha256=9f2b… (64 шестнайсетични знака)
User-Agent: Sendaro-Webhooks/1

{"event":"order.created","createdAt":"…","order":{…}}
ЗаглавкаСтойностЗа какво ти е
X-Sendaro-Eventимето на събитиетоКое се е случило — виж таблицата по-долу.
X-Sendaro-Deliveryномер на доставкатаЕдин и същ при всички повторни опити. Пази го и пропускай известие, което вече си обработил — иначе едно и също количество може да падне два пъти от склада.
X-Sendaro-Timestampunix секундиЧасът на изпращане. Влиза в подписа. Отказвай всичко по-старо от 5 минути.
X-Sendaro-Signaturesha256= + подписHMAC-SHA256 върху <час>.<тяло> с твоята тайна, в шестнайсетичен вид. Точно това проверяват примерите по-долу.
Подписът покрива и часа, не само тялото. Затова се подписва <timestamp>.<тяло> — с точка между тях. Само тялото би позволило „презапис“: записваш днешната заявка и я пускаш пак утре с валиден подпис.
СъбитиеКога точно тръгваЗа какво ти е
order.createdВ мига, в който поръчката се запише при нас — и по двата пътя на касата: и когато магазинът ти е на готова платформа, и когато сайтът е твой собствен.Ако товарителницата е издадена в същия миг, номерът ѝ вече е в тялото.
order.paidКогато поръчката стане платена: при потвърдено картово плащане, и при синхронизация с магазина ти, която я отбелязва като платена.Тук пускаш фактурата. При наложен платеж не го чакай в момента на поръчката — isCod е true, а парите идват при доставката.
order.cancelledКогато поръчката стане отказана: при отказ в опашката „Потвърждение“ в дашборда (клиентът се отказа или номерът е грешен), и при синхронизация, която я анулира.Тук връщаш количествата в склада и спираш документите.
order.deliveredКогато проверката на куриерския статус види, че пратката е доставена — часът на доставката идва от куриера, затова събитието тръгва точно веднъж, дори статусът после да се промени.Тук затваряш поръчката: наложеният платеж е събран, стоката е при клиента.
order.returnedКогато проверката на куриерския статус види, че пратката е върната на подателя (непотърсена или отказана при доставка).Тук връщаш количеството в склада и знаеш, че наложеният платеж няма да дойде.
waybill.createdКогато поръчката получи номер на товарителница: при издаване от дашборда — поединично или наведнъж за няколко поръчки — и при синхронизация, която донася номера.Тялото носи waybill и waybillTrackUrl — с тях пращаш „пратката тръгна“.
И четирите се излъчват днес — и всяко тръгва само веднъж. Известие се праща само при истинска промяна на състоянието: сравнява се какво е било преди действието с това, което е след него, и навън излиза само разликата — затова повторна синхронизация на една и съща вече платена поръчка не праща второ order.paid. А ако нещо все пак поиска едно и също два пъти, отдолу има втора мрежа: ключът (адрес, събитие, поръчка) е уникален и вторият POST просто не се записва. Двойно известие за едно и също е невъзможно.
Едно честно уточнение, за да не чакаш напразно: ако товарителница се пресъздаде (старата се отменя и за същата поръчка излиза нов номер), второ waybill.created няма — първото известие вече е стигнало при теб. Новият номер се вижда в поръчката в дашборда; ако при теб трябва да е винаги най-актуалният, вземай го оттам, а не от известието.
Не избереш ли събития — получаваш всички. Празният списък значи „всичко“ нарочно: по-добре едно известие в повече, отколкото търговец, който чака сигнал, който никога не е бил включен.

Тялото

json
{
  "event": "order.created",
  "createdAt": "2026-08-21T09:14:03.512Z",
  "order": {
    "id": "cm3f8k0a20001x9y7q2v1b8kd",
    "orderNumber": "#1410",
    "externalId": "6012345678901",
    "storeId": "cm2x9d0110000abcd1234efgh",
    "status": "NEW",
    "isTest": false,
    "financialStatus": "pending",
    "isCod": true,
    "totalEur": 141.64,
    "goodsEur": 135.9,
    "shippingEur": 5.74,
    "discountEur": 0,
    "currency": "EUR",
    "customer": {
      "name": "Иван Петров",
      "phone": "0888123456",
      "email": "ivan@example.com"
    },
    "delivery": {
      "type": "OFFICE",
      "city": "София",
      "officeName": "Офис Младост",
      "address": null,
      "courier": "ECONT"
    },
    "items": [
      { "title": "Кожен портфейл (Кафяв)", "sku": "WALLET-BRN", "quantity": 1, "priceEur": 129.9 }
    ],
    "waybill": "1055183955219",
    "waybillTrackUrl": "https://app.sendaro.bg/track?n=1055183955219"
  }
}
ПолеТипКакво е
eventтекстСъщото като заглавката X-Sendaro-Event.
order.idтекстНашият номер на поръчката. Стабилен — по него можеш да я намериш и после.
order.orderNumberтекстВидимият номер, който купувачът вижда (напр. "#1410").
order.isTestда/неtrue = тестова поръчка. Не пипай склад, не издавай документи. Виж раздел 14.
order.isCodда/неНаложен платеж. При false парите вече са платени по друг път.
order.totalEur
goodsEur shippingEur discountEur
числаЕвро, не центове. Общо, стока, доставка, отстъпка. currency е винаги "EUR".
order.customerобектname, phone, email — всяко може да е null.
order.deliveryобектtype (OFFICE/DOOR/…), city, officeName, address, courier.
order.items[]списъкПо артикул: title, sku, quantity, priceEur.
order.waybill
waybillTrackUrl
текст или nullНомер на товарителницата и линк за проследяване, ако вече са налични.

Проверка на подписа — PHP

php
<?php
// /sendaro-hook.php — адресът, който си въвел в дашборда (Webhooks → Добави адрес).
// Тайната е тази, която ти се показа ВЕДНЪЖ при създаването. Пази я като парола.
$SECRET = getenv("SENDARO_WEBHOOK_SECRET");

// 1) СУРОВОТО тяло — точно както е дошло. Никакъв json_decode преди проверката!
$raw = file_get_contents("php://input");
$ts  = isset($_SERVER["HTTP_X_SENDARO_TIMESTAMP"]) ? $_SERVER["HTTP_X_SENDARO_TIMESTAMP"] : "";
$sig = isset($_SERVER["HTTP_X_SENDARO_SIGNATURE"]) ? $_SERVER["HTTP_X_SENDARO_SIGNATURE"] : "";

// 2) ЧАСЪТ. По-стар (или по-нов) от 5 минути → отказ. Това спира „презапис“: записана
//    стара заявка, пусната пак утре, вече няма да мине, макар подписът ѝ да е верен.
if (!ctype_digit((string) $ts) || abs(time() - (int) $ts) > 300) {
    http_response_code(401); echo "stale"; exit;
}

// 3) ПОДПИСЪТ: HMAC-SHA256 върху „<час>.<тяло>" с тайната, с представка „sha256=".
$expected = "sha256=" . hash_hmac("sha256", $ts . "." . $raw, $SECRET);
if (!hash_equals($expected, $sig)) {   // ⚠ hash_equals, НЕ == и НЕ ===
    http_response_code(401); echo "bad signature"; exit;
}

// 4) Чак СЕГА тялото е доверено.
$data  = json_decode($raw, true);
$event = isset($_SERVER["HTTP_X_SENDARO_EVENT"]) ? $_SERVER["HTTP_X_SENDARO_EVENT"] : "";
$id    = isset($_SERVER["HTTP_X_SENDARO_DELIVERY"]) ? $_SERVER["HTTP_X_SENDARO_DELIVERY"] : "";
$order = isset($data["order"]) && is_array($data["order"]) ? $data["order"] : [];

// 5) ЕДИН ПЪТ. Същото известие може да дойде пак (повторен опит след мрежов проблем).
//    X-Sendaro-Delivery е един и същи при всички опити → ползвай го за отпечатък.
$dir  = __DIR__ . "/../data/sendaro-seen";
$seen = $dir . "/" . preg_replace("/[^A-Za-z0-9_-]/", "", $id);
if ($id !== "" && file_exists($seen)) { http_response_code(200); echo "duplicate"; exit; }

// 6) ТЕСТОВА ПОРЪЧКА → нищо реално. Без склад, без фактура, без товар за счетоводството.
if (!empty($order["isTest"])) { http_response_code(200); echo "test ok"; exit; }

// ── тук е твоята работа: сваляш количества, издаваш фактура, записваш поръчката ──
// $order["orderNumber"], $order["totalEur"], $order["items"], $order["customer"] …

@mkdir($dir, 0775, true);
@file_put_contents($seen, $event);
http_response_code(200);   // 2xx = прието. Всичко друго връща известието в опашката.
echo "ok";

Проверка на подписа — Node / Express

javascript
const express = require("express");
const crypto  = require("crypto");
const app = express();

const SECRET = process.env.SENDARO_WEBHOOK_SECRET;
const seen = new Set();   // за пример; в истински проект — база или Redis

// ⚠ express.raw, НЕ express.json. Подписът е върху тялото КАКТО Е ДОШЛО; express.json го
//   парсва, а JSON.stringify после го връща различен (интервали, ред на полетата) и
//   подписът пада „необяснимо". Това е причина №1 за „при мен не работи".
app.post("/sendaro-hook", express.raw({ type: "*/*", limit: "1mb" }), (req, res) => {
  const raw = req.body.toString("utf8");
  const ts  = String(req.get("X-Sendaro-Timestamp") || "");
  const sig = String(req.get("X-Sendaro-Signature") || "");

  // 1) часът: максимум 5 минути разлика
  if (!/^[0-9]+$/.test(ts) || Math.abs(Math.floor(Date.now() / 1000) - Number(ts)) > 300) {
    return res.status(401).send("stale");
  }

  // 2) подписът, сравнен по ПОСТОЯННО ВРЕМЕ (timingSafeEqual, не ===)
  const expected = "sha256=" + crypto.createHmac("sha256", SECRET)
    .update(ts + "." + raw).digest("hex");
  const a = Buffer.from(expected), b = Buffer.from(sig);
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
    return res.status(401).send("bad signature");
  }

  // 3) чак сега тялото е доверено
  const data  = JSON.parse(raw);
  const id    = req.get("X-Sendaro-Delivery") || "";
  const order = data.order || {};
  if (id && seen.has(id)) return res.status(200).send("duplicate"); // повторен опит
  if (id) seen.add(id);
  if (order.isTest) return res.status(200).send("test ok");         // тестова поръчка

  // 4) отговори БЪРЗО (чакаме до 12 секунди), а работата свърши после
  res.status(200).send("ok");
  setImmediate(() => {
    // сваляне от склад, фактура, запис в счетоводството…
  });
});

app.listen(3000);
Най-честата грешка в целия раздел: express.json() вместо express.raw(). Парснатото и после пак сглобено тяло се различава с един интервал или с реда на полетата — подписът пада, а причината изглежда мистична. Подписвай и проверявай суровия низ.

Правилата на доставката

ПравилоСтойност
Прието еКой да е код от 200 до 299. Всичко останало (включително 3xx пренасочване) се брои за неуспех — не следваме пренасочвания.
Колко чакаме12 секунди. Отговори бързо и свърши работата после — иначе бавната ти фактура ще изглежда като счупен адрес.
Повторни опити6 опита общо, с паузи 1, 5, 20, 60 и 180 минути. Последният е около 4 часа след събитието. Повтаряме и при 4xx — временно счупен адрес (изтекъл ключ, 401) не бива да ти струва поръчката.
След товаИзвестието се отбелязва като неуспяло и се вижда в дневника на екрана „Webhooks“ (последните 10 на адрес, със статуса и кода на отговора).
Размер на отговораЧетем най-много 64 KB. Отговаряй кратко — ok е напълно достатъчно.
РедНе е гарантиран. Ако получиш две известия за една поръчка, вярвай на това с по-нов createdAt.
Три неща, които не бива да правиш: да сравняваш подписа с == (ползвай hash_equals / timingSafeEqual); да парсваш тялото преди проверката; и да обработваш едно и също X-Sendaro-Delivery два пъти. Тайната стои в променлива на средата или в конфигурационен файл извън публичната папка — никога в HTML или в git.

13. Подписана количка — цените идват от твоя сървър

Дотук цената пътува през страницата: data-vsx-price или window.VSX_PRODUCT. Това е удобно, но всеки може да отвори инструментите на браузъра и да я смени на 0,01 € — и после да поръча с наложен платеж. За отстъпките Sendaro отдавна не вярва на браузъра; с този раздел същото важи и за артикулите.

Идеята е проста: твоят сървър подписва количката и получава кратък токен. Бутонът носи само токена. При поръчка ние четем артикулите и цените от токена — страницата вече не е източник на истината.

Откъде е ключът: дашборд → Webhooks → карето „Ключ за сървърна интеграция“Създай ключ. Ключът е за конкретен магазин, започва с ik_ и се показва само веднъж. Нов ключ убива стария веднага — смени го и в кода си.

Сървърът ти подписва и праща количката

php
<?php
// /lib/sendaro-cart.php — сървърът ти подписва количката и получава токен за бутона.
// Ключът е от дашборда: Webhooks → „Ключ за сървърна интеграция" → Създай ключ (започва с ik_).
function sendaro_cart_token(array $items, $note = null, $ref = null) {
    $STORE = getenv("SENDARO_STORE");            // ID на магазина (същото като data-store)
    $KEY   = getenv("SENDARO_INTEGRATION_KEY");  // ключът ik_…  — НИКОГА в HTML-а!

    // ⚠ Цените са в ЕВРО с точка, теглото е в КИЛОГРАМИ за един брой — както навсякъде тук.
    $payload = ["items" => $items];
    if ($note !== null) $payload["note"] = $note;
    if ($ref  !== null) $payload["ref"]  = $ref;   // твоят номер на количката

    $body = json_encode($payload, JSON_UNESCAPED_UNICODE);
    $ts   = (string) time();   // ⚠ часът на ТВОЯ сървър; разлика над 5 минути = отказ
    $sig  = "sha256=" . hash_hmac("sha256", $ts . "." . $body, $KEY);

    $ch = curl_init("https://app.sendaro.bg/api/cart");
    curl_setopt_array($ch, [
        CURLOPT_POST           => true,
        CURLOPT_POSTFIELDS     => $body,   // ⚠ ТОЧНО този низ — подписан е той, знак по знак
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 10,
        CURLOPT_HTTPHEADER     => [
            "Content-Type: application/json",
            "X-Sendaro-Store: "     . $STORE,
            "X-Sendaro-Timestamp: " . $ts,
            "X-Sendaro-Signature: " . $sig,
        ],
    ]);
    $res = curl_exec($ch);
    curl_close($ch);

    $out = json_decode((string) $res, true);
    return (is_array($out) && !empty($out["ok"])) ? $out["cart"] : null;  // токенът
}
КаквоСтойност
АдресPOST https://app.sendaro.bg/api/cart
ЗаглавкиX-Sendaro-Store — ID на магазина · X-Sendaro-Timestamp — unix секунди · X-Sendaro-Signaturesha256= + HMAC-SHA256 върху <час>.<сурово тяло> с ключа за интеграция. Същият формат като при webhook-ите — един и същи код ти върши работа и в двете посоки.
Тялоitems (задължително, до 60 артикула), по избор note (до 500 знака), ref (твоят номер на количката, до 80 знака) и ttlSec.
Артикулtitle · sku · variantTitle · image (само http(s)://) · quantity (1–999) · priceEur (или price) · weightKg. Евро и килограми, както в целия документ.
СрокttlSec (в секунди) — по подразбиране 7200 (2 часа), най-малко 60 секунди, най-много 86400 (24 часа). Подадеш ли стойност извън тези граници, тя се стяга до най-близката; липсваща или безсмислена (нула, отрицателна, текст) → 7200. Подписана вчера цена не бива да важи днес.
ЛимитДо 60 заявки в минута от един адрес. Подписвай количката, когато страницата се рисува, не при всяко кликване.
json — отговорът
{
  "ok": true,
  "cart": "eyJ2IjoxLCJzIjoiY20yeDlkMDExMDAwMGFiY2QiLCJleHAiOjE3NTU3ODcyMDAsIml0ZW1z…",
  "items": 2,
  "totalEur": 207.9,
  "expiresInSec": 7200
}
expiresInSec е срокът, който наистина е използван — не число за украса. Стойността минава през същото стягане (60 … 86400, по подразбиране 7200) и се връща вече стегната, така че токенът и отговорът не могат да се разминат. Поискаш ли "ttlSec": 5, получаваш "expiresInSec": 60; поискаш ли седмица — "expiresInSec": 86400. Смятай срока на количката по това поле, не по число, преписано в кода ти.

Токенът стига до бутона

php
<?php
require __DIR__ . "/lib/sendaro-cart.php";

// Количката, както я пази твоят сървър (сесия, база — все едно).
$items = [
  ["title" => "Кожен портфейл", "variantTitle" => "Кафяв", "sku" => "WALLET-BRN",
   "quantity" => 1, "priceEur" => 129.90, "weightKg" => 0.25,
   "image" => "https://tvoiat-sait.bg/img/portfeil.jpg"],
  ["title" => "Колан", "sku" => "BELT-40", "quantity" => 2, "priceEur" => 39.00, "weightKg" => 0.30],
];

$token = sendaro_cart_token($items, null, "cart-8871");
?>

<?php if ($token): ?>
  <button type="button" data-sendaro-checkout
          data-sendaro-cart="<?= htmlspecialchars($token, ENT_QUOTES, "UTF-8") ?>">
    Поръчай
  </button>
<?php else: ?>
  <!-- Токенът не се получи (мрежа, изтекъл ключ). Показваме обикновения бутон, за да не -->
  <!-- остане купувачът без каса — но цената пак идва от страницата.                     -->
  <button type="button" data-sendaro-checkout
          data-vsx-title="Кожен портфейл" data-vsx-price="129.90" data-vsx-weight="0.25">
    Поръчай
  </button>
<?php endif; ?>
ОтговорКакво значи
401 Липсва подписНяма X-Sendaro-Store, X-Sendaro-Timestamp или X-Sendaro-Signature.
401 Магазинът няма ключНе си създал ключ за интеграция за този магазин.
401 Подписът не съвпадаИли ключът е грешен/сменен, или часът на сървъра ти бяга с повече от 5 минути, или си подписал един низ, а си изпратил друг.
400 Количката е празнаНито един артикул не мина проверката — най-често липсва цена или количеството е 0.
Токенът не е тайна. Той е подписана оферта: купувачът може да я прочете (нищо лично няма вътре), но не може да я промени — подписът е с нашия ключ. Тайна е само ik_…, който остава на сървъра ти.
Носи ли бутонът токен, всичко останало се пренебрегваdata-vsx-price, data-vsx-product, window.VSX_PRODUCT. А ако токенът е нечетим или изтекъл, касата не пада тихо към цените от страницата: поръчката се отказва с ясно съобщение. Тихото падане би направило цялата защита театър.
Токенът е вързан за магазина в data-store и за срока си. Рисувай го наново при всяко зареждане на страницата. Кешираш ли го, кеширай го по expiresInSec от отговора (и с малко запас), не по константа в твоя код — иначе или сервираш вече изтекъл токен и купувачът вижда „количката е изтекла — презареди страницата“, или изхвърляш още валиден и подписваш количката без нужда.

14. Тестов режим — поръчка без пратка

Първата проверка на интеграцията не бива да струва товарителница. В тестов режим поръчката минава целия път — валидация, цени на доставка, отстъпки, запис, събития, webhook — но куриерът не се вика и купувачът не получава нищо.

html
<!-- Вариант 1: на реда със скрипта — цялата страница е в тестов режим. -->
<script src="https://app.sendaro.bg/checkout.js"
        data-store="ТВОЕТО-ID"
        data-test="1"
        defer></script>

<!-- Вариант 2: без да пипаш кода — отвори страницата си с този параметър: -->
<!--   https://tvoiat-sait.bg/produkt?sendaro_test=1                        -->
КаквоВ тестов режим
ПоръчкатаСъздава се и се вижда в хъба както всяка друга. Носи признак „тестова“ — той идва при теб в тялото на webhook-а като "isTest": true.
Куриер и товарителницаНЕ. Пратка не тръгва, номер не се издава, разход не се прави.
SMS до купувачаНЕ. Иначе първата ти проба щеше да стресне непознат човек с чужд номер — и да ти струва пари.
Webhook към твоя сървърДА — точно него тестваш. Тялото носи "isTest": true.
Цени, доставка, отстъпкиСмятат се както при истинска поръчка. Затова тестът има смисъл.
Събитието sendaro:orderИзлъчва се както обикновено.
Чистене на количката и thank-youРаботят както обикновено.
Махни data-test="1", преди да пуснеш сайта на живо. Забравен на реда със скрипта, той тихо превръща всяка истинска поръчка в тестова: клиентът поръчва, ти виждаш поръчка — а товарителница няма и никой нищо не изпраща. Това е най-скъпият начин да се провали пускане.
Кой вариант кога: ?sendaro_test=1 е за бърза проба на живия сайт, без да пипаш кода (важи само за раздела, който си отворил ти). data-test="1" е за копие на сайта — тестов/staging адрес, който така или иначе не е за клиенти.
Изчисти след себе си: тестовите поръчки стоят в хъба, докато не ги изтриеш. Изтрий ги, преди да пуснеш магазина — иначе се смесват с истинските в списъците и в статистиката.

Свързани

Какво може вграденият checkout на Sendaro Всички статии за касата Ако все пак си на готова платформа — виж своята Влез в дашборда и вземи ID-то на магазина си
Заби някъде?
Пиши ни в чата долу вдясно — прати адреса на страницата и снимка на конзолата, ще ти кажем какво липсва.