Wystaw / API v1.0.0

Wystaw API

Wystawiaj faktury z KSeF z własnego sklepu, CRM-u lub skryptu. Każdy endpoint woła tę samą akcję co aplikacja — uprawnienia, limity planu i walidacja działają identycznie. Plan Pro.

Szybki start

  1. Utwórz klucz w Ustawienia → API (plan Pro). Klucz wst_live_… pokazujemy raz.
  2. Sprawdź klucz, plan i limity (działa z każdym scope):
export WYSTAW_API=https://wystaw.io/api/v1
export WYSTAW_API_KEY=wst_live_…   # nigdy w URL ani w kodzie przeglądarki

curl -sS "$WYSTAW_API/me" -H "Authorization: Bearer $WYSTAW_API_KEY"

Wystaw fakturę od razu (?issue=true): numer zostaje nadany, a wysyłka do KSeF trafia do kolejki. O przyjęciu przez KSeF dowiesz się z webhooka invoice.ksef.accepted.

{
  "brandId": "<brandId z GET /brands>",
  "customerId": "<id z POST /customers>",
  "type": "vat",
  "buyer": {
    "name": "Kawiarnia Pod Lipą Sp. z o.o.",
    "nip": "2860913907",
    "vatUe": null,
    "otherTaxId": null,
    "address": {
      "street": "ul. Lipowa",
      "buildingNo": "5",
      "apartmentNo": null,
      "postalCode": "31-001",
      "city": "Kraków",
      "country": "PL"
    },
    "email": "[email protected]"
  },
  "issueDate": "2026-10-15",
  "saleDate": "2026-10-15",
  "dueDate": "2026-10-29",
  "placeOfIssue": null,
  "currency": "PLN",
  "lang": "pl",
  "lines": [
    {
      "productId": "<id z GET /products>",
      "name": "Projekt logo",
      "qty": "1",
      "unit": "usł.",
      "unitPriceNet": "1500.00",
      "vatRate": "23",
      "discountPct": null
    }
  ],
  "paymentMethod": "transfer",
  "bankAccountId": "<id z GET /bank-accounts>",
  "notes": null,
  "reverseCharge": false,
  "splitPayment": false,
  "exemption": null,
  "correction": null
}
curl -sS -X POST "$WYSTAW_API/invoices?issue=true" \
  -H "Authorization: Bearer $WYSTAW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d @faktura.json

curl -sS "$WYSTAW_API/invoices/$ID/pdf?lang=en" -H "Authorization: Bearer $WYSTAW_API_KEY" -o faktura.pdf

# po przyjęciu przez KSeF: e-mail z PDF i XML (domyślnie do nabywcy)
curl -sS -X POST "$WYSTAW_API/invoices/$ID/send" -H "Authorization: Bearer $WYSTAW_API_KEY"

Kwoty to stringi dziesiętne ("1500.00"), daty ISO 8601. Klient TypeScript: pakiet packages/sdk (openapi-fetch + typy z openapi.json).

Klucze i scope

Nagłówek Authorization: Bearer wst_live_… (lub wst_test_…). Klucz działa jak członek zespołu (rola member) ograniczony do swoich scope'ów; klucz w adresie URL jest odrzucany (400). Maks. 10 aktywnych kluczy na firmę; unieważnienie działa natychmiast. CORS jest wyłączony — API jest dla serwerów, nie przeglądarek.

Scope → endpointy
ScopeEndpointy
invoices:readGET /invoices, GET /invoices/{id}
invoices:writePOST /invoices, PUT /invoices/{id}, DELETE /invoices/{id}, POST /invoices/{id}/issue, POST /invoices/{id}/send, POST /invoices/{id}/corrections, POST /invoices/{id}/mark-paid
customers:readGET /customers, GET /customers/lookup, GET /customers/{id}
customers:writePOST /customers, PATCH /customers/{id}, DELETE /customers/{id}
products:readGET /products, GET /products/{id}
products:writePOST /products, PATCH /products/{id}, PUT /products/{id}, POST /products/{id}/archive
brands:readGET /brands, GET /bank-accounts
files:readGET /invoices/{id}/pdf, GET /invoices/{id}/xml, GET /invoices/{id}/upo
webhooks:manageGET /webhooks, POST /webhooks, DELETE /webhooks/{id}, POST /webhooks/{id}/test, POST /webhooks/{id}/enable

Idempotencja

POST /invoices wymaga nagłówka Idempotency-Key (np. UUID; pozostałe POST-y go akceptują). Ponowienie z tym samym kluczem i treścią przez 24 h zwraca zapisaną odpowiedź (nagłówek Idempotent-Replayed: true) — bez duplikatu faktury. Ten sam klucz z inną treścią → 422; równoległe żądanie z tym samym kluczem → 409.

Limity

60 żądań na minutę i 600 na godzinę na klucz (okno przesuwne). Każda odpowiedź ma nagłówki RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset; po przekroczeniu 429 rate_limited z Retry-After. Limity planu (np. liczba faktur) → 429 plan_limit; brak planu Pro → 402 plan_required. Body ≤ 1 MB.

Błędy

Każdy błąd to application/problem+json (RFC 9457) ze stałym polem code; walidacja zwraca listę pól w errors. Wszystkie kody (23).

HTTP/1.1 400 Bad Request
Content-Type: application/problem+json

