Browse the docs

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.

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"
      }'
FieldTypeRequiredDescription
transaction_typestringyesOne of AUTHORIZATION, REVERSAL, CLEARING, WITHDRAWAL, REFUND.
transaction_amountnumberyesPositive amount in transaction_currency, in major units (12.34, not 1234).
transaction_currencystringyesISO 4217 code, for example USD. If it differs from the card's currency, the amount is converted at the current rate.
original_transaction_idstringnoThe transaction_id of the earlier event this one applies to. Send it on CLEARING, REVERSAL and REFUND.
merchant_namestringnoDefaults to Simulated Merchant.
merchant_country_codestringnoISO 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"
}
FieldNotes
successtrue when the event was approved, false when it was declined.
message"<Transaction|Reversal|Clearing|Withdrawal|Refund> simulated successfully", or "<Type> declined: <reason>".
transaction_idA 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_FUNDS". Branch on success, not on the status code.

Chaining events with original_transaction_id

Each call returns its own transaction_id. To attach a clearing, reversal or refund to an earlier authorization, send that authorization's transaction_id as original_transaction_id — 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: A

Send original_transaction_id 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 sendOutcomeWebhook delivered
AUTHORIZATIONapprovedCARD_TRANSACTION
AUTHORIZATIONdeclinedCARD_DECLINE
REVERSALapprovedCARD_TRANSACTION
CLEARINGapprovedCARD_TRANSACTION
WITHDRAWALapprovedCARD_TRANSACTION
REFUNDapprovedCARD_AUTHORIZATION_REFUND

Every CARD_TRANSACTION and CARD_AUTHORIZATION_REFUND for one purchase carries the same transaction_id, so group on it and treat the latest event as the current state. Payload shapes are on the Webhooks page.

Errors

StatuscodeWhen
400— (errors[])A required field is missing, or transaction_amount is not a positive number, or transaction_type is not one of the five values.
403DEV_ONLY_ENDPOINTCalled against production.
404CARD_NOT_FOUNDThe card id is unknown or the card is not in your organization.
503EXCHANGE_RATE_UNAVAILABLENo rate is available to convert transaction_currency 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_TRANSACTION three times and CARD_AUTHORIZATION_REFUND once, all four carrying the same transaction_id. 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_FUNDS declines.
  • Simulate a decline on purpose: authorize more than the card holds, or freeze the card first, and confirm your handler reads CARD_DECLINE and its 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