Error Reference
Complete error catalog for the Trusteed API v1 and MCP Gateway. Use this to write precise retry logic and handle failures gracefully.
Error Response Format
REST and MCP fail differently, and the difference matters when you write retry logic. REST returns an HTTP status with a JSON envelope; an MCP tool returns HTTP 200 with isError on the result.
// REST — GET/POST /api/v1/...
{
"success": false,
"error": "Human-readable message",
"code": "MACHINE_CODE", // present on some endpoints, not all
"details": { ... } // optional
}
// MCP tool result — transport status is 200
{
"isError": true,
"content": [{ "type": "text", "text": "Human-readable message" }],
"structuredContent": {
"error": {
"code": "CART_NOT_MUTABLE",
"remedy": "refresh_state",
"retryable": false,
"scope": "call",
"details": { ... },
"reference": "https://trusteed.xyz/en/developers/errors#CART_NOT_MUTABLE"
}
}
}On REST, error carries the message and code is not yet uniform across every endpoint. Branch on the HTTP status. On MCP, structuredContent.error.code is the stable discriminator and the table below is generated from the same source the server emits.
Authentication Errors
Returned when the API key is missing, invalid, or lacks the required permissions to perform the requested action.
| HTTP Code | Error | Meaning | Recommended Action |
|---|---|---|---|
| 401 | unauthorized | Missing or invalid API key | Verify the X-Agent-Api-Key header is present and correctly formatted (agnt_xxx) |
| 401 | token_expired | OAuth token has expired | Refresh the token via POST /api/v1/oauth/token |
| 403 | forbidden | Key lacks the required scope for this action | Check key scopes in the Dashboard under Agent Keys |
| 403 | store_suspended | Merchant is not currently available | Choose a different merchant from the directory |
Rate Limit Errors
Rate limits are enforced per key and per tool. Read the X-RateLimit-Reset header to know when the window resets.
| HTTP Code | Error | Meaning | Recommended Action |
|---|---|---|---|
| 429 | rate_limit_exceeded | Per-key request limit hit | Pause and retry after the Unix timestamp in X-RateLimit-Reset |
Rate limit response headers
X-RateLimit-LimitMaximum requests allowed in the current windowX-RateLimit-RemainingRequests remaining before the limit is hitX-RateLimit-ResetUnix timestamp (seconds) when the window resets
Business Logic Errors
Returned when a request is structurally valid but fails due to application state (e.g. cart expired, store not found).
| HTTP Code | Error | Meaning | Recommended Action | Retryable |
|---|---|---|---|---|
| 404 | store_not_found | Store does not exist or is inactive | Verify the slug from the merchant directory | No |
| 500 | internal_error | Unexpected server error | Retry with exponential backoff (see Retry Guidance below) | Yes |
| 503 | service_unavailable | Temporary overload or maintenance | Retry after 30 seconds | Yes |
MCP Tool Errors
When an MCP tool fails, the transport still returns 200. The failure is on the result. Branch on structuredContent.error.remedy: it is a closed set, so you can handle it exhaustively. Codes marked funds captured mean a charge may already exist; retrying one of those can charge the buyer twice. PAYMENT_OUTCOME_UNKNOWN is the other possible-charge code: the charge request reached the processor and nobody knows what it did, so the checkout is held: stop, do not retry with any key or payment method, and escalate.
| Code | What it means | Remedy | Retry? | Holds for |
|---|---|---|---|---|
HUMAN_CONFIRMATION_DECLINED | A human was asked and declined. Nothing was changed. Re-asking without new information is not acceptable behaviour. | Stop: do not retry | No | This call only |
IDEMPOTENCY_KEY_CONFLICT | This session is already bound to a different idempotency_key. Reusing the session under a new key is refused because it cannot be distinguished from a double-submit. | Stop: do not retry | No | This call only |
PAYMENT_AMOUNT_MISMATCHFunds may already be captured | The captured amount does not match the amount the session authorised. Funds have moved. Do not retry; this needs reconciliation. | Stop: do not retry | No | This call only |
PAYMENT_CAPTURED_FINALIZATION_FAILEDFunds may already be captured | The capture SUCCEEDED and order finalization then failed. The buyer has been charged. Do not retry — a retry risks a second charge. Escalate with `details.capture_reference`. | Stop: do not retry | No | This call only |
PAYMENT_OUTCOME_UNKNOWNBuyer may have been charged | The charge request reached the payment processor and its outcome is unknown: the buyer MAY have been charged. Do not retry with this or any other key or payment method — the checkout session is held until the attempt is reconciled by the merchant. Escalate with the details (`rail`, and `paypal_order_id` when present). | Stop: do not retry | No | This call only |
HUMAN_CONFIRMATION_REQUIRED | The action needs a human to approve it and this path cannot ask one — the client does not support elicitation, or the approver is the merchant rather than the buyer. `details.approver` says who. Nothing was changed. | A human must decide | No | This call only |
NATIVE_CHECKOUT_REQUIRED | This merchant requires the purchase to complete on the platform's own hosted checkout. `details.checkout_url` carries where to send the buyer. | A human must decide | No | This merchant (cacheable) |
ORDER_NOT_CANCELLABLE | The order has advanced past the point where an agent may cancel it. `details.status` carries the current status. | A human must decide | No | This call only |
RECEIPT_ISSUANCE_OVERDUE | The purchase completed but its signed receipt did not arrive within the expected window. Do not retry; escalate with the checkout session id. | A human must decide | No | This call only |
RETURN_WINDOW_EXPIRED | The merchant's return window has closed for this order. Only a human on the merchant side can override it. | A human must decide | No | This call only |
AGENT_TOKEN_INVALID | The supplied agent token failed verification. `details.reason` carries the verifier's reason. Re-mint the token; do not retry the same one. | Fix the arguments | No | This call only |
CURRENCY_MISMATCH | The requested items do not share a single currency. Split them into one cart per currency. | Fix the arguments | No | This call only |
IDENTITY_TOKEN_REJECTED | The supplied identity token failed verification. `details.reason` carries the reason. | Fix the arguments | No | This call only |
IDENTITY_TOKEN_STORE_MISMATCH | The identity token is valid but was issued for a different store. Tokens are not portable across merchants. | Fix the arguments | No | This call only |
INSUFFICIENT_INPUT | The call is well-formed but does not carry enough material to act on — e.g. a comparison with fewer than two resolvable products. | Fix the arguments | No | This call only |
INSUFFICIENT_STOCK | The store catalog reports fewer units available than the line item asks for. `details` carries `requested_quantity`, `available_quantity` and `source` — the latter says which reading was compared (`catalog_snapshot` is the replicated catalog, which may lag the platform, not a live inventory call). Reduce the quantity; retrying the identical call will not change the answer. | Fix the arguments | No | This call only |
INVALID_ARGUMENT | An argument is malformed — a non-UUID id, an unparseable product id, or a value outside the accepted range. `details.field` names it when known. | Fix the arguments | No | This call only |
MANDATE_AUDIENCE_MISMATCH | The payment mandate was issued for a different store: its `aud` does not name this one. Mandates are not portable across merchants. The cart is not the reason — present a mandate whose `aud` is this store. | Fix the arguments | No | This call only |
MANDATE_CURRENCY_MISMATCH | The payment mandate authorises spending in a currency other than the one this cart is priced in, so its `max_amount_cents` cannot be compared against the cart total at all. The cart is not the reason — present a mandate denominated in the cart's currency. `details.currency` is the MANDATE's currency. | Fix the arguments | No | This call only |
MANDATE_EXPIRED | The payment mandate presented had already expired (`exp` in the past). The cart is not the reason and reducing it changes nothing — present a mandate with a later `exp`. `details.remediation` is `renew_mandate`. | Fix the arguments | No | This call only |
MANDATE_LIMIT_EXCEEDED | The cart total exceeds the `max_amount_cents` of the payment mandate the caller itself presented. `details` carries that limit and the cart total, because both are already known to the caller. Reduce the cart (or present a mandate that covers it); the identical call cannot succeed. The mandate's other boundaries have their own codes — `MANDATE_EXPIRED`, `MANDATE_AUDIENCE_MISMATCH`, `MANDATE_CURRENCY_MISMATCH`. | Fix the arguments | No | This call only |
MANDATE_SUBJECT_MISMATCH | The payment mandate's `sub` is not the identity this call authenticated as. A mandate is a spending cap the caller declares for itself, so `sub` must name the caller exactly as the server identifies it; `details.expected_sub` carries that value. The cart is not the reason — present the same mandate with `sub` set to `details.expected_sub`. | Fix the arguments | No | This call only |
MISSING_REQUIRED_ARGUMENT | A required argument was omitted, or a conditionally-required one was omitted for the chosen mode. `details.field` names it. | Fix the arguments | No | This call only |
ORDER_NOT_FOUND | No order matches the given id for this store. | Fix the arguments | No | This call only |
PAYMENT_METHOD_REQUIRED | This store requires the buyer's chosen payment method before completing the purchase. Ask the person which option they want from `details.payment_options`, then call again with `payment_option` set to their choice. Do not choose for them. | Fix the arguments | No | This call only |
PAYMENT_OPTION_MISMATCH | This checkout already has a saved `payment_option` from a previous call and this call asked for a different one. Retry with the saved option (`details.approved_option`), or start a new checkout if the person wants a different method. | Fix the arguments | No | This call only |
PAYMENT_OPTION_UNAVAILABLE | The requested `payment_option` is not available for this store. Ask the person to choose one of the available options in `details.payment_options`; this option will not become available on retry. | Fix the arguments | No | This merchant (cacheable) |
PAYMENT_REQUIRED | Nothing was charged: the checkout needs a signed payment. `details.x402_offer` carries the server-computed offer (network, payTo, asset, atomic amount). Sign exactly that and call again with `x402_payload`. | Fix the arguments | No | This call only |
PRODUCT_NOT_FOUND | No product matches the given id in this store. Re-run discovery rather than retrying the id. | Fix the arguments | No | This call only |
QUANTITY_LIMIT_EXCEEDED | A line item asks for more units than a single cart line accepts. `details` carries `requested_quantity` and `maximum_quantity`, and `details.field` names the line. Split the order or reduce the quantity; the identical call cannot succeed. This is the protocol's structural ceiling — a merchant rule capping quantities blocks earlier and reports as `CHECKOUT_BLOCKED_BY_POLICY`. | Fix the arguments | No | This call only |
TOOL_NOT_FOUND | No tool is registered under that name for this store. The set of tools a store exposes is fixed for the session — re-run discovery rather than guessing another name. | Fix the arguments | No | This merchant (cacheable) |
CART_NOT_FOUND | No cart session exists for that id, or it has expired. Create a new cart; do not retry with the same id. | Re-read state, then call again | No | This call only |
CART_NOT_MUTABLE | The cart was modified, completed or expired since it was read. Re-read the cart state and reapply the change against the current version. | Re-read state, then call again | No | This call only |
CHECKOUT_SESSION_NOT_FOUND | No checkout session exists for that id, or it has expired. Start from cart creation. | Re-read state, then call again | No | This call only |
CHECKOUT_STATUS_INVALID | The operation is not legal from the session's current status. `details.status` carries that status; advance the session through the documented transition first. | Re-read state, then call again | No | This call only |
CHECKOUT_TOTAL_NOT_RESOLVABLE | The session carries no readable total, so there is no amount to charge. This is NOT a free purchase: a missing total used to read as zero and skip the human-approval gate, completing an order that nobody paid. Re-price the cart (select shipping, apply discounts) so the session carries a `total` entry with `amount_cents`, then call again. | Re-read state, then call again | No | This call only |
MERCHANT_CATALOG_STALE | The merchant's catalogue has not been synchronised recently enough to verify the approved price against it, so the checkout was refused before any charge. `details` carries `age_seconds` and `max_age_seconds`. Nothing was charged. Retrying now will not help: the merchant must restore catalogue synchronisation first; once it is fresh, the same idempotency key may be reused. | Re-read state, then call again | No | This merchant (cacheable) |
PRECONDITION_NOT_MET | A prerequisite step has not been completed — no shipping method selected, no shipping options loaded yet. `details.required_step` names the tool to call first; retrying this one without it repeats the same refusal. | Re-read state, then call again | No | This call only |
SESSION_INACTIVE | The session is no longer active and accepts no further changes. | Re-read state, then call again | No | This call only |
STATE_PREVIEW_REQUIRED | This checkout could not proceed against a reconfirmed merchant state: the session could not be re-priced to it, its total changed after it was evaluated, or the reconfirmation no longer holds. `details.reason` says which. Nothing was charged. Call preview_checkout again to approve the current state, then complete_checkout — the same idempotency key may be reused. | Re-read state, then call again | No | This call only |
STATE_RECONFIRMATION_REQUIRED | The merchant's own state moved between the approved preview and this execution, by more than the tolerance but not past the blocking cut. `details.reconfirm_state_hash` is the hash of the state now in force; call again passing it as `reconfirmed_state_hash` to execute against THAT state. A retry without it, or with a stale hash, is refused again. | Re-read state, then call again | No | This call only |
VERIFICATION_NOT_FOUND | No attribute verification exists for that reference, or it has expired before being polled. Start a new verification. | Re-read state, then call again | No | This call only |
CHECKOUT_BLOCKED_BY_POLICY | A merchant enforcement rule blocked this checkout. `details.rule_code` names the rule. The block follows from THIS order — a different amount, item set or destination may pass — so retrying the identical checkout will not. | Try a different path | No | This call only |
FEATURE_NOT_ENABLED | The merchant has not enabled this capability. It is a configuration state, not a fault — retrying never flips it. | Try a different path | No | This merchant (cacheable) |
NOT_ELIGIBLE | The request is valid and the capability is enabled, but this subject does not qualify under the merchant's rules. | Try a different path | No | This call only |
PAYMENT_DECLINED | The payment instrument was declined by the processor. Nothing was captured. Retrying the same instrument reproduces the decline. | Try a different path | No | This call only |
PAYMENT_METHOD_NOT_CONFIGURED | The requested payment rail is not configured for this store. Call get_payment_methods and pick one that is. | Try a different path | No | This merchant (cacheable) |
PLAN_UPGRADE_REQUIRED | The merchant's plan does not include this path. Only the merchant can change that; pick another payment method or tool. | Try a different path | No | This merchant (cacheable) |
SHIPPING_COUNTRY_UNSUPPORTED | The merchant's enforced rules restrict shipping to a list of countries and the requested destination is not on it, so no rate is offered — an estimated rate towards an unserved country reads as usable and is not. `details` carries `requested_country`, `allowed_countries` and `derived_from` (the rules the list comes from). Retry with a destination on the list; nothing about the cart was changed. | Try a different path | No | This call only |
STATE_EXECUTION_BLOCKED | The merchant's own state diverged from the approved preview past the point where reconfirming is acceptable — insufficient stock, a price move too large, or a policy version change under a prior approval. `details.reasons` says which. No reconfirmation is offered: a different item set or quantity may pass. | Try a different path | No | This call only |
STORE_NOT_CONFIGURED | This store has not completed the configuration this tool depends on. Retrying will not change that; another store or another tool may work. | Try a different path | No | This merchant (cacheable) |
UPSTREAM_RESPONSE_INVALID | A dependency answered, but its response failed schema validation. Retrying reproduces it — the upstream contract is broken, not the connection. | Try a different path | No | This call only |
CHECKOUT_ALREADY_IN_PROGRESS | A checkout for this session is already running or has completed. Retrying with the SAME idempotency_key is safe and returns the existing order. | Retry the same call | Yes | This call only |
PAYMENT_FAILED_ROLLED_BACK | The payment attempt failed BEFORE any charge was made — the processor was never asked to charge, or it rejected the request — and the session was rolled back to READY_FOR_PAYMENT. Nothing was captured; retrying (with a fresh payment token for KYApay) is safe. | Retry the same call | Yes | This call only |
PAYMENT_NOT_APPROVED | The payer did not approve the payment. Nothing was captured and the session was rolled back to READY_FOR_PAYMENT — a fresh approval attempt is safe. | Retry the same call | Yes | This call only |
CART_ATTRIBUTE_WRITE_FAILED | An attribute the enforcement layer depends on could not be written to the platform cart, so the step was refused rather than leaving a rule unable to fire. Retry shortly. | Retry with backoff | Yes | This call only |
ENFORCEMENT_UNAVAILABLE | The enforcement layer could not be consulted — the signed policy snapshot was unreadable, or the evaluator itself errored. The operation was refused fail-closed rather than proceeding unprotected. Retry shortly. | Retry with backoff | Yes | This call only |
INTERNAL_ERROR | An unexpected fault on our side. Retry with backoff; if it persists, report it with the tool name and arguments. | Retry with backoff | Yes | This call only |
UPSTREAM_ERROR | The merchant's commerce platform returned an error for this read or write. Retry with backoff; if it persists the merchant's platform is at fault, not the request. | Retry with backoff | Yes | This call only |
UPSTREAM_UNAVAILABLE | A dependency this tool needs is temporarily unreachable. Retry with backoff. | Retry with backoff | Yes | This call only |
Retry Guidance
Not all errors are worth retrying. Follow these rules to build resilient integrations.
Retryable errors
429, 500, 503Backoff: 1s → 2s → 4s → 8s (max 4 retries)
Non-retryable errors
400, 401, 403, 404, 409, 422Fix the request before retrying
Recommended backoff strategy
// Exponential backoff: max 4 retries
const RETRYABLE = new Set([429, 500, 503]);
async function callWithRetry(fn: () => Promise<Response>): Promise<Response> {
let attempt = 0;
while (attempt <= 4) {
const res = await fn();
if (res.ok || !RETRYABLE.has(res.status)) return res;
if (res.status === 429) {
const reset = res.headers.get("X-RateLimit-Reset");
const waitMs = reset ? (Number(reset) * 1000 - Date.now()) : 1000;
await sleep(Math.max(waitMs, 0));
} else {
await sleep(1000 * 2 ** attempt); // 1s → 2s → 4s → 8s
}
attempt++;
}
return fn();
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}