> Fetch https://askmiles.ai/llms.txt first: it indexes every public Miles capability. This file is the markdown twin of https://askmiles.ai/docs/reference/error-codes.

# Error codes

Every refusal code the MCP tools, the chat tools and the Chat API emit, with what it means and what to send next.

## How to read a refusal

Every refusal names itself. The code is a closed vocabulary you can branch on, the message says what happened in words a person can read, and the `fix` says what to send next. Branch on the code, show the message, follow the fix.

A refusal that refuses the whole call also carries `docs`, an absolute link to that code's anchor on this page, so an agent that meets a code it does not recognize has somewhere to go without a search. The per-entry results below, which ride inside a successful response, do not carry it, and neither does the Chat API's envelope.

49 codes are listed below. A code that is not here is not one of them: Miles also returns plain sentences as `error` on some tools, and those are written for the model to read rather than to be matched on.

## Tool refusals

A tool that could not do what it was asked answers `{"success": false, "error": <code>, "message", "fix"}` and writes nothing. Over MCP the same result is flagged `isError: true`. Every code below also carries `docs`, the link to its own anchor on this page.

### `ambiguous_holding`

A `card_name` matched more than one holding the member has, so no single card was chosen.

**Fix.** Call again with `wallet_id` set to one of `holdings[].wallet_id`. Ask the member if the labels do not settle it.

