Browse the docs

Ask-First Cards

An ask-first card charges nothing until the person who holds it has approved that exact purchase. You ask before you pay; Moon emails them a one-time link; the authorization engine then lets through the charges that match what they approved.

It exists for software that spends on someone's behalf. A card can be capped and locked down in advance, but a cap cannot tell a purchase the person wanted from one they did not. Ask-first moves that decision to the person, per purchase, without putting them in the payment path.

An approval can be one of three things, and you choose which when you ask: a single purchase, an order budget at one merchant over a few days, or a standing permission bounded by a monthly limit. See Which kind to ask for.

Issuing an ask-first card

Set ask_first when you create the card:

cURL

curl --request POST \
     --url https://api.themooncard.com/v2/api-gateway/cards \
     --header 'content-type: application/json' \
     --header 'x-api-key: YOUR_API_KEY' \
     --data '{
       "cardholder_id": "9d4a1f70-2c8b-4f31-a6de-7b5c0e93af12",
       "account_id": "0b1c2d3e-4f50-4a6b-8c9d-0e1f2a3b4c5d",
       "ask_first": true
     }'

GET /v2/api-gateway/cards and GET /v2/api-gateway/cards/{card_id} report ask_first on every card.

Under the hood the card is issued with one fixed velocity control (INTENT over TOTAL), and that control is what the authorization engine reads. It lists and reads through the velocity-control endpoints like any other control, flagged fixed: true, and the generic update and delete calls refuse it with CONTROL_IS_FIXED. limit_amount is inert on it: an INTENT control measures no window.

A card can be ask-first and single-use and merchant-locked. Those are three independent fixed controls, and any one of them can decline an authorization on its own.

An agent connected over MCP can also turn the mode on and off for a card that already exists, with enable_ask_first and disable_ask_first. Both are idempotent and say which they were: enable_ask_first on a card that already asks first answers enabled: false. Disabling leaves approvals the person has already given alone β€” they simply stop being consulted, and count again if ask-first is turned back on before they expire. See MCP Server.

The flow

  1. Ask. POST /v2/api-gateway/purchase-intents with the card, the merchant, the price β€” in the card's currency, or as the store shows it in another currency β€” a one-sentence reason and, when you know it, the site.
  2. Moon emails the approver a one-time link and answers 201 with the intent in AWAITING_APPROVAL.
  3. Wait. For the PURCHASE_INTENT_APPROVED webhook, or by polling GET /v2/api-gateway/purchase-intents/{id}. Never attempt the payment first, and never retry it in a loop β€” the answer is a person's, and an authorization sent before they answer is simply declined.
  4. Pay. The engine matches the authorization against the approval.

An agent working over MCP does the same four steps with request_purchase, get_purchase_intent and get_payment_credential β€” the last of which is the only way to get the card number of an ask-first card. See MCP Server.

1. Request approval

FieldTypeNotes
card_idUUID, requiredMust be an ask-first card, or the call is refused with CARD_NOT_ASK_FIRST
merchantobject, requiredAt least one of name and mcc. name may be one name or an array of up to five. See Matching
amountnumberWhat you expect to be charged, in the card's own currency, in major units. Either this or merchant_amount + merchant_currency is required, never both
merchant_amountnumberThe price exactly as the store shows it, in merchant_currency, in major units. Moon converts it into the card's currency and stores the result as amount. See Prices in the store's currency
merchant_currencyISO 4217, uppercaseOne of USD, MXN, EUR, CAD, GBP, JPY, AUD, BRL. Required with merchant_amount
reasonstring, required, 1–200The sentence the person reads in the email. Write it for them
kindsingle (default) or orderWhat they are agreeing to. See Which kind to ask for. Cannot be combined with recurring
merchant_domainbare hostnameThe site the purchase is made on, e.g. shop.example.com. Advisory β€” see merchant_domain
currencyISO codeDefaults to the card's own currency, and must equal it when given
toleranceobjectHow far an authorization may sit from amount and still count as the same purchase. percent is a percentage of amount, at most 20; absolute is in the card's currency, in major units, at most amount. Default one minor unit on a single, 10 percent on an order, and 10 percent on either when the price is given as merchant_amount
answer_within_minutesinteger 5–1440How many minutes the person has to answer. Default 60. Sets expires_at on the new intent; it does not change how long an approval may be spent. See Lifetime
valid_for_daysinteger 1–90How many days an order may be spent over, counted from the moment the person approves. Default 14. Rejected on any other kind
recurringboolean, default falseAsk for a standing permission rather than a one-off. Requires monthly_limit
monthly_limitnumberHow much this merchant may charge per UTC calendar month, in the card's currency. Required with recurring, rejected without it, and at least amount
first_charge_within_daysinteger 1–60How long a recurring approval waits for its first charge. Default 45. Rejected without recurring

