Quickstart (V2)
Issue and fund a card end to end with the V2 API.
V2 models the things V1 keeps implicit: your organization, the accounts that hold balances, and cardholders as records you create and verify. A card is issued to a cardholder, funded by an account, and both must exist before you issue it.
This walkthrough goes from an empty organization to a card with a simulated purchase on it. If you only need a card keyed by your own customer id, Issuing your first card does that in three requests with V1.
Every request carries your API key in the x-api-key header. See Authentication and Environments.
1. Find your organization
Everything in V2 hangs off an organization. Yours already exists — read it rather than creating one.
cURL
curl --request GET \
--url https://api.themooncard.com/v2/api-gateway/organizations/main \
--header 'accept: application/json' \
--header 'x-api-key: YOUR_API_KEY'JSON
{
"id": "4f0f1c02-1b1a-4f1d-9a0e-2c9b7f4a8d31",
"name": "Acme Payments",
"parent_organization_id": null,
"created_at": "2026-01-14T09:22:04Z",
"updated_at": "2026-08-16T11:03:47Z"
}parent_ is null on your top-level organization. Store the id.
If you separate customers, brands or regions, create child organizations with POST /organizations (body: name, and parent_ to nest deeper than one level) and list them with GET /organizations/{id}/children. Children are ordinary organizations: accounts, cardholders and cards all belong to exactly one.
| Status | What happened |
|---|---|
401 | Missing or invalid x-api-key. |
404 | ORGANIZATION_ — the key has no top-level organization yet. Contact support. |
Reference: Get main organization, Get child organizations.
2. Create an account
An account holds a balance in one currency, and cards issued against it spend from that balance. One account per currency is enough to start; use more when you want spending segregated.
cURL
curl --request POST \
--url https://api.themooncard.com/v2/api-gateway/accounts \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--header 'x-api-key: YOUR_API_KEY' \
--data '{
"organization_id": "4f0f1c02-1b1a-4f1d-9a0e-2c9b7f4a8d31",
"name": "Card funding — USD",
"account_currency": "USD"
}'JSON
{
"id": "6c1c9a37-9c1b-4f6c-9e79-8f8a2f0d5f31",
"name": "Card funding — USD",
"organization_id": "4f0f1c02-1b1a-4f1d-9a0e-2c9b7f4a8d31",
"ledger_account_id": "a1d0f7f2-32c7-4a5d-b1e9-5c31f0a0be44",
"balance": 0,
"available_balance": 0,
"currency": "USD",
"created_at": "2026-08-16T17:44:10Z",
"updated_at": null
}Every field of the body is optional: omit organization_ to use your top-level organization, and account_ to get USD (the other accepted value is MXN). A new account starts at zero — you fund it in step 5.
| Status | What happened |
|---|---|
400 | Validation failed — errors[] names the offending field. |
401 | Missing or invalid x-api-key. |
404 | organization_ does not exist or is not one of yours. |
Reference: Create account, List accounts.
3. Create a cardholder
A cardholder is the person the card belongs to. external_ is your own identifier for them — the same value V1 calls external_ — and must be unique inside the organization.
cURL
curl --request POST \
--url https://api.themooncard.com/v2/api-gateway/cardholders \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--header 'x-api-key: YOUR_API_KEY' \
--data '{
"external_id": "customer_123",
"email": "ada@example.com",
"organization_id": "4f0f1c02-1b1a-4f1d-9a0e-2c9b7f4a8d31"
}'JSON
{
"id": "3f5b1c2e-8d4a-4b6f-9e2a-1c7d8e9f0a1b",
"external_id": "customer_123",
"organization_id": "4f0f1c02-1b1a-4f1d-9a0e-2c9b7f4a8d31",
"email": "ada@example.com",
"delegated_kyc_completed_at": null,
"banned_at": null,
"auth_enabled_at": null,
"last_login_at": null,
"created_at": "2026-08-16T17:46:02Z",
"updated_at": null
}id is the Moon cardholder id. Every V2 cardholder route takes that UUID in the path, not your external_. Keep both: external_ is what you look the cardholder up by later with GET /cardholders?email= or GET /cards?external_.
| Status | What happened |
|---|---|
400 | external_ missing or empty, or email is not a valid address. |
401 | Missing or invalid x-api-key. |
404 | organization_ is not visible to your key. |
409 | EXTERNAL_ — a cardholder with this external_ or email is already in the organization. Fetch it with GET /cardholders?email=… instead of retrying. |
Reference: Create cardholder, List cardholders.
4. Verify the cardholder
If your organization requires cardholder identity verification, the cardholder must be approved before a card can be issued to them. Verification is hosted: you request a link, the cardholder completes the flow in their browser, and you read the resulting status.
cURL
curl --request POST \
--url https://api.themooncard.com/v2/api-gateway/cardholders/3f5b1c2e-8d4a-4b6f-9e2a-1c7d8e9f0a1b/kyc-link \
--header 'accept: application/json' \
--header 'x-api-key: YOUR_API_KEY'JSON
{
"url": "https://verification.example/2f1a6b3c4d5e4f608a7b9c0d1e2f3a4b",
"status": "NOT_STARTED"
}This route returns 200, not 201, and it takes no body. Send the cardholder to url exactly as returned — the host belongs to the hosted verification flow and is not part of your integration. Requesting a link again reuses the existing one while it is still valid, so it is safe to call on every page load of your own flow.
url comes back null when the cardholder is already APPROVED, IN_ or DECLINED, since those states have no link to open. Read status for where the cardholder stands.
Poll the status, or wait for the webhook:
cURL
curl --request GET \
--url https://api.themooncard.com/v2/api-gateway/cardholders/3f5b1c2e-8d4a-4b6f-9e2a-1c7d8e9f0a1b/kyc \
--header 'accept: application/json' \
--header 'x-api-key: YOUR_API_KEY'JSON
{
"status": "APPROVED",
"url": "https://verification.example/2f1a6b3c4d5e4f608a7b9c0d1e2f3a4b",
"created_at": "2026-08-16T17:48:31Z",
"updated_at": "2026-08-16T18:02:15Z"
}status is one of NOT_, IN_, APPROVED, DECLINED or RETRY. RETRY means the cardholder can start again — request a new link. Prefer the webhook to polling; see Webhooks.
| Status | What happened |
|---|---|
403 | Verification is temporarily unavailable. Retry later. |
404 | On kyc-link: the cardholder does not exist or is not yours. On GET …/kyc: that, or verification was never started for this cardholder — request a link first. |
If you collect identity documents yourself, POST /cardholders/{id}/verification-documents uploads them and POST /cardholders/{id}/check asks for a review. Send one document per request as multipart/form-data under the file field: JPEG, PNG, WebP, HEIC, HEIF or PDF, up to 10 MB. Anything else is rejected with a 400 naming the accepted types.
Reference: Create cardholder KYC link, Get cardholder KYC status by cardholder id.
5. Issue the card
POST /cards takes three ids, and all three are required — omitting cardholder_ or account_ is a 400.
| Field | Required | Description |
|---|---|---|
cardholder_ | yes | The Moon cardholder id from step 3. Must belong to the same organization as account_. |
account_ | yes | The account that funds the card, from step 2. |
card_ | yes | The product to issue from. |
card_ | no | VIRTUAL (default) or PHYSICAL. PHYSICAL requires physical cards to be enabled for your organization. |
Card products are shared by both versions and listed from the V1 endpoint; the ids are the same:
cURL
curl --request GET \
--url 'https://api.themooncard.com/v1/api-gateway/card/card-products?current_page=1&per_page=10' \
--header 'accept: application/json' \
--header 'x-api-key: YOUR_API_KEY'Then issue:
cURL
curl --request POST \
--url https://api.themooncard.com/v2/api-gateway/cards \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--header 'x-api-key: YOUR_API_KEY' \
--data '{
"cardholder_id": "3f5b1c2e-8d4a-4b6f-9e2a-1c7d8e9f0a1b",
"account_id": "6c1c9a37-9c1b-4f6c-9e79-8f8a2f0d5f31",
"card_product_id": "8f1c611d-098d-4f61-b106-f7b6d344b1ae",
"card_type": "VIRTUAL"
}'The response is the card in detail form — the same shape GET /cards/{card_ returns. It carries the full pan and cvv: treat them as sensitive. card_ is the V2 addition to the card object and reads back lowercase ("virtual", "physical"), or null when the issuer reports no form factor.
A V2 card spends from account_, so you fund the account and the card draws on it. balance on the card object reflects what has been allocated to the card, and starts at zero.
| Status | code | When |
|---|---|---|
400 | — (errors[]) | cardholder_ or account_ missing, or an id that is not a UUID. |
400 | CARD_ | The product is not enabled for your organization. |
400 | USER_ | card_ and physical cards are not enabled for your organization. |
400 | CARDHOLDER_ | Your organization requires verification and this cardholder is not approved. Back to step 4. |
401 | UNAUTHORIZED | Missing or invalid x-api-key. |
403 | — | A card-issuance limit, or the lifetime card cap for this product, would be exceeded. |
404 | ACCOUNT_, CARDHOLDER_, CARD_ | An id is unknown or belongs to another organization. |
Reference: Create card, Get card.
6. Fund the account
Omit account_ to credit your organization's prepaid balance instead of a specific account.
| Status | What happened |
|---|---|
401 | Missing or invalid x-api-key. |
An empty body pays the full amount owed. In production this route returns 403 DEV_.
Confirm with GET /accounts/{id} — balance and available_ should now be 100.
7. Simulate a transaction
With the account funded, run a purchase through the card. The simulation follows the same processing path as a real transaction, including webhooks.
cURL
curl --request POST \
--url https://api.themooncard.com/v2/api-gateway/simulate/cards/8d2cd518-5ef2-4280-91e8-63b1a0e672e7/transactions \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--header 'x-api-key: YOUR_API_KEY' \
--data '{
"transaction_amount": 12.34,
"transaction_currency": "USD",
"transaction_type": "AUTHORIZATION",
"merchant_name": "Acme Coffee",
"merchant_country_code": "US"
}'JSON
{
"success": true,
"message": "Transaction simulated successfully",
"transaction_id": "2f1a6b3c-4d5e-4f60-8a7b-9c0d1e2f3a4b"
}transaction_ is AUTHORIZATION (place a hold), CLEARING (settle one), REVERSAL (release one), REFUND (credit back) or WITHDRAWAL. CLEARING, REVERSAL and REFUND need original_ set to the transaction_ of the earlier event.
A declined simulation is still 200, with success: false and the reason in message. Branch on success, not on the status code.
| Status | What happened |
|---|---|
403 | DEV_ — you called production. Simulation is Sandbox-only. |
404 | CARD_ — the card is unknown or belongs to another organization. |
503 | EXCHANGE_ — no rate for transaction_ right now. |
Read the result back with GET /cards/{card_, which pages with current_ and per_.
Reference: Simulate card transaction, Simulating card transactions, Fetching card transactions.
The same flow in V1
If you already run V1, this is where each step lands. Paths are relative to each version's base URL.
| Step | V2 | V1 equivalent |
|---|---|---|
| Organization | GET /organizations/main, POST /organizations, GET /organizations/{id}/children | Implicit — your API key resolves to one organization. |
| Account | POST /accounts, GET /accounts | Implicit — a single prepaid balance, read with GET /moon-reserve. |
| Cardholder | POST /cardholders with external_ | Implicit — pass external_ on create card and the cardholder is created if it does not exist. |
| Verification | POST /cardholders/{id}/kyc-link, GET /cardholders/{id}/kyc | POST /cardholder/kyc, POST /cardholder/{end_, GET /cardholder/{end_ |
| Issue a card | POST /cards with cardholder_ + account_ + card_ | POST /card/{card_ with external_, and optionally amount to load it at issuance |
| Card products | — (use the V1 route) | GET /card/card-products |
| Simulate a transaction | POST /simulate/cards/{card_ | POST /card/{card_ |
The two versions share one set of records: a card issued through V1 is readable through GET /v2/api-gateway/cards, and the cardholder V1 created implicitly is the one V2 returns, with your id in external_. See API versions for the full map, and Issuing your first card for the V1 walkthrough.
Last updated August 17, 2026