Exchange client_id and client_secret for a one-hour Bearer token, server-to-server only.
← Back to the API documentation
Your first invoice in 6 steps
- Mint the credential in the panel under Invoicing → Settings → API access and copy the client_secret immediately — it is shown once.
- Send POST /v1/auth/token with grant_type client_credentials, client_id and client_secret.
- You get back access_token, token_type Bearer, expires_in 3600 and the list of granted scopes.
- Put the token in an Authorization: Bearer <access_token> header on every other call.
- Check what you actually got with GET /v1/me: tenant, scopes and the current rate buckets.
Every endpoint
| Method | Endpoint | Scope | Idempotency-Key | Requests / minute |
|---|---|---|---|---|
| POST | /auth/token Exchange credentials for an access token | — | — | 120 |
| GET | /me Tenant, scopes and current limits | invoices:read | — | 120 |
What the 4invoices API is
bash
curl -sS -X POST https://api.4invoices.net/v1/auth/token \
-H 'Content-Type: application/json' \
-d '{
"grant_type": "client_credentials",
"client_id": "cli_XXXXXXXXXXXX",
"client_secret": "sec_XXXXXXXXXXXXXXXXXXXX"
}'
# → {"access_token":"eyJ...","token_type":"Bearer","expires_in":3600,
# "scope":"invoices:read invoices:write efactura:submit"}bash
curl -sS https://api.4invoices.net/v1/me \
-H "Authorization: Bearer $ACCESS_TOKEN"
# → tenant, granted scopes and the current rate-limit bucketsjavascript
// Cache the token; do not mint one per request.
let cached = { token: "", expiresAt: 0 };
async function getToken() {
if (cached.token && Date.now() < cached.expiresAt - 60_000) return cached.token;
const res = await fetch("https://api.4invoices.net/v1/auth/token", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
grant_type: "client_credentials",
client_id: process.env.FOURINVOICES_CLIENT_ID,
client_secret: process.env.FOURINVOICES_CLIENT_SECRET,
}),
});
if (!res.ok) throw new Error(`token ${res.status}`);
const data = await res.json();
cached = { token: data.access_token, expiresAt: Date.now() + data.expires_in * 1000 };
return cached.token;
}https://api.4invoices.net/v1 · https://api.4invoices.net/v1/openapi.json
Scopes
- invoices:read
- invoices:write
- clients:read
- clients:write
- efactura:read
- efactura:submit
Server-to-server only. Never call the API from browser JavaScript: the client_secret would reach the visitor, and CORS blocks every origin anyway.
Errors
| Status | Type | Code |
|---|---|---|
| 401 | authentication_error | invalid_client |
| 401 | authentication_error | token_expired |
| 401 | authentication_error | invalid_token |
| 403 | permission_error | insufficient_scope |
Common mistakes
- The token endpoint has its own per-IP limit. One token per process, refreshed before expiry — not one per request.
- The client_secret is stored only as a hash; lose it and you mint a new credential and revoke the old one.
- Issue separate credentials for the ERP, the shop and internal scripts, each with minimal scopes. Revoking one does not affect the others.
- CORS allows no browser origin: the API is server-to-server by design.
Developer questions and answers
What happens if the client_secret is wrong?
You get 401 with code invalid_client. A too-short secret fails earlier, at validation, with 422 validation_failed and param client_secret.
Is there a separate sandbox?
No. There is one production host. Test with proformas (document_kind proforma) and with /invoices/preview-totals, which consumes no fiscal number.
Can I narrow the scopes of an existing credential?
Yes, from the panel. Tokens already issued keep the scopes they were minted with until they expire, within one hour.