Send an Idempotency-Key header. A retry with the same key returns the first attempt's answer rather than emailing the person a second time. See Idempotency.

Nothing here charges the card and nothing here moves money. It asks a person a question.

deduplicated: true means nothing new was created: an identical request on the same card was still waiting for the person's answer, so that request came back and nobody was emailed again. See Identical requests are asked once.

merchant_names is every name the approval matches: merchant.name first, then the other spellings you gave and any Moon expanded from a brand it recognises. The approver is shown the whole list before deciding.

2. What the approver sees

The email carries the merchant, the amount, the card's last four digits, your reason and β€” when you sent one β€” the merchant_domain, with an Approve and a Deny button. The link takes no login, works once for the decision, and stops working at expires_at. Unknown, used and expired links all show the same page β€” which of the three it was is not a visitor's business.

For an order or a recurring approval the page also states, above the buttons, exactly what else they are agreeing to:

  • an order β€” "Approve up to max_total currency at merchant over the next N days. The store may charge this in several parts."
  • a standing approval β€” "Approving also allows future charges from merchant on this card, up to monthly_limit currency per calendar month, without asking again, until it is stopped."

For both, the same link keeps working after the decision and shows what has been charged and what is left, with a Stop button that ends the approval.

If the request was signed with a registered agent key, the email and the page also name the agent: "Requested by label (key …last 8 of the thumbprint)". An unsigned request reads only as "an agent". See Agent keys and signed requests.

The email is not the only way to answer:

ApproverCan also answer
A cardholderThrough an MCP client connected with their own cardholder token, using list_pending_approvals and decide_purchase_intent
The account (approver_type: account)Signed in to the Moon app, or through an assistant the account owner connected with OAuth and the cards:write permission, using the same two tools

approved_via on the intent says which it was: email, mcp or web. The two tools record a decision the person has stated; an agent must never decide on their behalf, and when one records a decision Moon emails the approver a receipt of it. A partner API key cannot answer at all β€” it is the credential that asks. See Purchase intents.

3. Wait for the answer

HowWhat
WebhookPURCHASE_INTENT_APPROVED, PURCHASE_INTENT_DENIED, PURCHASE_INTENT_EXPIRED or PURCHASE_INTENT_CANCELLED. See Webhooks
PollGET /v2/api-gateway/purchase-intents/{id}
ListGET /v2/api-gateway/purchase-intents?status=AWAITING_APPROVAL, newest first, current_page / per_page up to 100. card_id and recurring also filter
statusMeaning
AWAITING_APPROVALThe person has not answered yet
APPROVEDThey approved it. A single approval has not been spent yet; an order is open and has budget left
CONSUMEDAn authorization spent it β€” or, for an order, its budget ran out. A standing recurring approval also reads CONSUMED while it is still in force, because the first charge spent it
DENIEDThey said no. Terminal
EXPIREDNobody answered in time, or the window closed unused. Terminal
CANCELLEDIt was withdrawn or stopped. Terminal

status is evaluated against expires_at on every read, so a request nobody answered in time reports EXPIRED rather than an AWAITING_APPROVAL that can never be satisfied. The list filter matches the same effective status.

4. Pay

Once the intent is APPROVED, charge the card as you normally would. The engine matches the authorization against the approval; consumed_transaction_id then names the authorization that spent a single approval.

An agent that needs the card number for this step calls get_payment_credential({ intent_id }) over MCP. It returns the credential for that one approved purchase together with approved_for β€” merchant, amount, and the range the gate will actually accept: the symmetric band on a single approval, what is left of the budget on an order, the monthly limit on a standing one. One approval may hand the credential over at most 10 times. reveal_card_credentials refuses on an ask-first card. See MCP Server.

Which kind to ask for

