Webhooks
Subskrybuj zdarzenia, weryfikuj podpis HMAC, obsługuj retry - wszystko zgodne ze Stripe-style konwencjami.
Jak działają
Każda zmiana stanu istotna dla integracji (zakończona walidacja, stworzona korekta, przekroczenie quoty, start lub koniec trialu partnerskiego) generuje event. Event jest dostarczany do wszystkich aktywnych webhook endpointów Twojej organizacji, które są zasubskrybowane na ten typ.
Każda dostawa to:
- POST z
Content-Type: application/json - Nagłówek
X-NK-Signaturez podpisem HMAC-SHA256 - Body w postaci
{ id, type, created_at, data, meta }
Serwer oczekuje odpowiedzi 2xx w ciągu 10 s. Inne kody lub timeout powodują retry z exponential backoff (60 s, 5 min, 30 min, 2 h, 12 h - łącznie 5 prób). Po wyczerpaniu prób kolejne dostawy są przerywane, ale endpoint nie jest jeszcze wyłączany.
is_active = false, auto_disabled_reason). Włącz go ręcznie w dashboardzie gdy naprawisz przyczynę.Format podpisu
Inspirowany Stripe'em. Nagłówek X-NK-Signature zawiera unix timestamp i podpis HMAC-SHA256 hex:
X-NK-Signature: t=1716494400,v1=8e6b3d2f9a4c1e6d...Podpis obliczany jest jako HMAC-SHA256(secret, t + "." + raw_body). Tolerancja replay-window: 5 minut.
Weryfikacja - TypeScript
import { verifyWebhookSignature } from "@naprawksef/sdk/webhooks";
// Next.js App Router
export async function POST(req: Request) {
const rawBody = await req.text();
const header = req.headers.get("x-nk-signature");
const result = verifyWebhookSignature({
secret: process.env.NAPRAW_KSEF_WEBHOOK_SECRET!,
rawBody,
header,
});
if (!result.ok) {
return new Response("Invalid signature: " + result.reason, { status: 401 });
}
const event = JSON.parse(rawBody);
switch (event.type) {
case "validation.completed":
await onValidationCompleted(event.data);
break;
case "correction.created":
await onCorrectionCreated(event.data);
break;
case "quota.exceeded":
await alertOps(event.data);
break;
}
return new Response("ok");
}Weryfikacja - PHP
<?php
use NaprawKsef\Sdk\Webhooks\SignatureVerifier;
require __DIR__ . '/vendor/autoload.php';
$rawBody = file_get_contents('php://input');
$header = $_SERVER['HTTP_X_NK_SIGNATURE'] ?? null;
$result = SignatureVerifier::verify(
secret: $_ENV['NAPRAW_KSEF_WEBHOOK_SECRET'],
rawBody: $rawBody,
header: $header,
);
if (!$result['ok']) {
http_response_code(401);
exit('Invalid signature: ' . $result['reason']);
}
$event = json_decode($rawBody, true);
match ($event['type']) {
'validation.completed' => onValidationCompleted($event['data']),
'correction.created' => onCorrectionCreated($event['data']),
'quota.exceeded' => alertOps($event['data']),
default => null,
};
http_response_code(200);
echo 'ok';Weryfikacja - bez SDK (czysty Node)
const crypto = require("node:crypto");
function verify(secret, rawBody, header, toleranceSec = 300) {
if (!header) return false;
const parts = Object.fromEntries(
header.split(",").map((s) => s.split("=", 2)),
);
const t = parseInt(parts.t, 10);
const sig = parts.v1;
if (!t || !sig) return false;
if (Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(`${t}.${rawBody}`)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected, "hex"),
Buffer.from(sig, "hex"),
);
}Lista typów eventów
| Typ | Kiedy |
|---|---|
| validation.completed | Po każdej walidacji XML (live keys). |
| correction.created | Po wygenerowaniu korekty FA(3). |
| correction.deleted | Po usunięciu korekty z historii. |
| user.added_to_organization | Nowy członek dołączył do orgu. |
| user.removed_from_organization | Członek opuścił / usunięty. |
| organization.plan_changed | Zmiana planu (upgrade/downgrade). |
| quota.exceeded | Pierwsze przekroczenie quoty w danej dobie. |
| ksef.detection_changed | Status systemu KSeF zmienił się (op vs degr). |
| ksef.outage_alert | Wykryto poważną awarię KSeF. |
| partner.trial_started | Akceptacja magic invite - trial planu API. |
| partner.trial_ending_soon | 3 dni przed końcem trialu. |
| partner.trial_ended | Trial zakończony, plan zrolowany. |
Pełna lista i schematy eventów żyją w OpenAPI 3.1 spec (enum WebhookEventType).
Testowanie
W dashboardzie webhooków przycisk Wyślij testowe zdarzenie (lub POST /webhooks/{id}/test) wysyła syntetyczny event i pokazuje czas + status odpowiedzi z Twojego endpointu. Idealne do smoke testu wdrożenia.