Guides
Errors
Every error from /api/v1 is application/problem+json as defined by RFC 9457. The shape is always the same, so a program can handle every error in one place.
HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json
X-Request-Id: 7f3c2a9e-4b1d-4c8e-9a51-2d6f0e8b3c17
{
"type": "urn:nonox:problem:validation_failed",
"title": "Unprocessable Content",
"status": 422,
"code": "validation_failed",
"detail": "Uppgifterna kunde inte godtas. Se errors.",
"instance": "urn:nonox:request:7f3c2a9e-4b1d-4c8e-9a51-2d6f0e8b3c17",
"errors": {
"rows": ["Verifikationen balanserar inte: debet och kredit skiljer sig åt."],
"voucher_date": ["Det finns inget räkenskapsår som omfattar 2031-01-01."]
}
}The fields
| Field | Contents |
|---|---|
type |
urn:nonox:problem:<code>. |
title |
The HTTP status's short English name. |
status |
The HTTP status. |
code |
Branch on this. Stable and machine-readable; never changes within v1. |
detail |
A Swedish explanation for people. It may change; never branch on it. |
instance |
urn:nonox:request:<uuid>, the same id as the X-Request-Id header. Quote it in support questions. |
errors |
Only with 422 validation_failed: the errors per field. |
required_scopes |
Only with 403 insufficient_scope: the scopes needed. |
A server error (500) never reveals its cause, only instance.
Every code
The list is generated from the OpenAPI file, so it is always complete. detail is the Swedish text the API sends.
| Status | Code | Meaning |
|---|---|---|
| 400 | idempotency_key_invalid |
Idempotency-Key too long or not visible ASCII detail: ”Idempotency-Key ska vara 1–255 synliga ASCII-tecken.” |
| 400 | idempotency_key_required |
No Idempotency-Key on a POST detail: ”Skicka en unik Idempotency-Key (till exempel ett UUID) med varje POST, så att ett nytt försök aldrig bokför två gånger.” |
| 400 | invalid_json |
Malformed JSON detail: ”Kroppen är inte giltig JSON (ett objekt).” |
| 401 | unauthenticated |
No valid credential detail: ”Nyckeln eller token saknas, är ogiltig, har gått ut eller har återkallats.” |
| 403 | app_not_allowed |
The company no longer allows the app detail: ”Företaget tillåter inte längre den här appen.” |
| 403 | company_archived |
Write to an archived company detail: ”Företaget är arkiverat och kan bara läsas.” |
| 403 | forbidden |
The action refuses (for example changing a posted voucher) detail: ”Behörighet saknas för det här.” |
| 403 | insufficient_scope |
Scope missing or permission lost detail: ”Nyckeln eller appen saknar behörigheten vouchers:post, eller så har personen som gav den inte längre den behörigheten i företaget.” |
| 403 | ip_not_allowed |
Outside the IP allowlist detail: ”Företaget tillåter inte anrop från den här adressen.” |
| 404 | not_found |
Unknown id, other company's id, or module off detail: ”Det du söker finns inte.” |
| 409 | idempotency_in_progress |
Same key, first request not finished detail: ”En begäran med samma Idempotency-Key pågår. Vänta och försök igen.” |
| 415 | unsupported_media_type |
Wrong Content-Type detail: ”Skicka JSON (application/json).” |
| 422 | idempotency_key_reused |
Same key, different body detail: ”Idempotency-Key har redan använts för en annan begäran. Använd en ny nyckel för en ny begäran.” |
| 422 | validation_failed |
Field errors detail: ”Uppgifterna kunde inte godtas. Se errors.” |
| 429 | rate_limited |
Over the limit detail: ”För många anrop. Försök igen om 23 sekunder.” |
What a program should do
| Status | Do this |
|---|---|
400 |
Fix the call. Usually the Idempotency-Key is missing or the body is not a JSON object. |
401 |
Refresh the access token (OAuth) and try once more. When it answers 401 again, or invalid_grant on the refresh, the connection was revoked: ask the person to connect again. For an API key: it has expired or was revoked. |
403 |
Do not retry. Read code: a scope is missing, the address is not allowed, the company is archived or no longer allows the app. |
404 |
The record does not exist, belongs to another company, or NONOX Connect is off for the company. The API deliberately does not tell them apart. |
409 |
A call with the same Idempotency-Key is running. Wait Retry-After seconds and retry with the same key. |
415 |
Send Content-Type: application/json (or multipart/form-data for attachments). |
422 |
Fix the data according to errors, or use a new Idempotency-Key for a new operation. |
429 |
Wait Retry-After seconds. See Rate limits. |
5xx |
Retry with the same Idempotency-Key and a growing delay. |
Handling errors, an example
use Illuminate\Http\Client\RequestException;
try {
$voucher = Http::withToken(config('services.nonox.token'))
->withHeaders(['Idempotency-Key' => $key])
->post('https://nonox.app/api/v1/vouchers/'.$id.'/post')
->throw()
->json('data');
} catch (RequestException $exception) {
$problem = $exception->response->json();
match ($problem['code'] ?? null) {
'validation_failed' => $this->showErrors($problem['errors']),
'insufficient_scope' => $this->askForScopes($problem['required_scopes']),
'company_archived' => $this->markReadOnly(),
default => report($exception),
};
}const response = await fetch(`https://nonox.app/api/v1/vouchers/${id}/post`, {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.NONOX_TOKEN}`, 'Idempotency-Key': key },
});
if (!response.ok) {
const problem = await response.json();
switch (problem.code) {
case 'validation_failed': showErrors(problem.errors); break;
case 'insufficient_scope': askForScopes(problem.required_scopes); break;
default: throw new Error(`${problem.code} (${problem.instance})`);
}
}response = requests.post(
f"https://nonox.app/api/v1/vouchers/{voucher_id}/post",
headers={"Authorization": f"Bearer {os.environ['NONOX_TOKEN']}", "Idempotency-Key": key},
timeout=30,
)
if not response.ok:
problem = response.json()
if problem["code"] == "validation_failed":
show_errors(problem["errors"])
elif problem["code"] == "insufficient_scope":
ask_for_scopes(problem["required_scopes"])
else:
raise RuntimeError(f"{problem['code']} ({problem['instance']})")Errors from OAuth
The token endpoint (/oauth/token) answers in OAuth's own format (error, error_description), not with problem+json. See Authentication.