Keepable
Foundations

Errors

Every non-2xx response is an RFC 7807 problem document. Here is the full catalogue of problem types, what triggers each, and whether to retry.

Keepable never returns a bare status code with an opaque body. Every non-2xx response is an RFC 7807 Problem Details document with the content type application/problem+json.

The problem shape

{
  "type": "https://errors.keepable.co/problems/unprocessable_entity",
  "title": "Unprocessable Entity",
  "status": 422,
  "detail": "nin must be 11 digits",
  "instance": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"
}
FieldUse it for
typeA stable URI under https://errors.keepable.co/problems/. Branch your error handling on this, not on title or detail.
titleA short, human-readable summary. Stable per type; safe to surface to operators.
statusThe HTTP status code, repeated in the body for convenience.
detailA human-readable explanation specific to this occurrence. May vary; do not pattern-match on it.
instanceA trace id for this occurrence. Quote it when you contact support: it lets us find your exact request.
codePresent when one status has several causes worth branching on, e.g. insufficient_funds on a 402, document_in_use on a 409, consent_not_erasable on a 409. Stable, like type.

Catalogue

The type is the slug shown below, rooted at https://errors.keepable.co/problems/.

Statustype slugMeaningRetry?
400bad_requestThe request was malformed: bad JSON, or a required field missing.No, fix the request.
401unauthorizedMissing, malformed, or revoked credentials.After fixing the key.
402payment_requiredThe tenant's prepaid wallet lacks the balance for a live send (insufficient_funds).Yes, after topping up the wallet.
403forbiddenAuthenticated but missing the scope required by this operation.No, mint or select a correctly scoped key.
404not_foundThe resource does not exist (or your tenant cannot see it).No.
409conflictThe request conflicts with existing state, such as an Idempotency-Key reused with a different body or a run withdrawn after delivery.No, inspect the stable problem code.
413request_too_largeThe request body exceeds the 32 MiB limit.No, shrink the payload.
422unprocessable_entityWell-formed but semantically invalid: an 11-digit NIN rule violated, an unknown enum value.No, fix the data.
429rate_limitedToo many requests.Yes, back off (below).
500internalAn unexpected server error.Yes, with backoff; quote instance if it persists.
501not_implementedThe endpoint or capability is not enabled in this environment.No.
503unavailableA required upstream system did not answer.Yes, with backoff.

401 Unauthorized

{ "type": "https://errors.keepable.co/problems/unauthorized", "title": "Unauthorized", "status": 401, "detail": "api key is invalid" }

Check that your API key is sent as Authorization: Bearer <key> and has not been rotated or revoked. If the key looks right and still 401s, rotate it in Workspace and retry with the new secret.

403 Forbidden

Your key was not granted the scope the operation requires. Use a key with the right scope.

An address that reaches nobody is not a 403. An unmatched email appears in the plan run's rejected array without failing the whole run, while an unmatched NIN or eligible CAC number can be retained. See Recipients.

409 Conflict

{ "type": "https://errors.keepable.co/problems/conflict", "title": "Conflict", "status": 409, "detail": "this Idempotency-Key was used with a different request body" }

Most often an Idempotency-Key collision: you reused a key with a different request body. Use a fresh key for a genuinely new operation, and the same key only for retries of the same operation.

429 Rate limited

{ "type": "https://errors.keepable.co/problems/rate_limited", "title": "Too Many Requests", "status": 429, "detail": "rate limit exceeded; retry after 30s" }

Back off and retry. Use exponential backoff with jitter, and because every mutation already carries an Idempotency-Key, retrying is safe: you will not double-send.

Handling errors well

Branch on type, never on title or detail. The type URI is the stable contract; the human strings can change between versions.

Retry only 429, 503 and 5xx, with exponential backoff and jitter. 4xx errors other than 429 are your bug to fix: retrying them unchanged just repeats the failure.

Log instance on every failure. It is the trace id that lets Partner Engineering find your exact request when you escalate.

A typed guard keeps the handling in one place:

class KeepableError extends Error {
  constructor(
    readonly type: string,
    readonly status: number,
    readonly detail: string,
    readonly instance?: string,
  ) {
    super(`${status} ${type}: ${detail}`);
  }
}

async function call(res: Response) {
  if (res.ok) return res.json();
  const p = await res.json(); // application/problem+json
  throw new KeepableError(p.type, p.status, p.detail ?? "", p.instance);
}

On this page