Errors
Every failure returns the same JSON shape:
{ "error": "amount must be a base-10 integer", "code": "invalid_request" }error— a human-readable message. It names the specific reason (for a refused signing request, why it was refused). Show it to developers; do not branch on it.code— a stable machine slug. Branch on this (together with the HTTP status). It does not change when we reword a message.
Request error codes
Section titled “Request error codes”The HTTP status carries the category; code is the stable slug for it.
| Status | code | Meaning | Retryable |
|---|---|---|---|
| 400 | invalid_request | Malformed request, bad field, or a bad intent. | No — fix the request |
| 401 | unauthorized | Missing, expired, or invalid stamp; revoked key. | No — re-sign / re-key |
| 403 | forbidden | Authenticated but not allowed to do this. | No |
| 404 | not_found | No such vault, wallet, transaction, or key. | No |
| 409 | conflict | Idempotency key reused with different contents; or a state conflict. | No — resolve the conflict |
| 429 | rate_limited | Too many requests. | Yes — back off and retry |
| 503 | unavailable | Temporarily unable to serve (e.g. during maintenance). | Yes — back off and retry |
| 500 | internal | Unexpected server error. | Yes — retry, then contact support |
Treat rate_limited and unavailable (and internal) as retryable with
exponential backoff. Everything else is a caller-side problem to fix, not retry.
Handling in code
Section titled “Handling in code”import { XkovaMpcError } from "@xkova/mpc";
try { await xkova.sendNative({ /* … */ });} catch (e) { if (e instanceof XkovaMpcError) { const { code } = JSON.parse(e.body); if (code === "rate_limited" || code === "unavailable") { // retry with backoff } else if (code === "conflict") { // idempotency key reused with different contents — inspect } else { // surface e.status + code to the caller } }}XkovaMpcError carries the HTTP status and the raw body; parse the body for
code.
Rejected and failed transactions
Section titled “Rejected and failed transactions”A transaction that is refused or errors is not an HTTP error — the POST
succeeded and returned a transaction that later reaches a terminal state:
rejected— refused before signing by policy, spend limits, or a bad intent. No signature was produced.failed— a broadcast or on-chain error after signing.
Both carry an error_code on the transaction naming the reason. You see these via
webhooks or getTransaction, not as a thrown error. A
rejected transaction never moved funds and never produced a partial signature —
surface the reason and move on.