Skip to main content

How to handle Keyban API errors

When a Keyban API call fails, the response body is always a JSON document with a predictable shape. This guide shows how to read it, branch on what matters, and recover gracefully.

The error response shape

Whatever endpoint you call, an error response looks like this:

{
"type": "about:blank",
"title": "Validation failed",
"status": 422,
"detail": "amount: must be a positive number",
"instance": "/v1/loyalty/account/abc/mint",
"errors": [
{ "path": "amount", "code": "min_value", "message": "must be >= 1" }
]
}

Two fields drive the handling logic:

  • status — the HTTP status (400, 401, 403, 404, 409, 422, 429, 5xx). Same as the response status code; mirrored in the body for clients that flatten the envelope.
  • errors — present only on validation failures (status: 422). Lists the offending fields with their per-field codes.

title, detail, and instance are for humans (logs, dashboards). Don't pattern-match on them — they're free-form English.

1. Classify the error

The first decision is whether to retry, propagate, or report. The decision flows from the HTTP status:

  1. API error

    Non-2xx response received.

  2. Classify by status

    429 / 5xx → retry. 400 / 422 → propagate validation. 401 / 403 → report auth. 404 / 409 → branch.

  3. Recover or surface

    Apply the right strategy.

Decision tree for handling an API error: classify by status, then act.

2. Retry on transient errors (429 and 5xx)

The two retryable categories are rate limits (429) and server-side transients (502, 503, 504). Use exponential backoff with jitter; respect the Retry-After response header if present.

async function callWithRetry<T>(fn: () => Promise<T>, attempts = 5): Promise<T> {
for (let i = 0; i < attempts; i++) {
try {
return await fn();
} catch (err) {
const status = (err as { status?: number }).status;
const retryable = status === 429 || (status !== undefined && status >= 500);
if (!retryable || i === attempts - 1) throw err;

const backoff = Math.min(2 ** i + Math.random(), 30);
await new Promise((r) => setTimeout(r, backoff * 1000));
}
}
throw new Error("unreachable");
}

Cap the number of attempts (5 is a good default) — beyond that, surface the failure to the caller and let your monitoring page someone.

3. Surface validation errors (422)

Validation errors carry a structured errors array — turn it into a per-field UI message instead of dumping the whole body to the user.

type ValidationProblem = {
status: 422;
errors?: Array<{ path: string; code?: string; message?: string }>;
};

function fieldErrors(problem: ValidationProblem): Record<string, string> {
return Object.fromEntries(
(problem.errors ?? []).map((e) => [e.path, e.message ?? e.code ?? "invalid"]),
);
}

// In a React form:
const errors = fieldErrors(problem);
<input name="email" />;
{errors.email && <span className="error">{errors.email}</span>}

For non-form callers (a CSV import, a CLI), log the array and exit non-zero — your operator can fix the offending row and re-run.

4. Treat auth errors as configuration bugs (401 and 403)

401 and 403 are almost never transient. They mean:

  • 401 Unauthorized — the API key was missing, malformed, expired, or revoked. Check your secret store and the key's status in the Admin Console.
  • 403 Forbidden — the key is valid but lacks the resource/action it's calling. Compare the key's permissions to the called endpoint and add the missing entry (see API key permissions).

Don't retry — you'll just get 401/403 again. Page someone (or surface a config error to the operator).

5. Branch carefully on 404 and 409

404 means the resource doesn't exist or your key can't see it. 409 means state conflict (a passport already published, an account that already exists). The current API does not expose a stable, machine-readable code field on every error response — for now, branch on status and inspect the human-readable detail / title for triage in logs, but do not pattern-match on those strings in code.

For SDK-side errors (thrown by @keyban/sdk-base / @keyban/sdk-react before or after the network call), the SdkError class does expose a stable code from the SdkErrorCode enum — see Embedded Wallet — exceptions for the full list.

6. Propagate the body to your error reporter

When forwarding to Sentry / Datadog / a custom error log, attach the full error body as a structured field — the human-readable detail makes the alerts actionable, and the status makes them aggregatable.

Sentry.captureException(err, {
contexts: {
keyban_error: {
status: err.status,
title: err.problem?.title,
detail: err.problem?.detail,
instance: err.problem?.instance,
errors: err.problem?.errors,
},
},
});

Reference

HTTP statusStrategyWhen
400 / 422Surface to caller / fix the inputValidation, malformed JSON.
401Page on-callAPI key missing/expired.
403Page on-callPermissions mismatch.
404Verify id and key scopeResource missing or invisible to the calling org.
409Read state and reconcileConflict (already published, already exists, etc.).
429Retry with backoffRate limit.
5xxRetry with backoffServer transient.