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_KEYThere is no token exchange and no expiry to manage: a key works until you revoke it.
Base URLs
| Environment | Base URL |
|---|---|
| Production | https://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_ 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_; request a new code to reset the count. See Rate limits.
Using a token
Authorization: Bearer YOUR_CARDHOLDER_TOKENThese endpoints accept it. Every one of them also accepts an API key, except the last, which takes a session token only:
| Endpoint | What it does |
|---|---|
POST /v2/api-gateway/cards | Create a card |
GET /v2/api-gateway/cards | List cards |
GET /v2/api-gateway/cards/{card_ | Get one card |
POST /v2/api-gateway/cards/{card_ | Activate a card |
GET /v2/api-gateway/cards/{card_ | List a card's transactions |
GET /v2/api-gateway/cards/{card_ | Get one transaction |
GET /v2/api-gateway/controls/velocity/status | The 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_.
Two things to know if you are reading this page to decide how to authenticate:
- OAuth is for
/mcponly. 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 usingx-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.
| Endpoint | Purpose |
|---|---|
GET /v1/api-gateway/healthcheck | Liveness probe. Returns the plain-text body OK. |
GET /v2/api-gateway/healthcheck | Liveness probe. Returns the plain-text body OK. |
POST /v2/api-gateway/cardholders/auth/login | Cardholder login, both steps. |
GET /v1/public/2fa/session/id | Card activation, step 1: get a session id. |
POST /v1/public/2fa/card/activate/otp | Card activation, step 2: issue a one-time code. |
POST /v1/public/2fa/card/activate | Card 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:
message | What to do |
|---|---|
Authentication required | Neither x-api-key nor Authorization was sent. |
Invalid API Key | The key is unknown, revoked, or sent for the wrong environment. |
User is not allowed to access the API Gateway | The 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_ | The session token is malformed, expired, or was not issued for this environment. Run the login flow again. |
Cardholder authentication disabled, Cardholder is banned | This cardholder cannot sign in. |
A moon_ 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:
message | code | Meaning |
|---|---|---|
Cardholder is not allowed to access this resource | FORBIDDEN | A cardholder session token on an endpoint that requires an API key. |
Please contact Moon support. | FORBIDDEN | API access is not permitted for this account. |
Your access has been restricted, or a specific explanation | FORBIDDEN | Your 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