{
  "type": "https://wystaw.io/developers/errors#validation_failed",
  "title": "Validation failed",
  "status": 400,
  "detail": "Invalid input",
  "code": "validation_failed",
  "errors": [{ "field": "lines.0.unitPriceNet", "message": "Invalid decimal" }],
  "instance": "/api/v1/invoices",
  "requestId": "1b9d…"
}

Webhooki

Zdarzenia: invoice.issued, invoice.ksef.accepted, invoice.ksef.rejected, invoice.offline.pending, invoice.paid. Wysyłamy POST JSON na adres HTTPS; odpowiedz 2xx w 10 s. Błędy ponawiamy 5× z rosnącym odstępem; endpoint, który przez 24 h nie przyjął żadnego zdarzenia, wyłączamy i wysyłamy e-mail. Włączysz go ponownie w ustawieniach API albo przez POST /webhooks/{id}/enable. Dane w data to stan faktury w chwili wysyłki.

{
  "id": "6f1c…",                 // stałe między ponowieniami — deduplikuj po nim
  "event": "invoice.ksef.accepted",
  "createdAt": "2026-10-15T10:12:03.120Z",
  "tenantId": "0c4e…",
  "data": {
    "invoiceId": "a8d2…",
    "number": "FV/2026/10/001",
    "status": "accepted",
    "ksefNumber": "6260181591-20261015-0A1B2C3D4E5F-7A"
  }
}

Nagłówek Wystaw-Signature: t=…,v1=… to HMAC-SHA256 sekretem endpointu (whsec_…, pokazywany raz) z "<t>.<surowe body>". Weryfikuj na surowym body, przed parsowaniem JSON:

import { createHmac, timingSafeEqual } from 'node:crypto';

/** Wystaw-Signature: t=<unix>,v1=<hex hmac-sha256(secret, "<t>.<raw body>")> */
export function verifyWystawSignature(rawBody: string, header: string, secret: string): boolean {
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=', 2)));
  const t = Number(parts.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false; // replay window 5 min
  const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest();
  const got = Buffer.from(String(parts.v1 ?? ''), 'hex');
  return got.length === expected.length && timingSafeEqual(got, expected);
}

Endpointy

InvoicesDrafts, issuing (KSeF), corrections, payments, files.

GET/api/v1/invoices

List invoices

Scope: invoices:read · akcje: listInvoices

Newest first, cursor-paginated (`nextCursor`). Arrays (`status`, `type`) accept comma-separated values.

Parametry GET /invoices
PoleTyp
brandIdquery · string (uuid)
customerIdquery · string (uuid)
statusquery · "draft" | "issued" | "sending" | "accepted" | "rejected" | "offline" | "accepted_offksef"[]
typequery · "vat" | "advance" | "correction" | "proforma"[]
langquery · "pl" | "en" | "pl-en"
paymentStatusquery · "unpaid" | "partially_paid" | "paid"
paymentMethodquery · "transfer" | "cash" | "card" | "other"
dateFromquery · string
dateToquery · string
qquery · string
cursorquery · string
limitquery · integer

Odpowiedzi: 200 JSON · błędy: problem+json

curl -sS "$WYSTAW_API/invoices?status=accepted,issued&limit=20" \
  -H "Authorization: Bearer $WYSTAW_API_KEY"
TypeScript (fetch)
const res = await fetch(`${WYSTAW_API}/invoices?status=accepted,issued&limit=20`, {
  headers: { Authorization: `Bearer ${process.env.WYSTAW_API_KEY}` },
});
if (!res.ok) throw new Error((await res.json()).detail); // application/problem+json
const data = await res.json();

POST/api/v1/invoices

Create an invoice (draft, or issued with ?issue=true)

Scope: invoices:write · akcje: createInvoiceDraft → issueInvoice

Creates a draft. With `?issue=true` the draft is issued right away (number assigned, KSeF queued). Requires `Idempotency-Key`. If issuing fails the draft stays — the problem carries `invoiceId`. Advance invoices (`type: "advance"`) are not supported yet → 422 `not_supported_yet` (`feature: "advance_invoice"`); the value stays in the schema for forward compatibility.

Parametry POST /invoices
PoleTyp
issuequery · booleanIssue right away (assigns the number and queues KSeF).
offline24query · booleanWith `issue=true`: issue in offline24 mode.
skipKsefquery · booleanWith `issue=true`: "Pomiń KSeF" — consumer invoice kept outside KSeF (status `accepted_offksef`). Not allowed for a buyer with a Polish NIP (400 `validation`).
paidAtquery · stringWith `issue=true`: already paid on this date (payment recorded at issue, like `/paid`).
paidAmountquery · stringWith `paidAt`: amount paid; omitted = paid in full.
paymentMethodquery · "transfer" | "cash" | "card" | "other"With `paidAt`: payment method actually used.
Idempotency-Key *header · stringUnique key (e.g. UUID) — a retry with the same key and payload returns the stored response (24 h).
Body: InvoiceDraftInput
Body POST /invoices
PoleTyp
brandId *string (uuid)
customerId *string (uuid) | null
type *"vat" | "advance" | "correction" | "proforma"
buyer *object
issueDate *string
saleDate *string
dueDate *string | null
placeOfIssue *string | null
currency *"PLN" | "EUR" | "USD"
lang *"pl" | "en" | "pl-en"
lines *object[]
paymentMethod *"transfer" | "cash" | "card" | "other"
bankAccountId *string (uuid) | null
notes *string | null
reverseChargeboolean
splitPaymentboolean
exemption *object | null
correction *object | null

Odpowiedzi: 201 JSON · 422 JSON · błędy: problem+json

curl -sS -X POST "$WYSTAW_API/invoices" \
  -H "Authorization: Bearer $WYSTAW_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d @faktura.json
TypeScript (fetch)
const res = await fetch(`${WYSTAW_API}/invoices`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.WYSTAW_API_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': crypto.randomUUID() },
  body: JSON.stringify(invoice),
});
if (!res.ok) throw new Error((await res.json()).detail); // application/problem+json
const data = await res.json();

