Browse the docs

Request approval for a purchase

POST /v2/api-gateway/purchase-intents

Asks the person who approves an ask-first card to approve an upcoming purchase, by email. The approver is the card's cardholder, or the account that owns the card when it has none or when the card is on the organization's own cardholder (approver_type). Call it before you pay: on an ask-first card an authorization with no approved, unspent, unexpired intent is declined with INTENT_REQUIRED, and one whose merchant or amount does not match the approval on file is declined with INTENT_MISMATCH. kind picks what the person is agreeing to: a single purchase (the default), an order budget at one merchant over valid_for_days days (1–90, default 14), or, with recurring: true, a standing permission bounded by monthly_limit. State the price once: amount in the card's own currency, or merchant_amount + merchant_currency exactly as the store shows it, which Moon converts at a market rate and shows the approver both figures. The approver has answer_within_minutes to answer (5–1440, default 60). Once approved, a single approval may be spent for 15 minutes from the approval, once; read spendable_until. The response comes back AWAITING_APPROVAL: wait for the PURCHASE_INTENT_APPROVED webhook, or poll GET /purchase-intents/{id}. Asking again for an identical purchase while the first request is still unanswered returns that request with deduplicated: true, creates nothing and emails nobody. Nothing here charges the card and nothing here moves money: it asks a person a question. A $0 card verification is allowed on a merchant match alone and does not spend the approval. See the Ask-First Cards and Purchase Intents guides.

Request

Authentication: your API key in the x-api-key header.

Headers

FieldTypeDescription
Idempotency-KeystringA unique string you choose, up to 255 characters, that makes this request safe to retry. If a request times out or fails in transit, send it again with the same key: the status code and body of the first attempt are returned and the operation is not performed a second time. Stored replies — including failures — are replayed for 24 hours, after which the same key starts a fresh request. Reusing a key for a different request, or sending it while the first request is still running, returns 409. Omit the header and the request behaves exactly as before. Document upload endpoints do not accept it.

Body

