---
name: fakturnia-invoicing
description: >-
  Issue Polish invoices and send them to KSeF (the National e-Invoicing System)
  through the Fakturnia External REST API. Use when a task involves creating an
  invoice, proforma, advance, settlement, correction or order for a Polish
  company; computing VAT/gross amounts; downloading an invoice PDF; submitting an
  invoice to KSeF and waiting for the result; managing contractors, products,
  webhooks or KSeF connections; reading warehouse stock and issuing WZ
  documents; managing employee accounts; reading and pausing recurring
  invoicing rules; reading connected email accounts. Covers OAuth2 auth (MAX
  plan gate), the single document-create endpoint,
  every document type and amount rule, annotations (split payment, margin, VAT
  exemption, faktura do paragonu), the async KSeF flow, corrections, webhooks
  (HMAC), the warehouse/employees/recurring modules, errors, rate limits and
  idempotency.
license: Apache-2.0
---

# Fakturnia invoicing & KSeF

Fakturnia is a Polish invoicing platform. Its **External REST API** lets you
issue invoices and submit them to **KSeF** (Krajowy System e-Faktur, Poland's
National e-Invoicing System). This file is the working knowledge to integrate
correctly. It is exhaustive on the parts that bite (amounts, parties, the async
KSeF send, corrections); for the long form of any section, follow the linked
`.md` mirror — those are the authoritative docs in plain Markdown.

**Canonical machine resources** (fetch these directly, no HTML scraping):