GET/api/v1/invoices/{id}

Get an invoice with files and KSeF submissions

Scope: invoices:read · akcje: getInvoice

Parametry GET /invoices/{id}
PoleTyp
id *path · string (uuid)

Odpowiedzi: 200 JSON · błędy: problem+json

curl -sS "$WYSTAW_API/invoices/$ID" \
  -H "Authorization: Bearer $WYSTAW_API_KEY"
TypeScript (fetch)
const res = await fetch(`${WYSTAW_API}/invoices/${id}`, {
  headers: { Authorization: `Bearer ${process.env.WYSTAW_API_KEY}` },
});
if (!res.ok) throw new Error((await res.json()).detail); // application/problem+json
const data = await res.json();

PUT/api/v1/invoices/{id}

Replace a draft

Scope: invoices:write · akcje: updateInvoiceDraft

Only drafts can be changed; issued invoices are corrected with `/corrections`. Advance invoices (`type: "advance"`) are not supported yet → 422 `not_supported_yet` (`feature: "advance_invoice"`); the value stays in the schema for forward compatibility.

Parametry PUT /invoices/{id}
PoleTyp
id *path · string (uuid)
Body: InvoiceDraftInput
Body PUT /invoices/{id}
PoleTyp
brandId *string (uuid)
customerId *string (uuid) | null
type *"vat" | "advance" | "correction" | "proforma"
buyer *object
issueDate *string
saleDate *string
dueDate *string | null
placeOfIssue *string | null
currency *"PLN" | "EUR" | "USD"
lang *"pl" | "en" | "pl-en"
lines *object[]
paymentMethod *"transfer" | "cash" | "card" | "other"
bankAccountId *string (uuid) | null
notes *string | null
reverseChargeboolean
splitPaymentboolean
exemption *object | null
correction *object | null

Odpowiedzi: 200 JSON · 422 JSON · błędy: problem+json

curl -sS -X PUT "$WYSTAW_API/invoices/$ID" \
  -H "Authorization: Bearer $WYSTAW_API_KEY" \
  -H "Content-Type: application/json" \
  -d @body.json
