Browse the docs

Errors

How the API returns errors and how to handle them.

The API uses conventional HTTP status codes and returns a JSON body with every error. Handle errors in this order:

  1. Branch on the HTTP status code. It tells you whether the request was malformed, unauthenticated, not permitted, not found, in conflict, rate limited, or failed on our side.
  2. Then branch on code. Every error except a validation failure carries a stable, machine-readable code.
  3. Never branch on message. It is written for humans and its wording can change.

Both environments return the same shapes.

Error response format

Every error except a validation failure has the same two-field body:

JSON

{
  "message": "Card with id 6b1e2c3d-4f5a-4b6c-8d7e-9f0a1b2c3d4e not found",
  "code": "CARD_NOT_FOUND"
}
  • message — a human-readable description of what went wrong.
  • code — an UPPER_SNAKE_CASE identifier you can branch on. The full list is in Error codes below.

Validation errors (400)

When the request fails schema validation — a required field is missing, a value has the wrong type or format, a path or query parameter is malformed — the body is a list of offending fields instead of { message, code }:

JSON

{
  "errors": [
    {
      "msg": "Invalid value",
      "param": "external_id",
      "location": "body"
    }
  ]
}
  • msg — a human-readable description of the problem with this field.
  • param — the name of the field, without any parent path.
  • location — where the field was read from: body, query or params.

Two rules to encode in your client:

  • Query parameters are validated the same way: GET /v2/api-gateway/cards?limit=50 returns {"errors":[{"msg":"Too big: expected number to be <50","param":"limit","location":"query"}]}.
  • Validation bodies never include the rejected value, and never include top-level message or code. Detect a validation error by the presence of errors.

Some 400 responses are not validation failures but business rules — not enough balance, a card product that does not support the operation, a required id missing on a create. Those use the standard { message, code } body. Endpoint pages list which code values each operation can return.

Authentication errors (401)

Returned when the x-api-key header is missing, unknown, inactive, or not enabled for the API. The body follows the standard shape:

JSON

HTTP/1.1 401 Unauthorized

{"message":"Invalid API Key","code":"UNAUTHORIZED"}