Ask forWhat the person agrees toRight for
kind: "single" (default)One merchant, one amount inside the symmetric tolerance band, exactly one authorization, spendable for 15 minutes once approvedPay-now digital goods: a subscription renewal, a top-up, an API bill β€” anything charged at checkout for exactly the number that was quoted
kind: "order"A budget: up to max_total in total at that merchant, over any number of charges, for valid_for_days days from the moment they approvePhysical goods, marketplaces, food and ride apps, groceries, travel β€” anything that ships, tips, splits across shipments or saves the card on file
recurring: trueA standing permission at that merchant, bounded by monthly_limit per UTC calendar month, with no expirySubscriptions and free trials

order and recurring cannot be combined: one is a budget for a few days, the other a permission with no end date. The call is refused if you send both.

A single approval is declined by a small verification authorization, by a charge that arrives when the order ships rather than when it is placed, by a split shipment, and by a final amount a few percent off the estimate. Every one of those is ordinary, and every one of them is what order is for.

Order approvals

  • max_total is amount plus the tolerance band, computed when the intent is created and returned on every read alongside spent and remaining. Every authorization at that merchant is accepted while spent + this charge <= max_total, with no lower bound per charge.
  • The default tolerance for an order is 10 percent, not one minor unit. Shipping quoted at checkout, tax computed at the warehouse, a weight-priced grocery item, a tip and an acquirer's own conversion are all proportional, and a one-cent band would decline the ordinary case. Every point of tolerance is budget the person is agreeing to, so name your own when you can.
  • valid_for_days (1–90, default 14) is counted from the moment the person approves. answer_within_minutes still bounds how long they have to answer; on approval expires_at is rewritten to the window, and reads report it as spendable_until. Marketplaces and pre-orders charge at shipment, so set it to cover the latest delivery estimate plus a week.
  • An order closes only when its budget is gone. There is no "that was the final capture" signal in an authorization stream, so the arrival of the order total proves nothing: until the budget runs out the order stays APPROVED and ends when its window does. A closed order reads CONSUMED and authorizes nothing further β€” unlike a standing approval, it is not a permission.
  • At the end of its window, an order that was charged moves to CONSUMED and emits no PURCHASE_INTENT_EXPIRED: it is a purchase that happened, not an approval nobody answered. One that was never charged expires with the event like any other.
  • A charge that would take it past max_total, or that arrives after the window, is declined INTENT_MISMATCH and nothing is recorded.

Standing (recurring) approvals

  • monthly_limit β€” not the per-charge amount β€” is what bounds it, and it is the number the person is shown.
  • The first charge is accepted when the merchant matches and the amount fits monthly_limit. It does not have to match amount: a free trial charges nothing today and its first real charge arrives weeks later for a figure nobody quoted, so holding it to the exact band declined the commonest subscription there is. It consumes the intent exactly as a one-off would, and counts against its month.
  • first_charge_within_days (1–60, default 45) is how long the approval waits for that first charge. As with an order, expires_at is rewritten on approval, so the approval is still there weeks later.
  • Every later charge is let through when the merchant matches and spent this UTC calendar month + this charge <= monthly_limit. The per-charge band does not apply to it, because a usage-billed subscription varies month to month. recurring_spent_this_month and recurring_period_start report where the month stands.
  • A charge that would take the month past the limit is declined INTENT_MISMATCH and nothing is recorded. Ask for a higher monthly_limit with a fresh request.
  • A standing approval is pinned to the merchant category it was first used at (recurring_mcc). A later authorization carrying a different category is refused.
  • A card may hold at most 20 standing approvals at once (TOO_MANY_RECURRING_APPROVALS). A standing approval does not expire, so that cap is cleared by cancelling one, not by waiting.

Refunds and reversals do not give budget back, on an order or on a standing approval. That is deliberately conservative: a refund must not be a way to make room for another charge.

Matching

The merchant is 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:

  • name is matched case-insensitively as a substring of the acquirer's merchant descriptor. Acquirers pad and prefix that descriptor unpredictably, so TRAIN TICKETS matches SQ *TRAIN TICKETS SAN FRANCISCO and TRAIN TICKETS *BOOKING alike. Send the shortest string that identifies the merchant.
  • mcc is an exact four-digit merchant category code, never a substring.
  • Every field the intent names has to match. Give both and both must match.

Naming a merchant you cannot see yet

The acquirer descriptor does not exist until the first charge, and it is rarely the brand: a marketplace bills as an abbreviation, an app store as a billing domain, a hosted store under its platform's prefix. An approval naming only the brand misses all of them and the charge is declined INTENT_MISMATCH β€” for a purchase the person explicitly said yes to.