TypeScript (fetch)
const res = await fetch(`${WYSTAW_API}/invoices/${id}`, {
  method: 'PUT',
  headers: { Authorization: `Bearer ${process.env.WYSTAW_API_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify(body),
});
if (!res.ok) throw new Error((await res.json()).detail); // application/problem+json
const data = await res.json();

DELETE/api/v1/invoices/{id}

Delete a draft

Scope: invoices:write · akcje: deleteInvoiceDraft

Parametry DELETE /invoices/{id}
PoleTyp
id *path · string (uuid)

Odpowiedzi: 204 · błędy: problem+json

curl -sS -X DELETE "$WYSTAW_API/invoices/$ID" \
  -H "Authorization: Bearer $WYSTAW_API_KEY"
TypeScript (fetch)
const res = await fetch(`${WYSTAW_API}/invoices/${id}`, {
  method: 'DELETE',
  headers: { Authorization: `Bearer ${process.env.WYSTAW_API_KEY}` },
});
if (!res.ok) throw new Error((await res.json()).detail); // application/problem+json
// 204 No Content

POST/api/v1/invoices/{id}/issue

Issue a draft

Scope: invoices:write · akcje: issueInvoice

Assigns the number and queues the KSeF submission. Status changes are pushed as webhooks. `skipKsef: true` keeps a consumer invoice outside KSeF (status `accepted_offksef`, PDF e-mailed to the buyer; only `invoice.issued` is emitted). `paidAt` (+ `paidAmount`, `paymentMethod`) records a payment received before issue (FA(3) `Zaplacono`/`ZaplataCzesciowa`; `invoice.paid` when paid in full). Drafts of kinds that are not supported yet (advance invoices, corrections of advance or foreign-currency invoices) cannot be issued (422 `not_supported_yet`) — delete them instead.

Parametry POST /invoices/{id}/issue
PoleTyp
id *path · string (uuid)
Idempotency-Keyheader · stringUnique key (e.g. UUID) — a retry with the same key and payload returns the stored response (24 h).
Body: IssueInvoiceRequest
Body POST /invoices/{id}/issue
PoleTyp
offline24boolean
confirmationTokenstring
skipKsefboolean
paidAtstring
paidAmountstring
paymentMethod"transfer" | "cash" | "card" | "other"

Odpowiedzi: 200 JSON · 422 JSON · błędy: problem+json

curl -sS -X POST "$WYSTAW_API/invoices/$ID/issue" \
  -H "Authorization: Bearer $WYSTAW_API_KEY" \
  -H "Content-Type: application/json"
TypeScript (fetch)
const res = await fetch(`${WYSTAW_API}/invoices/${id}/issue`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.WYSTAW_API_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify(body),
});
if (!res.ok) throw new Error((await res.json()).detail); // application/problem+json
const data = await res.json();

POST/api/v1/invoices/{id}/send

E-mail the invoice to the customer

Scope: invoices:write · akcje: sendInvoiceEmail

Queues an e-mail with the PDF and the FA(3) XML — to the buyer e-mail of the invoice or to `to` (max 5), in the invoice language or `lang`. Only invoices accepted by KSeF, issued offline, or issued proformas (otherwise 409 `invalid_state`). Without `to` and without a buyer e-mail → 400.

Parametry POST /invoices/{id}/send
PoleTyp
id *path · string (uuid)
Idempotency-Keyheader · stringUnique key (e.g. UUID) — a retry with the same key and payload returns the stored response (24 h).
Body: SendInvoiceRequest
Body POST /invoices/{id}/send
PoleTyp
tostring (email)[]
lang"pl" | "en" | "pl-en"

Odpowiedzi: 202 JSON · błędy: problem+json

curl -sS -X POST "$WYSTAW_API/invoices/$ID/send" \
  -H "Authorization: Bearer $WYSTAW_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"to":["[email protected]"],"lang":"en"}'
TypeScript (fetch)
const res = await fetch(`${WYSTAW_API}/invoices/${id}/send`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.WYSTAW_API_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({"to":["[email protected]"],"lang":"en"}),
});
if (!res.ok) throw new Error((await res.json()).detail); // application/problem+json
const data = await res.json();

POST/api/v1/invoices/{id}/corrections

Create a correction draft

Scope: invoices:write · akcje: createCorrection

Corrections of advance invoices (KOR_ZAL) and of foreign-currency (non-PLN) invoices are not supported yet → 422 `not_supported_yet` (`feature: "advance_correction" | "fx_correction"`).

Parametry POST /invoices/{id}/corrections
PoleTyp
id *path · string (uuid)
Idempotency-Keyheader · stringUnique key (e.g. UUID) — a retry with the same key and payload returns the stored response (24 h).
Body: CreateCorrectionRequest
Body POST /invoices/{id}/corrections
PoleTyp
reason *string
effect *"1" | "2" | "3"

Odpowiedzi: 201 JSON · 422 JSON · błędy: problem+json

curl -sS -X POST "$WYSTAW_API/invoices/$ID/corrections" \
  -H "Authorization: Bearer $WYSTAW_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"reason":"Rabat posprzedażowy","effect":"1"}'
TypeScript (fetch)
const res = await fetch(`${WYSTAW_API}/invoices/${id}/corrections`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.WYSTAW_API_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({"reason":"Rabat posprzedażowy","effect":"1"}),
});
if (!res.ok) throw new Error((await res.json()).detail); // application/problem+json
const data = await res.json();

POST/api/v1/invoices/{id}/mark-paid

Record a payment

Scope: invoices:write · akcje: markPaid

`amount: null` marks the invoice as paid in full (fires `invoice.paid`).

Parametry POST /invoices/{id}/mark-paid
PoleTyp
id *path · string (uuid)
Idempotency-Keyheader · stringUnique key (e.g. UUID) — a retry with the same key and payload returns the stored response (24 h).
Body: MarkInvoicePaidRequest
Body POST /invoices/{id}/mark-paid
PoleTyp
paidAt *string
amountstring | nullnull = paid in full

Odpowiedzi: 200 JSON · błędy: problem+json

curl -sS -X POST "$WYSTAW_API/invoices/$ID/mark-paid" \
  -H "Authorization: Bearer $WYSTAW_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"paidAt":"2026-10-15","amount":null}'
TypeScript (fetch)
const res = await fetch(`${WYSTAW_API}/invoices/${id}/mark-paid`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.WYSTAW_API_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({"paidAt":"2026-10-15","amount":null}),
});
if (!res.ok) throw new Error((await res.json()).detail); // application/problem+json
const data = await res.json();

GET/api/v1/invoices/{id}/pdf

Download the PDF

Scope: files:read · akcje: getInvoiceFileUrl

`lang` = `pl`, `en` or `pl-en` (bilingual). Generated on first request.

Parametry GET /invoices/{id}/pdf
PoleTyp
id *path · string (uuid)
langquery · "pl" | "en" | "pl-en"

Odpowiedzi: 200 application/pdf · 202 JSON · błędy: problem+json

curl -sS "$WYSTAW_API/invoices/$ID/pdf?lang=en" \
  -H "Authorization: Bearer $WYSTAW_API_KEY" \
  -o faktura.pdf
TypeScript (fetch)
const res = await fetch(`${WYSTAW_API}/invoices/${id}/pdf?lang=en`, {
  headers: { Authorization: `Bearer ${process.env.WYSTAW_API_KEY}` },
});
if (!res.ok) throw new Error((await res.json()).detail); // application/problem+json
const file = Buffer.from(await res.arrayBuffer());

GET/api/v1/invoices/{id}/xml

Download the FA(3) XML

Scope: files:read · akcje: getInvoiceFileUrl

Parametry GET /invoices/{id}/xml
PoleTyp
id *path · string (uuid)

Odpowiedzi: 200 application/xml · błędy: problem+json

curl -sS "$WYSTAW_API/invoices/$ID/xml" \
  -H "Authorization: Bearer $WYSTAW_API_KEY"
TypeScript (fetch)
const res = await fetch(`${WYSTAW_API}/invoices/${id}/xml`, {
  headers: { Authorization: `Bearer ${process.env.WYSTAW_API_KEY}` },
});
if (!res.ok) throw new Error((await res.json()).detail); // application/problem+json
const file = Buffer.from(await res.arrayBuffer());

GET/api/v1/invoices/{id}/upo

Download the UPO (KSeF receipt)

Scope: files:read · akcje: getInvoiceFileUrl

Parametry GET /invoices/{id}/upo
PoleTyp
id *path · string (uuid)

Odpowiedzi: 200 application/xml · błędy: problem+json

curl -sS "$WYSTAW_API/invoices/$ID/upo" \
  -H "Authorization: Bearer $WYSTAW_API_KEY"
TypeScript (fetch)
const res = await fetch(`${WYSTAW_API}/invoices/${id}/upo`, {
  headers: { Authorization: `Bearer ${process.env.WYSTAW_API_KEY}` },
});
if (!res.ok) throw new Error((await res.json()).detail); // application/problem+json
const file = Buffer.from(await res.arrayBuffer());

CustomersCustomers and company lookup (GUS, VIES).

GET/api/v1/customers

List customers

Scope: customers:read · akcje: listCustomers

Parametry GET /customers
PoleTyp
qquery · string
cursorquery · string
limitquery · integer

Odpowiedzi: 200 JSON · błędy: problem+json

curl -sS "$WYSTAW_API/customers?q=kawiarnia" \
  -H "Authorization: Bearer $WYSTAW_API_KEY"
TypeScript (fetch)
const res = await fetch(`${WYSTAW_API}/customers?q=kawiarnia`, {
  headers: { Authorization: `Bearer ${process.env.WYSTAW_API_KEY}` },
});
if (!res.ok) throw new Error((await res.json()).detail); // application/problem+json
const data = await res.json();

POST/api/v1/customers

Create a customer

Scope: customers:write · akcje: upsertCustomer

Parametry POST /customers
PoleTyp
Idempotency-Keyheader · stringUnique key (e.g. UUID) — a retry with the same key and payload returns the stored response (24 h).
Body: CreateCustomerRequest
Body POST /customers
PoleTyp
kind *"company" | "person"
name *string
nip *string | null
vatUe *object | null
otherTaxId *string | null
address *object
email *string (email) | null
defaultLang *"pl" | "en" | "pl-en"
defaultCurrency *"PLN" | "EUR" | "USD"
notes *string | null

Odpowiedzi: 201 JSON · błędy: problem+json

curl -sS -X POST "$WYSTAW_API/customers" \
  -H "Authorization: Bearer $WYSTAW_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"kind":"company","name":"Kawiarnia Pod Lipą Sp. z o.o.","nip":"2860913907","vatUe":null,"otherTaxId":null,"address":{"street":"ul. Lipowa","buildingNo":"5","apartmentNo":null,"postalCode":"31-001","city":"Kraków","country":"PL"},"email":"[email protected]","defaultLang":"pl","defaultCurrency":"PLN","notes":null}'
TypeScript (fetch)
const res = await fetch(`${WYSTAW_API}/customers`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.WYSTAW_API_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({"kind":"company","name":"Kawiarnia Pod Lipą Sp. z o.o.","nip":"2860913907","vatUe":null,"otherTaxId":null,"address":{"street":"ul. Lipowa","buildingNo":"5","apartmentNo":null,"postalCode":"31-001","city":"Kraków","country":"PL"},"email":"[email protected]","defaultLang":"pl","defaultCurrency":"PLN","notes":null}),
});
if (!res.ok) throw new Error((await res.json()).detail); // application/problem+json
const data = await res.json();

GET/api/v1/customers/lookup

Look up a company by NIP or VAT-UE number

Scope: customers:read · akcje: lookupCompany

GUS / Biała lista (NIP) or VIES (VAT-UE) — use the result to prefill a customer.

Parametry GET /customers/lookup
PoleTyp
nipquery · string
vatUequery · object

Odpowiedzi: 200 JSON · błędy: problem+json

curl -sS "$WYSTAW_API/customers/lookup?nip=2860913907" \
  -H "Authorization: Bearer $WYSTAW_API_KEY"
TypeScript (fetch)
const res = await fetch(`${WYSTAW_API}/customers/lookup?nip=2860913907`, {
  headers: { Authorization: `Bearer ${process.env.WYSTAW_API_KEY}` },
});
if (!res.ok) throw new Error((await res.json()).detail); // application/problem+json
const data = await res.json();

GET/api/v1/customers/{id}

Get a customer

Scope: customers:read · akcje: getCustomer

Parametry GET /customers/{id}
PoleTyp
id *path · string (uuid)

Odpowiedzi: 200 JSON · błędy: problem+json

curl -sS "$WYSTAW_API/customers/$ID" \
  -H "Authorization: Bearer $WYSTAW_API_KEY"
TypeScript (fetch)
const res = await fetch(`${WYSTAW_API}/customers/${id}`, {
  headers: { Authorization: `Bearer ${process.env.WYSTAW_API_KEY}` },
});
if (!res.ok) throw new Error((await res.json()).detail); // application/problem+json
const data = await res.json();

PATCH/api/v1/customers/{id}

Update a customer (partial)

Scope: customers:writecustomers:read · akcje: getCustomer → upsertCustomer

Fields not sent keep their values (read with getCustomer, saved with upsertCustomer) — needs `customers:read` too.

Parametry PATCH /customers/{id}
PoleTyp
id *path · string (uuid)
Body: UpdateCustomerRequest
Body PATCH /customers/{id}
PoleTyp
kind"company" | "person"
namestring
nipstring | null
vatUeobject | null
otherTaxIdstring | null
addressobject
emailstring (email) | null
defaultLang"pl" | "en" | "pl-en"
defaultCurrency"PLN" | "EUR" | "USD"
notesstring | null

Odpowiedzi: 200 JSON · błędy: problem+json

curl -sS -X PATCH "$WYSTAW_API/customers/$ID" \
  -H "Authorization: Bearer $WYSTAW_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]"}'
TypeScript (fetch)
const res = await fetch(`${WYSTAW_API}/customers/${id}`, {
  method: 'PATCH',
  headers: { Authorization: `Bearer ${process.env.WYSTAW_API_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({"email":"[email protected]"}),
});
if (!res.ok) throw new Error((await res.json()).detail); // application/problem+json
const data = await res.json();

DELETE/api/v1/customers/{id}

Delete a customer

Scope: customers:write · akcje: deleteCustomer

Issued invoices keep their buyer snapshot.

Parametry DELETE /customers/{id}
PoleTyp
id *path · string (uuid)

Odpowiedzi: 204 · błędy: problem+json

curl -sS -X DELETE "$WYSTAW_API/customers/$ID" \
  -H "Authorization: Bearer $WYSTAW_API_KEY"
TypeScript (fetch)
const res = await fetch(`${WYSTAW_API}/customers/${id}`, {
  method: 'DELETE',
  headers: { Authorization: `Bearer ${process.env.WYSTAW_API_KEY}` },
});
if (!res.ok) throw new Error((await res.json()).detail); // application/problem+json
// 204 No Content

ProductsProduct catalogue used on invoice lines.

GET/api/v1/products

List catalogue products

Scope: products:read · akcje: listProducts

Recently used first, then by name; cursor-paginated (`nextCursor`). `q` matches the name or SKU.

Parametry GET /products
PoleTyp
qquery · string
brandIdquery · string (uuid)
includeInactivequery · boolean
cursorquery · string
limitquery · integer

Odpowiedzi: 200 JSON · błędy: problem+json

curl -sS "$WYSTAW_API/products?q=logo&limit=20" \
  -H "Authorization: Bearer $WYSTAW_API_KEY"
TypeScript (fetch)
const res = await fetch(`${WYSTAW_API}/products?q=logo&limit=20`, {
  headers: { Authorization: `Bearer ${process.env.WYSTAW_API_KEY}` },
});
if (!res.ok) throw new Error((await res.json()).detail); // application/problem+json
const data = await res.json();

POST/api/v1/products

Create a product

Scope: products:write · akcje: upsertProduct

Parametry POST /products
PoleTyp
Idempotency-Keyheader · stringUnique key (e.g. UUID) — a retry with the same key and payload returns the stored response (24 h).
Body: CreateProductRequest
Body POST /products
PoleTyp
brandId *string (uuid) | null
name *string
description *string | null
unit *string
priceNet *string
vatRate *"5" | "8" | "23" | "0 KR" | "0 WDT" | "0 EX" | "zw" | "oo" | "np I" | "np II"
currency *"PLN" | "EUR" | "USD"
pkwiu *string | null
gtu *"GTU_01" | "GTU_02" | "GTU_03" | "GTU_04" | "GTU_05" | "GTU_06" | "GTU_07" | "GTU_08" | "GTU_09" | "GTU_10" | "GTU_11" | "GTU_12" | "GTU_13" | null
sku *string | null
activeboolean

Odpowiedzi: 201 JSON · błędy: problem+json

curl -sS -X POST "$WYSTAW_API/products" \
  -H "Authorization: Bearer $WYSTAW_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"brandId":null,"name":"Projekt logo","description":null,"unit":"usł.","priceNet":"1500.00","vatRate":"23","currency":"PLN","pkwiu":null,"gtu":null,"sku":"LOGO-1","active":true}'
TypeScript (fetch)
const res = await fetch(`${WYSTAW_API}/products`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.WYSTAW_API_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({"brandId":null,"name":"Projekt logo","description":null,"unit":"usł.","priceNet":"1500.00","vatRate":"23","currency":"PLN","pkwiu":null,"gtu":null,"sku":"LOGO-1","active":true}),
});
if (!res.ok) throw new Error((await res.json()).detail); // application/problem+json
const data = await res.json();

GET/api/v1/products/{id}

Get a product

Scope: products:read · akcje: getProduct

Parametry GET /products/{id}
PoleTyp
id *path · string (uuid)

Odpowiedzi: 200 JSON · błędy: problem+json

curl -sS "$WYSTAW_API/products/$ID" \
  -H "Authorization: Bearer $WYSTAW_API_KEY"
TypeScript (fetch)
const res = await fetch(`${WYSTAW_API}/products/${id}`, {
  headers: { Authorization: `Bearer ${process.env.WYSTAW_API_KEY}` },
});
if (!res.ok) throw new Error((await res.json()).detail); // application/problem+json
const data = await res.json();

PATCH/api/v1/products/{id}

Update a product (partial)

Scope: products:writeproducts:read · akcje: getProduct → upsertProduct

Fields not sent keep their values (read with getProduct, saved with upsertProduct) — needs `products:read` too.

Parametry PATCH /products/{id}
PoleTyp
id *path · string (uuid)
Body: UpdateProductRequest
Body PATCH /products/{id}
PoleTyp
brandIdstring (uuid) | null
namestring
descriptionstring | null
unitstring
priceNetstring
vatRate"5" | "8" | "23" | "0 KR" | "0 WDT" | "0 EX" | "zw" | "oo" | "np I" | "np II"
currency"PLN" | "EUR" | "USD"
pkwiustring | null
gtu"GTU_01" | "GTU_02" | "GTU_03" | "GTU_04" | "GTU_05" | "GTU_06" | "GTU_07" | "GTU_08" | "GTU_09" | "GTU_10" | "GTU_11" | "GTU_12" | "GTU_13" | null
skustring | null
activeboolean

Odpowiedzi: 200 JSON · błędy: problem+json

curl -sS -X PATCH "$WYSTAW_API/products/$ID" \
  -H "Authorization: Bearer $WYSTAW_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"priceNet":"1800.00"}'
TypeScript (fetch)
const res = await fetch(`${WYSTAW_API}/products/${id}`, {
  method: 'PATCH',
  headers: { Authorization: `Bearer ${process.env.WYSTAW_API_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({"priceNet":"1800.00"}),
});
if (!res.ok) throw new Error((await res.json()).detail); // application/problem+json
const data = await res.json();

PUT/api/v1/products/{id}

Replace a product

Scope: products:write · akcje: upsertProduct

Full replacement (all fields). For a partial update use `PATCH`.

Parametry PUT /products/{id}
PoleTyp
id *path · string (uuid)
Body: CreateProductRequest
Body PUT /products/{id}
PoleTyp
brandId *string (uuid) | null
name *string
description *string | null
unit *string
priceNet *string
vatRate *"5" | "8" | "23" | "0 KR" | "0 WDT" | "0 EX" | "zw" | "oo" | "np I" | "np II"
currency *"PLN" | "EUR" | "USD"
pkwiu *string | null
gtu *"GTU_01" | "GTU_02" | "GTU_03" | "GTU_04" | "GTU_05" | "GTU_06" | "GTU_07" | "GTU_08" | "GTU_09" | "GTU_10" | "GTU_11" | "GTU_12" | "GTU_13" | null
sku *string | null
activeboolean

Odpowiedzi: 200 JSON · błędy: problem+json

curl -sS -X PUT "$WYSTAW_API/products/$ID" \
  -H "Authorization: Bearer $WYSTAW_API_KEY" \
  -H "Content-Type: application/json" \
  -d @body.json
TypeScript (fetch)
const res = await fetch(`${WYSTAW_API}/products/${id}`, {
  method: 'PUT',
  headers: { Authorization: `Bearer ${process.env.WYSTAW_API_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify(body),
});
if (!res.ok) throw new Error((await res.json()).detail); // application/problem+json
const data = await res.json();

POST/api/v1/products/{id}/archive

Archive a product

Scope: products:write · akcje: archiveProduct

Archived products are hidden from the catalogue; past invoices are not affected.

Parametry POST /products/{id}/archive
PoleTyp
id *path · string (uuid)
Idempotency-Keyheader · stringUnique key (e.g. UUID) — a retry with the same key and payload returns the stored response (24 h).

Odpowiedzi: 200 JSON · błędy: problem+json

curl -sS -X POST "$WYSTAW_API/products/$ID/archive" \
  -H "Authorization: Bearer $WYSTAW_API_KEY"
TypeScript (fetch)
const res = await fetch(`${WYSTAW_API}/products/${id}/archive`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.WYSTAW_API_KEY}` },
});
if (!res.ok) throw new Error((await res.json()).detail); // application/problem+json
const data = await res.json();

BrandsBrands (numbering, look) and bank accounts.

GET/api/v1/brands

List brands

Scope: brands:read · akcje: listBrands

Every invoice belongs to a brand (`brandId`): numbering, look and defaults.

Odpowiedzi: 200 JSON · błędy: problem+json

curl -sS "$WYSTAW_API/brands" \
  -H "Authorization: Bearer $WYSTAW_API_KEY"
TypeScript (fetch)
const res = await fetch(`${WYSTAW_API}/brands`, {
  headers: { Authorization: `Bearer ${process.env.WYSTAW_API_KEY}` },
});
if (!res.ok) throw new Error((await res.json()).detail); // application/problem+json
const data = await res.json();

GET/api/v1/bank-accounts

List bank accounts

Scope: brands:read · akcje: listBankAccounts

`bankAccountId` for invoices paid by transfer.

Odpowiedzi: 200 JSON · błędy: problem+json

curl -sS "$WYSTAW_API/bank-accounts" \
  -H "Authorization: Bearer $WYSTAW_API_KEY"
TypeScript (fetch)
const res = await fetch(`${WYSTAW_API}/bank-accounts`, {
  headers: { Authorization: `Bearer ${process.env.WYSTAW_API_KEY}` },
});
if (!res.ok) throw new Error((await res.json()).detail); // application/problem+json
const data = await res.json();

WebhooksSigned status notifications.

GET/api/v1/webhooks

List webhook endpoints with recent deliveries

Scope: webhooks:manage · akcje: listWebhooks

Odpowiedzi: 200 JSON · błędy: problem+json

curl -sS "$WYSTAW_API/webhooks" \
  -H "Authorization: Bearer $WYSTAW_API_KEY"
TypeScript (fetch)
const res = await fetch(`${WYSTAW_API}/webhooks`, {
  headers: { Authorization: `Bearer ${process.env.WYSTAW_API_KEY}` },
});
if (!res.ok) throw new Error((await res.json()).detail); // application/problem+json
const data = await res.json();

POST/api/v1/webhooks

Create a webhook endpoint

Scope: webhooks:manage · akcje: createWebhook

HTTPS only. The signing `secret` (`whsec_…`) is returned once — store it.

Parametry POST /webhooks
PoleTyp
Idempotency-Keyheader · stringUnique key (e.g. UUID) — a retry with the same key and payload returns the stored response (24 h).
Body: CreateWebhookRequest
Body POST /webhooks
PoleTyp
url *string (uri)
events *"invoice.issued" | "invoice.ksef.accepted" | "invoice.ksef.rejected" | "invoice.offline.pending" | "invoice.paid"[]
descriptionstring | null

Odpowiedzi: 201 JSON · błędy: problem+json

curl -sS -X POST "$WYSTAW_API/webhooks" \
  -H "Authorization: Bearer $WYSTAW_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/wystaw/webhooks","events":["invoice.ksef.accepted","invoice.ksef.rejected"],"description":"Sklep"}'
TypeScript (fetch)
const res = await fetch(`${WYSTAW_API}/webhooks`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.WYSTAW_API_KEY}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({"url":"https://example.com/wystaw/webhooks","events":["invoice.ksef.accepted","invoice.ksef.rejected"],"description":"Sklep"}),
});
if (!res.ok) throw new Error((await res.json()).detail); // application/problem+json
const data = await res.json();

DELETE/api/v1/webhooks/{id}

Delete a webhook endpoint

Scope: webhooks:manage · akcje: deleteWebhook

Parametry DELETE /webhooks/{id}
PoleTyp
id *path · string (uuid)

Odpowiedzi: 204 · błędy: problem+json

curl -sS -X DELETE "$WYSTAW_API/webhooks/$ID" \
  -H "Authorization: Bearer $WYSTAW_API_KEY"
TypeScript (fetch)
const res = await fetch(`${WYSTAW_API}/webhooks/${id}`, {
  method: 'DELETE',
  headers: { Authorization: `Bearer ${process.env.WYSTAW_API_KEY}` },
});
if (!res.ok) throw new Error((await res.json()).detail); // application/problem+json
// 204 No Content

POST/api/v1/webhooks/{id}/test

Send a signed `test` event now

Scope: webhooks:manage · akcje: testWebhook

Parametry POST /webhooks/{id}/test
PoleTyp
id *path · string (uuid)
Idempotency-Keyheader · stringUnique key (e.g. UUID) — a retry with the same key and payload returns the stored response (24 h).

Odpowiedzi: 200 JSON · błędy: problem+json

curl -sS -X POST "$WYSTAW_API/webhooks/$ID/test" \
  -H "Authorization: Bearer $WYSTAW_API_KEY"
TypeScript (fetch)
const res = await fetch(`${WYSTAW_API}/webhooks/${id}/test`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.WYSTAW_API_KEY}` },
});
if (!res.ok) throw new Error((await res.json()).detail); // application/problem+json
const data = await res.json();

