Skip to content

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.

The HTTP status carries the category; code is the stable slug for it.

StatuscodeMeaningRetryable
400invalid_requestMalformed request, bad field, or a bad intent.No — fix the request
401unauthorizedMissing, expired, or invalid stamp; revoked key.No — re-sign / re-key
403forbiddenAuthenticated but not allowed to do this.No
404not_foundNo such vault, wallet, transaction, or key.No
409conflictIdempotency key reused with different contents; or a state conflict.No — resolve the conflict
429rate_limitedToo many requests.Yes — back off and retry
503unavailableTemporarily unable to serve (e.g. during maintenance).Yes — back off and retry
500internalUnexpected 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.

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.

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.