Skip to content

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
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.

StatusCodeMeaning
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

PHP
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),
    };
}
JavaScript
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})`);
  }
}
Python
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.