POST/api/v1/webhooks/{id}/enable

Re-enable a disabled webhook endpoint

Scope: webhooks:manage · akcje: enableWebhook

Endpoints failing for 24 h are disabled automatically (`active: false`, `disabledReason`). This turns deliveries back on and resets the failure window. Idempotent.

Parametry POST /webhooks/{id}/enable
PoleTyp
id *path · string (uuid)
Idempotency-Keyheader · stringUnique key (e.g. UUID) — a retry with the same key and payload returns the stored response (24 h).

Odpowiedzi: 200 JSON · błędy: problem+json

curl -sS -X POST "$WYSTAW_API/webhooks/$ID/enable" \
  -H "Authorization: Bearer $WYSTAW_API_KEY"
TypeScript (fetch)
const res = await fetch(`${WYSTAW_API}/webhooks/${id}/enable`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${process.env.WYSTAW_API_KEY}` },
});
if (!res.ok) throw new Error((await res.json()).detail); // application/problem+json
const data = await res.json();

AccountThe key, its tenant and plan.

GET/api/v1/me

The key, its tenant, plan and limits

Scope: dowolny · akcje: getApiContext

Works with any scope.

Odpowiedzi: 200 JSON · błędy: problem+json

curl -sS "$WYSTAW_API/me" \
  -H "Authorization: Bearer $WYSTAW_API_KEY"
TypeScript (fetch)
const res = await fetch(`${WYSTAW_API}/me`, {
  headers: { Authorization: `Bearer ${process.env.WYSTAW_API_KEY}` },
});
if (!res.ok) throw new Error((await res.json()).detail); // application/problem+json
const data = await res.json();
Dokumentacja