The error body
array
required
One or more error details.
Always read
code, never message. Message text is written for humans and may be reworded; codes are stable.Status codes
400 and 422 both mean “fix the request”, and the difference is where it was caught. A 422 came from schema validation — a field is the wrong type, missing, or out of bounds, and message describes the rule that failed. A 400 means the body parsed and every field was individually valid, but the request is refused as it stands: a keyed quote missing fromAddress or toAddress, an address on the wrong chain family for its chain id, a swap destination outside the key owner’s account, or an amount below the network’s minimum.The same code can arrive with either status — see the note under the Send and Swap table below.Error codes
code is the stable identifier. Branch on it, never on message.
Every endpoint
Payment links
Send and Swap
Two codes carry two statuses. The status says where the request was stopped: a
422 came from schema validation, so the value you sent is malformed; a 400 means every field parsed and the request is refused as it stands — a keyed rule, or an address on the wrong chain family. Branch on the pair, not on the code alone.A malformed request body is also a
422, raised by request validation before any of the codes above. Its message describes the rule that failed and its code is not from this list — treat any unrecognised 422 as “fix the request”.The ones worth understanding
409 — the account is not set up for this operation
409 — the account is not set up for this operation
The key is valid and correctly scoped, but the account itself is not ready. It means one of two different things depending on the product, and neither is transient.Receiving: the owner has no default wallet, or no handle. They have to claim a Moove Handle and set a default wallet with a settlement token.Sending or swapping (
SEND_ACCOUNT_NOT_READY): fromAddress is not a wallet the owner connected by signature. A receive-only address, added by pasting it into settings, cannot be the source of a send or a swap, because pasting an address is not proof that you hold its key. This is the single most common integration failure.With a key, POST /v1/quote/send and POST /v1/quote/swap both check this before you sign, so it normally arrives at the quote and nothing has moved. No amount of retrying will connect a wallet; the owner has to do it in the Moove App or the dashboard.Surface it to the user rather than retrying.400 — the request is refused as it stands
400 — the request is refused as it stands
Three kinds of
400 reach a quote, and they differ in who they apply to.Rules for keyed callers only. The website and the Moove App are not subject to these; an agent is, because it has no person watching the screen to notice an address is wrong.INVALID_SOURCE/INVALID_DESTINATION— an address the schema marks optional, because the logged-out website quotes without one, but which a key must supply. Left out, the quote would be built against a placeholder you do not control.INVALID_DESTINATIONis also returned for atoAddressno keyed payout or swap may be sent to, such as a wallet moove.xyz operates itself.UNOWNED_SWAP_DESTINATION— a swap returns its output to you, so itstoAddressmust be a wallet on your own account. To pay somebody else, use Moove Send.INVALID_SWAP_PAIR— the same token on the same chain is a transfer, not a swap. UsePOST /v1/quote/send.
fromAddress that is not of fromChainId’s family returns INVALID_SOURCE, and a toAddress that is not of toChainId’s family returns INVALID_DESTINATION, with a message naming both families — which is how you tell it apart from a missing address. On a send, a handle is checked through the wallet it pays, its owner’s default wallet. A bridge across families is fine as long as each address matches its own chain.Rules for every send. CANNOT_CREATE_SELF_QUOTE (source, destination and token all identical), and on Bitcoin and Tron SEND_AMOUNT_BELOW_MINIMUM (below the network’s minimum, which the message names) and INSUFFICIENT_TOKEN_BALANCE.Every one is refused before you sign, so nothing has moved. None is retryable. The keyed ones are cheap to avoid: supply both addresses, and use the endpoint that matches what you are doing.422 — validation
422 — validation
The request body did not match the schema.
message describes the rule that failed.Common causes: toAmount missing or not greater than zero, description over 500 characters, maxUsage below 1, expirationDate not an ISO 8601 timestamp or not in the future.There is also one 422 that schema validation cannot catch: INVALID_PAYMENT_LINK_AMOUNT. toAmount is quantised to the settlement token’s decimals, and rejected if that changes the value — so 49.9999999 fails against a 6-decimal USDC wallet while 49.999999 succeeds. Trailing zeros are not significant, so 100.0000000 is fine.The limit belongs to the destination wallet’s token, not to your request. See toAmount.422 INVALID_TRANSACTION_PRODUCT — send product
422 INVALID_TRANSACTION_PRODUCT — send product
A keyed
POST /v1/transaction must set product to "send" or "swap" — a bridge is "swap" too. The scope the request needs is derived from it, so a missing or unsupported value has no scope to check against and is rejected rather than defaulted — defaulting would pick a permission on your behalf.Anonymous callers may omit product; a key may not.No 401 for a missing key on the Send and Swap routes
No 401 for a missing key on the Send and Swap routes
POST /v1/quote/send, POST /v1/quote/swap, POST /v1/transaction and GET /v1/transaction/{id} also serve callers with no credential, because the website and the Moove App use them logged out. A request to one of them without an X-API-Key header is not refused — it runs anonymously, with none of the key’s checks, nothing attributed to the key, and redacted record and poll responses. Only GET /v1/transaction answers a missing key with 401 UNAUTHENTICATED.An unset or empty key variable is the usual way to get there: curl drops a header whose value is empty. Check the key is set before the first call. A key that is sent and is invalid still fails with 401 INVALID_API_KEY or 401 EXPIRED_API_KEY — it never falls back to anonymous.500 CANNOT_PROCESS_EXISTING_TRANSACTION — the hash is already recorded
500 CANNOT_PROCESS_EXISTING_TRANSACTION — the hash is already recorded
With a key, re-posting the same body — the same hash and the same
product — for a transaction your own account already recorded is not an error: it returns that transaction again, so a record call whose response you lost is safe to retry unchanged.This code means the hash is recorded by another account, was re-posted with a different product (a swap’s hash as "send", or the reverse), or was re-posted without a key. Retrying will not change it.429 — rate limited
429 — rate limited
Limits apply per API key and per source IP, whichever is reached first. Back off exponentially with jitter.Rate Limits →
Handling errors
Next
Rate Limits
What triggers a 429.
Authentication
401 and 403 in detail.
Moove Payment Links
The endpoints.