Browse the docs

Authentication

The API accepts two kinds of credential:

  • An API key, sent as x-api-key. This is what your servers use. It acts on behalf of your organization and reaches every endpoint.
  • A cardholder session token, sent as Authorization: Bearer. This is what an app or web page acts with on behalf of one of your cardholders. It reaches a small set of V2 card endpoints and nothing else.

A handful of endpoints need no credential at all. They are listed under Endpoints that need no credential.

There is a third credential, and it is not for this API: an application connecting to the MCP server can obtain an OAuth access token instead of being handed a key. See OAuth, and where it applies.

API keys

Send your key on every request:

x-api-key: YOUR_API_KEY

There is no token exchange and no expiry to manage: a key works until you revoke it.

Base URLs

EnvironmentBase URL
Productionhttps://api.themooncard.com

A key belongs to one environment. A Sandbox key is rejected against the production host and the reverse is also true. See Environments.

Getting a key

Create, rotate and revoke keys in your Moon dashboard, choosing Sandbox or production when you create one. The key value is shown once, when it is created. If your dashboard does not offer API keys yet, write to our team.

Example request

cURL

curl --request GET \
     --url https://api.themooncard.com/v1/api-gateway/card/card-products \
     --header 'accept: application/json' \
     --header 'x-api-key: YOUR_API_KEY'

Swap in https://api.themooncard.com and your production key when you go live.

Cardholder session tokens

A cardholder session token lets one of your cardholders read and act on their own cards without your API key ever reaching the client. It is a bearer token you obtain for them and hand to their session.

Getting a token

POST /v2/api-gateway/cardholders/auth/login (reference) is a two-step, passwordless login. It needs no credential of its own.

Step 1 — request a code. Send the cardholder's email and your top-level organization id (from GET /v2/api-gateway/organizations/main). Leave code out.

cURL

curl --request POST \
     --url https://api.themooncard.com/v2/api-gateway/cardholders/auth/login \
     --header 'content-type: application/json' \
     --data '{
        "email": "jane@example.com",
        "organization_id": "5c1e2d3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f"
      }'

JSON

HTTP/1.1 200 OK

{
  "success": true,
  "message": "Login code sent to your email"
}

A six-digit code is issued and is valid for 10 minutes. If no cardholder in that organization tree has this email, one is created; pass external_id on this step to set your own identifier on it.

Step 2 — exchange the code for a token. Send the same body plus code.

cURL

curl --request POST \
     --url https://api.themooncard.com/v2/api-gateway/cardholders/auth/login \
     --header 'content-type: application/json' \
     --data '{
        "email": "jane@example.com",
        "organization_id": "5c1e2d3f-4a5b-4c6d-8e7f-9a0b1c2d3e4f",
        "code": "482913"
      }'

JSON

HTTP/1.1 200 OK

{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "expiresAt": "2026-09-15T17:24:08.000Z"
}

Lifetime

A token is valid for 30 days from issue, and expiresAt states the exact moment. To keep a cardholder signed in past that point, run the login flow again. A token stops working earlier if the cardholder is restricted or their sign-in is turned off.

Five wrong codes for the same login code return 429 TOO_MANY_ATTEMPTS; request a new code to reset the count. See Rate limits.

Using a token

Authorization: Bearer YOUR_CARDHOLDER_TOKEN

These endpoints accept it. Every one of them also accepts an API key, except the last, which takes a session token only:

EndpointWhat it does
POST /v2/api-gateway/cardsCreate a card
GET /v2/api-gateway/cardsList cards
GET /v2/api-gateway/cards/{card_id}Get one card
POST /v2/api-gateway/cards/{card_id}/activateActivate a card
GET /v2/api-gateway/cards/{card_id}/transactionsList a card's transactions
GET /v2/api-gateway/cards/{card_id}/transactions/{transaction_id}Get one transaction
GET /v2/api-gateway/controls/velocity/statusThe cardholder's own spending status. Session token only — an API key is refused with 403.

JSON

HTTP/1.1 403 Forbidden

{
  "message": "Cardholder is not allowed to access this resource",
  "code": "FORBIDDEN"
}

On the endpoints it does reach, a session token is scoped to that cardholder: a card that is not theirs answers 404, where the same request with an API key answers 200 with an empty list.

