Purchase Intents
Single, order and recurring approvals, prices in the store's currency, the answer and spend windows, duplicate requests, and how the approver answers.
A purchase intent is a question put to a person before any money moves: may this agent spend this much, at this merchant, for this reason? It is how an ask-first card is paid with. The card charges nothing on its own; each purchase needs an intent the approver said yes to, and the authorization engine lets through only the charges that fit it.
This page is the overview: the three kinds of approval, how to state a price the store shows in another currency, how long the approver has to answer and how long an approval lasts, what happens when the same purchase is asked for twice, how the approver answers, and what you receive while you wait. The field-by-field reference, the matching rules and the error codes are on Ask-first cards.
Who is asked
The approver is always a person, never your organization:
| The card has… | The approver is | approver_ |
|---|---|---|
| a cardholder | that cardholder, at their own email address | cardholder |
| no cardholder | the account that owns the card, at the account's email address | account |
| your organization's own cardholder — the one a card issued without naming a cardholder is placed on | the account holder, at the address on that cardholder, which is the account's | account |
A request for a cardholder is never quietly forwarded to your organization. If the approver has no email address, the request is refused with APPROVAL_, because nobody can be asked.
Asking
| Over | Call |
|---|---|
| REST | POST /v2/api-gateway/purchase-intents with an Idempotency-Key header |
| MCP | request_ with an idempotency_ |
cURL
curl --request POST \
--url https://api.themooncard.com/v2/api-gateway/purchase-intents \
--header 'content-type: application/json' \
--header 'x-api-key: YOUR_API_KEY' \
--header 'Idempotency-Key: order-9f31b2' \
--data '{
"card_id": "3f0c8a12-5b6d-4e7f-9a01-2b3c4d5e6f70",
"merchant": { "name": "ACME STORE" },
"merchant_domain": "shop.example.com",
"amount": 84.90,
"reason": "Order the replacement desk chair you picked",
"kind": "order",
"valid_for_days": 21,
"answer_within_minutes": 120
}'The answer is 201 with the intent in AWAITING_, and Moon has emailed the approver. Nothing is charged and nothing moves: you have asked a question. A retry with the same idempotency key returns the first answer instead of emailing the person twice.
Write reason for the person — it is the sentence they read before they decide. Send merchant_ whenever you know the site: the approver is shown it next to the merchant name, which is often an unfamiliar statement descriptor.
Prices in the store's currency
State the price one way:
| Send | When |
|---|---|
amount | You know the price in the card's own currency, in major units |
merchant_ + merchant_ | The store shows the price in another currency — for example R$ 29.90 on a Brazilian checkout: "merchant_. Send it exactly as the page shows it and do not convert it yourself |
- Exactly one form is accepted. Both together, neither, or
merchant_withoutamount merchant_(or the other way round) is a validation error.currency merchant_is an uppercase ISO 4217 code, one ofcurrency USD,MXN,EUR,CAD,GBP,JPY,AUDandBRL. Anything else is a validation error: sendamountin the card's currency instead.- Moon converts the price into the card's currency when the request is made, at a market rate with no spread, and rounds it half-up to the card currency's minor unit. The result is stored as
amount— still the one number every authorization is compared against, in the card's currency. - The intent keeps the store's figure and the rate:
merchant_,amount merchant_andcurrency request_on every read and on thefx_ rate PURCHASE_webhooks. All three areINTENT_ * nullon an intent whose price was given asamount. - The approver sees both figures in the email, on the approval page and in the Moon app, for example "R$ 29.90, about $5.60".
- With no
toleranceof your own, a price given this way gets 10 percent, onsingleandorderalike: the card network converts the real charge at its own rate, some time later. A tolerance you name wins.tolerance.absoluteis still in the card's currency and may not exceed the convertedamount, andmonthly_may not be below it.limit - When Moon has no rate for the currency pair at that moment, the request is refused with
422 MERCHANT_(CURRENCY_ UNSUPPORTED merchant_over MCP) and nothing is written or emailed. Ask again withcurrency_ unsupported amountin the card's currency.
Which kind to ask for
| Ask for | What the person agrees to | Right for |
|---|---|---|
kind: "single" (default) | One merchant, one amount inside a symmetric tolerance band, exactly one authorization | Pay-now digital goods: a subscription renewal, a top-up, an API bill |
kind: "order" | A budget at one merchant: up to max_ in total, over any number of charges, for valid_ days (1–90, default 14) from the moment they approve | Physical goods, marketplaces, travel — anything that ships, splits, tips or verifies the card first |
recurring: true | A standing permission at one merchant, bounded by monthly_ per UTC calendar month, with no expiry until it is stopped | Subscriptions and free trials |
order and recurring cannot be combined. When in doubt between single and order, choose order: a small verification charge, a charge at shipment, a split shipment and a final amount a few percent off the estimate each decline a single approval, and each is ordinary.
Marketplaces and pre-orders charge at shipment, so set valid_ to cover the latest delivery estimate plus a week. The approval stays live for the whole window, so ask for what the store actually needs.
Tolerance
tolerance is how far a charge may sit from amount and still count as the purchase that was approved. percent is a percentage of amount, at most 20; absolute is in the card's currency, in major units, at most amount. The wider of the two applies.
| Kind | How the tolerance is used | Default |
|---|---|---|
single | A band around amount, in both directions — a charge below it is outside the band just as one above it is | One minor unit |
order | Added to amount to make the budget, max_. Any number of charges fit while their total stays inside it | 10 percent |
recurring | Not what bounds it: monthly_ is | — |
When the price is given as merchant_ + merchant_, the default is 10 percent on single and order alike — see Prices in the store's currency.
Every point of tolerance is money the person is agreeing to, so name your own when you can. The band is symmetric on purpose: a merchant that authorizes one cent before the real charge must not be able to spend a large approval on the cent.
The answer window
Two clocks apply, one after the other: how long the approver has to answer, and how long an approval may be spent once they have given it.
Answering. The approver has answer_ to answer, counted from the moment you ask: anything from 5 to 1440 minutes (a day), 60 by default. It is the same for every kind. Until they answer, expires_ is that deadline. The email states it in words, for example "Answer by 14:45 UTC (in 60 minutes)", and over MCP request_ returns the same sentence as answer_. Ask for a longer window when the person is unlikely to see the email soon — overnight, or for a pre-order — not to avoid asking. A request cannot be extended once it has been sent; when it expires, ask again.
Spending. The moment they approve, expires_ is rewritten, in the same write that records the decision, to how long the approval may be spent:
| Kind | How long the approval can be spent |
|---|---|
single | 15 minutes from the approval, for exactly one purchase — however much of the answer window the person used. Pay promptly once it is approved |
order | valid_ days (1–90, default 14) from the moment of approval |
recurring | first_ days (1–60, default 45) to take the first charge, then no expiry until it is stopped |
Once an intent is approved, reads also return spendable_: the same instant as expires_, named for what it now means — the last moment the approval may be charged. It is null while the intent is still awaiting an answer (when expires_ is the answer deadline) and on a recurring approval, which has no spend deadline once it is in force. A denial keeps the original expires_.
An intent nobody answered in time reads EXPIRED on the next read. There is no sweep job: status is evaluated against expires_ whenever the intent is read, and the PURCHASE_ webhook is emitted at that point — so treat expires_ as the deadline and the webhook as a convenience.
Identical requests are asked once
Asking again for a purchase the approver has not answered yet does not ask them again. If the card already holds a request that is still AWAITING_ and inside its expires_, and the new request is identical to it, Moon creates nothing, sends no email and emits no second PURCHASE_. It answers with the request already waiting, marked deduplicated: true; every other answer carries deduplicated: false. Over MCP, request_ answers status: approval_ with the same flag and the waiting request's answer_.
Identical means all of these are the same:
- the kind —
single,order, or arecurringapproval; - the merchant: the whole set of names (case, order and repeats do not matter), the MCC and
merchant_;domain - the amount in the card's currency;
- the terms: the tolerance, the window (
valid_, orfor_ days first_) andcharge_ within_ days monthly_, compared after defaults are applied — naming the default and naming nothing are the same request;limit - for a price given as
merchant_+amount merchant_, the store's price. Becausecurrency amountis the converted figure, the same store price converted at a different rate is a different request.
reason is not compared — it is prose, and the approver keeps reading the words they were emailed — and neither is answer_: the request that comes back keeps its own deadline. Anything else that differs is a new question and is asked. Once the waiting request has been answered or has expired, asking again asks again.
This is a safety net for an agent that repeats itself, not a replacement for idempotency: two identical requests sent at the same instant can still both be created. Send an Idempotency-Key (REST) or idempotency_ (MCP) for your own retries.
Duplicate approvals are cancelled once one is spent. If the person approved the same purchase more than once, the first authorization that spends one of those one-off approvals ends the rest: every other approval on the same card that is still APPROVED, single and not recurring, unexpired, for the same merchant (names, MCC and site), amount and currency is cancelled, and PURCHASE_ is emitted for each with cancelled_ and cancel_. The purchase has been paid for, so the spares can neither pay for it again nor linger. This happens just after the authorization is committed, never inside it; a spare that another authorization is spending at the same moment is either spent or cancelled, never both.
How the approver answers
Every approver gets the email. Depending on who they are, they can also answer somewhere else:
| Channel | Who can use it | approved_ |
|---|---|---|
| The one-time link in the email, and the page it opens | Every approver. No login; the link works once for the decision and stops at expires_ | email |
| The Moon app, signed in | The account, when approver_ is account | web |
| An assistant the approver connected themselves | A cardholder, through a connection made with their own cardholder token; the account, through an assistant its owner connected with OAuth and the cards:write permission | mcp |
The page shows the merchant, the amount, the card's last four digits, your reason and — when you sent one — the site. For an order or a recurring approval it also states, above the buttons, what else they are agreeing to: the total over the next N days, or future charges up to the monthly limit. After they approve, the same link shows what has been charged and what is left, with a Stop button.
When an assistant records the answer, it records what the person told it — an agent must never decide on anyone's behalf — and Moon emails the approver a receipt of the decision, so nobody has a purchase answered in their name without hearing about it. A partner API key can never answer: it is the credential that asks. See MCP Server.
approved_ is an open set. Branch on the values you know and fall through on the rest.
Waiting for the answer
| How | What |
|---|---|
| Webhook | PURCHASE_, PURCHASE_, PURCHASE_ or PURCHASE_ |
| Poll | GET /v2/api-gateway/purchase-intents/{id}, or get_ over MCP |
| List | GET /v2/api-gateway/purchase-intents, filtered by card_, status or recurring; list_ over MCP |
Never attempt the payment before the answer, and never retry a declined payment in a loop. A charge sent before the approval is simply declined INTENT_.
The five PURCHASE_ webhooks share one payload, described on Webhooks. They are emitted after the change has been committed, and they only ever concern cards you put in ask-first mode.
| Event | Sent when |
|---|---|
PURCHASE_ | The request was created and the approver has been emailed. Not emitted again for an identical request that was asked once |
PURCHASE_ | The approver said yes. This is the one to act on |
PURCHASE_ | The approver said no. Terminal |
PURCHASE_ | Nobody answered in time, or an approval's window closed with nothing charged. Emitted on the first read after expires_ |
PURCHASE_ | You withdrew or closed it (cancelled_), the approver stopped it (cancelled_), or Moon ended a duplicate approval after an identical one was spent (cancelled_, cancel_) |
Paying
Once the intent is APPROVED, charge the card as you normally would. The engine matches each authorization against the approvals on the card: the merchant by the same rules as a merchant lock, and the amount by the kind's rule. A zero-amount card verification passes on a merchant match alone and does not spend anything.
An agent working over MCP gets the card number for this one purchase with get_, which also returns exactly what the approval covers. See MCP Server.
A decline tells you what to do next: INTENT_ means ask, and INTENT_ means the person approved something else — a different merchant name on the statement, an amount outside the band, or a charge past an order's budget or a standing approval's monthly limit. Ask again for the real purchase; over MCP, list_ shows the descriptor the network actually sent.
Ending an intent
DELETE /v2/api-gateway/purchase-intents/{id} (or cancel_ over MCP) ends an intent in three cases: a request nobody has answered yet, an open order you want to close early, and a standing recurring approval that has taken its first charge. An approval the person gave for one purchase is theirs and cannot be taken back. The intent is kept and returned as CANCELLED, and PURCHASE_ is emitted. The person can stop an order or a standing approval from their link at any time. Moon itself cancels only duplicate one-off approvals, once one of them has been spent — see Identical requests are asked once.
Limits
| Limit | Value |
|---|---|
| Live intents per card | 20 (TOO_) |
| Standing approvals per card | 20 (TOO_) — cancel one to make room; they do not expire |
Answer window (answer_) | 5–1440 minutes, default 60 |
Spend window of a single approval | 15 minutes from the approval |
Order window (valid_) | 1–90 days, default 14 |
First-charge window of a recurring approval (first_) | 1–60 days, default 45 |
| Merchant names per intent | 5 |
reason | 1–200 characters |
| Credential hand-overs per approval over MCP | 10 |
Related
- Ask-first cards — issuing the card, every request field, matching rules and error codes.
- Webhooks — the
PURCHASE_payload.INTENT_ * - MCP Server — the same flow for an agent.
- Coded tool errors and next_step — what an agent receives when a call is refused.
Last updated September 26, 2026