Stan wdrożenia#
Zdarzenia#
| Zdarzenie | Kiedy | Co zrobić po odebraniu |
|---|---|---|
order.created | Kupujący złożył zamówienie z Twoją pozycją. Jeszcze nieopłacone — bez danych kupującego. | Zarezerwuj towar, jeśli prowadzisz rezerwacje. Nie wysyłaj. |
order.paid | Płatność zaksięgowana; status API processing. Dane kupującego i adres są od teraz dostępne. | GET /v1/orders/{id}, przekaż do realizacji, po nadaniu POST …/shipments. |
order.cancelled | Zamówienie anulowane (przez kupującego przed wysyłką albo przez obsługę). | Zwolnij rezerwację; jeśli paczka poszła — skontaktuj się z obsługą. |
return.created | Kupujący zgłosił zwrot lub reklamację pozycji z Twojej oferty. | Utwórz sprawę po swojej stronie; szczegóły w zasobie zwrotów (zapowiedziany, zakres returns:read). |
offer.rejected | Moderacja odrzuciła kartę produktu; oferta ma status rejected i nie jest widoczna. | GET /v1/offers/{id}, popraw dane w panelu, zgłoś ponownie. |
Nazwy zdarzeń są stabilne. Nowe zdarzenia dochodzą bez zmiany wersji — subskrybujesz je jawnie, więc nie dostaniesz nic, o co nie prosiłeś.
Subskrypcja#
POST /v1/webhooks — adres wyłącznie HTTPS z publicznym hostem, co najmniej jedno zdarzenie. W odpowiedzi jedyny raz dostajesz secret do weryfikacji podpisu — zapisz go po stronie odbiornika. Nie da się go odczytać ponownie; zgubiony sekret = usuń subskrypcję i załóż nową.
curl -s -X POST https://api.premiumsupply.pl/v1/webhooks \
-H "Authorization: Bearer $TOKEN" \
-H "User-Agent: MojaIntegracja/1.0.0 (+https://example.com/moja-integracja)" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 0b6f1a2c-9d3e-4f5a-8b7c-6d5e4f3a2b1c" \
-d '{
"url": "https://app.example.com/webhooks/premiumsupply",
"events": ["order.paid", "order.cancelled", "return.created"]
}'{
"id": "wh_01J9K2M3N4P5Q6R7S8T9V0W1X2",
"url": "https://app.example.com/webhooks/premiumsupply",
"events": ["order.paid", "order.cancelled", "return.created"],
"active": true,
"created_at": "2026-09-02T12:00:00.000Z",
"secret": "whsec_7Qm3nX9vL2pR8tK4wZ6yB1cD5fG0hJ3k"
}GET /v1/webhooks zwraca listę subskrypcji (bez sekretów), DELETE /v1/webhooks/{id} usuwa (204). Subskrypcja należy do aplikacji w kontekście sprzedawcy, na którego token została założona — aplikacja obsługująca 50 sprzedawców ma 50 subskrypcji (może wskazywać ten sam URL).
Dostarczenie#
POST na Twój adres z ciałem JSON. Treść jest celowo cienka: mówi, co się stało i którego zasobu dotyczy — szczegóły pobierasz z API. Dzięki temu webhook nigdy nie przenosi danych osobowych kupującego, a Twoja integracja zawsze pracuje na aktualnym stanie, nie na migawce z chwili zdarzenia.
POST /webhooks/premiumsupply HTTP/1.1
Host: app.example.com
Content-Type: application/json
X-Event: order.paid
X-Delivery-Id: whd_01J9K2Q7R8S9T0V1W2X3Y4Z5A6
X-Signature: sha256=5f8c6b2e1d0a9f3e7b4c2a1d8e6f5c4b3a2d1e0f9c8b7a6d5e4f3c2b1a0d9e8f
{
"id": "whd_01J9K2Q7R8S9T0V1W2X3Y4Z5A6",
"event": "order.paid",
"occurred_at": "2026-09-02T12:34:56.000Z",
"data": {
"order_id": "ord_01J9K2P5Q6R7S8T9V0W1X2Y3Z4",
"order_number": "PS-2026-004512"
}
}| Nagłówek | Znaczenie |
|---|---|
X-Signature | sha256=<hex> — HMAC-SHA256 z surowych bajtów ciała, kluczem jest secret subskrypcji, wynik w małych literach hex. |
X-Event | Nazwa zdarzenia (jak w tabeli wyżej). Ta sama wartość jest w polu event ciała — nagłówek pozwala routować bez parsowania. |
X-Delivery-Id | Identyfikator dostarczenia, stały przy ponowieniach. Klucz deduplikacji po Twojej stronie. |
Pole data zawiera identyfikatory zasobu: dla order.* — order_id i order_number; dla offer.rejected — offer_id; dla return.created — return_id i order_id. Nieznane pola w data ignoruj — możemy je dokładać bez zmiany wersji.
Weryfikacja podpisu#
Policz HMAC-SHA256 z surowego ciała kluczem secret, poprzedź sha256= i porównaj z X-Signature w stałym czasie. Odrzucaj niepodpisane i błędnie podpisane dostarczenia kodem 401 — nie przetwarzaj ich „na próbę”.
Node.js (Express)#
import { createHmac, timingSafeEqual } from "node:crypto";
import express from "express";
const app = express();
// Surowe bajty ciała — podpis liczy się z tego, co przyszło po sieci,
// nie z JSON-a po ponownej serializacji (zmienia kolejność kluczy i spacje).
app.post("/webhooks/premiumsupply", express.raw({ type: "application/json" }), (req, res) => {
const expected = "sha256=" + createHmac("sha256", process.env.PS_WEBHOOK_SECRET).update(req.body).digest("hex");
const given = req.get("X-Signature") || "";
const ok = expected.length === given.length && timingSafeEqual(Buffer.from(expected), Buffer.from(given));
if (!ok) return res.status(401).end();
const deliveryId = req.get("X-Delivery-Id");
const event = req.get("X-Event");
const payload = JSON.parse(req.body.toString("utf8"));
// Odpowiedz od razu, przetwarzaj w tle. Ponowienie przyjdzie z tym samym
// X-Delivery-Id — użyj go jako klucza deduplikacji.
enqueue({ deliveryId, event, payload });
res.status(200).end();
});PHP#
<?php
$secret = getenv('PS_WEBHOOK_SECRET');
$raw = file_get_contents('php://input'); // surowe ciało, przed json_decode
$given = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
$expected = 'sha256=' . hash_hmac('sha256', $raw, $secret);
if (!hash_equals($expected, $given)) { // porównanie w stałym czasie
http_response_code(401);
exit;
}
$deliveryId = $_SERVER['HTTP_X_DELIVERY_ID'] ?? '';
$event = $_SERVER['HTTP_X_EVENT'] ?? '';
$payload = json_decode($raw, true);
// Zapisz do kolejki pod $deliveryId (deduplikacja), odpowiedz natychmiast.
enqueue($deliveryId, $event, $payload);
http_response_code(200);Odpowiedź i ponowienia#
- Odpowiedz dowolnym 2xx bez treści, tak szybko jak się da — zapisz dostarczenie do kolejki i przetwarzaj po odpowiedzi. Długie przetwarzanie w handlerze kończy się timeoutem i niepotrzebnym ponowieniem.
- Brak 2xx (timeout, 3xx, 4xx, 5xx, błąd połączenia) → ponowienia według harmonogramu: 1 min, 5 min, 30 min, 2 h, 12 h od poprzedniej próby. Łącznie 6 prób w ciągu ~14,5 h.
- Przekierowań nie śledzimy — adres subskrypcji musi odpowiadać bezpośrednio.
- Ponowienie ma ten sam
X-Delivery-Idi to samo ciało; podpis też jest ten sam. Jeśli już przetworzyłeś ten identyfikator — odpowiedz 200 i nic nie rób. - Kolejność nie jest gwarantowana:
order.cancelledmoże dotrzeć przed spóźnionym ponowieniemorder.paid. Zawsze weryfikuj aktualny stan przezGET /v1/orders/{id}, zanim podejmiesz działanie nieodwracalne.
Historia dostarczeń#
GET /v1/webhooks/{id}/deliveries zwraca ostatnie 100 dostarczeń z kodem odpowiedzi, czasem trwania, numerem próby i terminem następnego ponowienia — to samo, co widzisz w panelu aplikacji. Podczas wdrożenia to najszybszy sposób sprawdzenia, czy Twój odbiornik odpowiada 2xx.
{
"items": [
{
"id": "whd_01J9K2Q7R8S9T0V1W2X3Y4Z5A6",
"event": "order.paid",
"attempt": 2,
"response_status": 200,
"duration_ms": 184,
"delivered_at": "2026-09-02T12:36:01.000Z",
"next_retry_at": null
},
{
"id": "whd_01J9K2N1P2Q3R4S5T6V7W8X9Y0",
"event": "order.created",
"attempt": 3,
"response_status": 503,
"duration_ms": 5002,
"delivered_at": null,
"next_retry_at": "2026-09-02T14:31:00.000Z"
}
]
}Bezpieczeństwo#
- Tylko HTTPS — adresy
http://, localhost i prywatne IP są odrzucane przy tworzeniu subskrypcji. - Sekret per subskrypcja, pokazany raz. Rotacja = usuń subskrypcję i załóż nową; przez chwilę możesz mieć dwie (stary i nowy sekret), potem usuń starą.
- Nie ufaj samemu adresowi IP nadawcy — weryfikuj podpis. Adresy, z których wysyłamy, mogą się zmienić bez zapowiedzi.
- Porównanie w stałym czasie (
timingSafeEqual,hash_equals) — zwykłe==zdradza długość dopasowanego prefiksu. - Webhook nie zastępuje uprawnień: aby pobrać szczegóły zamówienia po
order.paid, token nadal musi miećorders:read.