message is Authentication required (no credentials sent), Invalid API Key (unknown or inactive key), or User is not allowed to access the API Gateway (the key's account is not enabled for the API). An invalid or expired cardholder token also returns 401 UNAUTHORIZED. See Authentication.

A 403 with code: "FORBIDDEN" means the credentials are valid but this action is not allowed for them — for example a cardholder token used on an endpoint that requires an organization API key, or an account whose API access has been restricted.

Requests that never reach an endpoint

Three kinds of request are rejected before they reach an endpoint and return a different, six-field body: an unknown path (typo in the URL, wrong version prefix), a request body that is not valid JSON, and a body larger than the size limit.

GET https://api.themooncard.com/v2/api-gateway/nope

JSON

HTTP/1.1 404 Not Found

{
  "statusCode": 404,
  "code": "HTTP_EXCEPTION",
  "message": "Cannot GET /v2/api-gateway/nope",
  "timestamp": "2026-08-16T19:39:42.900Z",
  "path": "/v2/api-gateway/nope",
  "requestId": "04127f3f-630b-440e-a085-79a82f8d63b9"
}

A malformed JSON body returns 400 with the same six fields ("message": "Unterminated string in JSON at position 10 …"). Write your error parser to accept this shape: read the HTTP status, then treat a body with errors as a validation failure, and otherwise read code and message. The presence of statusCode in the body tells you the request never reached an endpoint.

HTTP status codes

StatusMeaningHow to handle
200 / 201Success. 201 is returned by most create operations.—
204Success, no body. Returned by webhook and velocity-control deletes.—
400Validation failed (errors body), or a business rule rejected the request ({ message, code }).Fix the request. Do not retry unchanged.
401Missing or invalid API key.Check the x-api-key header and that you are calling the environment the key belongs to.
403Not permitted: an issuance limit or cap reached, a feature not enabled for your organization, a restricted account, the wrong kind of credential for the endpoint, a Sandbox-only endpoint called in production.Read code; the condition must be resolved before retrying.
404The resource does not exist, or exists but belongs to another organization. Never a malformed UUID (that is 400).Verify the id. Do not treat 404 as "deleted".
409Conflict with current state, e.g. a duplicate cardholder external_id, or a verification that is already complete.Read code; reconcile before retrying.
429Rate limited, or too many failed cardholder login attempts.Wait for Retry-After and retry — see Rate limits.
500Unexpected error on our side.Retry with exponential backoff and jitter; if it persists, contact support with the x-correlation-id.
503Temporarily unavailable: scheduled maintenance, or no usable exchange rate for the currency pair.Retry with backoff. For MAINTENANCE_MODE, back off for minutes rather than seconds.

422 and 501 are not used.

Error codes

Codes are grouped by the HTTP status they are returned with. A code always comes with the same status unless noted. Endpoint pages list the codes each operation can return.

400 — request rejected by a business rule

CodeWhen
INSUFFICIENT_BALANCENot enough the prepaid balance, card balance or account balance for the operation. On card create, the balance must cover the initial amount plus the card-creation fee.
CARD_PRODUCT_NOT_SUPPORTEDThe card product is not enabled for your organization, or does not support this operation (reload, freeze, an initial load on create).
USER_NOT_ALLOWED_PHYSICAL_CARDCard create with card_type: "PHYSICAL": physical cards are not enabled for your organization.
USER_BALANCE_NOT_FOUNDV1 card create: your organization has no prepaid balance to pay for the card. On the balance endpoints the same condition is a 404, see below.
INVALID_CARD_CURRENCYCard create: card_currency differs from the currency of your prepaid balance.
AMOUNT_LESS_THAN_MINIMUMCard create: the initial amount is below the minimum first load (5 USD in production, 1 USD in Sandbox).
CARD_NOT_RELOADABLEMove-balance called on a card that is not a reloadable prepaid card.
CARD_HAS_NEGATIVE_BALANCEThe card cannot be deleted while its balance is negative.
CARD_HAS_PENDING_TRANSACTIONSThe card cannot be deleted while transactions are pending.
CARD_NOT_DELETABLEThe card could not be terminated.
END_CUSTOMER_ID_REQUIREDV1 create card: external_id is blank.
ACCOUNTS_NOT_IN_SAME_ORGANIZATIONAccount transfer between accounts of different organizations.
CARD_ID_OR_CARDHOLDER_ID_REQUIRED / CARDHOLDER_ID_OR_END_CUSTOMER_ID_REQUIREDVelocity control create: no target given, or both cardholder_id and external_id given.
KYC_NOT_INITIATED / KYB_NOT_INITIATED / APPLICANT_NOT_FOUNDThe cardholder (KYC) or organization (KYB) has no verification session that can accept documents or be submitted.
EXCHANGE_RATE_NOT_SUPPORTEDExchange rate requested for a pair that is not quoted.
WEBHOOK_URL_NOT_ALLOWEDWebhook create: the URL is not a publicly reachable http(s) endpoint.
ERROR_SENDING_POST_REQUESTWebhook create: the URL did not answer the registration probe, or answered with a 5xx.
NO_ACTIVE_CODE / CODE_EXPIRED / INVALID_CODECardholder login: no code requested, code older than 10 minutes, or wrong code.
INVALID_SESSION / OTP_EXPIRED / INVALID_OTP / MAX_ATTEMPTS_EXCEEDEDCard activation with a one-time code (/v1/public/2fa/…): unknown session, expired code, wrong code, or more than three attempts.
CARD_NOT_FOUNDCard activation with a one-time code only: no card matches. Everywhere else CARD_NOT_FOUND is a 404.

401 — not authenticated

CodeWhen
UNAUTHORIZEDMissing, invalid or inactive API key; invalid or expired cardholder token.

403 — not permitted

CodeWhen
DEV_ONLY_ENDPOINTA simulation endpoint was called in production.
HTTP_EXCEPTIONGeneric. Card create: an issuance limit or the lifetime card cap for this product would be exceeded. Some feature-gating 403s also use it.

404 — not found

The message is usually <Resource> with id <id> not found or <Resource> not found. Resources that belong to another organization are reported as not found.

CodeWhen
CARD_NOT_FOUNDCard id unknown or not yours.
TRANSACTION_NOT_FOUNDTransaction id unknown or not on this card.
CARDHOLDER_NOT_FOUNDCardholder id or external_id / external_id unknown or not yours.
ORGANIZATION_NOT_FOUND / ORGANIZATIONS_NOT_FOUND / TOP_LEVEL_ORGANIZATION_NOT_FOUNDOrganization id unknown or not yours; no organizations on the requested page; caller has no top-level organization.
ACCOUNT_NOT_FOUND / ORIGIN_ACCOUNT_NOT_FOUND / DESTINATION_ACCOUNT_NOT_FOUNDAccount id unknown or not yours (transfer names the side that failed).
VELOCITY_CONTROL_NOT_FOUNDVelocity control id unknown, deleted, or not yours.
WEBHOOK_NOT_FOUNDRotate signing secret: no webhook is registered.
USER_BALANCE_NOT_FOUND / MOON_CREDIT_LEDGER_ACCOUNT_NOT_FOUND / CARD_LEDGER_ACCOUNT_NOT_FOUNDYour organization or the card has no balance record yet, so there is nothing to move or refund. V1 card create returns USER_BALANCE_NOT_FOUND as a 400, not a 404.
HTTP_EXCEPTIONGeneric. Examples: Organization not found, KYC not initiated for this cardholder, End customer not found. Branch on the status.

409 — conflict

CodeWhen
EXTERNAL_ID_ALREADY_EXISTSCardholder create: a cardholder with this external_id or email already exists in the organization.
CARDHOLDER_ALREADY_VERIFIEDKYC link requested for a cardholder whose KYC is already complete.
ORGANIZATION_ALREADY_VERIFIEDKYB link requested for an organization whose KYB is already complete.

429 — too many requests

CodeWhen
HTTP_EXCEPTIONThe per-endpoint rate limit was exceeded. Retry-After header is set. See Rate limits.
TOO_MANY_ATTEMPTSCardholder login: five wrong codes for the current login code. Request a new code. No Retry-After.

500 — server error

CodeWhen
INTERNAL_SERVER_ERRORUnexpected failure. message is Internal server error or a short description such as Failed to create cardholder.
HTTP_EXCEPTIONGeneric; a few operations report unexpected failures with this code. Treat exactly like INTERNAL_SERVER_ERROR.

503 — temporarily unavailable

CodeWhen
MAINTENANCE_MODEScheduled maintenance. message: Moon is under maintenance.
EXCHANGE_RATE_UNAVAILABLENo exchange rate available for the currency pair (simulated transactions in a foreign currency).
EXCHANGE_RATE_OUT_OF_BANDThe quoted rate is outside the accepted band; retry shortly.

Card declines are not HTTP errors

A declined card transaction is not returned as a 4xx on an API call. Declines are delivered asynchronously as a CARD_DECLINE webhook with a customer_friendly_description and a decline_type_id you can branch on. Do not treat an HTTP error as a decline, or a decline as an HTTP error.

Retries and idempotency

Send an idempotency key and any POST becomes safe to retry. With Idempotency-Key set, a repeat of a request that timed out returns the first attempt's result instead of performing the operation again — that is the mechanism this section used to work around.

Without a key, a request that timed out may or may not have landed, so retry according to what the operation does:

Operation typeRetry after a timeout or 5xx?
Reads (GET)Yes, freely.
State-setting updates: freeze/unfreeze, activate, assign cardholder, update or delete a velocity control, delete a webhook, update a cardholder or organizationYes. Repeating them yields the same end state.
Register a webhook, rotate the signing secretYes, but each successful call issues a new signing secret and invalidates the previous one — always store the secret from the last response you received.
Create a cardholder (V2)Yes: external_id is unique per organization, so a duplicate returns 409 EXTERNAL_ID_ALREADY_EXISTS instead of a second cardholder.

How to check before retrying a create, when it was sent without a key:

  • Card issued? List cards for the cardholder — GET /v1/api-gateway/card?external_id=… or GET /v2/api-gateway/cards?external_id=… — and look for a card created after your request. Both lists are newest first. V1 card create also debits your prepaid balance, so a second attempt that lands charges you twice.
  • Balance added or moved? Fetch the card (balance) or its transactions with include_loads=true; for accounts, GET /v2/api-gateway/accounts/{id} or its transactions.
  • Account, organization or velocity control created? List them and match on the name or target you sent.

General rules:

  • Retry 429 after Retry-After, and 5xx with exponential backoff and jitter. Cap retries; if a 503 MAINTENANCE_MODE persists, wait minutes, not seconds.
  • Do not retry other 4xx unchanged — the request needs to change first. A changed request needs a new idempotency key.
  • Webhook deliveries can be retried by Moon; deduplicate on webhook_event_id and always return HTTP 200 from your webhook endpoint.

Getting help with an error

Every response to a request that reached an endpoint carries an x-correlation-id header (send your own value and it is echoed back). Log it together with the status and the full body. When you contact our team, quote the correlation id, the endpoint, the timestamp and the response body — that lets us find the request. Responses rejected before an endpoint (401, 403 from authentication, 429, unknown paths) do not carry the header; for the six-field body quote its requestId instead.

Show message to your own users only after mapping it to your own copy — the wording can change; code will not.

Last updated August 17, 2026