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.
3. Format payloadu (Zaawansowany JSON)
Webhook wysyła POST z nagłówkami Content-Type: application/json i SHOPLO_HMAC_SHA256 (podpis — 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": []
}
}
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ą. |
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. |
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 nagłówku SHOPLO_HMAC_SHA256. 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
Jeśli Twoja aplikacja zwróci odpowiedź 5xx (lub nie odpowie wcale), SalesCRM zapisze próbę w tabeli WebhookFailure i nie będzie automatycznie retryować. 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 retry administracyjny nie duplikował danych.
- Failed webhooks można retryować ręcznie z panelu admina SalesCRM (
Konfiguracja → Webhooki → Failures).
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.