Browse the docs

Coded Tool Errors and next_step

The MCP tool error envelope, when to retry, and every error code with what to do next.

When a Moon MCP tool cannot do what it was asked, it does not fail the protocol call. It answers normally, with a result marked isError: true whose single text block is a JSON error envelope: a stable code to branch on, and a next_step sentence that tells the agent what to do about it. This page is that contract, and the list of codes.

It applies to the MCP server only. The REST API uses its own error envelope.

The envelope

FieldMeaning
codeStable, lower-case, and the thing to branch on. It does not change once published
messageWhat happened, for a person or a log. Its wording can change — do not parse it
next_stepOne or two sentences telling the agent what to do next, naming the tool to call. Written to be followed as-is
detailsStructured facts about the failure — the fields that failed validation, the balance available, the status an approval is in. Omitted when there are none
retryabletrue only when repeating the same call can succeed

details never contains a card number or security code, whatever raised the error, and any run of digits shaped like a card number is replaced with [redacted] in message and in string details.

How to handle a refusal

  1. Branch on code. Two refusals can share a message and mean different things, and the same refusal can be reworded.
  2. Follow next_step. It is the recovery path Moon would take, and it names the tool.
  3. Retry only when retryable is true, and then with the same idempotency_key. A retry with the same key replays the first answer, so it can never issue a second card or top up twice. Never retry a false: the same call will get the same answer.
  4. Do not loop. Several refusals mean wait for a person (purchase_intent_not_usable with AWAITING_APPROVAL — call wait_for_approval) or ask a person (INTENT_REQUIRED declines). Calling again does not make a person answer faster.

retryable is true for a Moon-side failure (internal_error), for rate limiting (too_many_requests) and for idempotency_in_progress, where the first call with that key is still running. It is false for everything else.

Failures outside the envelope

FailureWhat you receive
No usable credential, or an unsupported HTTP methodA real HTTP 401 or 405, before any tool runs. The 401 carries a WWW-Authenticate challenge — see MCP Server
Maintenance, an oversized request or batch, a restricted account, a rate limitA JSON-RPC error before any tool runs, whose error.data carries code, message, next_step and retryable — for example rate_limited with Retry-After. See Limits
Arguments that do not match a tool's advertised schemaA result with isError: true whose text is plain, not JSON, and starts with Input validation error:. Treat any isError text that does not parse as JSON as this case
A tool behind a permission an OAuth connection lacksInside the envelope: insufficient_scope, with details.missing_scopes. The tool is listed in tools/list with a line naming the permission, and never runs. See What the agent sees when a tool is missing

Codes

Arguments and lookups

CodeMeaningnext_step in short
validation_errorAn argument failed a rule the tool checks itself. details.errors lists { path, message } for eachFix the named arguments and call again
bad_requestThe request was refused for a reason with no more specific codeCheck the arguments and retry
card_not_foundNo such card for this connection. On a cardholder connection, a card belonging to someone else answers this tooCheck the id, or call list_cards
cardholder_not_foundNo such cardholderCheck the id, or create_cardholder first
not_foundSomething else named in the call does not existCheck the id you passed
cardholder_id_and_end_customer_id_exclusivecreate_card got bothPass one of them
velocity_control_target_required, cardholder_status_target_requiredA status read needs exactly one targetPass exactly one of the named arguments
amount_cannot_be_negativeA negative amountPass a positive amount

Money and verification

CodeMeaningnext_step in short
kyc_requiredThe cardholder (or your organization) is not verified. details.source_code keeps the original codecreate_cardholder_kyc_link, share it, poll get_cardholder_kyc_status
cardholder_bank_kyc_not_approved, cardholder_required_for_issuerA verification gate specific to the card productSame as kyc_required
moon_card_agreement_not_acceptedThe cardholder has not accepted the card agreementsThe cardholder accepts them first

Cards and controls

CodeMeaningnext_step in short
card_product_not_supportedThe card product does not support thisPick another from list_card_products
merchant_lock_unsupportedThe product cannot carry a merchant lockPick one from details.merchant_lock_card_product_ids, or drop merchant_lock
merchant_lock_not_foundThe card has no lock to changeSet the lock when the card is issued
control_is_fixedThe control belongs to a card feature (single-use, merchant lock, ask-first)Use the card-level tool, or terminate_card
velocity_control_already_existsThe target already has a controlFind it with list_velocity_controls and update it
velocity_control_reserved_metadata_keyfixed is a reserved metadata keyRemove it
velocity_control_reserved_typeINTENT is not a type you createUse enable_ask_first
agent_cards_must_ask_firstThe account owner requires every card a connected agent creates to ask firstcreate_card without ask_first — it is issued asking first — then request_purchase for each purchase. Do not ask the person to turn the setting off

Ask-first cards and purchase intents

