Developers/Webhooks

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-Signature z 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.

Auto-disable po 100 failure'ach
Endpoint który w ciągu 24 h zaliczył ≥100 nieudanych dostaw zostaje automatycznie wyłączony (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:

http
X-NK-Signature: t=1716494400,v1=8e6b3d2f9a4c1e6d...

Podpis obliczany jest jako HMAC-SHA256(secret, t + "." + raw_body). Tolerancja replay-window: 5 minut.

Weryfikacja - TypeScript

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
<?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)

javascript
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

TypKiedy
validation.completedPo każdej walidacji XML (live keys).
correction.createdPo wygenerowaniu korekty FA(3).
correction.deletedPo usunięciu korekty z historii.
user.added_to_organizationNowy członek dołączył do orgu.
user.removed_from_organizationCzłonek opuścił / usunięty.
organization.plan_changedZmiana planu (upgrade/downgrade).
quota.exceededPierwsze przekroczenie quoty w danej dobie.
ksef.detection_changedStatus systemu KSeF zmienił się (op vs degr).
ksef.outage_alertWykryto poważną awarię KSeF.
partner.trial_startedAkceptacja magic invite - trial planu API.
partner.trial_ending_soon3 dni przed końcem trialu.
partner.trial_endedTrial 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.

webhook.site dla lokalnych testów
Najszybszy sposób na przetestowanie subskrypcji bez wystawiania publicznego serwera: utwórz webhook ze URL-em z webhook.site i kliknij „Wyślij testowe zdarzenie".