Authentication
eFakturuj's API supports three authentication modes. Pick the one that matches how your code is calling us.
Decision matrix
| Scenario | Use |
|---|---|
| Flutter / iOS / Android mobile app | BearerJWT |
| Browser SPA (your own users signing in) | BearerJWT |
| Your accounting backend → eFakturuj | ApiKey |
| Third-party app acting on behalf of an eFakturuj user | OAuth2 (client credentials) |
BearerJWT — interactive sign-in
Short-lived 15-minute RS256 JWT issued by POST /auth/login (or
POST /auth/refresh). Send it on every request as
Authorization: Bearer <jwt>. Use this for anything where a real human
is signing in.
Login
curl -X POST https://api.efakturuj.sk/api/v1/auth/login \
-H 'Content-Type: application/json' \
-d '{"email": "you@example.sk", "password": "..."}'
Response (LoginResponse):
{
"access_token": "eyJhbGciOi...",
"refresh_token": "eyJhbGciOi...",
"expires_in": 900,
"user": { "id": "...", "email": "...", "name": "...", "is_platform_admin": false }
}
Dart:
final res = await dio.post('/auth/login', data: {
'email': email,
'password': password,
});
final accessToken = res.data['access_token'] as String;
final refreshToken = res.data['refresh_token'] as String;
Python:
res = httpx.post(
"https://api.efakturuj.sk/api/v1/auth/login",
json={"email": email, "password": password},
)
res.raise_for_status()
tokens = res.json()
Refresh
When you get a 401 because the access token expired, exchange the
refresh token for a new pair:
curl -X POST https://api.efakturuj.sk/api/v1/auth/refresh \
-H 'Content-Type: application/json' \
-d '{"refresh_token": "eyJhbGciOi..."}'
POST /auth/refresh rotates the refresh token — the response
contains both a new access_token and a new refresh_token. Discard
the old refresh token immediately and persist the new one.
ApiKey — server-to-server
Long-lived API key created at POST /api-keys from the dashboard
(requires the admin role). Send it as X-Api-Key: <key>. Use this
for headless backends, cron jobs, ETL pipelines.
curl -X POST https://api.efakturuj.sk/api/v1/api-keys \
-H 'Authorization: Bearer <admin-jwt>' \
-H 'Content-Type: application/json' \
-d '{
"name": "Production backend",
"scopes": ["invoices:read", "invoices:write", "invoices:send"],
"expires_in_days": 365
}'
The response includes the plaintext api_key exactly once. Store
it immediately — only the SHA-256 hash is persisted server-side and
the value cannot be recovered.
Available scopes: invoices:read, invoices:write, invoices:send,
validate:only. Keys are formatted efk_live_<32-char-suffix>.
Use it on subsequent requests:
curl https://api.efakturuj.sk/api/v1/invoices \
-H 'X-Api-Key: efk_live_...'
The same key may also be sent as a Bearer token — the server
recognises any value beginning with efk_live_ or efk_test_ as an
API key.
OAuth2 — third-party integrations
OAuth2 client-credentials grant (RFC 6749 §4.4) for headless apps
acting on behalf of an organisation. The dashboard provisions a
client_id + client_secret pair via POST /oauth2/clients; both
values are returned exactly once and only the SHA-256 hash of the
secret is stored.
Note. Today only the
client_credentialsgrant is supported. The authorization-code redirect flow described in the OpenAPI security scheme is reserved for a future release.
Exchange credentials for a 15-minute JWT:
curl -X POST https://api.efakturuj.sk/api/v1/oauth2/token \
-H 'Content-Type: application/json' \
-d '{
"grant_type": "client_credentials",
"client_id": "efk_client_...",
"client_secret": "efk_secret_..."
}'
Response:
{
"access_token": "eyJhbGciOi...",
"token_type": "Bearer",
"expires_in": 900
}
Send the resulting JWT as Authorization: Bearer <token>. No
refresh token is issued — call /oauth2/token again with the same
static credentials when the access token expires.
Token lifetimes
| Token | TTL | Source |
|---|---|---|
| Access token (login) | 15 min | jwt_access_token_expire_minutes |
| Refresh token (login) | 30 days | jwt_refresh_token_expire_days |
| OAuth2 access token | 15 min | hard-coded in /oauth2/token |
| API key | until revoked, or expires_in_days if set (1–365) | ApiKey.expires_at |
All JWTs are RS256-signed; verify with the public key from the JWKS endpoint if you need offline validation.
Where to store credentials
- Mobile (iOS / Android / Flutter) — Keychain on iOS, EncryptedSharedPreferences / Keystore on Android. Never write tokens to logs or analytics.
- Browser SPA — keep the access token in memory and the refresh
token in an HTTP-only cookie issued by your own backend. Avoid
localStoragefor refresh tokens. - Server-to-server — API key in an environment variable or secret
manager (AWS Secrets Manager, HashiCorp Vault, Doppler). Never
commit keys to git. Rotate on a schedule using
PATCH /api-keys/{id}followed byDELETE /api-keys/{id}for the old key.
Brute-force protection
Failed login attempts are recorded as login_failed security events
with the source IP and User-Agent. Repeated invalid API key
presentations from the same IP trigger a 429 Too Many Requests
response — back off and verify the key before retrying.
See also
- SDKs → Dart — uses BearerJWT and ApiKey end-to-end
- API reference → Auth tag — full request/response schemas