`analyze_keep_cancel` has always answered this way. `get_wallet_card` joined it on 2026-09-21, where it used to answer `multiple_matches` with `matches`; both old names ride along as `previous_error` and `matches` until 2026-12-31, and the [stability page](https://askmiles.ai/docs/trust/stability) has the window.

*Where:* Chat, MCP.

### `batch_too_large`

The batch held more entries than the tool accepts in one call. Nothing was written.

**Fix.** Split the list into batches of `max_entries` or fewer, which the refusal names, then call again.

*Where:* Chat, MCP.

### `card_not_found`

The name is not in the member's wallet and matches no card in the catalog.

**Fix.** Check the name against `did_you_mean`, or call `search_cards` to find the product, then call again.

*Where:* Chat, MCP.

### `conflicting_update`

A field was given a value and named in `clear_fields` in the same call, so the intent is ambiguous.

**Fix.** Send the value or the clear, not both, then call `update_card` again. Nothing was written.

*Where:* Chat, MCP.

### `empty_batch`

The tool was called with an empty entry list, so there was nothing to write.

**Fix.** Send at least one entry, or skip the call.

*Where:* Chat, MCP.

### `invalid_anniversary_date`

`anniversary_date` was not a calendar date written YYYY-MM-DD, so `add_card` and `update_card` refused the call at the door.

**Fix.** Send the date as YYYY-MM-DD, or omit the field. Ask the member for the exact date rather than guessing one. Nothing was written.

One of ten `invalid_<field>` codes, one per date field the write tools guard. A date is refused rather than stored because the member's own form is a date input, which shows an empty box for a value it cannot read, so a guessed date makes a row nobody can repair.

*Where:* MCP.

### `invalid_bonus_accepted_on`

`bonus_accepted_on` was not a calendar date written YYYY-MM-DD, so `add_card` and `update_card` refused the call at the door.

**Fix.** Send the date as YYYY-MM-DD, or omit the field. Ask the member for the exact date rather than guessing one. Nothing was written.

One of ten `invalid_<field>` codes, one per date field the write tools guard. A date is refused rather than stored because the member's own form is a date input, which shows an empty box for a value it cannot read, so a guessed date makes a row nobody can repair.

*Where:* MCP.

### `invalid_card_settings`

An `add_card` entry's `card_settings` or `benefit_selections` failed validation, so the card was not added.

**Fix.** Correct the setting or selection the message names, then call `add_card` again.

*Where:* Chat, MCP.

### `invalid_closed_on`

`closed_on` was not a calendar date written YYYY-MM-DD, so `add_card` and `update_card` refused the call at the door.

**Fix.** Send the date as YYYY-MM-DD, or omit the field. Ask the member for the exact date rather than guessing one. Nothing was written.

One of ten `invalid_<field>` codes, one per date field the write tools guard. A date is refused rather than stored because the member's own form is a date input, which shows an empty box for a value it cannot read, so a guessed date makes a row nobody can repair.

*Where:* MCP.

### `invalid_date_opened`

`date_opened` was not a calendar date written YYYY-MM-DD, so `add_card` and `update_card` refused the call at the door.

**Fix.** Send the date as YYYY-MM-DD, or omit the field. Ask the member for the exact date rather than guessing one. Nothing was written.

One of ten `invalid_<field>` codes, one per date field the write tools guard. A date is refused rather than stored because the member's own form is a date input, which shows an empty box for a value it cannot read, so a guessed date makes a row nobody can repair.

*Where:* MCP.

### `invalid_entries`

The entries argument was not a list, so the batch was never read and nothing was written.

**Fix.** Send the entries as a JSON list of objects, then call again.

*Where:* Chat, MCP.

### `invalid_expiration`

`expiration` was not a calendar date written YYYY-MM-DD, so `add_gift_card` and `update_gift_card` refused the call at the door.

**Fix.** Send the date as YYYY-MM-DD, or omit the field. Ask the member for the exact date rather than guessing one. Nothing was written.

One of ten `invalid_<field>` codes, one per date field the write tools guard. A date is refused rather than stored because the member's own form is a date input, which shows an empty box for a value it cannot read, so a guessed date makes a row nobody can repair.

*Where:* MCP.

### `invalid_fee_posts_on`

`fee_posts_on` was not a calendar date written YYYY-MM-DD, so `update_card` refused the call at the door.

**Fix.** Send the date as YYYY-MM-DD, or omit the field. Ask the member for the exact date rather than guessing one. Nothing was written.

One of ten `invalid_<field>` codes, one per date field the write tools guard. A date is refused rather than stored because the member's own form is a date input, which shows an empty box for a value it cannot read, so a guessed date makes a row nobody can repair.

*Where:* MCP.

### `invalid_new_anniversary_date`

`new_anniversary_date` was not a calendar date written YYYY-MM-DD, so `update_card` refused the call at the door.

**Fix.** Send the date as YYYY-MM-DD, or omit the field. Ask the member for the exact date rather than guessing one. Nothing was written.

One of ten `invalid_<field>` codes, one per date field the write tools guard. A date is refused rather than stored because the member's own form is a date input, which shows an empty box for a value it cannot read, so a guessed date makes a row nobody can repair.

*Where:* MCP.

### `invalid_review_by`

`review_by` was not a calendar date written YYYY-MM-DD, so `update_card` refused the call at the door.

**Fix.** Send the date as YYYY-MM-DD, or omit the field. Ask the member for the exact date rather than guessing one. Nothing was written.

One of ten `invalid_<field>` codes, one per date field the write tools guard. A date is refused rather than stored because the member's own form is a date input, which shows an empty box for a value it cannot read, so a guessed date makes a row nobody can repair.

*Where:* MCP.

### `invalid_signup_bonus_received_on`

`signup_bonus_received_on` was not a calendar date written YYYY-MM-DD, so `add_card` and `update_card` refused the call at the door.

**Fix.** Send the date as YYYY-MM-DD, or omit the field. Ask the member for the exact date rather than guessing one. Nothing was written.

One of ten `invalid_<field>` codes, one per date field the write tools guard. A date is refused rather than stored because the member's own form is a date input, which shows an empty box for a value it cannot read, so a guessed date makes a row nobody can repair.

*Where:* MCP.

### `invalid_sub_deadline`

`sub_deadline` was not a calendar date written YYYY-MM-DD, so `add_card` and `update_card` refused the call at the door.

**Fix.** Send the date as YYYY-MM-DD, or omit the field. Ask the member for the exact date rather than guessing one. Nothing was written.

One of ten `invalid_<field>` codes, one per date field the write tools guard. A date is refused rather than stored because the member's own form is a date input, which shows an empty box for a value it cannot read, so a guessed date makes a row nobody can repair.

*Where:* MCP.

### `invalid_update`

An `update_card` argument failed validation: `card_settings`, `benefit_selections`, or a cardholder who is not in the household.

**Fix.** Correct the argument the message names, then call `update_card` again. Nothing was written.

*Where:* Chat, MCP.

### `issuer_mismatch`

The name you sent and the only wallet card it could mean come from different issuers, so nothing was analyzed.

**Fix.** Ask the member which card they mean, then call again with that name or a `wallet_id`.

*Where:* Chat, MCP.

### `merchant_lookup_quota_exceeded`

`get_best_card` was given a merchant name it had never resolved before, and this member's own daily cap on new merchant lookups (25) is already spent. No category was found, so no card was recommended.

**Fix.** Call again with `category` set directly (dining, groceries, travel, ...) instead of relying on `merchant` to resolve one. The cap resets at midnight Pacific.

*Where:* Chat, MCP.

### `missing_identifier`

The call named neither an id nor a lookup name, so the tool cannot tell which record you mean.

**Fix.** Send the record's id, or the lookup name the tool accepts. Wallet ids come from `get_wallet_cards`.

*Where:* Chat, MCP.

### `multiple_matches`

A name matched more than one watchlist entry, gift card, or card to write to, so nothing was chosen.

**Fix.** Call again with an id from `matches`, or narrow the name. Wallet ids come from `get_wallet_cards`.

Still current for those tools, where the rows are not holdings and the retry takes a different argument. Wallet lookups moved to `ambiguous_holding` on 2026-09-21 and carry this code as `previous_error` until 2026-12-31; see the [stability page](https://askmiles.ai/docs/trust/stability).

*Where:* Chat, MCP.

### `not_a_wallet_id`

The `wallet_id` you sent is not in the `cc_N` shape every wallet id uses, so it was not looked up.

**Fix.** Send a `cc_N` id from `get_wallet_cards`, or pass the name as `card_name` instead.

*Where:* Chat, MCP.

### `not_clearable`

`clear_fields` was not a list, named a field `update_card` cannot clear, or rode along with `product_change`.

**Fix.** Send `clear_fields` as a list of clearable field names, in a call of its own. Nothing was written.

*Where:* Chat, MCP.

### `not_found`

No wallet holding carries that id, or no watchlist entry matches that name.

**Fix.** Read the current ids with `get_wallet_cards` or `get_watchlist`, then call again with one of them.

*Where:* Chat, MCP.

### `unknown_cardholder`

The `cardholder` you sent matches no member of this household, so no holding was picked.

**Fix.** Call again with a household member's name, or leave `cardholder` out for the account holder's own card.

*Where:* Chat, MCP.

### `wallet_unreadable`

The wallet could not be read to validate this write, so nothing was written.

**Fix.** Retry the call. If it repeats, the wallet read is failing, not the entries you sent.

*Where:* Chat, MCP.

### `write_failed`

The write was staged but never committed, so the ledger holds nothing from this call.

**Fix.** Retry the call. Read the message, which names what the storage layer reported.

*Where:* Chat, MCP.

## Per-entry results

These are not refusals of the call. They ride inside a 200 response, one per entry, saying what happened to that entry while the rest of the batch went through. `period_not_writable` and `period_required` are rejections; `already_recorded` and `member_recorded` are skips, and a skip means the ledger already held the better answer.

### `already_recorded`

The period already holds an answer, and `only_if_absent` leaves an answered period alone. The entry was skipped.

**Fix.** Leave it, or send the entry again without `only_if_absent` to replace the recorded answer.

*Where:* Chat, MCP.

### `member_recorded`

The member recorded this period themselves, and an agent's write does not replace the member's own answer.

**Fix.** Leave it. On a write, pass `overwrite: true` only when the member told you in this conversation to replace it.

*Where:* Chat, MCP.

### `period_not_writable`

The period named is outside the window this ledger can still be corrected for, so the entry was rejected.

**Fix.** Send a label from `writable_periods`. A period later than the current one cannot be recorded at all.

*Where:* Chat, MCP.

### `period_required`

A `clear_credit_usage` entry left `period` out, and a removal never defaults to the current period.

**Fix.** Send the period label of the entry to remove, then call `clear_credit_usage` again.

*Where:* Chat, MCP.

## MCP connection and quota gates

These come from the MCP server itself, before the tool runs. They arrive as a tool result with `isError: true`, with the code under `structuredContent.result`'s `error` object alongside the message a person can read. `/docs/mcp/quotas` has the four ceilings and what the advisory looks like before one is spent.

### `account_not_found`

The token is valid, but the Miles account behind it is gone. This is not a spent ceiling.

**Fix.** The member reconnects the app from Miles settings. Waiting does not clear this.

*Where:* MCP.

### `no_identity`

The call carried no signed-in Miles identity, so no wallet could be read.

**Fix.** Connect the app again so every call carries a Miles token.

*Where:* MCP.

### `not_allowed`

The tool named is not available over the remote MCP connection.

**Fix.** Call a tool the connection lists. The listed set is what this connection can reach.

*Where:* MCP.

### `paused`

The member turned MCP access off in Miles settings, so the connection cannot call tools.

**Fix.** The member re-enables MCP access in Miles settings. The token stays valid, so nothing needs re-minting.

*Where:* MCP.

### `quota_exceeded`

One of the four daily ceilings is spent: this tool, all calls, writes, or changed rows.

**Fix.** Wait for midnight Pacific, or send a smaller batch. Reads keep working when the write ceiling is spent.

*Where:* MCP.

### `refused`

A connection gate refused this call, and the refusal carried no more specific code.

**Fix.** Read the message. Check the connection's identity, pause state, and scope before retrying.

*Where:* MCP.

### `service_unavailable`

`knowledge_search` did not run: the tool is off on this deployment, or the backend is unreachable.

**Fix.** Read the message to tell the two apart. Do not read the result as evidence the corpus lacks an answer.

*Where:* MCP.

### `supporter_required`

The tool is part of Membership, and this account is not a Member.

**Fix.** The member joins Membership. No scope grants this tool, and the code keeps its historical spelling.

*Where:* MCP.

### `write_scope`

The connection holds read-only access, and the tool needs the write or the reconciliation scope.

**Fix.** The member disconnects the app in Miles settings and reconnects with the matching toggle checked.

*Where:* MCP.

## Chat API

The OpenAI-compatible surface answers in OpenAI's envelope, so these are `type` and `code` values rather than Miles refusal codes, and no `docs` field is added to them. `/docs/chat-api/errors` has the full status table, the mid-stream shape and the headers.

### `connections_paused`

The account paused its AI app connections, so a valid key is refused with 403.

**Fix.** The member turns them back on at Settings, Connected AI Apps. Nothing needs re-minting.

*Where:* The `code` field on the Chat API.

### `insufficient_quota`

The account's monthly budget is spent, so the turn was refused with 429.

**Fix.** Wait for `Retry-After`, which is the seconds until the reset date and can be days. Members get a larger budget.

*Where:* The `type` field on the Chat API.

### `invalid_api_key`

The personal API key was revoked, never existed, or belongs to a deleted account.

**Fix.** Mint a new key in Miles settings, or refresh the OAuth token if you sent one.

*Where:* The `code` field on the Chat API.

### `invalid_request_error`

The request was refused before the turn ran, for its body, its credential, or paused connections.

**Fix.** Read the `code` and message to see which, then correct the body or the credential. Retrying changes nothing.

*Where:* The `type` field on the Chat API.

### `pipeline_error`

The chat pipeline failed to answer. It rides on `server_error`, before the stream opens or mid-stream.

**Fix.** Retry once. A mid-stream failure arrives as a final `data:` event, so read that event.

*Where:* The `code` field on the Chat API.

### `rate_limit_exceeded`

A per-minute window, the concurrency cap, or the daily spend allowance refused this turn.

**Fix.** Wait for `Retry-After`, then retry. Reduce parallelism when the message names concurrency. Retrying is safe.

*Where:* The `type` field on the Chat API.

### `server_error`

Miles could not produce an answer. The turn failed before, during, or after the provider call.

**Fix.** Retry once. If it repeats, the fault is on Miles' side, not in your request.

*Where:* The `type` field on the Chat API.

### `timeout_error`

A non-streaming turn passed the 90 second deadline and was refused with 504.

**Fix.** Retry with `stream: true`, which has no turn timeout. Retrying is safe, since nothing was half-written.

*Where:* The `type` field on the Chat API.

## Codes you will not see here

The first-party app routes that the Miles website and the iOS app call have refusal codes of their own, and they are not on this page because no public surface emits them. `past_deadline`, `invalid_until`, `invalid_surface`, `overlapping_interval`, `invalid_since`, `invalid_intervals`, `too_many_intervals`, `credit_not_found` and `invalid_request` are all of that kind. If you are reading one of those, you are reading a response from an endpoint that is not part of the developer surface and is free to change.