FieldTypeDescription
card_id requiredstring, uuidThe ask-first card the purchase will be made on.
merchant requiredobjectWhere the money is going. Give at least one of name or mcc; when you give both, both must match the authorization. Matched with exactly the same rules as a card merchant lock, so an intent and a lock on the same card can never disagree about what "this merchant" means.
merchant.namestring or array of stringMatched case-insensitively as a substring of the acquirer merchant descriptor, e.g. TRAIN TICKETS matches TRAIN TICKETS *BOOKING. May be one name or an array of up to five: the acquirer descriptor is not knowable before the first charge, so any one of them matching is enough. Moon also expands a brand it recognises into the names its charges are known to carry, and the approver is shown every name the approval will match.
merchant.mccstringExact four-digit merchant category code.
merchant_domainstringThe site the purchase is being made on, as a bare hostname — shop.example.com, not https://shop.example.com/checkout. Trimmed and lower-cased; a URL, an IP address or a unicode domain is rejected rather than repaired (send the punycode form of an internationalised name). Advisory: the card networks carry no domain, so the authorization gate does not and cannot check it. What it does is tell the approver where the money is going — the approval email and page show it next to the merchant — and scope where the agent is told to use the credential.
amountnumberWhat you expect to be charged, in the card's own currency (the currency of the ledger account it draws on), in major units. Give either this or merchant_amount + merchant_currency, never both.
merchant_amountnumberThe price exactly as the merchant shows it, in merchant_currency, major units (e.g. 29.90 on a BRL checkout). Instead of amount, when the checkout is priced in a currency that is not the card's. Moon converts it into the card's currency at request time with a market rate (no spread), stores the result in amount, keeps both figures and the rate (request_fx_rate) on the intent, and shows the approver both ("R$ 29.90, about $5.60"). With no tolerance, the default becomes 10 percent on either kind, for the network's own exchange rate.
merchant_currencystringThe merchant currency, uppercase ISO 4217, one of USD, MXN, EUR, CAD, GBP, JPY, AUD, BRL. Required with merchant_amount. When Moon has no rate for it the request is refused with 422 MERCHANT_CURRENCY_UNSUPPORTED: send amount in the card's currency instead.One of: USD, MXN, EUR, CAD, GBP, JPY, AUD, BRL
currencystringISO code. Optional: it defaults to the card's own currency and must equal it when given (PURCHASE_INTENT_CURRENCY_MISMATCH otherwise).
reason requiredstringOne sentence, in the words the cardholder should read. This is the line they see in the approval email before they decide, so write it for them.
kindstringWhat the person is being asked for. single (the default, and what every approval was before this field existed) is ONE merchant, ONE amount inside the symmetric tolerance band, and exactly ONE authorization, spendable for 15 minutes once approved. order is a budget: up to amount plus the band in TOTAL at that merchant, over any number of authorizations, for valid_for_days days from the moment the person approves. Use order for anything that ships, tips, splits across shipments or runs a small verification authorization first — a single approval declines all of those. Marketplaces and pre-orders charge at shipment, so set valid_for_days to cover the latest delivery estimate plus a week. An order closes only when its budget is gone, so it stays usable after the order total has arrived and ends when its window does. order cannot be combined with recurring.One of: single, order
valid_for_daysintegerHow many days an order approval may be spent over, counted from the moment the person approves it. 1–90, default 14. Rejected on any other kind. answer_within_minutes still bounds ANSWERING the request; this is how long the answer is worth anything. Marketplaces and pre-orders charge at shipment, so set valid_for_days to cover the latest delivery estimate plus a week.
first_charge_within_daysintegerHow long a recurring approval waits for its FIRST charge. 1–60, default 45. Use it for a free trial, where nothing is charged today and the first real charge arrives weeks later. Rejected without recurring.
toleranceobjectHow far the authorization may sit from amount and still count as the same purchase. The wider of the two wins, and the band is symmetric — a charge below amount is outside it too, so a merchant that authorizes one cent before the real charge cannot spend the approval on the cent. Defaults to one minor unit of slack (0.01 on a USD card), 10 percent on an order, and 10 percent when the price is given as merchant_amount + merchant_currency.
tolerance.percentnumberPercentage of amount, at most 20. Use it when tax or a fee is proportional.
tolerance.absolutenumberSlack in the card's own currency, in major units, exactly like amount — 0.50 is fifty cents on a USD card. Use it for a flat fee you cannot know in advance. It may not exceed amount.
answer_within_minutesintegerHow many minutes the approver has to ANSWER, from the moment the request is made: 5–1440, default 60. It sets expires_at on the new intent and is stated in the approval email ("Answer by 14:45 UTC (in 60 minutes)"). It does not change how long an approval may be SPENT: a single approval gets 15 minutes from the moment it is approved, an order its valid_for_days.
recurringbooleanAsk for a standing approval rather than a one-off. Once the first authorization spends it, any later authorization from the same merchant for the same amount (within the same tolerance) is let through with no new request — with no expiry, until it is cancelled with DELETE /purchase-intents/{id}. The cardholder is told exactly that, in those words, in the approval email and on the approval page before they decide. A card may hold at most 20 recurring approvals at once (TOO_MANY_RECURRING_APPROVALS).
monthly_limitnumberHow much this merchant may charge per UTC calendar month, in the card's own currency, major units. Required when recurring is true and rejected without it. At least amount. This is the number the cardholder is shown, and it is the only thing bounding a standing approval — the per-charge amount band applies to the first charge only, because a usage-billed subscription varies month to month.

Example request

cURL

curl --request POST \
     --url 'https://api.themooncard.com/v2/api-gateway/purchase-intents' \
     --header 'x-api-key: YOUR_API_KEY' \
     --header 'content-type: application/json' \
     --data '{
       "card_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
       "merchant": {
         "mcc": "5734"
       },
       "merchant_domain": "shop.example.com",
       "amount": 24.99,
       "merchant_amount": 29.9,
       "merchant_currency": "BRL",
       "currency": "USD",
       "reason": "Renew the design team'\''s subscription for August",
       "kind": "single",
       "valid_for_days": 14,
       "first_charge_within_days": 45,
       "tolerance": {
         "percent": 5,
         "absolute": 0.5
       },
       "answer_within_minutes": 60,
       "recurring": false,
       "monthly_limit": 120
     }'

Responses

