Przejdź do treści
Premium Supply Developer

Webhooki

Zdarzenia pushowane na Twój adres, podpisane HMAC-SHA256

Zamiast odpytywać o nowe zamówienia, rejestrujesz adres HTTPS i listę zdarzeń. Każde dostarczenie ma podpis w nagłówku X-Signature, nazwę zdarzenia w X-Event i identyfikator w X-Delivery-Id. Brak odpowiedzi 2xx uruchamia ponowienia: po 1 min, 5 min, 30 min, 2 h i 12 h.

Stan wdrożenia#

Zdarzenia#

ZdarzenieKiedyCo zrobić po odebraniu
order.createdKupują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.paidPł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.cancelledZamó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.createdKupują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.rejectedModeracja 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ą.

Żądanie
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"]
  }'
Odpowiedź 201 — secret tylko tutaj
{
  "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.

Przykład dostarczenia
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łówekZnaczenie
X-Signaturesha256=<hex> — HMAC-SHA256 z surowych bajtów ciała, kluczem jest secret subskrypcji, wynik w małych literach hex.
X-EventNazwa zdarzenia (jak w tabeli wyżej). Ta sama wartość jest w polu event ciała — nagłówek pozwala routować bez parsowania.
X-Delivery-IdIdentyfikator dostarczenia, stały przy ponowieniach. Klucz deduplikacji po Twojej stronie.

Pole data zawiera identyfikatory zasobu: dla order.*order_id i order_number; dla offer.rejectedoffer_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)#

webhooks.js
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#

webhooks.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-Id i 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.cancelled może dotrzeć przed spóźnionym ponowieniem order.paid. Zawsze weryfikuj aktualny stan przez GET /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.

Odpowiedź 200
{
  "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.