CodeMeaningnext_step in short
card_not_ask_firstrequest_purchase on a card that spends on sightenable_ask_first if that is what you wanted
approval_email_missingNobody can be asked. details.approver_type says whose address is missingGive the cardholder, or the account, an email address
purchase_intent_currency_mismatchcurrency is not the card's ownOmit it, or send the card's currency
merchant_currency_unsupportedThe price was given as merchant_amount + merchant_currency, and Moon has no rate to convert that currency into the card's right now. Nothing was createdAsk again with amount in the card's own currency (details.card_currency): read the card-currency price off the checkout if it shows one, or convert it yourself and add a tolerance for the difference. Not retryable as-is
purchase_intent_not_pendingThe intent was already answered, spent, cancelled or expiredRead it with get_purchase_intent; ask again if the purchase still has to happen
purchase_intent_decision_invalidA decision other than approve or denyPass what the person actually said
too_many_pending_intentsThe card holds as many live approvals as it mayCancel one you no longer need, or wait for them to expire
too_many_recurring_approvalsThe card holds as many standing approvals as it mayCancel one — waiting will not clear it
purchase_intent_requiredreveal_card_credentials on an ask-first cardrequest_purchase, wait, then get_payment_credential
purchase_intent_not_usableThe approval cannot be paid with now. details.status says whyAWAITING_APPROVAL: wait_for_approval. Anything else: ask again
too_many_credential_revealsThis approval has handed the number over as often as it mayAsk for a new approval; call get_payment_credential once per purchase

Checkout details

These come from request_purchase with profile_fields, and from get_checkout_details.

CodeMeaningnext_step in short
checkout_profile_field_missingA detail named in profile_fields is not saved. details.missing names itThe person adds it at /settings/checkout-details, then ask again. Never ask them to type it to you
merchant_domain_required_for_profileprofile_fields without merchant_domainAsk again naming the site
profile_fields_not_allowed_on_recurringprofile_fields on a recurring approvalUse a single purchase or kind: "order", or leave the fields out
profile_field_not_fillableThe national ID on file is a type that cannot be filledAsk again without national_id; the person enters it at checkout
profile_reveal_not_enabledAn API-key connection has not opted in to receiving approved detailsSend x-moon-allow-profile: true. On OAuth, the profile:reveal permission takes its place
checkout_details_not_availableThe account does not release checkout details to agentsThe person enters them at checkout
checkout_details_not_for_this_connectionThe approval was requested by another connectionOnly the connection that asked receives the details
profile_fields_not_approvedThe approval did not name the details asked for. details.not_approved lists themrequest_purchase naming them in profile_fields
checkout_profile_changedThe details changed after the person approvedrequest_purchase again and wait for a new approval
too_many_profile_releasesThis approval has released its details as often as it mayAsk for a new approval; the count never resets

Permissions and connections

CodeMeaningnext_step in short
reveal_not_enabledAn API-key connection has not opted in to card numbersSend x-moon-allow-pan: true. On OAuth the permissions cards:pay (get_payment_credential) and cards:reveal (reveal_card_credentials) take its place
insufficient_scopeThe connection was not granted the permission this needs. details.missing_scopes names itAsk the client to reconnect with that permission — a one-step re-approval. If it cannot, the person reconnects at Settings → Connected apps (/settings/connected-apps), approving it
forbiddenThis connection may not perform that operationUse a connection that may
unauthorizedThe credential is not valid for thisCheck the bearer
invalid_agent_signatureA signed request did not verify. Never says which check failedCheck keyid, the time window, the nonce and the covered components — or send the request unsigned
agent_key_invalid_jwk, agent_key_already_registered, agent_key_not_foundAgent-key administration refusalsSend a public Ed25519 JWK; reuse the existing key_id; check the id

Idempotency, environment and Moon-side failures

CodeMeaningretryablenext_step in short
idempotency_in_progressThe first call with this key is still runningtrueWait a moment, retry with the same key
idempotency_mismatchThe key is bound to different argumentsfalseUse a new key, or resend the original arguments
conflictThe resource is in a state that refuses this callfalseRead it back before retrying
too_many_requestsRate limitedtrueWait, then retry with the same key
internal_errorMoon failedtrueRetry once with the same key; then contact support

A code not listed here falls back to the same rules: a code ending in _not_found means check the id, and anything else is answered with a next_step that says whether to fix the arguments or retry. New codes are added without notice as tools are added, so handle an unknown code by following next_step rather than failing.

When money may already have moved

create_card and add_card_balance take an idempotency_key. If a call fails after the money-moving step began, the key is bound to that failure and a retry with the same key replays the failure rather than running again; the next_step then tells the agent what to read — list_cards, or the card's balance — before deciding whether to try again with a new key. That is what keeps one unlucky attempt from becoming two funded cards. A call refused before any money moved releases its key, so the same key works once the cause is fixed. See MCP Server.

  • MCP Server — the tools, connections and permissions.
  • Purchase intents — what the ask-first codes are about.
  • Errors — the REST API's error envelope, for comparison.

Last updated October 22, 2026