The same token is also what a cardholder's own MCP client uses as its bearer. See MCP Server.

OAuth, and where it applies

Moon runs an OAuth 2.1 authorization server so an application — a chat product, an editor, a coding agent — can connect to the MCP endpoint on behalf of a Moon account holder without anybody pasting a key into it. The person signs in, ticks the permissions they are willing to give, and the client receives an opaque access token prefixed moon_oat_.

Two things to know if you are reading this page to decide how to authenticate:

  • OAuth is for /mcp only. An access token is bound to that endpoint and is not accepted by this REST API. Nothing on this page changes: server-to-server integrations keep using x-api-key, and client sessions keep using a cardholder token.
  • It changes nothing about API keys. They are not narrowed, not expired and not migrated.

The whole flow — discovery, dynamic client registration, PKCE, the permissions, the consent screen and where a person reviews and revokes connected applications — is documented on MCP Server.

Endpoints that need no credential

Send these without x-api-key and without Authorization. They are still subject to rate limits.

EndpointPurpose
GET /v1/api-gateway/healthcheckLiveness probe. Returns the plain-text body OK.
GET /v2/api-gateway/healthcheckLiveness probe. Returns the plain-text body OK.
POST /v2/api-gateway/cardholders/auth/loginCardholder login, both steps.
GET /v1/public/2fa/session/idCard activation, step 1: get a session id.
POST /v1/public/2fa/card/activate/otpCard activation, step 2: issue a one-time code.
POST /v1/public/2fa/card/activateCard activation, step 3: check the code and activate.

A credential sent to one of these is ignored.

When both credentials are sent

x-api-key wins. If a request carries both headers, the Authorization header is not read at all — the request is authenticated as your organization, and cardholder scoping does not apply. Send exactly one credential so the identity of a request is never ambiguous.

Keeping keys safe

  • One key per environment, per system. Separate keys make it possible to revoke one integration without disturbing the others, and keep Sandbox traffic out of production.
  • Server side only. An API key acts for your whole organization. Never ship it in a browser bundle, a mobile app, a repository or a support attachment. When a client needs to act, give it a cardholder session token instead; when an application needs to act, let it connect over OAuth.
  • Rotate deliberately. Rotating a key issues a new value and revokes the old one immediately — there is no overlap window. Deploy the new value first, then rotate. Revoke any key you no longer recognise.
  • Treat keys as opaque. Do not parse them or derive anything from their shape.

When authentication fails

Authentication failures return the standard error body. Branch on the HTTP status first, then on code — never on message. See Errors.

JSON

HTTP/1.1 401 Unauthorized
Content-Type: application/json; charset=utf-8

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

401 covers every case where we could not establish who you are, and code is always UNAUTHORIZED. message tells you which case it was:

messageWhat to do
Authentication requiredNeither x-api-key nor Authorization was sent.
Invalid API KeyThe key is unknown, revoked, or sent for the wrong environment.
User is not allowed to access the API GatewayThe key is valid but your account does not have API access enabled yet. Contact support.
Your request went to the Moon!Your account is restricted. Contact support.
Invalid JWT: missing cardholder_idThe session token is malformed, expired, or was not issued for this environment. Run the login flow again.
Cardholder authentication disabled, Cardholder is bannedThis cardholder cannot sign in.

A moon_oat_ OAuth access token sent to this API falls into the same bucket: it is not a cardholder session token, so it is refused with 401. Use it against /mcp.

403 means we know who you are and the request is not permitted:

messagecodeMeaning
Cardholder is not allowed to access this resourceFORBIDDENA cardholder session token on an endpoint that requires an API key.
Please contact Moon support.FORBIDDENAPI access is not permitted for this account.
Your access has been restricted, or a specific explanationFORBIDDENYour account is restricted.

A correlation id is assigned once a request is authenticated, so quote the endpoint, the timestamp and the response body when you bring one of these to support.

Next

  • Environments — which host and which key.
  • Errors — the full error contract and code catalogue.
  • Issuing your first card — the first authenticated call worth making.
  • MCP Server — API keys, cardholder tokens and OAuth on the agent endpoint.
  • Quickstart (V2) — the organization, account and cardholder your session tokens are issued against.

Last updated September 20, 2026