WritingCommerce

Lock the exchange rate at quote, at checkout or at charge

Pick when a cross-border order's exchange rate is fixed, size the margin to the gap before you buy the goods, and store the lock so every capture can prove it.

A customer in Dhaka adds a USD 120.00 pair of headphones from a US marketplace to their cart at 09:10 and sees BDT 14,400 at an example rate of 120.00. They pay at 09:25 on Monday. Your procurement team buys the item on Thursday, when the rate is 121.50, so the goods cost you BDT 14,580 and the order is BDT 180 under water before freight is priced. Somewhere between the cart, the payment and the purchase, one moment has to be the one where the rate stops moving for that order.

Anyone running cross-border procurement, or any checkout that charges in one currency and buys in another, meets this choice the first time a monthly FX report shows a loss, and usually settles it by accident. My rule is that the rate is locked at checkout, with a margin sized to the gap between the customer's authorisation and the moment the foreign-currency cost is incurred; locking at quote is only for a short window you have priced, and passing the rate through at charge is only for customers who agreed to pay an estimate. I have built cross-border procurement from Amazon, Alibaba, AliExpress and 1688 with Stripe and SSLCommerz as gateways, and the gap between paying and buying, not the rate itself, is what the design has to manage.

What decides it

The three options differ in who carries the movement between the rate the customer saw and the rate you paid, and for how long. What the cart shows and how the capture works both follow from that one allocation.

CriterionLock at quoteLock at checkoutLock at charge
Rate the customer paysThe cart rate, until expires_atThe rate fetched when they authoriseThe rate on the day the goods are bought
Who carries movement before purchaseYou, from the first page viewYou, from authorisationThe customer
What the cart showsA fixed local price and how long it holdsAn indicative price, final at paymentAn estimate and a ceiling
Margin on the rateQuote window plus the purchase gapThe purchase gap onlyNone on FX; a buffer on the authorised ceiling
Payment flowAuthorise and capture the quoted amountAuthorise and capture the checkout amountAuthorise the ceiling, capture the cost, refund the rest
Breaks whenA quote is honoured after expires_atProcurement slips past the margin's horizonThe real cost exceeds the authorised ceiling

The margin is measured, not guessed. Take the days between authorisation and purchase across your recent orders, find the largest move the pair made over that many days in the same period, and add it to the rate in basis points. Recompute it monthly and store the margin_bps you used on every lock.

TypeScript
// simplified: a rate lock written at checkout and settled at purchase
type RateLock = {
  orderId: string;
  rateId: string;        // the rate row you fetched, kept forever
  rate: number;          // BDT per USD, as published
  marginBps: number;     // the margin applied on this order
  foreignMinor: number;  // 12000 for USD 120.00
  localMinor: number;    // what the customer authorised
  lockedAt: Date;
  honourUntil: Date;     // lockedAt plus the margin horizon
};

function localAmount(lock: RateLock): number {
  const factor = lock.rate * (1 + lock.marginBps / 10000);
  return Math.round(lock.foreignMinor * factor);
}

function settle(lock: RateLock, costLocalMinor: number, boughtAt: Date) {
  const fxLineMinor = lock.localMinor - costLocalMinor;
  const late = boughtAt > lock.honourUntil;
  // the customer's amount never changes; the difference is yours to book
  return { captureMinor: lock.localMinor, fxLineMinor, late };
}

honourUntil is the field most teams leave out. It is the date after which the margin no longer covers the order, and the purchase job has to read it before it buys.

Where a checkout lock fails

Take the Monday order again, locked at checkout at 120.00 with a 150 basis point margin, so the customer authorised BDT 14,616 and honourUntil is Wednesday. On Tuesday the listing goes out of stock and procurement re-sources the item from a second seller. The purchase happens on Friday at 123.00, costing BDT 14,760. The system saw a successful purchase and an fxLineMinor of minus 144; the customer saw a parcel on time and nothing else, which is what they paid for.

What contains it is the purchase job refusing to stay silent after honourUntil. On Friday it computes the projected loss before it buys, compares it with the order's max_fx_loss_minor (say 2% of the local amount), and either buys and books the loss, or parks the order in a reprice state where an operator offers the customer the new price or a full refund. Without that check the loss surfaces in the monthly report, after hundreds of orders have crossed the same horizon.

Lock at quote

Best for Short priced windows on your own inventory

The number in the cart is the number on the receipt, for as long as the quote row says it holds.

Strengths

  • The customer never sees a price move between browsing and paying.
  • One amount is authorised, captured and reconciled, with no estimate or refund leg.

Costs

  • You carry the rate from the first page view, so the window must be short, priced into the margin, and expired by the database on expires_at, never by a timer in the browser.
  • A cart revisited after expires_at needs a fresh quote and a visible price change, and the checkout must refuse the old quote id rather than extend it.

Lock at checkout

Best for Most orders where you take the FX risk

The rate fetched at authorisation becomes the order's rate, and the cart says so.

Strengths

  • The margin only has to cover the purchase gap, which you can measure from your own orders.
  • The amount is fixed at the moment the customer consents to it, so no later capture or refund needs explaining.

Costs

  • The cart price can move by a few units before payment, and the checkout has to show the final amount before the authorise call, not after.
  • Every order needs a honourUntil and a purchase job that reads it; a stalled purchase otherwise ships at an unbounded loss.

Lock at charge

Best for Agreed estimates with a known ceiling

The customer is told the price is an estimate, you authorise a ceiling, and you capture what the goods cost on the day.

Strengths

  • No FX margin at all, because no rate movement lands on you.
  • Orders with long or uncertain purchase gaps, such as sourcing from several sellers, stay viable.

Costs

  • A capture above the authorised ceiling is refused, so a move larger than the buffer forces a new authorisation and a conversation with the customer before the goods can be bought.
  • The refund of the unused ceiling is a second leg to reconcile on every order, and a hold larger than the estimate draws support tickets.
flowchart TD
  S[New cross border order] --> Q1{Was the price an estimate}
  Q1 -- yes --> C[Lock at charge with a ceiling]
  Q1 -- no --> Q2{Is the gap under one day}
  Q2 -- yes --> Q3{Must the cart price hold}
  Q3 -- yes --> A[Lock at quote with expires at]
  Q3 -- no --> B[Lock at checkout]:::accent
  Q2 -- no --> Q4{Does the margin cover the gap}
  Q4 -- yes --> B
  Q4 -- no --> D[Shorten gap or sell estimate]
Which lock fits an order, decided from what the customer was told and how long you take to buy

Whichever option wins, build the same three things first: a rates table that keeps every rate you ever fetched with its id and fetched_at, a rate lock row on the order carrying rate_id, margin_bps, locked_at, honour_until and the local amount, and an FX line on the order holding the difference between the locked amount and the realised cost. Without that line you cannot tell which option you are running, because the loss has nowhere to appear until the month closes.

Written by Md Nasim Anjum, senior full-stack engineer in Manchester. He builds payment orchestration, KYC and KYB compliance platforms and conversational AI.

Get in touchAll writingRSS

More writing