Three things address that, and they stack:

  1. merchant.name takes an array of up to five names, and any one of them matching is enough.
  2. Moon expands a brand it recognises into the statement descriptors that brand is known to bill under, at request time. The expanded list comes back as merchant_names and the approver is shown every name before deciding β€” nothing is hidden. That table is curated by hand and is best-effort: treat a miss as ordinary rather than a bug.
  3. After a mismatch, read the descriptor the network actually sent and ask again naming it. An agent over MCP does this with list_card_declines, which returns the recent declines on a card with merchant_descriptor exactly as it arrived. Do not retry the payment against the old approval.

Amounts

KindWhat an authorization has to satisfy
singleInside the tolerance band β€” the wider of percent and absolute, applied symmetrically
orderspent + this charge <= max_total, with no lower bound per charge
recurring, first chargeAt most monthly_limit
recurring, later chargesThis UTC month's total plus this charge, at most monthly_limit

The single band is symmetric on purpose: a charge below the approved amount is outside it too, so a merchant that authorizes one cent before the real charge cannot spend a $500 approval on the cent.

A zero-amount card verification β€” what many subscription merchants send before a real charge β€” passes on a merchant match alone and does not spend the approval. The approval is still there when the real charge arrives.

When several approvals could take one charge

A card can hold an open order and a subscription at the same merchant at once. Moon tries them in this order, and stops at the first that authorizes:

  1. a single or recurring approval whose exact band the charge fits;
  2. the open order budgets at that merchant, oldest first;
  3. a recurring approval's first charge, against its monthly limit;
  4. the standing approvals already in force.

A recurring first charge accepts anything up to a month's budget, which is a very wide match β€” so it is tried after the orders rather than before them, or a shipment would spend a subscription's one first charge.

merchant_domain

A bare hostname: shop.example.com, never https://shop.example.com/checkout. It is trimmed and lower-cased, and a URL, a port, an @, an IP address or a unicode domain is rejected with a validation error rather than repaired β€” send the punycode form of an internationalised name. Guessing which part of a URL was the merchant's site, in a value that goes in front of a person deciding whether to spend money, is worse than a clear refusal.

It is advisory at the network layer, and always will be. The card networks carry no domain in an authorization, so the gate does not and cannot check it, and no charge is ever declined for coming from a different site. What it buys is three things:

  • the approval email and page show it next to the merchant, so ACME STORE stops being a descriptor the person has to take on trust;
  • it is part of the idempotency fingerprint, because a different site is a different question to ask;
  • get_payment_credential names it as the one place an agent may use the credential.

Lifetime

An unanswered request lasts answer_within_minutes β€” 60 minutes by default, anything from 5 to 1440 if you set it. That deadline applies only to answering; until then expires_at is that deadline. How long the answer is then worth something depends on the kind:

KindHow long the approval is spendable
single15 minutes from the approval, and exactly one purchase β€” however long the person took to answer
ordervalid_for_days days from the moment it was approved, until the budget runs out
recurringfirst_charge_within_days days to take the first charge; then no expiry, until it is cancelled or stopped

On approval expires_at is rewritten to that window, in the same statement that records the decision, and the reads also return it as spendable_until β€” null before approval and on a recurring approval. A denial keeps the original deadline.

There is no sweep job. An intent past expires_at is recognised as expired the first time it is read β€” by these endpoints, by an agent's tools, or by the approval page β€” and PURCHASE_INTENT_EXPIRED is emitted at that point. The authorization path never emits it: it answers the card network under a deadline, so it treats an expired approval as absent and declines INTENT_REQUIRED, exactly as if the approval had never existed.

Treat expires_at as the deadline and the webhook as a convenience. It can arrive well after expires_at, and an intent nobody ever looks at again never produces one.

A card may hold at most 20 live intents at once (TOO_MANY_PENDING_INTENTS). An approved order stays live for its whole window, so that cap is cleared by cancelling one rather than by waiting.

Cancelling and stopping

cURL

curl --request DELETE \
     --url https://api.themooncard.com/v2/api-gateway/purchase-intents/6c1f9a84-0b73-4d52-9e18-3a7c5d2b4e60 \
     --header 'x-api-key: YOUR_API_KEY'

