Simulating Card Transactions
Simulate transactions on a card in our sandbox environment.
A simulated event runs through the same processing path as a real one: it applies the same spending controls and fees, moves the same ledger balances, appears in the card's transaction feed and delivers the same webhooks to your endpoint.
| Purpose | V1 | V2 |
|---|---|---|
| Simulate a card transaction | POST /v1/api-gateway/card/{card_ | POST /v2/api-gateway/simulate/cards/{card_ |
Both versions take the same request body and behave identically. All four authenticate with your Sandbox x-api-key; a cardholder session token is not accepted.
The lifecycle of a card transaction
A purchase moves through several events, and each one is a separate call here:
AUTHORIZATION— the merchant asks to hold funds. The transaction becomes pending. If the available balance is lower than the amount requested, the authorization may be approved for part of it.REVERSAL— releases an authorized amount, in whole or in part. Reversing the full amount effectively cancels the transaction.CLEARING— settles an authorized amount. A fully cleared transaction is settled.REFUND— credits money back after clearing. Refunds are always settled.WITHDRAWAL— pulls funds off the card. Withdrawals settle immediately.
Simulating a card transaction
Request
cURL
curl --request POST \
--url https://api.themooncard.com/v1/api-gateway/card/7c9e6679-7425-40de-944b-e07fc1f90ae7/transactions \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--header 'x-api-key: YOUR_API_KEY' \
--data '{
"transaction_type": "AUTHORIZATION",
"transaction_amount": 100,
"transaction_currency": "USD",
"merchant_name": "ONLINE STORE"
}'| Field | Type | Required | Description |
|---|---|---|---|
transaction_ | string | yes | One of AUTHORIZATION, REVERSAL, CLEARING, WITHDRAWAL, REFUND. |
transaction_ | number | yes | Positive amount in transaction_, in major units (12.34, not 1234). |
transaction_ | string | yes | ISO 4217 code, for example USD. If it differs from the card's currency, the amount is converted at the current rate. |
original_ | string | no | The transaction_ of the earlier event this one applies to. Send it on CLEARING, REVERSAL and REFUND. |
merchant_ | string | no | Defaults to Simulated Merchant. |
merchant_ | string | no | ISO 3166 alpha-2, for example US. Used by geography checks on AUTHORIZATION. |
Response
JSON
HTTP/1.1 200 OK
{
"success": true,
"message": "Transaction simulated successfully",
"transaction_id": "2f1a6b3c-4d5e-4f60-8a7b-9c0d1e2f3a4b"
}| Field | Notes |
|---|---|
success | true when the event was approved, false when it was declined. |
message | "<Transaction|Reversal|Clearing|Withdrawal|Refund> simulated successfully", or "<Type> declined: <reason>". |
transaction_ | A UUID identifying this event. Every call returns a new one. |
A decline is not an HTTP error. It comes back as 200 with success: false and the reason in message, for example "Transaction declined: INSUFFICIENT_. Branch on success, not on the status code.
Chaining events with original_transaction_id
Each call returns its own transaction_. To attach a clearing, reversal or refund to an earlier authorization, send that authorization's transaction_ as original_ — never the id the follow-up call itself returns.
AUTHORIZATION → transaction_id: A (keep A)
REVERSAL → original_transaction_id: A
CLEARING → original_transaction_id: A
REFUND → original_transaction_id: ASend original_ on every REVERSAL, CLEARING and REFUND so the event attaches to the authorization you are testing. AUTHORIZATION and WITHDRAWAL do not use the field.
Which webhooks each event fires
| You send | Outcome | Webhook delivered |
|---|---|---|
AUTHORIZATION | approved | CARD_ |
AUTHORIZATION | declined | CARD_ |
REVERSAL | approved | CARD_ |
CLEARING | approved | CARD_ |
WITHDRAWAL | approved | CARD_ |
REFUND | approved | CARD_ |
Every CARD_ and CARD_ for one purchase carries the same transaction_, so group on it and treat the latest event as the current state. Payload shapes are on the Webhooks page.
Errors
| Status | code | When |
|---|---|---|
400 | — (errors[]) | A required field is missing, or transaction_ is not a positive number, or transaction_ is not one of the five values. |
403 | DEV_ | Called against production. |
404 | CARD_ | The card id is unknown or the card is not in your organization. |
503 | EXCHANGE_ | No rate is available to convert transaction_ into the card's currency. Retry, or simulate in the card's own currency. |
Worked example
A $100 purchase where one $20 item goes out of stock, the remaining $80 ships and settles, and a $30 item is later returned. Fund the card first, or step 1 declines.
1. Authorize $100. A $100 hold is placed and the transaction is pending.
cURL
curl --request POST \
--url https://api.themooncard.com/v1/api-gateway/card/7c9e6679-7425-40de-944b-e07fc1f90ae7/transactions \
--header 'content-type: application/json' \
--header 'x-api-key: YOUR_API_KEY' \
--data '{
"transaction_type": "AUTHORIZATION",
"transaction_amount": 100,
"transaction_currency": "USD",
"merchant_name": "ONLINE STORE"
}'JSON
HTTP/1.1 200 OK
{
"success": true,
"message": "Transaction simulated successfully",
"transaction_id": "2f1a6b3c-4d5e-4f60-8a7b-9c0d1e2f3a4b"
}Keep 2f1a6b3c-4d5e-4f60-8a7b-9c0d1e2f3a4b. Every remaining step references it.
2. Reverse $20. The hold drops to $80; the transaction is still pending.
cURL
curl --request POST \
--url https://api.themooncard.com/v1/api-gateway/card/7c9e6679-7425-40de-944b-e07fc1f90ae7/transactions \
--header 'content-type: application/json' \
--header 'x-api-key: YOUR_API_KEY' \
--data '{
"transaction_type": "REVERSAL",
"transaction_amount": 20,
"transaction_currency": "USD",
"original_transaction_id": "2f1a6b3c-4d5e-4f60-8a7b-9c0d1e2f3a4b"
}'3. Clear $80. Nothing is left on hold, so the transaction settles.
cURL
curl --request POST \
--url https://api.themooncard.com/v1/api-gateway/card/7c9e6679-7425-40de-944b-e07fc1f90ae7/transactions \
--header 'content-type: application/json' \
--header 'x-api-key: YOUR_API_KEY' \
--data '{
"transaction_type": "CLEARING",
"transaction_amount": 80,
"transaction_currency": "USD",
"original_transaction_id": "2f1a6b3c-4d5e-4f60-8a7b-9c0d1e2f3a4b"
}'4. Refund $30. A settled refund is credited back to the card.
cURL
curl --request POST \
--url https://api.themooncard.com/v1/api-gateway/card/7c9e6679-7425-40de-944b-e07fc1f90ae7/transactions \
--header 'content-type: application/json' \
--header 'x-api-key: YOUR_API_KEY' \
--data '{
"transaction_type": "REFUND",
"transaction_amount": 30,
"transaction_currency": "USD",
"original_transaction_id": "2f1a6b3c-4d5e-4f60-8a7b-9c0d1e2f3a4b"
}'Your endpoint receives CARD_ three times and CARD_ once, all four carrying the same transaction_. Reading the card's transactions afterwards shows the settled purchase and the refund.
Tips
- Register your Sandbox webhook URL before you start, so you can watch the events each step produces. It is the fastest way to validate a receiver.
- Simulations spend real Sandbox balances. Fund the prepaid balance, then load the card, then simulate — otherwise you get
INSUFFICIENT_declines.FUNDS - Simulate a decline on purpose: authorize more than the card holds, or freeze the card first, and confirm your handler reads
CARD_and itsDECLINE decline_.type_ id - Keep the ids you get back. A test that regenerates its authorization id on every run cannot exercise clearing or refunds.
Last updated August 18, 2026