| Resource | URL |
|---|---|
| OpenAPI 3 spec (executable contract) | `https://developers.fakturnia.pl/openapi.json` |
| Full docs corpus, one file | `https://developers.fakturnia.pl/llms-full.txt` |
| Docs index | `https://developers.fakturnia.pl/llms.txt` |
| Postman collection | `https://developers.fakturnia.pl/fakturnia.postman_collection.json` |
| CLI (curl wrapper, built-in OAuth) | `https://developers.fakturnia.pl/fakturnia.sh` |
| Any guide as Markdown | `https://developers.fakturnia.pl/<slug>.md` |
| MCP server (act on a user's account from ChatGPT, Claude, Gemini, Mistral Vibe, Perplexity, GitHub Copilot or Cursor, without an API account) | `https://prod.api.fakturnia.pl/mcp` (guide: `https://developers.fakturnia.pl/mcp.md`) |

Every guide is listed with its link in **[All documentation](#all-documentation)**
at the bottom.

## Golden rules (read first)

1. **Start in the sandbox.** Two isolated environments with separate accounts
   and credentials. Rehearse everything — including KSeF sends — in sandbox
   before production. Critical when the integration is LLM-written: a production
   send is a real, legally-binding e-invoice that can only be undone with a
   formal correction.
   - Production: base `https://prod.api.fakturnia.pl/external/v1`, token
     `https://prod.api.fakturnia.pl/v1/oauth/token`
   - Sandbox: base `https://sandbox.api.fakturnia.pl/external/v1`, token
     `https://sandbox.api.fakturnia.pl/v1/oauth/token`
   - The token URL is **outside** `/external/v1`.
2. **Send prices, never totals.** Per line send `netPrice` *or* `grossPrice`
   plus `vatRate`. The server computes `netValue`, `amountOfVat`, `grossValue`.
   Never precompute or "correct" sums — your numbers will drift from the server,
   which is the single source of truth. If you mirror amounts locally, copy them
   from the API response.
3. **Idempotency on every write.** Send a unique `Idempotency-Key` (UUID v4)
   header on `POST`s that create resources. Retry `5xx`/`429` with backoff —
   safe only because of the key. Keys are cached 24h.
4. **Branch on `message`, not human text.** Every response carries a stable
   machine code in `message` (e.g. `DOCUMENT_CREATED`, `VALIDATION_ERROR`,
   `KSEF_SEND_NOT_ALLOWED`). Switch on that; the prose can change.
5. **KSeF needs a full buyer address.** A document can be *created* with just
   `nip` + `name` (and the PDF will render), but a KSeF *send* is rejected
   without the buyer's `street`, `postalCode`, `city`, `countryCode`. If KSeF is
   in play, treat the full address as required.
6. **Dictionaries are the source of truth for enums.** Fetch
   `GET /dictionaries` once at startup instead of hard-coding document types,
   VAT rates, payment methods, GTU, currencies, country codes — so a new entry
   never breaks you.
7. **Forward-compatible responses.** Ignore unknown response fields. The API is
   path-versioned (`/external/v1`); additive changes ship within v1.

## Authentication — OAuth 2.0 client credentials

Exchange `client_id` / `client_secret` for a short-lived bearer JWT. **Cache and
reuse** the token until shortly before `expires_in` (≈3600s); never fetch one
per call. Keep the secret server-side only — never in a browser, mobile app or
public repo, and exclude it from an agent's file reach.

```bash
curl -s https://prod.api.fakturnia.pl/v1/oauth/token \
  -H "Content-Type: application/json" \
  -d '{ "grant_type": "client_credentials", "client_id": "client_...", "client_secret": "secret_..." }'
# -> { "access_token": "eyJ...", "token_type": "Bearer", "expires_in": 3600, "scope": "documents:create ..." }
```

Send `Authorization: Bearer <access_token>` on every request. Bad/missing token
→ `401 UNAUTHORIZED`; expired → `401 TOKEN_EXPIRED`; valid token missing a scope
→ `403 NO_PERMISSIONS` (`errors[0]` names it). **The REST API is a MAX plan
module**: without an active MAX plan the token endpoint answers `403
access_denied` (`error_code: SUBSCRIPTION_REQUIRED`) and every call answers `403
SUBSCRIPTION_REQUIRED`. That is a plan problem, not a scope problem: tell the
user, do not retry. Revoking an API account cuts off its tokens immediately.
`GET /auth/me` returns the identity and granted scopes behind a token (useful
for debugging which app a token belongs to).

**Scopes** (request only what you need):

| Resource | Scopes |
|---|---|
| Documents | `documents:browse` · `documents:read` · `documents:create` · `documents:update` · `documents:delete` · `documents:download` · `documents:send-to-ksef` · `documents:update-payment-status` · `documents:update-external-reference` |
| Clients | `clients:browse` · `clients:read` · `clients:create` · `clients:update` · `clients:delete` |
| Products | `products:browse` · `products:read` · `products:create` · `products:update` · `products:delete` |
| KSeF | `ksef:read` · `ksef:configure` · `ksef:test` |
| Settings | `settings:read` · `settings:update` |
| Webhooks | `webhooks:read` · `webhooks:create` · `webhooks:update` · `webhooks:delete` |
| Email accounts (read-only) | `email-accounts:browse` · `email-accounts:read` |
| Company | `gus:company:read` |
| Warehouse (MAX plan) | `warehouse:browse` · `warehouse:create` · `warehouse:update` · `warehouse:delete` · `warehouse:stock:read` · `warehouse:adjust` · `warehouse:wz:create` · `warehouse:documents:create` |
| Employees | `employees:browse` · `employees:create` · `employees:update` · `employees:delete` |
| Recurring rules (MAX plan) | `recurring:browse` · `recurring:read` · `recurring:toggle` |
| Activity | `activity:read` |

Full guide: `https://developers.fakturnia.pl/authentication.md`

## Creating documents — `POST /external/v1/documents`

One endpoint issues every document type. The **seller is never sent** — it comes
from your company profile. Minimum viable request: `documentType`, `issueDate`,
a `buyer` (full address if KSeF), one line item.

```bash
curl -s -X POST https://prod.api.fakturnia.pl/external/v1/documents \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "documentType": "FakturaVAT",
    "issueDate": "2026-06-10",
    "buyer": { "identifierType": "nip", "nip": "5261040828", "name": "Acme Corp Sp. z o.o.",
      "street": "Marszałkowska 1", "postalCode": "00-001", "city": "Warszawa", "countryCode": "PL" },
    "products": [ { "name": "Usługa programistyczna", "unitOfMeasure": "szt.",
      "quantity": 1, "netPrice": 100, "vatRate": 23 } ],
    "paymentMethodId": 6, "paymentStatusId": 1, "paymentDeadlineDate": "2026-06-24"
  }'
```

Response envelope (read `data`, check `message`):

```json
{ "success": true, "message": "DOCUMENT_CREATED",
  "data": { "document": { "id": "681a...", "documentNumber": "FV/1/06/2026",
    "netSum": 100, "vatSum": 23, "grossSum": 123,
    "products": [ { "netValue": 100, "amountOfVat": 23, "grossValue": 123, "vatRate": 23 } ] } } }
```

Keep `data.document.id` — needed for the PDF, KSeF send, edits.

Full field-by-field guide: `https://developers.fakturnia.pl/creating-documents.md`

### Transaction parties: seller, buyer, receiver

- **Seller** — always your company profile (`user.company`). To change it, edit
  the profile, not the request. The buyer NIP must differ from the seller NIP
  (`422`).
- **Buyer** (required) — pass `buyerId` (existing contractor) **or** an inline
  `buyer` object, not both (`400 AMBIGUOUS_PARTY`; neither → `400 BUYER_REQUIRED`).
  With an inline `nip`, the backend looks up the contractor and creates one if
  none exists — no need to pre-create. For an individual set `type: "person"`
  with `firstName`/`lastName` instead of `name`.
- **Identifiers** — always send `identifierType`: `nip` (Polish NIP, 10 digits,
  check digit verified), `vatEu` with the prefixed number in `vatUE`
  (`"identifierType": "vatEu", "vatUE": "DE123456789"`; Greece `EL`; never a
  foreign number in `nip`), `other` with `countryCode` + `otherTaxIdentifier`
  (non-EU, also Northern Ireland `XI`), `idWew` (receiver only), `none`
  (consumer). Without it the API infers the type and reports it in
  `data.warnings` (`IDENTIFIER_TYPE_INFERRED`, `IDENTIFIER_MOVED`, ...);
  contradictions answer `422 PARTY_IDENTIFIER_INVALID` (documents) or
  `422 VALIDATION_ERROR` (`/clients`) naming the field. Guide:
  `https://developers.fakturnia.pl/clients.md`
- **Receiver (Podmiot3)** (optional) — a *separate entity* that receives the
  invoice, distinct from the buyer. Typical case: local-government units (JST) —
  buyer = the unit holding the NIP (a municipality), receiver = the subordinate
  unit. Pass `receiverId` or inline `receiver`. The receiver may use an internal
  identifier (`identifierType: "idWew"`, `idWew` format `NIP-NNNNN`, e.g.
  `5252248481-16006`). **Only the receiver can have `idWew`** — in FA(3) it
  exists only for Podmiot3. The receiver is NOT a delivery address.

Contractor object fields (same for `buyer`/`receiver`): `id`, `type`
(`company`|`person`), `name` | `firstName`+`lastName`, `nip`, `vatUE`, `pesel`,
`regon`, `krs`, `bdo`, `street`, `postalCode`, `city`, `country`/`countryCode`
(only `countryCode` matters for KSeF/PDF), `email`, `phoneNumber`, `www`,
`identifierType` (`nip`|`vatEu`|`other`|`idWew`|`none`), `otherTaxIdentifier`,
`idWew`, `fieldsToShow[]`.

### Line items (`products`) and the amount rules

Per line send `name`, `unitOfMeasure`, `quantity`, `vatRate`, and `netPrice` OR
`grossPrice`. Optional: `discount` (0–100%), `gtu` (`GTU_01`..`GTU_13`), `pkwiu`.

The backend computes, per line (round half-up to 2 decimals):

```
netValue    = round2(quantity × netPrice × (1 − discount/100))
amountOfVat = round2(netValue × rate / 100)      // NOT net × (rate/100)
grossValue  = round2(netValue + amountOfVat)
round2(x)   = Math.round((x + Number.EPSILON) * 100) / 100   // 0.345 -> 0.35
```

If you send `grossPrice`, math runs downward: `netValue = round2(gross/(1+rate/100))`,
`amountOfVat = round2(gross − netValue)`. **Invariant:** per line
`netValue + amountOfVat === grossValue`; per document `netSum + vatSum === grossSum`.
A one-grosz mismatch is a bug — report it. Same engine as `fakturnia-shared`, so
API and web-app documents are grosz-identical.

**VAT rates** (`vatRate`): numeric `23, 22, 8, 7, 5, 4, 3`, or a special string
code (VAT = 0, net = gross): `zw` (exempt), `np I` / `np II` (not subject), `oo`
(reverse charge), `0 KR` (domestic 0%), `0 WDT` (intra-EU), `0 EX` (export).
There is no bare `0` and no bare `np`. Unknown rate → `400 VALIDATION_ERROR`.

### Document types (all 10) and their extra fields

| Type | What it is | KSeF | Extra required |
|---|---|---|---|
| `Faktura` | Invoice outside KSeF (VAT-exempt entities, until end 2026) | no | — |
| `FakturaVAT` | VAT invoice (KSeF basic-invoice equivalent) | yes | — |
| `FakturaProforma` | Proforma | no | — |
| `FakturaProformaBezVAT` | Proforma without VAT | no | — |
| `FakturaZaliczka` | Advance invoice | yes | `linkedOrderId`, `advancePaymentAmount` (no `products`) |
| `FakturaRozliczeniowa` | Settlement (final) invoice | yes | `linkedAdvanceInvoiceIds`, `products` |
| `FakturaKorygujaca` | Correction | yes | `correctedInvoiceId`, `products` (full "after" state) |
| `FakturaKorygujacaZaliczkowa` | Advance correction | yes | `correctedInvoiceId`, `correctionAdvanceAmountAfter` |
| `FakturaKorygujacaRozliczeniowa` | Settlement correction | yes | `correctedInvoiceId`, `correctionSettlementAmountAfter` |
| `Zamowienie` | Order | no | — |

There are no separate types for split payment / margin / reverse charge / cash
method — use **annotations** (below). Five legacy types are intentionally not
exposed. Use `FakturaVAT` as the basic invoice (it becomes `FakturaPodstawowa`
from 2027-01-01).

**Sums are not always `Σ products`:**
- `FakturaZaliczka`: `grossSum === advancePaymentAmount`; the advance is split
  proportionally across the order's VAT rates. Must not exceed the order
  (`422 ADVANCE_EXCEEDS_ORDER_VALUE`).
- `FakturaRozliczeniowa`: `grossSum = max(0, Σ products.gross − Σ advances.gross)`
  (remaining to pay), unless `isSettlementAmountManual: true` + `settlementAmount`.
- Corrections: `grossSum` is a **delta** (after − before) and can be negative.
  See [Corrections](#corrections).

### Payment, dates, bank accounts

- `paymentMethodId`: `1` Cash · `2` Card · `3` Voucher · `4` Cheque · `5` Credit
  · `6` Transfer · `7` Mobile.
- `paymentStatusId`: `0` Paid (requires `additionalPaymentDate`) · `1` Unpaid
  (requires `paymentDeadlineDate`).
- Instalments (only with `paymentStatusId: 1`): for EQUAL instalments do not compute the amounts,
  send `paymentInstallmentsSplit: { "count": 3, "firstDate"?: "YYYY-MM-DD", "every"?: "month" | "week" | "14days" }`;
  the API splits the amount due to the grosz (remainder on the last one: 100.00 / 3 = 33.33 + 33.33 + 33.34)
  and returns them as `paymentInstallments`. Custom amounts (e.g. a larger first instalment) go in
  `paymentInstallments: [{ "date", "amount" }]` and must add up to the amount due. Never send both.
- For transfer (`6`) supply `bankAccounts[]` (unless a default exists): first is
  primary; fields `accountNumber`, `bank`, `swift`, `accountDescription`,
  `bankOwnAccountType`. Split-payment VAT account: `vatAccountNumber`, etc.
- Dates: `issueDate` (required, `YYYY-MM-DD`), `subDate` (sale/delivery date,
  defaults to `issueDate` for KSeF types). Currency defaults to `PLN`;
  non-PLN takes `exchangeRate`.

### Annotations & additional info (KSeF FA(3) flags)

`annotations`: `isSplitPayment` (MPP), `isReverseCharge`, `isCashMethod`
(mutually exclusive with split payment → `ANNOTATIONS_VALIDATION_ERROR`),
`isSelfInvoicing`, `marginProcedure` ({`enabled`, `procedureId`}),
`vatExemption` ({`enabled`, `exemptionType` `A`/`B`/`C`, `legalBasis`,
`showOnDocument`}).

`additionalInfo.isInvoiceToReceipt: true` → "faktura do paragonu" (art. 109
ust. 3d): sets the `<FP>1</FP>` flag in KSeF. The document type does NOT change —
it stays a normal VAT invoice with a marker. KSeF has no receipt-number field, so
the API accepts none.

### Numbering & validation

Numbering is automatic by default (`symbol` + sequence `documentNo` + date
`numberingFormat`, e.g. `FV/4/05/2026`). Omit `documentNo` to auto-number;
counters are atomic per company+type+period, monthly or yearly. The three
correction types share one counter. Manual: send `documentNo` (≥1), optionally
`documentNumber` for 1:1 validation (mismatch → `422 NUMBER_MISMATCH`; duplicate
→ `409 DOCUMENT_NUMBER_EXISTS`).

**`POST /documents/validate`** runs the full pipeline (numbering, party
resolution, sums) and returns a preview **without persisting** — use it for a
confirmation screen or a dry run.

## Document lifecycle

- **PDF:** `GET /documents/{id}/download` (binary).
- **Read/list:** `GET /documents/{id}`, `GET /documents` (paginated).
- **Edit:** `PUT` / `PATCH /documents/{id}`. **Delete:** `DELETE /documents/{id}`.
  Allowed only by KSeF state: a document with a **KSeF number** is final
  (`400 KSEF_CONFIRMED`); one **sent and awaiting confirmation** is locked
  (`400 KSEF_SENT`); a **failed** send unlocks it. Never-sent documents are
  always editable. Check first with `GET /documents/{id}/ksef-status`.
- **Payment status:** `PATCH /documents/{id}/payment-status`.
- **External reference** (your own id): `PATCH` / `DELETE /documents/{id}/external-reference`.

## Sending to KSeF (two steps, asynchronous)

Create and send are deliberately separate. The send waits briefly for KSeF; if
KSeF needs longer it returns `processing` and finishes in the background — so
**wait for the outcome correctly**. Requires an authorized KSeF connection on
the account (a **test** certificate for sandbox, **production** for production —
not interchangeable) and scope `documents:send-to-ksef`.

```bash
curl -s -X POST https://prod.api.fakturnia.pl/external/v1/documents/{id}/send-to-ksef \
  -H "Authorization: Bearer $TOKEN" -H "Idempotency-Key: $(uuidgen)"
```

The `200` comes in **two shapes** — branch on `data.ksefNumber`:
- **Accepted now:** `data.ksefNumber`, `referenceNumber`, `status.code: 200`,
  `hasUpo: true`. Done — store `ksefNumber`.
- **Still processing:** `data.status: "processing"`. The invoice **was handed to
  KSeF** — do NOT resend. Wait for the result.

**Refusals** (returned before sending, safe to fix and retry):
`409 KSEF_CONNECTION_REQUIRED`, `409 KSEF_ALREADY_PROCESSING`,
`409 KSEF_SEND_NOT_ALLOWED`, `422 KSEF_CONNECTION_NOT_AUTHORIZED`,
`400 VALIDATION_ERROR`/`BAD_REQUEST`.

**Wait for the result** — `GET /documents/{id}/ksef-status` is the source of
truth. Branch on `state`: `not_sent` | `processing` (keep waiting) | `sent`
(`ksefNumber` set, terminal) | `failed` (`lastError` set, terminal). This
endpoint is **rate-limited to 5/min** — poll ~every 15s, give up after ~10 min,
honor `429` + `Retry-After`.

**Recommended pattern:**
1. **Burst-poll** right after send: up to 3 checks, 3s apart (many sends finish
   here, stays under the limit). `sent`/`failed` → done.
2. Still `processing` → stop polling, tell the user, and wait for the
   `document.sentToKSeF` **webhook**.
3. **Backstop:** a periodic job (≈1/min, throttled) reconciles any document
   still `processing` from the status endpoint, so nothing hangs on a missed
   webhook.

On `failed`: the document is unlocked — fix or delete it and resend with a
**fresh** `Idempotency-Key`. Full guide + code:
`https://developers.fakturnia.pl/ksef.md`

## Corrections

Same endpoint, three types. Always send `correctedInvoiceId` and the **full
"after" state** — the backend computes the delta; you never send +/− lines.
Totals are a delta and can be negative; copy them from the response.

- `FakturaKorygujaca`: `products` = full lines after correction. To cancel an
  invoice, resend the **same lines with `quantity: 0`** (keep the unit price).
- `FakturaKorygujacaZaliczkowa`: + `correctionAdvanceAmountAfter`;
  `grossSum = after − advancePaymentAmount(before)`, VAT from the difference of
  proportional splits.
- `FakturaKorygujacaRozliczeniowa`: + `correctionSettlementAmountAfter`; same
  delta logic on the settlement amount.

Errors: `422 CORRECTED_INVOICE_REQUIRED`, `404 CORRECTED_INVOICE_NOT_FOUND`,
`422 CORRECTION_ADVANCE_AMOUNT_REQUIRED`, `422 CORRECTION_ADVANCE_BASE_MISSING`,
`422 CORRECTION_SETTLEMENT_AMOUNT_REQUIRED`. Full guide:
`https://developers.fakturnia.pl/correcting-documents.md`

## Dictionaries, pagination, rate limits

- **Dictionaries:** `GET /dictionaries` (only a valid token, no extra scope)
  returns `documentTypes`, `vatRates`, `paymentMethods`, `paymentStatuses`,
  `currencies` (with `availableExchangeRate`), `countries`, `gtu`,
  `marginProcedures`, `vatExemptionTypes`, `paymentDateHeaders`,
  `additionalDateHeaders`. Fetch once at startup. Guide:
  `https://developers.fakturnia.pl/dictionaries.md`
- **Pagination:** list endpoints (`GET /documents`, `/clients`, `/products`) take
  `currentPage` (1-based), `itemsPerPage` (max 100), `searchQuery`; response includes
  `totalCount` and `totalPages`. Iterate until past `totalPages`. Guide:
  `https://developers.fakturnia.pl/pagination.md`
- **Rate limits** (counted per API account, i.e. per application, whatever IP the
  calls come from; on top, 240 requests/min per IP across all endpoints, checked
  before auth; `429` + `Retry-After`, back off with jitter):
  `POST /documents` and `/validate` 30/60s · `POST /warehouse/documents` 30/60s ·
  `GET /documents|/clients|/products` 10/30s · `send-to-ksef` 10/60s ·
  `company/nip-lookup` 10/60s · `download` 5/60s · `ksef-status` 5/60s ·
  `ksef/connection/test` 5/60s · other endpoints 45/60s. Ignoring `429` triggers a
  growing cooldown. Guide: `https://developers.fakturnia.pl/rate-limits.md`

## Webhooks

Create `POST /webhooks` `{ endpoint, events[], description? }` (201, `secret`
returned once), list `GET /webhooks`, one `GET /webhooks/{id}`, change
`PATCH /webhooks/{id}` (`events` replaces the list; `status: ACTIVE|INACTIVE`),
remove `DELETE /webhooks/{id}`, rotate `POST /webhooks/{id}/rotate-secret`,
debug `GET /webhooks/{id}/deliveries`, ping `POST /webhooks/{id}/test`
(`webhook.test`, 5/min). Plugins may use `POST /webhooks/initial` (all events,
idempotent per URL). One webhook per URL (`409`); private/local URLs → `422`.
Events: `document.created|updated|deleted|sentToKSeF|ksefRejected|paymentStatusChanged|ksefImported`,
`client.created|updated|deleted`, `product.created|updated|deleted`.
Events fire whoever acted: the API, MCP, a recurring rule, the KSeF queue or a
user in the web or mobile app (a draft saved in the app fires `document.created`
too; issuing it, turning it back into a draft or changing `isSent`, `isPrinted`,
`isDownloaded` in the app fires `document.updated`, only on a real change; a
correction also fires `document.updated` for the corrected invoice).
Invoices downloaded from KSeF never fire `document.created`: a sync sends
`document.ksefImported` with `data.documentIds` (up to 100 per delivery, more
deliveries for a larger import). Documents carry `source` (`local`|`ksef`) and
`direction` (`income`|`expense`|`order`): check `direction` before treating a
document as your own sale.
Delivery is a `POST` with headers `X-Webhook-Event`, `X-Webhook-Delivery`
(delivery id, stable across retries), `X-Webhook-Signature`
(`HMAC-SHA256(rawBody, secret)` lowercase hex), `Content-Type: application/json`;
body `{ id, event, timestamp, data: { documentId | clientId | productId | webhookId }, resourceId }`
(schema `WebhookEventPayload` in the OpenAPI spec).

**Verify the signature** over the **raw bytes** in constant time before trusting
anything. Respond `2xx` fast and process async; be idempotent (events can repeat
— dedupe on `id`); treat webhooks as hints and fetch `GET /documents/{id}` (or
`/ksef-status` after `sentToKSeF` / `ksefRejected`) for the authoritative state.
Retries: up to 5 (immediately, +5min, +30min, +2h, +8h); 10 consecutive failures
switch the webhook to `ERROR` (re-enable with `PATCH status: ACTIVE`). No
deliveries without the MAX plan.
Guide: `https://developers.fakturnia.pl/webhooks.md`

## Account modules: warehouse, employees, recurring rules

Three modules beyond invoicing, plus a read-only view of connected email
accounts. The whole REST API requires the **MAX plan** on the account (see
Authentication); `403 SUBSCRIPTION_REQUIRED` is a plan problem, not a scope
problem; do not retry.

**Warehouse** — stock is a ledger of movements; the level is their sum.
- `GET /warehouse` warehouses (CRUD via `POST`/`PUT`/`DELETE`); the first one
  created becomes the default. Deletion is refused (`409`) if the warehouse has
  movements or is the only one.
- `GET /warehouse/stock` is the authoritative stock report (`?asOf=YYYY-MM-DD`
  for a historical level, `?warehouseId=` to narrow). Read levels from here
  instead of keeping your own tally.
- `POST /warehouse/adjustments` records a manual stock adjustment (positive
  quantity = receipt, negative = issue; also used for opening balances). It does
  NOT check the current level — guard against negative stock yourself.
- `POST /warehouse/wz-from-invoice/{id}` issues a WZ (goods-issue document) for
  a sales invoice: only catalog lines with `productType: "goods"` are included
  (skipped names come back in `skipped`), one WZ per invoice (`409` on repeat).
- `POST /warehouse/documents` issues a WZ, PZ, PW, RW or MM from catalog lines
  (`productId`) and books the stock movements (scope `warehouse:documents:create`,
  `Idempotency-Key` supported). `POST /warehouse/documents/validate` is the same
  pipeline without saving: the number, resolved lines and stock before/after.
  An outbound document (WZ, RW, MM) that would take stock below zero is rejected
  with `422 STOCK_SHORTAGE`; `allowNegativeStock: true` issues it anyway, so only
  send it on the user's explicit request.
- Warehouse documents appear on the regular document list:
  `GET /documents?documentType=WZ` (comma-separated list allowed).
  Guide: `https://developers.fakturnia.pl/warehouse.md`

**Employees** — login identities working on the owner's data. Employee
permissions are NOT API scopes: the API token always acts as the owner.
- `GET /employees/permissions` is the permission dictionary (no extra scope
  needed) — fetch it instead of hard-coding keys; unknown keys in writes are
  silently dropped, implied permissions are added automatically.
- `GET`/`POST /employees`, `PUT`/`DELETE /employees/{id}`. Email is unique
  across ALL of Fakturnia (`409 EMAIL_TAKEN`), password min 8 chars, max 20
  accounts (`409 EMPLOYEE_LIMIT_REACHED`). Responses never include passwords.
- Offboarding: `isActive: false` blocks immediately and ends sessions;
  `DELETE` frees the email. A password change does not end sessions.
  Guide: `https://developers.fakturnia.pl/employees.md`

**Recurring rules** — subscription-style invoicing. Rules are created in the
Fakturnia app; the API is deliberately read-only plus pause/resume.
- `GET /recurring-rules` lists rules with a summary (`?status=` filters the
  list, the summary always covers everything; `monthlyRecurringGross` counts
  active PLN rules only). Instead of the internal template each rule carries
  `documentSummary` (type, buyer, gross, currency).
- `GET /recurring-rules/{id}` details, `GET /recurring-rules/{id}/history` the
  generation journal (statuses `GENERATED`/`DRAFT`/`REMINDED`/`SKIPPED`/
  `FAILED`/`PENDING`, with `documentId`/`documentNumber` of issued documents).
- `PATCH /recurring-rules/{id}/toggle` with `{ "active": bool }` pauses or
  resumes. Resuming recomputes `nextRunAt` from today — missed occurrences are
  NOT backfilled. A `FINISHED` rule cannot be resumed (`409`).
- Documents issued by rules emit the normal `document.created` webhook — listen
  for that instead of polling. Guide:
  `https://developers.fakturnia.pl/recurring-rules.md`

## History & activity

Four read-only journals answer "what happened": `GET /documents/{id}/history`
(one document: CREATE, UPDATE with a field diff, SENT_BY_EMAIL, KSEF_SENT,
KSEF_ERROR, PAYMENT_RECEIVED, ...; `actor` owner|employee, `origin`
external_api|app|import; page-based), `GET /activity` (the whole account:
documents, employee logins and management, warehouse; scope `activity:read`;
filters `scope`, `employeeId` or `main`, `from`/`to`, `q`), `GET
/employees/{id}/activity` (one employee) and `GET /warehouse/movements` (stock
ledger; filters `productId`, `warehouseId`, `documentId`, `type`, `dateFrom`,
`dateTo`; page-based). Feeds use a time cursor: pass `nextBefore` as `before`
until `hasMore` is false. Guide: `https://developers.fakturnia.pl/activity.md`

## Errors & resilience

Consistent envelope; per-field problems under `errors[]`. **Branch on `message`.**

```json
{ "success": false, "data": null, "message": "AMBIGUOUS_PARTY", "executionTime": 5,
  "errors": [ "Podaj albo buyerId, albo buyer, ale nie oba." ] }
```

Status codes: `400 INVALID_PARAMS` / `BAD_REQUEST` · `401 UNAUTHORIZED` /
`TOKEN_EXPIRED` · `403 NO_PERMISSIONS` / `SUBSCRIPTION_REQUIRED` / `FORBIDDEN` /
`DOCUMENT_LIMIT_EXCEEDED` · `404` not found · `409` conflict · `422
VALIDATION_ERROR` (body) · `429 RATE_LIMIT_EXCEEDED` / `WAIT_PDF_QUEUE` · `500`.
`VALIDATION_ERROR` is usually `422`, but `POST /documents`, `/documents/validate`
and `/warehouse/documents` (+ `/validate`) answer `400 VALIDATION_ERROR` for
request-shape problems (missing `documentType`/`issueDate`/`products`, bad date,
invalid line): treat both statuses as a validation failure. `AMBIGUOUS_PARTY` and
`BUYER_REQUIRED` are `400` with the code in `message`.
`errors[]` holds `{ field, code?, message }` objects for field problems or plain
sentences otherwise. Every response carries `X-Request-ID` (quote it to support).
Key codes: `VALIDATION_ERROR`, `INVALID_PARAMS`,
`AMBIGUOUS_PARTY`, `BUYER_REQUIRED`, `DOCUMENT_NUMBER_EXISTS`, `KSEF_CONFIRMED`,
`KSEF_SENT`, `CORRECTED_INVOICE_REQUIRED`, `LINKED_ORDER_REQUIRED`,
`ADVANCE_EXCEEDS_ORDER_VALUE`, `BANK_ACCOUNT_REQUIRED`,
`ANNOTATIONS_VALIDATION_ERROR`, `KSEF_CONNECTION_REQUIRED`. Retry `5xx`/`429`
only with the same `Idempotency-Key`. Guide:
`https://developers.fakturnia.pl/errors.md`

## Endpoint reference

| Method & path | Purpose |
|---|---|
| `GET /auth/me` | Identity + granted scopes for the token |
| `GET /documents` | List (paginated) |
| `POST /documents` | Create any document type |
| `POST /documents/validate` | Dry-run create (no persist) |
| `GET /documents/{id}` | Read one |
| `PUT` / `PATCH /documents/{id}` | Replace / partial-update |
| `DELETE /documents/{id}` | Delete (KSeF-state permitting) |
| `GET /documents/{id}/download` | PDF |
| `PATCH /documents/{id}/payment-status` | Update payment status |
| `PATCH` / `DELETE /documents/{id}/external-reference` | Your external id |
| `POST /documents/{id}/send-to-ksef` | Submit to KSeF |
| `GET /documents/{id}/ksef-status` | KSeF outcome (source of truth) |
| `GET /documents/{id}/history` | Document journal (who did what, when; page-based) |
| `GET/POST /clients`, `GET/PUT/DELETE /clients/{id}` | Contractors |
| `GET/POST /products`, `GET/PUT/DELETE /products/{id}` | Products |
| `GET /dictionaries` | All enums |
| `GET/POST/DELETE /ksef/connection`, `POST /ksef/connection/test` | KSeF connection |
| `GET /webhooks`, `POST /webhooks`, `GET|PATCH|DELETE /webhooks/{id}`, `POST /webhooks/{id}/rotate-secret`, `GET /webhooks/{id}/deliveries`, `POST /webhooks/{id}/test`, `POST /webhooks/initial` | Webhooks |
| `GET /email-accounts`, `GET /email-accounts/{id}` | Connected email accounts (read-only, no secrets) |
| `GET /settings`, `PATCH /settings/general`, `PATCH /settings/numbering` | Settings |
| `GET /company/nip-lookup` | Look up a company by NIP (GUS) |
| `GET/POST /warehouse`, `PUT/DELETE /warehouse/{id}` | Warehouses (MAX plan) |
| `GET /warehouse/{id}` | One warehouse |
| `GET /warehouse/stock` | Stock report (`asOf`, `warehouseId`, `productId`) |
| `GET /warehouse/movements` | Stock ledger (filters, page-based) |
| `POST /warehouse/adjustments` | Manual stock adjustment / opening balance |
| `POST /warehouse/wz-from-invoice/{id}` | Auto-WZ for a sales invoice |
| `GET /employees/permissions` | Employee-permission dictionary |
| `GET /employees/{id}`, `GET /employees/{id}/activity` | One employee; the employee's activity feed (cursor) |
| `GET /activity` | Account-wide activity feed (cursor; scope `activity:read`) |
| `GET/POST /employees`, `PUT/DELETE /employees/{id}` | Employee accounts |
| `GET /recurring-rules`, `GET /recurring-rules/{id}` | Recurring rules (MAX plan, read-only) |
| `GET /recurring-rules/{id}/history` | Rule generation journal |
| `PATCH /recurring-rules/{id}/toggle` | Pause / resume a rule |

The OpenAPI spec is the authoritative schema for every parameter and response:
`https://developers.fakturnia.pl/openapi.json`.

## All documentation

Each guide is also available as raw Markdown (`<slug>.md`) for clean parsing.

| Guide | Page | Markdown |
|---|---|---|
| Quickstart | `https://developers.fakturnia.pl/docs/quickstart` | `/quickstart.md` |
| MCP server (ChatGPT, Claude, Claude Code, Gemini, Mistral Vibe, Perplexity, GitHub Copilot, Cursor, Windsurf) | `https://developers.fakturnia.pl/docs/mcp` | `/mcp.md` |
| Authentication | `https://developers.fakturnia.pl/docs/authentication` | `/authentication.md` |
| Document types & amounts | `https://developers.fakturnia.pl/docs/documents` | `/documents.md` |
| Creating documents | `https://developers.fakturnia.pl/docs/creating-documents` | `/creating-documents.md` |
| Sending to KSeF | `https://developers.fakturnia.pl/docs/ksef` | `/ksef.md` |
| Correcting documents | `https://developers.fakturnia.pl/docs/correcting-documents` | `/correcting-documents.md` |
| KSeF connection | `https://developers.fakturnia.pl/docs/ksef-connection` | `/ksef-connection.md` |
| Order references | `https://developers.fakturnia.pl/docs/external-references` | `/external-references.md` |
| Clients (contractors) | `https://developers.fakturnia.pl/docs/clients` | `/clients.md` |
| Products | `https://developers.fakturnia.pl/docs/products` | `/products.md` |
| Settings | `https://developers.fakturnia.pl/docs/settings` | `/settings.md` |
| Warehouse | `https://developers.fakturnia.pl/docs/warehouse` | `/warehouse.md` |
| Employees & permissions | `https://developers.fakturnia.pl/docs/employees` | `/employees.md` |
| Recurring documents | `https://developers.fakturnia.pl/docs/recurring-rules` | `/recurring-rules.md` |
| Company lookup (NIP) | `https://developers.fakturnia.pl/docs/company-lookup` | `/company-lookup.md` |
| PDF download queue | `https://developers.fakturnia.pl/docs/pdf-download-queue` | `/pdf-download-queue.md` |
| Errors | `https://developers.fakturnia.pl/docs/errors` | `/errors.md` |
| Dictionaries | `https://developers.fakturnia.pl/docs/dictionaries` | `/dictionaries.md` |
| Rate limits | `https://developers.fakturnia.pl/docs/rate-limits` | `/rate-limits.md` |
| Pagination | `https://developers.fakturnia.pl/docs/pagination` | `/pagination.md` |
| Idempotency | `https://developers.fakturnia.pl/docs/idempotency` | `/idempotency.md` |
| Webhooks | `https://developers.fakturnia.pl/docs/webhooks` | `/webhooks.md` |
| Email accounts | `https://developers.fakturnia.pl/docs/email-accounts` | `/email-accounts.md` |
| History & activity | `https://developers.fakturnia.pl/docs/activity` | `/activity.md` |
| Tools (Postman, CLI, codegen) | `https://developers.fakturnia.pl/docs/tools` | `/tools.md` |
| For agents & bots | `https://developers.fakturnia.pl/docs/agents` | `/agents.md` |
| Changelog | `https://developers.fakturnia.pl/docs/changelog` | `/changelog.md` |
| API reference (interactive) | `https://developers.fakturnia.pl/api` | — |
| OpenAPI spec | — | `https://developers.fakturnia.pl/openapi.json` |
| Full corpus (one file) | — | `https://developers.fakturnia.pl/llms-full.txt` |

Generate a client from the OpenAPI spec, or use
the ready-made Postman collection and the `fakturnia.sh` CLI (built-in OAuth)
linked at the top.