The intent is not deleted β€” it comes back with status: CANCELLED, and a PURCHASE_INTENT_CANCELLED webhook is emitted carrying cancelled_by (partner when you called it, approver when the person did). Moon itself cancels only duplicate one-off approvals once an identical one has been spent, with cancel_reason: superseded_by_consumed_sibling β€” see Identical requests are asked once.

Three cases are allowed, and all three are endings rather than reversals:

StateWhat DELETE does
AWAITING_APPROVALWithdraws a request nobody has answered yet
APPROVED, kind: "order"Closes an open order early, so the rest of its budget cannot be charged
CONSUMED, recurring: trueStops a standing permission that has taken its first charge. This is the only way for you to end one

Anything else is refused with 409 PURCHASE_INTENT_NOT_PENDING: an approval a person gave for one purchase is theirs, not yours to take back, so an APPROVED single intent, a spent one-off CONSUMED intent and one that has already expired are all left alone. A standing approval that is APPROVED but has not been charged yet is refused too; the person can stop it from their link at any time.

The person can end an order or a standing approval themselves, from the same emailed link, with a Stop button. Either party doing it emits the same webhook, so a partner whose subscription stops being authorized learns why without polling.

Declines

Two decline types are specific to ask-first cards, and they mean different things:

decline_type_idNameMeaningWhat to do
25INTENT_REQUIREDNo approved, unspent, unexpired intent for this cardAsk. Create a purchase intent and wait
26INTENT_MISMATCHAn approval exists, but for a different merchant, outside the tolerance band, past an order's budget or window, or past a standing approval's monthly limitAsk again for the real purchase. The decline carries the merchant and amount actually seen

Both arrive as an ordinary CARD_DECLINE webhook. Software that cannot tell them apart will re-ask for the wrong thing forever, so branch on decline_type_id.

An agent over MCP can read the same declines directly with list_card_declines, which returns the acquirer descriptor the network sent β€” which is what an INTENT_MISMATCH is almost always about.

Errors

These are returned by POST /v2/api-gateway/purchase-intents in the standard error envelope.

StatuscodeMeaning
400CARD_NOT_ASK_FIRSTThe card is not in ask-first mode
400APPROVAL_EMAIL_MISSINGThere is no email address to send the request to β€” the card's cardholder, or the account that owns the card when it has no cardholder
400PURCHASE_INTENT_CURRENCY_MISMATCHcurrency was given and is not the card's own currency
400TOO_MANY_PENDING_INTENTSThe card already holds the maximum number of live intents. Cancel one you no longer need, or wait for them to expire
400TOO_MANY_RECURRING_APPROVALSThe card already holds the maximum number of standing approvals. Cancel one β€” waiting will not clear it
400(validation)Both amount and merchant_amount, or neither; merchant_amount without merchant_currency or the other way round; a merchant_currency outside the supported list; answer_within_minutes outside 5–1440; order combined with recurring; valid_for_days without kind: "order"; first_charge_within_days or monthly_limit without recurring; recurring without monthly_limit; monthly_limit below amount; tolerance.absolute above amount; more than five merchant names; a merchant_domain that is not a bare hostname
404CARD_NOT_FOUNDcard_id does not exist, or is not owned by your organization
409IDEMPOTENCY_KEY_REUSED / IDEMPOTENCY_KEY_IN_PROGRESSThe Idempotency-Key is bound to a different request, or the first request with it has not finished yet
422MERCHANT_CURRENCY_UNSUPPORTEDThe price was given as merchant_amount + merchant_currency and Moon has no rate to convert that currency into the card's right now. Nothing was created. Send amount in the card's own currency instead

On the reads and on cancel:

StatuscodeMeaning
404PURCHASE_INTENT_NOT_FOUNDNo such purchase intent for your organization
409PURCHASE_INTENT_NOT_PENDINGCancel was called on something that cannot be cancelled β€” see Cancelling and stopping
  • Purchase intents β€” the three kinds of approval side by side, prices in the store's currency, the answer and spend windows, duplicate requests, and every way the approver can answer.
  • MCP Server β€” request_purchase, get_payment_credential, list_card_declines and the rest of the flow for an agent, plus agent keys.
  • Webhooks β€” the PURCHASE_INTENT_* events and their payload.
  • Errors β€” the error envelope and how to branch on a failure.
  • Idempotency β€” the Idempotency-Key header.

Last updated September 26, 2026