KSeF Faktury
EN
Join the start list
Early access

The API is in early access: we hand out test keys (KSeF TEST) in batches to people on the launch list. Live mode comes later.

Request access

Guides

Webhooks

Instead of polling for an invoice's status, register a URL. When KSeF accepts or rejects a document, we send a signed event.

Event types

Events are created when a document reaches a final state.

Event typeWhen
invoice.acceptedKSeF accepted the invoice: it has a KSeF number and the PDF with the QR code is ready or about to be.
invoice.rejectedKSeF rejected the invoice. Code and text in last_error (e.g. ksef_duplicate_number).
invoice.skippedThe order cannot be invoiced (e.g. no NIP, no VAT rate). Reason in last_error.
correction.acceptedKSeF accepted the correction invoice.
correction.rejectedKSeF rejected the correction invoice.
correction.skippedThe refund cannot become a correction (e.g. the original invoice was rejected).

Registering an endpoint

Register a URL through the API. The whsec_... secret comes only in this one response: store it right away. enabled_events: ["*"] means every type. Live mode needs an https:// URL; test mode also allows http://. Each mode has up to 16 endpoints.

POST /v1/webhook_endpoints
curl -X POST https://app.ksefshop.pl/v1/webhook_endpoints \
  -H "Authorization: Bearer $KSEF_API_KEY" \
  -H "Idempotency-Key: webhook-shop-backend" \
  -H "Content-Type: application/json" \
  -d '{
  "url": "https://example.com/ksef/webhook",
  "enabled_events": [
    "*"
  ],
  "description": "Shop backend"
}'

What arrives

A POST with an Event in the body and two headers: KSeF-Signature: t=<unix>,v1=<hex> and KSeF-Event-Id. data.object holds the invoice or correction as it was at the time of the event.

Response
{
  "id": "evt_Qa1Ws2Ed3Rf4Tg5Yh6Uj7Ik8",
  "object": "event",
  "type": "invoice.accepted",
  "livemode": false,
  "created": 1760865214,
  "data": {
    "object": {
      "id": "inv_4fJ2kP9xQwErTy7uI0oPaSdF",
      "object": "invoice",
      "livemode": false,
      "status": "accepted",
      "order": {
        "external_id": "SO-1001",
        "number": "1001"
      },
      "number": "FV/2026/10/1",
      "ksef_number": "4235405606-20261019-0E7570400000-57",
      "ksef_environment": "test",
      "issue_date": "2026-10-19",
      "currency": "PLN",
      "total_gross": "163.99",
      "buyer_nip": "5260250274",
      "pdf_url": "https://app.ksefshop.pl/pdf/cmgxa1b2c0000/Qk9tZ2FsX3ZhbGlkX3Rva2VuX2V4YW1w",
      "last_error": null,
      "created": 1760865185,
      "updated": 1760865214,
      "corrections": []
    }
  }
}

Verifying the signature

v1 is an HMAC-SHA256 keyed with your secret over the text t + "." + raw body. To trust an event:

  1. Split the KSeF-Signature header into t and v1.
  2. Reject the event when t is older than 5 minutes (replay protection).
  3. Compute the HMAC over the raw body, before your framework parses it. Re-serialized JSON has different bytes and the signature will not match.
  4. Compare in constant time (hash_equals, timingSafeEqual, compare_digest).
Verifying KSeF-Signature
# Send your receiver a test event signed exactly like ours
SECRET=whsec_shownOnlyOnceExampleValue0000
BODY='{"id":"evt_test","object":"event","type":"invoice.accepted","livemode":false,"created":1760865214,"data":{"object":{"id":"inv_test"}}}'
T=$(date +%s)
SIG=$(printf '%s.%s' "$T" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | sed 's/^.* //')
curl -X POST http://localhost:8000/ksef/webhook \
  -H "Content-Type: application/json" \
  -H "KSeF-Signature: t=$T,v1=$SIG" \
  -H "KSeF-Event-Id: evt_test" \
  -d "$BODY"

The Shell tab sends your receiver a test event signed exactly like ours, so you can check verification locally without KSeF.

Responses and retries

Any 2xx answer counts as delivered. Answer quickly and do heavy work in the background. When the answer is not 2xx or never comes, we retry:

Attempt After
1 1 minute
2 5 minutes
3 30 minutes
4 2 hours
5 5 hours
6 10 hours
7 24 hours
8 24 hours

After eight retries (about 3 days in total) we stop, but the event stays at /v1/events.

Duplicates and order

Delivery is at least once and not ordered. The same event can arrive twice, so store handled KSeF-Event-Id values and skip repeats. When you need the current state, fetch the document with GET /v1/invoices/{id}.

Reconciling with the event list

If your server was down, fetch the events you did not handle:

GET /v1/events
curl "https://app.ksefshop.pl/v1/events?limit=10" \
  -H "Authorization: Bearer $KSEF_API_KEY"

Local testing

Our server has to reach your URL, so localhost will not work. For tests use a tunnel (e.g. ngrok or Cloudflare Tunnel) and register its URL in test mode.

KSeF Faktury

API early access

We hand out test keys in batches. Leave your email: we will write when your access is ready.