201 Success
FieldTypeDescription
id requiredstringPurchase intent id.
card_id requiredstringThe ask-first card this approval is for.
cardholder_id requiredstring, nullableThe cardholder who was asked, or null when the card has no cardholder.
approver_type requiredstringWho was asked to approve. cardholder when the card has one — the approval is that person’s and is never forwarded to the organization. account when the card has no cardholder, in which case the request goes to the email address of the account that owns the card, and when the card is on the organization’s own cardholder (the one a card issued without naming a cardholder lands on), which is the account holder. Always one of the two.One of: cardholder, account
status requiredstringAWAITING_APPROVAL until the cardholder answers; APPROVED once they have and until an authorization spends it; CONSUMED afterwards. DENIED, EXPIRED and CANCELLED are terminal and mean nothing will be charged. Expiry is evaluated when the intent is read, so a row past expires_at reports EXPIRED here whether or not anything had written it down yet.One of: AWAITING_APPROVAL, APPROVED, CONSUMED, DENIED, EXPIRED, CANCELLED
kind requiredstringsingle — one merchant, one amount inside the symmetric tolerance band, and exactly one authorization, spendable for 15 minutes once approved. order — a budget: up to max_total in total at that merchant, over any number of authorizations, for valid_for_days days from the moment it was approved. An approval created before this field existed reads single, which is what it was.One of: single, order
merchant requiredobjectThe merchant the approval covers.
merchant.name requiredstring, nullableMerchant name, or null when only an MCC was given.
merchant.mcc requiredstring, nullableMerchant category code, or null when only a name was given.
merchant_names requiredarray of stringEVERY merchant name this approval matches: merchant.name first, then the other spellings the request gave and the ones Moon expanded from a brand it recognises. Any of them matching the acquirer descriptor is enough, and the approver was shown the whole list before deciding. Empty when the approval named only an MCC.
merchant_domain requiredstring, nullableThe site the purchase is being made on, as a bare hostname, or null when the request did not name one. Advisory: a card network carries no domain, so the authorization gate neither checks this nor could. It is what the approver was shown next to the merchant and where the agent was told to use the credential.
amount requirednumberThe approved amount, in the card's own currency, in major units.
currency requiredstringISO code of the card's ledger account currency.
merchant_amount requirednumber, nullableThe price as the merchant stated it, in merchant_currency, when the request gave it that way (merchant_amount + merchant_currency). amount is then Moon's conversion of it into the card's currency, which is what the authorization is compared with. Null when the request gave amount.
merchant_currency requiredstring, nullableThe merchant currency (uppercase ISO 4217) of merchant_amount, or null.
request_fx_rate requirednumber, nullableThe rate (merchant_currency to the card's currency) amount was converted with when the request was made: a market rate with no spread. The network converts the real charge at its own rate, which the tolerance absorbs. Null when the request gave amount.
tolerance requiredobjectHow far an authorization may sit from amount and still match. The wider of the two applies, symmetrically in both directions.
tolerance.percent requirednumber, nullablePercentage slack on amount, or null when none applies.
tolerance.absolute requirednumber, nullableSlack in the card's own currency, in major units like amount, or null when none applies.
reason requiredstringThe sentence the cardholder read before deciding.
recurring requiredbooleanA standing approval. Once the first authorization spends it, any later authorization from the same merchant for the same amount (within the same tolerance) is let through without a new request — with no expiry, until it is cancelled. Such an approval reads CONSUMED while it is still in force, because the first charge spent it; DELETE /purchase-intents/{id} is what ends it.
monthly_limit requirednumber, nullableHow much this merchant may charge per UTC calendar month, in the card's own currency. Null on a one-off approval. This is what bounds a standing approval: the per-charge tolerance decides the first charge only.
recurring_spent_this_month requirednumberTaken from the current calendar month's budget so far. 0 before the first charge, and 0 again on the first read of a new month.
recurring_period_start requiredstring, nullableFirst day of the month recurring_spent_this_month is counting, as YYYY-MM-DD. Null until the first charge.
recurring_mcc requiredstring, nullableThe merchant category the standing approval was first used at, pinned at that first charge. A later authorization carrying a different category is refused. Null when the network sent none, in which case nothing is pinned.
valid_for_days requirednumber, nullableHow many days the approval stays spendable once it has been approved — an order's window, or a recurring approval's grace before its first charge. Null on a plain single approval, which may be spent for 15 minutes once approved. answer_within_minutes (60 by default) always bounds ANSWERING the request; this is how long the answer is worth anything, and expires_at is rewritten to it on approval.
max_total requirednumber, nullableAn order's budget: amount plus the tolerance band, in the card's own currency. Every authorization at that merchant is accepted while the running total stays inside it, with no lower bound per charge — which is what lets a split shipment or a small verification authorization through. Null on anything that is not an order.
spent requirednumber, nullableHow much of that budget has been taken. Null on anything that is not an order. Refunds and reversals do not give budget back.
remaining requirednumber, nullablemax_total minus spent, floored at zero. Null on anything that is not an order. An order that reaches its total closes itself and reads CONSUMED; unlike a standing recurring approval, it then authorizes nothing further.
expires_at requiredstringAfter this instant the approval is worthless and an authorization on the card is declined with INTENT_REQUIRED again. Until the approver answers it is the deadline to ANSWER (answer_within_minutes after the request, 60 by default). On approval it is rewritten to how long the approval may be SPENT: valid_for_days days out for an order and a recurring approval, 15 minutes out for a single one (see spendable_until). ISO-8601 UTC with milliseconds, e.g. "2026-09-16T17:05:00.000Z".
spendable_until requiredstring, nullableUntil when an APPROVED approval may be spent: the same instant as expires_at once the intent has been approved, and null before that — while it is awaiting an answer expires_at is the deadline to ANSWER, not to spend. Null on a recurring approval too, which has no spend deadline once it is in force. ISO-8601 UTC with milliseconds, e.g. "2026-09-16T17:05:00.000Z".
created_at requiredstringWhen the request was made. ISO-8601 UTC with milliseconds, e.g. "2026-09-16T17:05:00.000Z".
approved_at requiredstring, nullableWhen the cardholder approved it, or null. ISO-8601 UTC with milliseconds, e.g. "2026-09-16T17:05:00.000Z".
decided_at requiredstring, nullableWhen it was approved, denied or cancelled, or null. ISO-8601 UTC with milliseconds, e.g. "2026-09-16T17:05:00.000Z".
approved_via requiredstring, nullableHow the person answered: email (the one-time link or the page it opens), mcp (an agent connection recorded the answer the person gave it) or web (the account owner answered in the Moon app, signed in). Null until answered. Treat this as an open set: branch on the values you know and fall through on the rest.
autonomy_grant_id requiredstring, nullableReserved for a feature that is not generally available yet. Always null today; ignore it.
consumed_transaction_id requiredstring, nullableThe authorization that spent this approval, or null.
agent_key_id requiredstring, nullableThe key_id of the agent key that signed the request — the RFC 7638 JWK thumbprint register_agent_key returned. Null when the request was not signed, which is allowed.
agent_label requiredstring, nullableThat key’s current label. Read from the key on every request, so renaming a key renames it on intents that already exist.
profile_fields requiredarray of stringReserved for a feature that is not generally available yet. Always null today; ignore it.
profile_shipping_address requiredobject, nullableReserved for a feature that is not generally available yet. Always null today; ignore it.
profile_releases_remaining requiredinteger, nullableReserved for a feature that is not generally available yet. Always null today; ignore it.
autonomy requiredobjectReserved for a feature that is not generally available yet. Today applied is always false; ignore this object.
deduplicated requiredbooleantrue when nothing new was created: an identical request on the same card was still waiting for an answer, so that request is returned and the approver is not emailed again. Identical means the same kind (single, order or recurring), merchant names, MCC and merchant_domain, the same amount in the card's currency, the same terms (tolerance, valid_for_days or first_charge_within_days, monthly_limit) and, for a price given as merchant_amount + merchant_currency, the same store price. reason and answer_within_minutes are not compared. false on every other answer.
400 Error

The card is not in ask-first mode (CARD_NOT_ASK_FIRST), the cardholder has no email address to send the request to (APPROVAL_EMAIL_MISSING), currency is not the card's own (PURCHASE_INTENT_CURRENCY_MISMATCH), or the card already holds the maximum number of live intents (TOO_MANY_PENDING_INTENTS) — cancel one you no longer need, or wait for them to expire.

The body is the standard error object: see Errors.

401 Error

Missing or invalid credentials, e.g. { "message": "Invalid API Key", "code": "UNAUTHORIZED" }.

The body is the standard error object: see Errors.

404 Error

card_id does not exist or is not owned by this organization (CARD_NOT_FOUND).

The body is the standard error object: see Errors.

409 Error

The Idempotency-Key you sent is already in use. IDEMPOTENCY_KEY_REUSED — the key was used for a different request; send a new key, or repeat the original request exactly to receive its stored response. IDEMPOTENCY_KEY_IN_PROGRESS — the first request with this key has not finished yet; retry in a few seconds.

The body is the standard error object: see Errors.

422 Error

The price was given as merchant_amount + merchant_currency and Moon has no rate to convert that currency into the card's right now (MERCHANT_CURRENCY_UNSUPPORTED). Send amount in the card's own currency instead.

The body is the standard error object: see Errors.

500 Error

Unexpected server error. Body is { message: "Internal server error", code: "INTERNAL_SERVER_ERROR" }.

The body is the standard error object: see Errors.

Last updated September 25, 2026