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_ 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_ report ask_ 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_. limit_ 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_ and disable_. Both are idempotent and say which they were: enable_ 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
- Ask.
POST /v2/api-gateway/purchase-intentswith 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. - Moon emails the approver a one-time link and answers
201with the intent inAWAITING_.APPROVAL - Wait. For the
PURCHASE_webhook, or by pollingINTENT_ APPROVED 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. - Pay. The engine matches the authorization against the approval.
An agent working over MCP does the same four steps with request_, get_ and get_ β the last of which is the only way to get the card number of an ask-first card. See MCP Server.
1. Request approval
| Field | Type | Notes |
|---|---|---|
card_ | UUID, required | Must be an ask-first card, or the call is refused with CARD_ |
merchant | object, required | At least one of name and mcc. name may be one name or an array of up to five. See Matching |
amount | number | What you expect to be charged, in the card's own currency, in major units. Either this or merchant_ + merchant_ is required, never both |
merchant_ | number | The price exactly as the store shows it, in merchant_, 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_ | ISO 4217, uppercase | One of USD, MXN, EUR, CAD, GBP, JPY, AUD, BRL. Required with merchant_ |
reason | string, required, 1β200 | The sentence the person reads in the email. Write it for them |
kind | single (default) or order | What they are agreeing to. See Which kind to ask for. Cannot be combined with recurring |
merchant_ | bare hostname | The site the purchase is made on, e.g. shop.example.com. Advisory β see merchant_ |
currency | ISO code | Defaults to the card's own currency, and must equal it when given |
tolerance | object | How 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_ |
answer_ | integer 5β1440 | How many minutes the person has to answer. Default 60. Sets expires_ on the new intent; it does not change how long an approval may be spent. See Lifetime |
valid_ | integer 1β90 | How many days an order may be spent over, counted from the moment the person approves. Default 14. Rejected on any other kind |
recurring | boolean, default false | Ask for a standing permission rather than a one-off. Requires monthly_ |
monthly_ | number | How 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_ | integer 1β60 | How 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_ 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_, with an Approve and a Deny button. The link takes no login, works once for the decision, and stops working at expires_. 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:
| Approver | Can also answer |
|---|---|
| A cardholder | Through an MCP client connected with their own cardholder token, using list_ and decide_ |
The account (approver_) | 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_ 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
| How | What |
|---|---|
| Webhook | PURCHASE_, PURCHASE_, PURCHASE_ or PURCHASE_. See Webhooks |
| Poll | GET /v2/api-gateway/purchase-intents/{id} |
| List | GET /v2/api-gateway/purchase-intents?status=AWAITING_, newest first, current_ / per_ up to 100. card_ and recurring also filter |
status | Meaning |
|---|---|
AWAITING_ | The person has not answered yet |
APPROVED | They approved it. A single approval has not been spent yet; an order is open and has budget left |
CONSUMED | An 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 |
DENIED | They said no. Terminal |
EXPIRED | Nobody answered in time, or the window closed unused. Terminal |
CANCELLED | It was withdrawn or stopped. Terminal |
status is evaluated against expires_ on every read, so a request nobody answered in time reports EXPIRED rather than an AWAITING_ 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_ then names the authorization that spent a single approval.
An agent that needs the card number for this step calls get_ over MCP. It returns the credential for that one approved purchase together with approved_ β 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_ refuses on an ask-first card. See MCP Server.
Which kind to ask for
| Ask for | What the person agrees to | Right for |
|---|---|---|
kind: "single" (default) | One merchant, one amount inside the symmetric tolerance band, exactly one authorization, spendable for 15 minutes once approved | Pay-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_ in total at that merchant, over any number of charges, for valid_ days from the moment they approve | Physical goods, marketplaces, food and ride apps, groceries, travel β anything that ships, tips, splits across shipments or saves the card on file |
recurring: true | A standing permission at that merchant, bounded by monthly_ per UTC calendar month, with no expiry | Subscriptions 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_istotal amountplus the tolerance band, computed when the intent is created and returned on every read alongsidespentandremaining. Every authorization at that merchant is accepted whilespent + this charge <= max_, with no lower bound per charge.total - 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_(1β90, default 14) is counted from the moment the person approves.for_ days answer_still bounds how long they have to answer; on approvalwithin_ minutes expires_is rewritten to the window, and reads report it asat spendable_. Marketplaces and pre-orders charge at shipment, so set it to cover the latest delivery estimate plus a week.until - 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
APPROVEDand ends when its window does. A closed order readsCONSUMEDand 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
CONSUMEDand emits noPURCHASE_: it is a purchase that happened, not an approval nobody answered. One that was never charged expires with the event like any other.INTENT_ EXPIRED - A charge that would take it past
max_, or that arrives after the window, is declinedtotal INTENT_and nothing is recorded.MISMATCH
Standing (recurring) approvals
monthly_β not the per-charge amount β is what bounds it, and it is the number the person is shown.limit - The first charge is accepted when the merchant matches and the amount fits
monthly_. It does not have to matchlimit 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_(1β60, default 45) is how long the approval waits for that first charge. As with an order,charge_ within_ days expires_is rewritten on approval, so the approval is still there weeks later.at - Every later charge is let through when the merchant matches and
spent this UTC calendar month + this charge <= monthly_. The per-charge band does not apply to it, because a usage-billed subscription varies month to month.limit recurring_andspent_ this_ month recurring_report where the month stands.period_ start - A charge that would take the month past the limit is declined
INTENT_and nothing is recorded. Ask for a higherMISMATCH monthly_with a fresh request.limit - A standing approval is pinned to the merchant category it was first used at (
recurring_). A later authorization carrying a different category is refused.mcc - A card may hold at most 20 standing approvals at once (
TOO_). A standing approval does not expire, so that cap is cleared by cancelling one, not by waiting.MANY_ RECURRING_ APPROVALS
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:
nameis matched case-insensitively as a substring of the acquirer's merchant descriptor. Acquirers pad and prefix that descriptor unpredictably, soTRAIN TICKETSmatchesSQ *TRAIN TICKETS SAN FRANCISCOandTRAIN TICKETS *BOOKINGalike. Send the shortest string that identifies the merchant.mccis 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_ β for a purchase the person explicitly said yes to.
Three things address that, and they stack:
merchant.nametakes an array of up to five names, and any one of them matching is enough.- 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_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.names - After a mismatch, read the descriptor the network actually sent and ask again naming it. An agent over MCP does this with
list_, which returns the recent declines on a card withcard_ declines merchant_exactly as it arrived. Do not retry the payment against the old approval.descriptor
Amounts
| Kind | What an authorization has to satisfy |
|---|---|
single | Inside the tolerance band β the wider of percent and absolute, applied symmetrically |
order | spent + this charge <= max_, with no lower bound per charge |
recurring, first charge | At most monthly_ |
recurring, later charges | This UTC month's total plus this charge, at most monthly_ |
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:
- a
singleor recurring approval whose exact band the charge fits; - the open order budgets at that merchant, oldest first;
- a recurring approval's first charge, against its monthly limit;
- 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 STOREstops 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_names it as the one place an agent may use the credential.payment_ credential
Lifetime
An unanswered request lasts answer_ β 60 minutes by default, anything from 5 to 1440 if you set it. That deadline applies only to answering; until then expires_ is that deadline. How long the answer is then worth something depends on the kind:
| Kind | How long the approval is spendable |
|---|---|
single | 15 minutes from the approval, and exactly one purchase β however long the person took to answer |
order | valid_ days from the moment it was approved, until the budget runs out |
recurring | first_ days to take the first charge; then no expiry, until it is cancelled or stopped |
On approval expires_ is rewritten to that window, in the same statement that records the decision, and the reads also return it as spendable_ β null before approval and on a recurring approval. A denial keeps the original deadline.
There is no sweep job. An intent past expires_ 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_ 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_, exactly as if the approval had never existed.
Treat expires_ as the deadline and the webhook as a convenience. It can arrive well after expires_, and an intent nobody ever looks at again never produces one.
A card may hold at most 20 live intents at once (TOO_). 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_ webhook is emitted carrying cancelled_ (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_ β see Identical requests are asked once.
Three cases are allowed, and all three are endings rather than reversals:
| State | What DELETE does |
|---|---|
AWAITING_ | Withdraws 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: true | Stops 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_: 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_ | Name | Meaning | What to do |
|---|---|---|---|
| 25 | INTENT_ | No approved, unspent, unexpired intent for this card | Ask. Create a purchase intent and wait |
| 26 | INTENT_ | An 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 limit | Ask again for the real purchase. The decline carries the merchant and amount actually seen |
Both arrive as an ordinary CARD_ webhook. Software that cannot tell them apart will re-ask for the wrong thing forever, so branch on decline_.
An agent over MCP can read the same declines directly with list_, which returns the acquirer descriptor the network sent β which is what an INTENT_ is almost always about.
Errors
These are returned by POST /v2/api-gateway/purchase-intents in the standard error envelope.
| Status | code | Meaning |
|---|---|---|
400 | CARD_ | The card is not in ask-first mode |
400 | APPROVAL_ | There 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 |
400 | PURCHASE_ | currency was given and is not the card's own currency |
400 | TOO_ | The card already holds the maximum number of live intents. Cancel one you no longer need, or wait for them to expire |
400 | TOO_ | The card already holds the maximum number of standing approvals. Cancel one β waiting will not clear it |
400 | (validation) | Both amount and merchant_, or neither; merchant_ without merchant_ or the other way round; a merchant_ outside the supported list; answer_ outside 5β1440; order combined with recurring; valid_ without kind: "order"; first_ or monthly_ without recurring; recurring without monthly_; monthly_ below amount; tolerance.absolute above amount; more than five merchant names; a merchant_ that is not a bare hostname |
404 | CARD_ | card_ does not exist, or is not owned by your organization |
409 | IDEMPOTENCY_ / IDEMPOTENCY_ | The Idempotency-Key is bound to a different request, or the first request with it has not finished yet |
422 | MERCHANT_ | The price was given as merchant_ + merchant_ 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:
| Status | code | Meaning |
|---|---|---|
404 | PURCHASE_ | No such purchase intent for your organization |
409 | PURCHASE_ | Cancel was called on something that cannot be cancelled β see Cancelling and stopping |
Related
- 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_and the rest of the flow for an agent, plus agent keys.card_ declines - Webhooks β the
PURCHASE_events and their payload.INTENT_ * - Errors β the error envelope and how to branch on a failure.
- Idempotency β the
Idempotency-Keyheader.
Last updated September 26, 2026