Connect your own system to Zaksio: read and write products, counterparties and invoices, check stock levels and receive events by webhook. Document version: 1.0.0.
Quick start
The public Zaksio API has the base address https://api.zaksio.pl and the version in the path (/v1). Current version of the document: 1.0.0. We will introduce breaking changes only as a new path version; new fields in responses and new endpoints may appear within /v1, so ignore fields you do not recognise.
In the app, go to Settings, "API and integrations", click "New key", choose the company and scopes, confirm with your password and copy the key (we show it only once).
Save the key in an environment variable, e.g. ZAKSIO_API_KEY, and check that it works:
The response shows the company the key belongs to and its scopes. From here on, use the addresses described in the "API addresses" section.
Authentication
Every request requires the header Authorization: Bearer zk_live_.... The key acts on behalf of the person who created it, but only within the selected scopes, and it never has more permissions than that person (checked on every request). The key is tied to one company: all endpoints operate on that company's data. App session tokens do not work in /v1, and API keys do not work outside /v1.
Missing or invalid key: 401 (codes missing_api_key, invalid_api_key).
Revoked or expired key: 401 (api_key_revoked, api_key_expired).
IP address outside the key's list: 403 (ip_not_allowed).
Missing scope: 403 (insufficient_scope).
Licence in read-only mode: writes return 402 (licence_read_only).
Scopes
The scope defines what the key can do. A key can never do more than the permissions of the person who created it.
API key scopes
Scope
Description
products:read
List and details of products from the catalogue.
products:write
Dodawanie i zmiana produktow w kartotece.
stock:read
Stany magazynowe produktow (suma i podzial na magazyny).
contractors:read
List and details of counterparties.
contractors:write
Dodawanie, zmiana i usuwanie kontrahentow.
invoices:read
List, details and PDF of sales invoices.
invoices:write
Creating and issuing sales invoices (numbering and posting as in the app).
Key limits and security
Request limit: 120 per minute per key by default (configurable when creating the key, up to 6000). Every response has the headers RateLimit-Limit, RateLimit-Remaining i RateLimit-Reset (seconds until the end of the window). When exceeded: 429 (rate_limited) with the header Retry-After. Repeat the request after this time.
Failed authentication: after 30 failed attempts per minute from one IP address, further ones receive 429 (too_many_failed_auth).
IP address list, expiry, rotation and revocation: you set them in the app. Rotation creates a new key, and the old one keeps working for the transition period you choose; revocation takes effect immediately.
Retention: do not put the key in browser or mobile app code; use it only on the server side.
Conventions: amounts, dates, pagination
Amounts are integers in grosze (12300 = 123.00 PLN).
Quantities is a decimal string with a dot, e.g. "2", "14.5", with a maximum of four decimal places.
Dates is RRRR-MM-DD, and timestamps are ISO 8601 in UTC. Identifiers are UUIDs.
Cursor pagination: lists accept limit (1 to 200, default 50), cursor i order (asc oldest first, desc newest first). The response has data i pagination with the fields hasMore and nextCursor; you fetch the next page by passing nextCursor as cursor. The cursor is opaque: do not parse it. Records added while you are paging do not shift the pages.
Partial update (PATCH): you send only the fields to change. Omitted fields stay unchanged.
Write idempotency
Write requests (POST, PATCH, DELETE) accept the header Idempotency-Key (up to 255 ASCII characters, e.g. a UUID). The first request with a given key is executed normally, and its response is stored for 24 hours. Repeating the same request with the same key returns the stored response with the header Idempotent-Replayed: true and does not perform the write a second time. This lets you safely retry a request after a network error, e.g. you will not issue two invoices.
Same key with different data: 422 (idempotency_key_reuse).
Same key while the first request is still in progress: 409 (idempotency_in_progress), retry after a moment.
5xx and 429 errors are not stored, so a retry will be executed from scratch.
Idempotency keys are independent for each API key.
Errors
Errors use the RFC 9457 format (application/problem+json). Base your handling on the field code and the HTTP code, not on the text detail. Field requestId (also in the header X-Request-Id) include in your support request.
{
"type": "https://zaksio.pl/dokumentacja-api#blad-insufficient-scope",
"title": "Brak dostepu",
"status": 403,
"detail": "Klucz nie ma zakresu products:write (albo tworca klucza utracil odpowiednie uprawnienie)",
"instance": "/v1/products",
"code": "insufficient_scope",
"requestId": "req-1a2b3c"
}
For validation_error (400) field errors contains a list of invalid fields with the pointer pointer, e.g. /lines/0/quantity.
Error codes
Code (code)
HTTP
Meaning
missing_api_key
401
Missing Authorization: Bearer zk_live_... header
invalid_api_key
401
The key has an invalid format or does not exist
api_key_revoked
401
The key has been revoked
api_key_expired
401
The key has expired (also after the rotation grace period)
api_key_owner_removed
401
The person who created the key no longer belongs to the organisation
ip_not_allowed
403
The request IP address is not on the key's allowlist
insufficient_scope
403
The key lacks the scope required by the operation (or the key's creator has lost the permission)
feature_disabled
403
The public API is disabled for the organisation
module_disabled
403
The module (catalogue, warehouse) is not enabled in the business profile
tenant_deleted
403
The organisation has been deleted
licence_read_only
402
Licence expired or payment overdue: read-only access allowed
document_limit_reached
402
Wyczerpano miesieczny limit dokumentow w planie
validation_error
400
Invalid data; list of fields in errors[]
invalid_cursor
400
The pagination cursor is invalid
invalid_idempotency_key
400
Idempotency-Key contains disallowed characters or is too long
not_found
404
No such endpoint in API v1
product_not_found
404
Product not found
contractor_not_found
404
Counterparty not found (when creating an invoice: 422)
invoice_not_found
404
Invoice not found
sku_or_ean_taken
409
The SKU or EAN code is already used in this company
product_has_stock_history
409
Batch tracking cannot be changed for a product with movement history
contractor_in_use
409
The counterparty appears on documents and cannot be deleted
invoice_not_draft
409
An issued invoice cannot be changed or issued again
period_closed
409
The accounting period is closed
idempotency_in_progress
409
A request with this Idempotency-Key is currently being processed; retry shortly
idempotency_key_reuse
422
Ten Idempotency-Key byl juz uzyty z innymi danymi zadania
invalid_service
422
A service has no batches, expiry date or ADR attributes
exemption_basis_required
422
VAT-exempt items (zw) require an exemption basis
non_positive_total
422
The invoice amount must be positive
too_many_failed_auth
429
Zbyt wiele blednych prob uwierzytelnienia z jednego adresu
rate_limited
429
Per-minute request limit exceeded for the key
maintenance
503
Tryb serwisowy: zapisy chwilowo zablokowane
internal_error
500
Server error; include the requestId in your report
Possible errors: 400, 401, 403, 404, 429, 500 (format described in the "Errors" section).
Zmiana produktu
PATCH/v1/products/{id}
Required scope: products:write
Changes only the fields provided. The kind (`kind`) cannot be changed; batch and expiry date tracking can be changed only for a product with no stock movement history.
Parameters: Zmiana produktu
Parameter
Where
Type
Description
idrequired
adres
string (uuid)
Idempotency-Key
header
string
Unikalny klucz zapisu (np. UUID). Powtorzenie zadania z tym samym kluczem zwraca zapamietana odpowiedz zamiast wykonywac zapis drugi raz (24 h).
Stock level of every product that has stock movements: total and breakdown by warehouse. The list is ordered by product identifier, and the cursor points to the last returned product.
Parameters: Stock levels
Parameter
Where
Type
Description
limit
zapytanie
integer
cursor
zapytanie
string
warehouseId
zapytanie
string (uuid)
Limit to one warehouse
Request example
curl
curl -X GET "https://api.zaksio.pl/v1/stock?limit=50" \
-H "Authorization: Bearer $ZAKSIO_API_KEY"
JavaScript (Node.js)
const response = await fetch("https://api.zaksio.pl/v1/stock?limit=50", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.ZAKSIO_API_KEY}`,
},
});
const data = await response.json();
if (!response.ok) throw new Error(`${data.code}: ${data.detail}`); // application/problem+json
console.log(data);
Python
import os
import requests
response = requests.request(
"GET",
"https://api.zaksio.pl/v1/stock",
headers={
"Authorization": f"Bearer {os.environ['ZAKSIO_API_KEY']}",
},
params={
"limit": "50",
},
timeout=30,
)
if not response.ok:
problem = response.json() # application/problem+json
raise RuntimeError(f"{problem['code']}: {problem['detail']}")
print(response.json())
Invoices (and credit notes) of the company assigned to the key, without line items (details: `GET /v1/invoices/{id}`). Cursor pagination; for incremental sync use `order=asc` and remember the last cursor.
Parameters: List sales invoices
Parameter
Where
Type
Description
limit
zapytanie
integer
cursor
zapytanie
string
order
zapytanie
asc | desc
status
zapytanie
draft | issued
contractorId
zapytanie
string (uuid)
issueDateFrom
zapytanie
string (date)
issueDateTo
zapytanie
string (date)
Request example
curl
curl -X GET "https://api.zaksio.pl/v1/invoices?status=issued&issueDateFrom=2026-10-01&limit=50" \
-H "Authorization: Bearer $ZAKSIO_API_KEY"
JavaScript (Node.js)
const response = await fetch("https://api.zaksio.pl/v1/invoices?status=issued&issueDateFrom=2026-10-01&limit=50", {
method: "GET",
headers: {
Authorization: `Bearer ${process.env.ZAKSIO_API_KEY}`,
},
});
const data = await response.json();
if (!response.ok) throw new Error(`${data.code}: ${data.detail}`); // application/problem+json
console.log(data);
Python
import os
import requests
response = requests.request(
"GET",
"https://api.zaksio.pl/v1/invoices",
headers={
"Authorization": f"Bearer {os.environ['ZAKSIO_API_KEY']}",
},
params={
"status": "issued",
"issueDateFrom": "2026-10-01",
"limit": "50",
},
timeout=30,
)
if not response.ok:
problem = response.json() # application/problem+json
raise RuntimeError(f"{problem['code']}: {problem['detail']}")
print(response.json())
Possible errors: 400, 401, 403, 429, 500 (format described in the "Errors" section).
Issue an invoice
POST/v1/invoices
Required scope: invoices:write
Creates a sales invoice and (by default) issues it immediately: gapless numbering, posting and VAT summary as in the app. Amounts in grosze, quantity as a decimal string. Issuing and saving are a single transaction: on error no draft is left behind. **Always provide `Idempotency-Key`** so that a retry after a network error does not issue the invoice a second time.
Parameters: Issue an invoice
Parameter
Where
Type
Description
Idempotency-Key
header
string
Unikalny klucz zapisu (np. UUID). Powtorzenie zadania z tym samym kluczem zwraca zapamietana odpowiedz zamiast wykonywac zapis drugi raz (24 h).
Possible errors: 400, 401, 403, 404, 429, 500 (format described in the "Errors" section).
Issue a draft invoice
POST/v1/invoices/{id}/issue
Required scope: invoices:write
Issues an invoice previously saved as a draft (from the API or the app). An issued invoice cannot be issued again (409 `invoice_not_draft`). Supports `Idempotency-Key`.
Parameters: Issue a draft invoice
Parameter
Where
Type
Description
idrequired
adres
string (uuid)
Idempotency-Key
header
string
Unikalny klucz zapisu (np. UUID). Powtorzenie zadania z tym samym kluczem zwraca zapamietana odpowiedz zamiast wykonywac zapis drugi raz (24 h).
Possible errors: 400, 401, 402, 403, 404, 409, 422, 429, 500 (format described in the "Errors" section).
Webhooks
A webhook is a POST request that Zaksio sends to your address (https, public) when an event occurs. You add subscriptions in the app (Settings, "API and integrations", "Webhooks" tab): you enter the address, choose the events, optionally a single company, and save the signing secret (shown only once).
Envelope and headers
The body is a JSON object with the fields id (event identifier, constant across retries), type, apiVersion, createdAt, companyId i data (data in the same shape as in API responses). The request has the headers Zaksio-Event, Zaksio-Event-Id, Zaksio-Delivery i Zaksio-Signature.
Webhook events
Event
When
invoice.issued
Invoice issued. A sales invoice (or credit note) was issued and posted.
invoice.paid
Invoice paid. Na fakturze zarejestrowano pierwsza wplate (pole paidAt ma wartosc).
contractor.created
Counterparty added. A counterparty was added (from the app, the API or an import).
contractor.updated
Counterparty changed. Counterparty details were changed.
product.created
Product added. A product was added to the catalogue.
product.updated
Product changed. Product details in the catalogue were changed (including deactivation).
product.stock_changed
Stan produktu zmieniony. Zmienil sie stan magazynowy produktu. Kilka ruchow zapisanych w krotkim czasie (zanim zdarzenie zostanie rozeslane) zlewa sie w jedno zdarzenie z aktualnym stanem.
You can find sample data for the other events in the OpenAPI file (field x-webhook-events). The "Test" button sends the event webhook.test with sample content so you can check the address and signature verification.
Signature verification
Header Zaksio-Signature has the form t=<unix>,v1=<hex>. The signature is an HMAC-SHA256 of the subscription secret over the text <t>.<raw request body>. Verify the signature on the raw body (not on re-serialised JSON), compare it in constant time and reject requests with a timestamp t older than 5 minutes. While the secret is being rotated, the header may have several values v1=: it is enough that one matches.
Checking the signature
JavaScript (Node.js)
import { createHmac, timingSafeEqual } from "node:crypto";
// rawBody: surowa tresc zadania (Buffer lub string), NIE ponownie zserializowany JSON
export function verifyZaksioSignature(secret, header, rawBody, toleranceSeconds = 300) {
const parts = header.split(",").map((p) => p.trim());
const timestamp = Number(parts.find((p) => p.startsWith("t="))?.slice(2));
if (!Number.isInteger(timestamp)) return false;
if (Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false;
const expected = createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest();
return parts
.filter((p) => p.startsWith("v1="))
.some((p) => {
const given = Buffer.from(p.slice(3), "hex");
return given.length === expected.length && timingSafeEqual(given, expected);
});
}
Python
import hashlib
import hmac
import time
def verify_zaksio_signature(secret: str, header: str, raw_body: bytes, tolerance: int = 300) -> bool:
parts = [p.strip() for p in header.split(",")]
try:
timestamp = int(next(p[2:] for p in parts if p.startswith("t=")))
except (StopIteration, ValueError):
return False
if abs(time.time() - timestamp) > tolerance:
return False
signed = f"{timestamp}.".encode() + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return any(
hmac.compare_digest(p[3:], expected) for p in parts if p.startswith("v1=")
)
#!/usr/bin/env bash
# Sprawdzenie podpisu z wiersza polecen (do diagnozy): SECRET, HEADER i plik z surowa trescia zadania
SECRET="whsec_..."
HEADER="t=1760261400,v1=..." # naglowek Zaksio-Signature
BODY_FILE="body.json" # surowa tresc zadania, bajt w bajt
T=$(echo "$HEADER" | tr ',' '\n' | sed -n 's/^t=//p')
EXPECTED=$( (printf '%s.' "$T"; cat "$BODY_FILE") | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //')
if echo "$HEADER" | tr ',' '\n' | grep -qx "v1=$EXPECTED"; then echo "podpis poprawny"; else echo "podpis NIEPOPRAWNY"; fi
# pamietaj tez o sprawdzeniu, ze t= nie jest starsze niz 5 minut
Delivery and retries
Delivery is successful after a 2xx response within 10 seconds. Redirects are not supported (we treat them as an error).
After an error we retry after 1 minute, 5 minutes, 30 minutes, 2, 6 and 12 hours (7 attempts in total). After that the delivery is marked "failed"; you can retry it manually in the delivery log.
After 25 consecutive failed attempts, the subscription is disabled automatically.
Delivery is "at least once": the same event may arrive more than once, and the order is not guaranteed. Identify events by the field id and respond quickly (2xx), and do heavy processing asynchronously.
Product stock events may be merged: several changes saved within a short time produce a single event with the current stock level.
Internal, local and private addresses are blocked (SSRF protection), also after DNS name resolution.
Change history
1.0.0: First version of API v1 (12 October 2026)
API key authentication (Authorization: Bearer zk_live_...), scopes, IP address list, expiry, rotation with a transition period and revocation.
Products: list, details, adding and partial update.
Stock levels: list and product details (total and breakdown by warehouse).
Counterparties: list, details, adding, partial update and deletion.
Sales invoices: list, details, PDF, issuing (immediately or from a draft) with numbering and posting as in the application.
Idempotency-Key on writes, cursor pagination, errors in RFC 9457 format and RateLimit-* limit headers.
Webhooks: invoice issued and paid, counterparty and product added or changed, product stock change; HMAC-SHA256 signature with a timestamp, retries with increasing intervals, delivery log and connection test.