Webhooks — powiadomienia o zdarzeniach w zamówieniach
SalesCRM wysyła webhook (POST HTTP) za każdym razem gdy zamówienie zmieni status na skonfigurowany przez Ciebie. Dzięki temu Twoja zewnętrzna aplikacja może w czasie rzeczywistym reagować na sprzedaż, anulowania i zwroty.
1. Do czego służą webhooki
Webhook to automatyczne powiadomienie HTTP wysyłane przez SalesCRM do wskazanego przez Ciebie URL-a. Możesz go wykorzystać do:
- Aktywacji konta klienta na platformie kursowej / społecznościowej po opłaceniu zamówienia.
- Wysyłki danych do własnego CRM-u, hurtowni danych albo dashboardu metryk.
- Wyzwolenia automatyzacji w Make/Zapier/n8n.
- Wycofania dostępu po zwrocie lub anulacji zamówienia.
2. Konfiguracja w panelu admina
Wejdź w Konfiguracja → Webhooki i kliknij „Dodaj webhook”. Pojedynczy webhook to jedna kombinacja statusu + URL-a + klucza.
| Pole | Opis |
|---|---|
| Aktywność | Checkbox włączający/wyłączający webhook bez usuwania konfiguracji. |
| Status zamówienia | Status na który ma reagować webhook (np. Opłacone, Anulowane, Zwrócone, Elektroniczne lub własny). Każdy status to osobny webhook record — możesz mieć wiele wskazujących na ten sam URL. |
| Typ webhooka | Zaawansowany JSON — rekomendowany; wysyła pełny payload jako application/json z podpisem HMAC. Alternatywa: Key-value (form-urlencoded) — uproszczona forma dla starszych integracji. |
| Adres URL | Pełny URL endpointu Twojej aplikacji (musi być HTTPS w produkcji). |
| Klucz zabezpieczający | Wygenerowany losowo sekret. SalesCRM podpisuje nim payload (HMAC-SHA256), Twoja aplikacja go weryfikuje. Trzymaj w sekrecie — każdy kto go ma może udawać webhooki SalesCRM. |
Typowa konfiguracja dla platformy członkowskiej
Jeśli budujesz aplikację która przyznaje/wycofuje dostęp na podstawie zamówień, skonfiguruj trzy webhooki na ten sam URL (z tym samym kluczem):
- Opłacone — aktywuje dostęp, przedłuża subskrypcję.
- Anulowane — wycofuje dostęp.
- Zwrócone — wycofuje dostęp po zwrocie pieniędzy.
Twoja aplikacja rozróżnia eventy po polu status w payloadzie. To pole zawsze niesie status, pod który webhook jest podpięty – nawet jeśli zamówienie zdążyło już przejść do kolejnego statusu, zanim powiadomienie wyszło.
Webhook przy każdej próbie pobrania subskrypcji
Jeśli w planie subskrypcji ustawisz status zamówienia po nieudanym pobraniu i podepniesz pod ten status webhook, powiadomienie przyjdzie po każdej z trzech prób - choć status zmienia się tylko raz. Ładunek niesie wtedy blok charge_attempt z numerem próby i polem final, więc odróżnisz „spróbujemy ponownie” od „to była ostatnia próba” i dopasujesz treść wiadomości do klienta.
Webhook tylko dla wybranych produktów
Webhook możesz zawęzić do zamówień z wybranymi produktami. Bez wybranych produktów webhook idzie przy każdym zamówieniu w danym statusie. Jeśli usuniesz albo ukryjesz ostatni produkt z listy, webhook wyłączy się sam, zamiast zacząć wysyłać dane przy każdym zamówieniu.
3. Format payloadu (Zaawansowany JSON)
Webhook wysyła POST z nagłówkami Content-Type: application/json oraz podpisem w dwóch nagłówkach: SHOPLO_HMAC_SHA256 i X-Shoplo-Hmac-Sha256 (szczegóły w sekcji 5). Ciało żądania ma strukturę:
{
"order": {
"order_identifier": "ZAM-1234",
"status": "Opłacone",
"placed_at": "2026-05-22T10:00:00Z",
"email_address": "klient@example.pl",
"first_name": "Jan",
"last_name": "Kowalski",
"phone": "+48123456789",
"newsletter": "true",
"delivery_address": "ul. Przykładowa 1",
"delivery_city": "Warszawa",
"delivery_postal_code": "00-001",
"delivery_country": "PL",
"invoice_company_name": null,
"invoice_nip": null,
"payment_type": "TpayPaymentType",
"delivery_type": "UserDeliveryType",
"currency": "PLN",
"products_cost": "180.00",
"delivery_cost": "15.00",
"payment_cost": "5.00",
"discount": "0.00",
"total": "200.00",
"discount_code_code": "",
"order_items": [
{
"product_id": 42,
"name": "ImkerClub miesięczny",
"sku": "IMKERCLUB-30",
"unit_price": "30.00",
"vat_rate": "23",
"quantity": 1,
"discount": "0.00",
"subscriber_identifier": "SUB-abc123",
"subscription_period_days": 30,
"ref": null,
"gift_packing": false
},
{
"product_id": 88,
"name": "Książka 'Rok Twórcy'",
"sku": "ROK-TW-PRINT",
"unit_price": "50.00",
"vat_rate": "5",
"quantity": 1,
"discount": "0.00",
"ref": null,
"gift_packing": false
}
],
"order_attributes": [],
"consents": {
"marketing_agreements": [
{ "id": 15, "kind": "newsletter", "text": "Chcę dostawać newsletter" }
],
"shop_rules_version": { "id": 7, "version_date": "2026-10-01T09:00:00+02:00", "source_url": null },
"electronic_order_acceptance": { "text": "Wyrażam zgodę na dostarczenie treści cyfrowych...", "version_date": "2026-10-13T22:10:00+02:00" }
}
}
}
4. Pola payloadu — szczegóły
Pola na poziomie order
| Pole | Typ | Opis |
|---|---|---|
order_identifier | string | Identyfikator zamówienia widoczny w panelu admina (np. ZAM-1234). |
status | string | Nazwa statusu który wywołał webhook (np. Opłacone, Anulowane). |
placed_at | datetime (ISO 8601) | Data złożenia zamówienia. |
email_address | string | Email klienta (zawsze lower-case). |
first_name, last_name | string | Imię i nazwisko z adresu dostawy. |
total, products_cost, delivery_cost, payment_cost, discount | decimal (string) | Kwoty w walucie zamówienia. total to suma do zapłaty. |
currency | string | Aktualnie zawsze PLN. |
payment_type, delivery_type | string | Klasa typu płatności/dostawy (np. TpayPaymentType). |
discount_code_code | string | Kod rabatowy użyty w zamówieniu, lub pusty string. |
subscriber_identifier | string | Opcjonalne — obecne tylko gdy całe zamówienie jest powiązane z subskrypcją. |
service_ref | string | Opcjonalne — stały klucz usługi, wspólny dla całego łańcucha odnowień. Obecny w planach z identyfikacją usług po zamówieniu; w odróżnieniu od subscriber_identifier nigdy się nie zmienia, więc to po nim najlepiej wiązać webhook z usługą. |
charge_attempt | object | Opcjonalne — obecne tylko na zamówieniach odnowieniowych, którym karta odmówiła. Pola: number (numer próby), reason (surowy powód odmowy od operatora), attempted_at, final (czy to była ostatnia próba), service_ref. |
payment_token | string | Opcjonalne — obecne tylko dla Tpay/PayU (token transakcji w bramce płatności). |
ref, url_params | string / object | Opcjonalne — parametry afiliacyjne / UTM jeśli były obecne w koszyku. |
consentsNowe | object | Zgody z chwili złożenia zamówienia. marketing_agreements to lista zaznaczonych zgód (id, kind, text). shop_rules_version to zaakceptowana wersja regulaminu (id, version_date, source_url) albo null. electronic_order_acceptance to treść zgody na zakup produktu elektronicznego z datą wersji albo null (zapisywana od 13.10.2026). To samo pole zwraca API szczegółów zamówienia. |
Pola na poziomie order_items[]
| Pole | Typ | Opis |
|---|---|---|
product_id | integer | ID produktu w SalesCRM (stałe między zamówieniami). |
name, sku | string | Nazwa produktu i SKU z aktualnej rewizji produktu. |
unit_price | decimal (string) | Cena jednostkowa brutto. |
vat_rate | decimal (string) | Stawka VAT (np. 23, 5, 0). |
quantity | integer | Ilość. Dla pozycji subskrypcyjnych zawsze 1 (patrz niżej). |
discount | decimal (string) | Rabat na pozycję. |
subscriber_identifier | string | Obecne tylko dla pozycji powiązanych z subskrybentem (rebill subskrypcji albo świeży zakup planu). Pasuje do custom_identifier lub alias rekordu Subscriber w SalesCRM. |
subscription_period_daysNowe | integer | Liczba dni dostępu, które ta pozycja przyznaje subskrybentowi. Wartość pochodzi z pola Interwał (dni) konfigurowanego w Subskrypcje → Plany subskrypcji przy produkcie. Dostępne tylko dla pozycji z subscriber_identifier. Aplikacje członkowskie używają tej wartości do przedłużenia dostępu (accessExpiresAt = max(now, current) + subscription_period_days). |
ref | string | Opcjonalny identyfikator referencyjny pozycji. |
gift_packing | boolean | Czy pozycja ma opakowanie prezentowe. |
subscription_period_days jest per pozycja, więc Twoja aplikacja musi liczyć MRR sumując tylko te pozycje gdzie subscriber_identifier jest obecne — nie po całym order.total.
Individual items vs bulk items
Pozycje subskrypcyjne (z subscriber_identifier) są rozbijane na pojedyncze itemy — nawet jeśli klient kupił 3 subskrypcje naraz, dostaniesz 3 rekordy z quantity: 1, każdy z własnym subscriber_identifier i subscription_period_days. Pozycje jednorazowe (bez subskrybenta) są zbiorcze z rzeczywistym quantity.
5. Weryfikacja podpisu (HMAC-SHA256)
SalesCRM podpisuje całe ciało żądania kluczem zabezpieczającym i wysyła wynik w dwóch nagłówkach o tej samej wartości: SHOPLO_HMAC_SHA256 i X-Shoplo-Hmac-Sha256. Jeśli Twoja aplikacja stoi za nginxem albo innym reverse proxy, czytaj X-Shoplo-Hmac-Sha256. Nginx domyślnie odrzuca nagłówki z podkreślnikiem, więc pierwszy z nich może do aplikacji nie dotrzeć. Twoja aplikacja musi:
- Przeczytać RAW body żądania (przed parsowaniem JSON).
- Policzyć
HMAC-SHA256(secret, body)→ zwraca hex digest (64 znaki). - Zakodować ten hex jako base64url.
- Porównać w sposób timing-safe z wartością z nagłówka.
- Odrzucić żądanie (HTTP 403) jeśli nie pasuje.
Przykład weryfikacji w Node.js
import crypto from 'crypto'
function verifySalesCrmWebhook(rawBody, headerValue, secret) {
const hexHmac = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex')
const expected = Buffer.from(hexHmac)
const received = Buffer.from(headerValue, 'base64url')
if (received.length !== expected.length) return false
return crypto.timingSafeEqual(received, expected)
}
Przykład w PHP
function verifySalesCrmWebhook($rawBody, $headerValue, $secret) {
$hexHmac = hash_hmac('sha256', $rawBody, $secret);
$expected = $hexHmac;
$received = base64_decode(strtr($headerValue, '-_', '+/'));
return hash_equals($expected, $received);
}
6. Idempotencja i ponowne dostarczanie
Każda odpowiedź inna niż 200/201 trafia do logu nieudanych webhooków (Konfiguracja → Webhooki → Failures). Przy odpowiedzi 5xx SalesCRM dodatkowo ponawia wysyłkę, do 25 razy w coraz dłuższych odstępach (łącznie około 3 tygodni), a każda nieudana próba to kolejny wpis w logu. Przy 4xx ponowień nie ma. Wpis z błędem 4xx możesz wysłać ponownie przyciskiem Ponów w logu. Przy wpisach 5xx zamiast przycisku zobaczysz Ponawiany automatycznie. Jeśli Twoja aplikacja nie odpowie wcale albo przekroczy limit czasu, SalesCRM ponawia tak samo jak przy 5xx, ale takie próby nie trafiają do logu. Zaleca się więc:
- Zawsze zwracaj 200/201 po walidacji podpisu — nawet jeśli wewnętrznie odkryjesz duplikat lub błąd biznesowy. To zapobiega lawinie failures w panelu admina SalesCRM.
- Implementuj idempotency po podpisie HMAC — ten sam payload da ten sam HMAC. Trzymaj UNIQUE INDEX na zapisanych eventach żeby ponowienia nie duplikowały danych.
- Failed webhooks można retryować ręcznie z panelu admina SalesCRM (
Konfiguracja → Webhooki → Failures).
Limity wywołania
Od 21 września 2026 SalesCRM pilnuje czasu, rozmiaru i adresu każdego webhooka:
- 45 sekund na całą odpowiedź, łącznie z przekierowaniami. Połączenie musi się nawiązać w 10 sekund. Jeśli Twoja aplikacja przetwarza zamówienie dłużej, odpowiedz od razu 200 i dokończ pracę w tle.
- Do 3 przekierowań. Przy 301, 302 i 303 kolejne żądanie idzie jako GET bez treści, przy 307 i 308 jako POST z tą samą treścią.
- Najwyżej 64 KB odpowiedzi. Dalszą część pomijamy, także w logu nieudanych webhooków.
- Adres musi być publiczny. Adresy lokalne i sieci wewnętrznych (
localhost,127.0.0.1,10.x.x.x,192.168.x.x, nazwy bez kropki) odrzucamy przy zapisie webhooka i przed każdą wysyłką.
7. Statusy odpowiedzi HTTP
| Kod | Interpretacja SalesCRM |
|---|---|
| 200, 201 | Sukces. Wpisuje do logów, nie zapisuje failure. |
| 202-499 (poza 200/201) | Failure. Zapis do WebhookFailure, ale bez retry. Brak wyjątku w procesie. |
| 500-599 | Failure z rzuceniem wyjątku WebhookError. Zapis do WebhookFailure. |
8. Format alternatywny: Key-value (form-urlencoded)
Starsza forma webhooka (typ Key-value) wysyła application/x-www-form-urlencoded z ograniczonym zestawem pól (głównie order_identifier, email, name, surname, product_id, quantity). Nie zawiera pola subscription_period_days. Używaj wyłącznie do prostych integracji ze starszymi platformami; dla nowych integracji zawsze wybieraj Zaawansowany JSON.