Currency conversion API
Base paths through the gateway: /api/pay/v1 (Payment), /api/cart/v1 (Cart), /api/order/v1 (Order).
A bearer is needed except for the policy read. Amounts are minor units of the currency named beside them,
at its ISO 4217 exponent (JPY has none, KWD has three).
The cart and order routes answer the buyer: the cart’s owner, or the order’s buyer, acting as that profile
(platform operators aside). Anyone else gets 403 (Cart_Shopping_VL_Authorization_RJ for a cart), and a cart is
created only for the profile the caller acts as.
Vocabulary: the settlement currency is the seller’s — the cart / order currency the seller is paid in; the presentment currency is what the buyer’s card is charged in. A quote converts a settlement amount into a presentment amount at a rate the payment provider honours for a fixed window, plus the platform’s currency-conversion add-on (default 3 %) which the platform keeps. The seller always receives the full settlement amount.
GET /pay/v1/fx-policy (anonymous)
Section titled “GET /pay/v1/fx-policy (anonymous)”Whether the option is offered and the add-on to disclose.
{ "data": { "isEnabled": true, "markupBps": 300, "defaultLockDuration": "hour" } }PUT /cart/v1/carts/{cartId}/currency
Section titled “PUT /cart/v1/carts/{cartId}/currency”Body { "currencyCode": "CAD" }. Records the buyer’s presentment currency on the cart and locks a rate for the
cart’s estimate total. Answer: the cart detail with an fxLock block:
| Field | Meaning |
|---|---|
presentmentCurrencyCode | The buyer’s currency |
fxQuoteId | The Payment BC quote backing the cart (null while none — e.g. an empty cart) |
lockedRate | Buyer-facing rate, presentment per one settlement unit, add-on included (1 USD = 1.41 CAD) |
markupBps | The platform add-on, basis points |
settlementTotalEstimateMinor / markupEstimateMinor / presentmentTotalEstimateMinor | The estimate total, the add-on on it, and what the buyer’s card would be charged |
expiresAtUtc, lockDuration | When the lock lapses; none / five_minutes / hour / day |
isUsable | False once the lock lapsed (2-minute grace) or the total outgrew the quote — lock again |
Passing the cart’s own currency (or null) clears the preference and the lock. A usable lock for the same
currency is kept (each provider lock carries a premium). 503 Cart_Currency_UP_Lock_FL = no rate could be
locked (the provider does not quote the pair, or the option is switched off); the cart is unchanged and the
buyer can still pay in the seller’s currency.
Checkout (POST /cart/v1/carts/{id}/checkout) re-locks a lapsed quote for the refreshed total and carries
the lock onto the order (CartCheckedOut.v1 → fxLock).
PUT /order/v1/orders/{orderId}/currency
Section titled “PUT /order/v1/orders/{orderId}/currency”Same body and answer shape (OrderDetailDto.fxLock, amounts for the order’s outstanding total —
settlementAmountMinor / markupMinor / presentmentAmountMinor). The order is the invoice the buyer pays:
this is where a lapsed lock is refreshed, or a currency chosen for an order that was placed without one
(an appointment invoice, for instance). Only while the order awaits payment
(409 Order_OrderCurrency_UP_NotPayable_RJ otherwise); 503 Order_OrderCurrency_UP_Lock_FL = no rate.
GET /order/v1/orders/{id} and …/by-source-cart/{cartId} return the same fxLock block.
Paying under the lock: POST /pay/v1/checkout
Section titled “Paying under the lock: POST /pay/v1/checkout”Send the order’s settlement amount and currency as usual, plus fxQuoteId from the order’s fxLock:
{ "accountId": "…", "subjectProfileId": "…", "orderId": "…", "amountInBaseMinor": 10000, "baseCurrencyCode": "USD", "fxQuoteId": "7f1e5a0c-…", "idempotencyKey": "order-pay-…"}The Payment BC opens the provider intent for presentmentAmountMinor in the presentment currency with the
provider’s quote attached, so the rate is honoured; the ledger and the order keep the seller’s amount. The
intent’s originalAmount / originalCurrencyCode are the presentment figures. Refusals, all 409:
Payment_FxQuote_UP_Repriced_RJ (see below),
Payment_FxQuote_VL_Mismatch_RJ (the quote is for another currency or account),
Payment_FxQuote_VL_Cap_RJ (the amount outgrew the quote — refresh),
Payment_FxQuote_UP_Expired_RJ (only when a fresh quote could not be taken either).
The quote you send is an estimate (2026-09-22)
Section titled “The quote you send is an estimate (2026-09-22)”A quote that is not an active honoured lock — it lapsed, it was indicative (lockDuration: "none"),
or the provider refuses it at charge time — does not fail the checkout. The Payment BC takes a fresh
quote right before opening the provider intent and compares its presentment amount with the estimate
your quote implied:
| Drift from the estimate | What happens |
|---|---|
| ≤ the platform’s refresh tolerance (admin-set, default 1 %) | The intent opens on the fresh quote. The answer carries an fx block — fxQuoteId, presentmentAmountMinor, refreshedFlag: true, refreshedFromQuoteId, estimatedPresentmentAmountMinor, driftBps — so you can show the buyer the amount actually charged. |
| > the tolerance | 409 Payment_FxQuote_UP_Repriced_RJ. Nothing is charged. Re-lock (PUT …/currency) to read the new amount and let the buyer confirm before opening the checkout again. |
GET /pay/v1/fx-policy (anonymous) returns refreshToleranceBps alongside markupBps and
defaultLockDuration, so a storefront can tell the buyer up front how far the rate may move without
a second confirmation.
The PaymentIntentSucceeded.v1 event carries the quote the charge actually rode — fxQuoteId,
fxLockedRate, fxPspExchangeRate, fxMarkupBps, fxLockDuration plus fxRefreshedFromQuoteId
and fxEstimatedPresentmentMinor — and the Order BC re-aligns the invoice’s fxLock with it.
Reading a quote directly
Section titled “Reading a quote directly”POST /pay/v1/fx-quotes (settlementCurrencyCode, presentmentCurrencyCode, settlementAmountMinor,
optional lockDuration, subjectKind CART | ORDER | BOOKING | INVOICE | OTHER, subjectId) answers an FxQuoteDto
with both rate directions (providerRate / providerExchangeRate), the add-on and the amounts;
GET /pay/v1/fx-quotes/{id}?amount= re-prices it; GET /pay/v1/fx-quotes/{id}/convert?amount= returns just the
buyer-facing amount. 503 Payment_FxQuote_AD_Unavailable_FL = the provider cannot quote the pair right now.
Rounding and refunds
Section titled “Rounding and refunds”markup = ceil(settlement × bps / 10000); presentment = ceil((settlement + markup) / providerExchangeRate) — both
rounded up in minor units so the seller never nets less than the price. Refunds and disputes are converted back
by the provider at the current rate, not the locked one; neither the provider’s FX fee nor the add-on is returned.