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_ 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
| Field | Meaning |
|---|---|
code | Stable, lower-case, and the thing to branch on. It does not change once published |
message | What happened, for a person or a log. Its wording can change — do not parse it |
next_ | One or two sentences telling the agent what to do next, naming the tool to call. Written to be followed as-is |
details | Structured facts about the failure — the fields that failed validation, the balance available, the status an approval is in. Omitted when there are none |
retryable | true 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
- Branch on
code. Two refusals can share a message and mean different things, and the same refusal can be reworded. - Follow
next_. It is the recovery path Moon would take, and it names the tool.step - Retry only when
retryableistrue, and then with the sameidempotency_. A retry with the same key replays the first answer, so it can never issue a second card or top up twice. Never retry akey false: the same call will get the same answer. - Do not loop. Several refusals mean wait for a person (
purchase_withintent_ not_ usable AWAITING_— callAPPROVAL wait_) or ask a person (for_ approval INTENT_declines). Calling again does not make a person answer faster.REQUIRED
retryable is true for a Moon-side failure (internal_), for rate limiting (too_) and for idempotency_, where the first call with that key is still running. It is false for everything else.
Failures outside the envelope
| Failure | What you receive |
|---|---|
| No usable credential, or an unsupported HTTP method | A 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 limit | A JSON-RPC error before any tool runs, whose error.data carries code, message, next_ and retryable — for example rate_ with Retry-After. See Limits |
| Arguments that do not match a tool's advertised schema | A 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 lacks | Inside the envelope: insufficient_, with details.missing_. 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
| Code | Meaning | next_ in short |
|---|---|---|
validation_ | An argument failed a rule the tool checks itself. details.errors lists { path, message } for each | Fix the named arguments and call again |
bad_ | The request was refused for a reason with no more specific code | Check the arguments and retry |
card_ | No such card for this connection. On a cardholder connection, a card belonging to someone else answers this too | Check the id, or call list_ |
cardholder_ | No such cardholder | Check the id, or create_ first |
not_ | Something else named in the call does not exist | Check the id you passed |
cardholder_ | create_ got both | Pass one of them |
velocity_, cardholder_ | A status read needs exactly one target | Pass exactly one of the named arguments |
amount_ | A negative amount | Pass a positive amount |
Money and verification
| Code | Meaning | next_ in short |
|---|---|---|
kyc_ | The cardholder (or your organization) is not verified. details.source_ keeps the original code | create_, share it, poll get_ |
cardholder_, cardholder_ | A verification gate specific to the card product | Same as kyc_ |
moon_ | The cardholder has not accepted the card agreements | The cardholder accepts them first |
Cards and controls
| Code | Meaning | next_ in short |
|---|---|---|
card_ | The card product does not support this | Pick another from list_ |
merchant_ | The product cannot carry a merchant lock | Pick one from details.merchant_, or drop merchant_ |
merchant_ | The card has no lock to change | Set the lock when the card is issued |
control_ | The control belongs to a card feature (single-use, merchant lock, ask-first) | Use the card-level tool, or terminate_ |
velocity_ | The target already has a control | Find it with list_ and update it |
velocity_ | fixed is a reserved metadata key | Remove it |
velocity_ | INTENT is not a type you create | Use enable_ |
agent_ | The account owner requires every card a connected agent creates to ask first | create_ without ask_ — it is issued asking first — then request_ for each purchase. Do not ask the person to turn the setting off |
Ask-first cards and purchase intents
| Code | Meaning | next_ in short |
|---|---|---|
card_ | request_ on a card that spends on sight | enable_ if that is what you wanted |
approval_ | Nobody can be asked. details.approver_ says whose address is missing | Give the cardholder, or the account, an email address |
purchase_ | currency is not the card's own | Omit it, or send the card's currency |
merchant_ | The price was given as merchant_ + merchant_, and Moon has no rate to convert that currency into the card's right now. Nothing was created | Ask again with amount in the card's own currency (details.card_): 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_ | The intent was already answered, spent, cancelled or expired | Read it with get_; ask again if the purchase still has to happen |
purchase_ | A decision other than approve or deny | Pass what the person actually said |
too_ | The card holds as many live approvals as it may | Cancel one you no longer need, or wait for them to expire |
too_ | The card holds as many standing approvals as it may | Cancel one — waiting will not clear it |
purchase_ | reveal_ on an ask-first card | request_, wait, then get_ |
purchase_ | The approval cannot be paid with now. details.status says why | AWAITING_: wait_. Anything else: ask again |
too_ | This approval has handed the number over as often as it may | Ask for a new approval; call get_ once per purchase |
Checkout details
These come from request_ with profile_, and from get_.
| Code | Meaning | next_ in short |
|---|---|---|
checkout_ | A detail named in profile_ is not saved. details.missing names it | The person adds it at /settings/checkout-details, then ask again. Never ask them to type it to you |
merchant_ | profile_ without merchant_ | Ask again naming the site |
profile_ | profile_ on a recurring approval | Use a single purchase or kind: "order", or leave the fields out |
profile_ | The national ID on file is a type that cannot be filled | Ask again without national_; the person enters it at checkout |
profile_ | An API-key connection has not opted in to receiving approved details | Send x-moon-allow-profile: true. On OAuth, the profile:reveal permission takes its place |
checkout_ | The account does not release checkout details to agents | The person enters them at checkout |
checkout_ | The approval was requested by another connection | Only the connection that asked receives the details |
profile_ | The approval did not name the details asked for. details.not_ lists them | request_ naming them in profile_ |
checkout_ | The details changed after the person approved | request_ again and wait for a new approval |
too_ | This approval has released its details as often as it may | Ask for a new approval; the count never resets |
Permissions and connections
| Code | Meaning | next_ in short |
|---|---|---|
reveal_ | An API-key connection has not opted in to card numbers | Send x-moon-allow-pan: true. On OAuth the permissions cards:pay (get_) and cards:reveal (reveal_) take its place |
insufficient_ | The connection was not granted the permission this needs. details.missing_ names it | Ask 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 |
forbidden | This connection may not perform that operation | Use a connection that may |
unauthorized | The credential is not valid for this | Check the bearer |
invalid_ | A signed request did not verify. Never says which check failed | Check keyid, the time window, the nonce and the covered components — or send the request unsigned |
agent_, agent_, agent_ | Agent-key administration refusals | Send a public Ed25519 JWK; reuse the existing key_; check the id |
Idempotency, environment and Moon-side failures
| Code | Meaning | retryable | next_ in short |
|---|---|---|---|
idempotency_ | The first call with this key is still running | true | Wait a moment, retry with the same key |
idempotency_ | The key is bound to different arguments | false | Use a new key, or resend the original arguments |
conflict | The resource is in a state that refuses this call | false | Read it back before retrying |
too_ | Rate limited | true | Wait, then retry with the same key |
internal_ | Moon failed | true | Retry once with the same key; then contact support |
A code not listed here falls back to the same rules: a code ending in _ means check the id, and anything else is answered with a next_ 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_ rather than failing.
When money may already have moved
create_ and add_ take an idempotency_. 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_ then tells the agent what to read — list_, 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.
Related
- 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