Authentication
The Fakturnia API authenticates with OAuth 2.0 client credentials. Your application exchanges a client_id / client_secret pair for a short-lived bearer token (JWT), then sends that token on every request.
The REST API requires the MAX plan. API accounts, every
/external/v1endpoint and webhooks are a MAX plan module. Without an active plan the token endpoint answers403witherror: "access_denied"anderror_code: "SUBSCRIPTION_REQUIRED", and a call with a token issued earlier ends with403 SUBSCRIPTION_REQUIRED. When the plan lapses the integration stops until it is renewed; no webhooks are delivered in the meantime. Employee accounts and in-app permissions play no part: a token always acts as the account owner.
Getting credentials
Create an API application in Inne → Integracje → REST API (the app UI is in Polish). Each application has:
- a
client_id(public identifier, e.g.client_5c62e069…), - a
client_secret(secret, store it server-side only), - a set of scopes that bound what the application may do.
You can run multiple applications (e.g. one per integration) and revoke any of them independently. Revocation is immediate: tokens issued earlier get 401 UNAUTHORIZED on their next call, not only when they expire. You can also restrict an application to a list of IP addresses; a call from any other address ends with 403 FORBIDDEN.
API credentials are tied to their environment. Credentials from the test environment (
https://sandbox.app.fakturnia.pl) work only againsthttps://sandbox.api.fakturnia.pl, and production credentials only againsthttps://prod.api.fakturnia.pl. Start in the sandbox and switch to production only after the integration has been tested.
Requesting a token
curl -s https://prod.api.fakturnia.pl/v1/oauth/token \
-H "Content-Type: application/json" \
-d '{ "grant_type": "client_credentials", "client_id": "...", "client_secret": "..." }'
{ "access_token": "eyJ…", "token_type": "Bearer", "expires_in": 3600, "scope": "documents:create documents:read" }
Tokens are valid for expires_in seconds. Cache and reuse the token until it is close to expiry rather than requesting a new one per call. After expiry the API answers 401 TOKEN_EXPIRED: get a new token and retry.
The token endpoint answers as OAuth 2.0 prescribes: 400 invalid_request / unsupported_grant_type for a malformed request, 401 invalid_client for bad credentials (without revealing whether the client_id exists), 403 access_denied with error_code: "SUBSCRIPTION_REQUIRED" without the MAX plan, and 403 access_denied without an error_code when the request comes from an IP address outside the application's allowlist.
let cached: { token: string; exp: number } | null = null;
async function getToken(): Promise<string> {
const now = Date.now();
if (cached && cached.exp - now > 60_000) return cached.token;
const res = await fetch('https://prod.api.fakturnia.pl/v1/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'client_credentials',
client_id: process.env.FAKTURNIA_CLIENT_ID,
client_secret: process.env.FAKTURNIA_CLIENT_SECRET,
}),
});
const json = await res.json();
cached = { token: json.access_token, exp: now + json.expires_in * 1000 };
return cached.token;
}
Using the token
Authorization: Bearer <access_token>
A missing or invalid token returns 401 with code UNAUTHORIZED, an expired one 401 TOKEN_EXPIRED. A valid token that lacks the required scope returns 403 with code NO_PERMISSIONS, and errors[0] names the missing scope. Order of checks: token, API account status, IP address (allowlist), MAX plan, then the scope.
Scopes
Each endpoint declares the scope it requires. Request only the scopes your integration needs.
| 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 |
| Warehouse | 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 documents | recurring:browse · recurring:read · recurring:toggle |
| KSeF | ksef:read · ksef:configure · ksef:test |
| Settings | settings:read · settings:update |
| Webhooks | webhooks:read · webhooks:create · webhooks:update · webhooks:delete |
| Email accounts | email-accounts:browse · email-accounts:read |
| Activity | activity:read |
| Company | gus:company:read |
The exact scope for any endpoint is shown in the API reference.
Security checklist
- Keep
client_secretserver-side. Never embed it in browsers, mobile apps, or public repos. - Store tokens in memory or a short-lived cache. They expire, so persisting them long-term adds risk without benefit.
- Use HTTPS only. The API rejects plaintext.
- Rotate a secret immediately if it leaks (revoke the application and create a new one); revocation also cuts off tokens already issued.
- Restrict the source IP addresses of an application when the integration runs from a server with a fixed address.
- Scope down: an integration that only reads should not hold
documents:create.
Checking a token
GET /auth/me returns the identity and granted scopes behind the current token. Use it after deploying an integration to confirm the token works and carries the right permissions, or to debug which application a token belongs to. The endpoint only requires a valid token, no extra scope.
curl -s https://prod.api.fakturnia.pl/external/v1/auth/me \
-H "Authorization: Bearer $TOKEN"
{
"success": true,
"data": {
"clientId": "507f1f77bcf86cd799439011",
"name": "Shop integration",
"scopes": ["documents:read", "clients:browse"],
"userId": "507f1f77bcf86cd799439012"
}
}