Errors and limits
Error format of the NovaFisko API, HTTP codes, machine codes, rate limits per route and what to do after a 429.
The API uses standard HTTP codes and always returns a JSON body when an error occurs.
Error format
{
"message": "Le plafond de 5 dossiers de votre licence est atteint.",
"code": "company_limit_reached",
"errors": {
"name": ["Le plafond de 5 dossiers de votre licence est atteint."]
}
}
| Field | Presence | Description |
|---|---|---|
message |
always | Readable text, translated into the user's language |
code |
when it is a business error | Stable identifier, in English, to use in your code |
errors |
validation errors | Object field → list of messages. Nested fields use dot notation: lines.0.account_id |
Some errors add fields of their own, for example required_ability, server_version and server_data, or blocker_codes for Peppol.
Handle the HTTP status first, then code if present. Never parse the text of message.
HTTP codes
| Status | Meaning | What to do |
|---|---|---|
200 |
Success | |
201 |
Resource created | |
202 |
Request accepted, processed immediately or later (webhook test) | |
204 |
Success with no body (deletion, logout) | |
401 |
Token absent, invalid, revoked or expired | Log in again or use another token |
403 |
Action forbidden for this token or this role | Read code |
404 |
Unknown resource, or company the user has no access to | Check the public_token and the rights |
409 |
Version conflict (X-Base-Version) |
Reload, merge, resend |
422 |
Data refused (validation or accounting rule) | Fix according to errors and code |
429 |
Too many requests | Wait Retry-After seconds |
500 |
Internal error | Retry later with the same idempotency key |
503 |
Service unavailable or under maintenance, always with a code |
Retry with an increasing delay |
A company that exists but that you have no access to responds 404 and not 403. This is intentional: the API does not reveal the existence of a company to anyone who is not authorised to see it.
Machine codes
Authentication and access
code |
Status | Meaning |
|---|---|---|
token_ability_missing |
403 | The integration token lacks the ability given in required_ability |
session_token_required |
403 | Route reserved for session tokens |
token_limit_reached |
422 | At most 50 integration tokens per user |
webhook_limit_reached |
422 | At most 20 endpoints per firm |
webhook_url_refused |
422 | Webhook URL refused (HTTPS required, private or unresolved host) |
demo_mode |
403 | External effect blocked in the demo firm |
demo_unavailable |
503 | The demo firm is not available |
service_unavailable |
503 | General unavailability |
Licence and caps
code |
Status | Meaning |
|---|---|---|
company_limit_reached |
422 | Company cap of the licence or plan reached |
user_limit_reached |
422 | User cap of the licence reached |
invalid_key |
422 | Unknown activation key |
expired_key |
422 | Expired activation key |
exhausted_key |
422 | Key already used the allowed number of times |
key_reserved |
422 | Key reserved for another firm |
already_active |
422 | This key is already active on the firm |
Concurrency and history
code |
Status | Meaning |
|---|---|---|
conflict |
409 or 422 | The record has changed on the server since you read it |
period_locked |
422 | Locked period or closed fiscal year |
vat_locked |
422 | VAT return validated or filed |
entry_immutable |
422 | A posted entry cannot be modified: it must be reversed |
already_reverted, already_reversed |
422 | The revert or the reversal has already taken place |
in_use, booked |
422 | Deletion refused: the record is in use or booked |
code_taken |
422 | Restore refused: the code has been reused in the meantime |
not_revertible, nothing_to_revert, operation_not_found |
422 or 404 | Revert impossible |
force_forbidden, read_only |
403 | Insufficient role |
Synchronisation
These codes appear in the result of each sync/push mutation, not as an HTTP status.
code |
Meaning |
|---|---|
locked |
Accounting data posted or locked on the server side |
validation |
Data refused, details in errors |
not_supported_offline |
This operation does not go through sync/push |
not_found, forbidden, invalid_mutation, unknown_temp_id, unprocessable |
Mutation refused permanently |
server_error |
Temporary error, retryable: true |
conflict_already_resolved, conflict_choice_unavailable |
Conflict resolution refused (422) |
Peppol
Peppol refusals return blocker_codes, a list among: provider_not_configured, vat_number_missing, address_missing, email_missing, already_registered, managed_by_novadesko, registered_elsewhere, not_registered, provider_error.
{
"message": "Le dossier ne peut pas être inscrit sur Peppol.",
"errors": {"peppol": ["Le dossier ne peut pas être inscrit sur Peppol."]},
"blockers": ["L'adresse e-mail de contact est manquante."],
"blocker_codes": ["email_missing"],
"state": {"status": "none", "can_register": false}
}
Not all 422 errors carry a code. A simple validation error only contains message and errors. Allow for this case.
Validation error
{
"message": "Le champ entry date est obligatoire. (et 1 erreur supplémentaire)",
"errors": {
"entry_date": ["Le champ entry date est obligatoire."],
"lines.1.account_id": ["Le champ lines.1.account_id est obligatoire quand lines.1.account n'est pas présent."]
}
}
Rate limits
Limits apply per minute. They are counted per user for authenticated routes and per IP address for public routes. Routes that do not appear in this table have no limit of their own.
| Route | Limit per minute |
|---|---|
POST auth/login |
20 |
POST auth/demo |
10 |
POST auth/tokens |
20 |
POST firms/{firm}/webhooks/{id}/test |
12 |
GET pricing, GET pricing/simulate |
120 |
POST firms/{firm}/licence/activate |
10 |
POST firms/{firm}/sync |
6 |
GET lookup/search, lookup/companies/{identifier}, lookup/vat |
60 |
GET lookup/cities, lookup/streets |
120 |
POST companies/{company}/apply-sheet, third-parties/{id}/apply-sheet |
30 |
POST document-imports |
30 |
POST document-imports/{id}/process, .../validate |
120 |
POST document-imports/{id}/retry |
60 |
POST documents/book-all |
5 |
POST bank-transactions/match |
10 |
POST bank-transactions/book-matched |
5 |
POST payments/sepa |
20 |
GET vies, POST third-parties/{id}/vies |
30 |
POST sync/novadesko |
10 |
POST integrations/{id}/test, .../sync |
12 |
POST peppol/register |
6 |
POST peppol/refresh |
12 |
GET peppol/lookup |
30 |
GET third-parties/{id}/peppol |
60 |
POST third-parties/peppol/check-all |
6 |
GET exports/{type} |
30 |
GET exports/{type}/preview-url |
60 |
GET exports/full-dossier/{fiscalYear} |
10 |
POST fiscal-years/{id}/year-end |
5 |
GET history/export |
30 |
Every response from a rate-limited route shows your consumption:
HTTP/1.1 200 OK
X-RateLimit-Limit: 30
X-RateLimit-Remaining: 27
429 response
HTTP/1.1 429 Too Many Requests
Retry-After: 41
X-RateLimit-Limit: 30
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1791188421
Content-Type: application/json
{"message": "Too Many Attempts."}
Wait the number of seconds given by Retry-After before retrying. X-RateLimit-Reset gives the reset time in Unix seconds.
import time
import requests
def call(method, url, **kwargs):
for attempt in range(5):
response = requests.request(method, url, timeout=60, **kwargs)
if response.status_code == 429:
# Honour the server delay, never hammer the API
time.sleep(int(response.headers.get("Retry-After", "5")) + 1)
continue
if response.status_code in (500, 503):
time.sleep(2 ** attempt)
continue
return response
raise RuntimeError("NovaFisko API unavailable")
When you retry a write after a network error, a 500 or a 503, send the same X-Client-Mutation-Id again. Otherwise you risk creating a duplicate. See Idempotency.
Other limits
| Limit | Value |
|---|---|
| Files per upload in the importer | 20 |
| Size of an uploaded file | 25 MB |
| Files extracted from a ZIP archive | 50 |
Mutations per sync/push call |
200 |
Changes per sync/pull call |
500 by default, 1000 at most |
| Items per page | 50 by default, 200 at most (500 for bank transactions) |
| Lifetime of a signed export preview URL | 15 minutes |