Browse the docs

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 isapprover_type
a cardholderthat cardholder, at their own email addresscardholder
no cardholderthe account that owns the card, at the account's email addressaccount
your organization's own cardholder — the one a card issued without naming a cardholder is placed onthe account holder, at the address on that cardholder, which is the account'saccount

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_EMAIL_MISSING, because nobody can be asked.

Asking

OverCall
RESTPOST /v2/api-gateway/purchase-intents with an Idempotency-Key header
MCPrequest_purchase with an idempotency_key

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_APPROVAL, 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_domain 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:

SendWhen
amountYou know the price in the card's own currency, in major units
merchant_amount + merchant_currencyThe store shows the price in another currency — for example R$ 29.90 on a Brazilian checkout: "merchant_amount": 29.90, "merchant_currency": "BRL". Send it exactly as the page shows it and do not convert it yourself
  • Exactly one form is accepted. Both together, neither, or merchant_amount without merchant_currency (or the other way round) is a validation error.
  • merchant_currency is an uppercase ISO 4217 code, one of USD, MXN, EUR, CAD, GBP, JPY, AUD and BRL. Anything else is a validation error: send amount in 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_currency and request_fx_rate on every read and on the PURCHASE_INTENT_* webhooks. All three are null on an intent whose price was given as amount.
  • 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 tolerance of your own, a price given this way gets 10 percent, on single and order alike: the card network converts the real charge at its own rate, some time later. A tolerance you name wins. tolerance.absolute is still in the card's currency and may not exceed the converted amount, and monthly_limit may not be below it.
  • When Moon has no rate for the currency pair at that moment, the request is refused with 422 MERCHANT_CURRENCY_UNSUPPORTED (merchant_currency_unsupported over MCP) and nothing is written or emailed. Ask again with amount in the card's currency.

Which kind to ask for

Ask forWhat the person agrees toRight for
kind: "single" (default)One merchant, one amount inside a symmetric tolerance band, exactly one authorizationPay-now digital goods: a subscription renewal, a top-up, an API bill
kind: "order"A budget at one merchant: up to max_total in total, over any number of charges, for valid_for_days days (1–90, default 14) from the moment they approvePhysical goods, marketplaces, travel — anything that ships, splits, tips or verifies the card first
recurring: trueA standing permission at one merchant, bounded by monthly_limit per UTC calendar month, with no expiry until it is stoppedSubscriptions 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_for_days 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.

KindHow the tolerance is usedDefault
singleA band around amount, in both directions — a charge below it is outside the band just as one above it isOne minor unit
orderAdded to amount to make the budget, max_total. Any number of charges fit while their total stays inside it10 percent
recurringNot what bounds it: monthly_limit is—

When the price is given as merchant_amount + merchant_currency, 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_within_minutes 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_at is that deadline. The email states it in words, for example "Answer by 14:45 UTC (in 60 minutes)", and over MCP request_purchase returns the same sentence as answer_by. 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_at is rewritten, in the same write that records the decision, to how long the approval may be spent:

KindHow long the approval can be spent
single15 minutes from the approval, for exactly one purchase — however much of the answer window the person used. Pay promptly once it is approved
ordervalid_for_days days (1–90, default 14) from the moment of approval
recurringfirst_charge_within_days 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_until: the same instant as expires_at, 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_at 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_at.

An intent nobody answered in time reads EXPIRED on the next read. There is no sweep job: status is evaluated against expires_at whenever the intent is read, and the PURCHASE_INTENT_EXPIRED webhook is emitted at that point — so treat expires_at 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_APPROVAL and inside its expires_at, and the new request is identical to it, Moon creates nothing, sends no email and emits no second PURCHASE_INTENT_REQUESTED. It answers with the request already waiting, marked deduplicated: true; every other answer carries deduplicated: false. Over MCP, request_purchase answers status: approval_pending with the same flag and the waiting request's answer_by.

Identical means all of these are the same:

  • the kind — single, order, or a recurring approval;
  • 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_for_days, or first_charge_within_days) and monthly_limit, compared after defaults are applied — naming the default and naming nothing are the same request;
  • for a price given as merchant_amount + merchant_currency, the store's price. Because amount is 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_within_minutes: 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_key (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_INTENT_CANCELLED is emitted for each with cancelled_by: partner and cancel_reason: superseded_by_consumed_sibling. 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:

ChannelWho can use itapproved_via
The one-time link in the email, and the page it opensEvery approver. No login; the link works once for the decision and stops at expires_atemail
The Moon app, signed inThe account, when approver_type is accountweb
An assistant the approver connected themselvesA cardholder, through a connection made with their own cardholder token; the account, through an assistant its owner connected with OAuth and the cards:write permissionmcp

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_via is an open set. Branch on the values you know and fall through on the rest.

Waiting for the answer

HowWhat
WebhookPURCHASE_INTENT_APPROVED, PURCHASE_INTENT_DENIED, PURCHASE_INTENT_EXPIRED or PURCHASE_INTENT_CANCELLED
PollGET /v2/api-gateway/purchase-intents/{id}, or get_purchase_intent over MCP
ListGET /v2/api-gateway/purchase-intents, filtered by card_id, status or recurring; list_purchase_intents 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_REQUIRED.

The five PURCHASE_INTENT_* 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.

EventSent when
PURCHASE_INTENT_REQUESTEDThe request was created and the approver has been emailed. Not emitted again for an identical request that was asked once
PURCHASE_INTENT_APPROVEDThe approver said yes. This is the one to act on
PURCHASE_INTENT_DENIEDThe approver said no. Terminal
PURCHASE_INTENT_EXPIREDNobody answered in time, or an approval's window closed with nothing charged. Emitted on the first read after expires_at
PURCHASE_INTENT_CANCELLEDYou withdrew or closed it (cancelled_by: partner), the approver stopped it (cancelled_by: approver), or Moon ended a duplicate approval after an identical one was spent (cancelled_by: partner, cancel_reason: superseded_by_consumed_sibling)

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_payment_credential({ intent_id }), which also returns exactly what the approval covers. See MCP Server.

A decline tells you what to do next: INTENT_REQUIRED means ask, and INTENT_MISMATCH 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_card_declines shows the descriptor the network actually sent.

Ending an intent

DELETE /v2/api-gateway/purchase-intents/{id} (or cancel_purchase_intent 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_INTENT_CANCELLED 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

LimitValue
Live intents per card20 (TOO_MANY_PENDING_INTENTS)
Standing approvals per card20 (TOO_MANY_RECURRING_APPROVALS) — cancel one to make room; they do not expire
Answer window (answer_within_minutes)5–1440 minutes, default 60
Spend window of a single approval15 minutes from the approval
Order window (valid_for_days)1–90 days, default 14
First-charge window of a recurring approval (first_charge_within_days)1–60 days, default 45
Merchant names per intent5
reason1–200 characters
Credential hand-overs per approval over MCP10

Last updated September 26, 2026