Developers

Guide

Errors

Every error is an RFC 9457 problem document (application/problem+json) carrying a machine-readable code. Programs branch on code and status; the human-readable fields exist for logs.

The shape

Error response
HTTP/1.1 403 Forbidden
Content-Type: application/problem+json
X-Request-Id: 0f0a2f6c-3f2f-4f7a-9f0e-2d1b8c4a55e1

{
  "type": "https://kelomo.fi/developers/guides/error-codes/#code-read_only_key",
  "title": "Forbidden",
  "status": 403,
  "detail": "read_only_key",
  "code": "read_only_key",
  "requestId": "0f0a2f6c-3f2f-4f7a-9f0e-2d1b8c4a55e1"
}
FieldMeaning
type Problem type URI. For a catalogued code it links to that code's entry in the error code reference; about:blank otherwise.
title Short human-readable phrase for the status class.
status The HTTP status code, repeated in the body.
detail Human-readable specifics for this occurrence.
requestId Identifier of this request, mirrored in the X-Request-Id response header. Quote it in support requests.
code Stable machine-readable identifier — the field to branch on. Translation into user-facing language is the client's job.

Every response — success or failure — also carries X-Request-Id, and the problem document repeats it as requestId. Log it: it is the one value support can use to find your exact request.

Status codes and representative codes

StatusWhenExample code
400 Malformed input, failed validation, or a missing required header. idempotency_key_required
401 Missing, revoked or expired credentials. api_key_expired, api_key_owner_inactive
403 Authenticated but not allowed — scope, capability or key mode. read_only_key, report_type_not_public
404 The resource does not exist in your organisation.
409 Conflict with current state — often an idempotency collision. idempotency_key_reused, idempotency_key_in_flight
422 Well-formed but semantically rejected. key_scope_exceeds_creator
429 Rate limit exceeded; honour Retry-After.
5xx Server-side failure — safe to retry idempotent requests with backoff.

Handling errors

Node.js
const response = await fetch(url, { headers });

if (!response.ok) {
  const problem = await response.json().catch(() => null);
  switch (problem?.code) {
    case 'api_key_expired':
      // mint or rotate the key, then retry
      break;
    case 'idempotency_key_reused':
      // same Idempotency-Key sent with a different payload — bug on our side
      break;
    default:
      if (response.status === 429) {
        const wait = Number(response.headers.get('retry-after') ?? '5');
        await new Promise((r) => setTimeout(r, wait * 1000));
        // …retry
      } else {
        throw new Error(`Kelomo API ${response.status}: ${problem?.code ?? 'unknown'}`);
      }
  }
}
  • Branch on code, not on detail or title — codes are part of the contract, wording is not.
  • Retry 429 after Retry-After and 5xx with exponential backoff. Retry POSTs only with an Idempotency-Key.
  • Treat unknown codes as fatal for the request but not for the integration: new codes are added over time as new operations join the public surface. Every code is listed in the error code reference.