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:
API error
Non-2xx response received.
Classify by status
429 / 5xx → retry. 400 / 422 → propagate validation. 401 / 403 → report auth. 404 / 409 → branch.
Recover or surface
Apply the right strategy.
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 status | Strategy | When |
|---|---|---|
400 / 422 | Surface to caller / fix the input | Validation, malformed JSON. |
401 | Page on-call | API key missing/expired. |
403 | Page on-call | Permissions mismatch. |
404 | Verify id and key scope | Resource missing or invisible to the calling org. |
409 | Read state and reconcile | Conflict (already published, already exists, etc.). |
429 | Retry with backoff | Rate limit. |
5xx | Retry with backoff | Server transient. |
Related
- Embedded Wallet — exceptions — the full SDK error catalogue (auto-generated).
- API key permissions — fix
401/403. - Node.js integration — the
keyban()helper used here. - Python integration —
KeybanAPIErrorin the official client.