Rate Locks
A rate lock is one of the two ways to price Adaptive Currency Conversion — the alternative to the default per-session rate minted at checkout. It lets you fetch an FX rate once and reuse it across many transactions, instead of a fresh quote for every session. It’s built for merchants who price in one currency (for example USD) and collect in another (for example KES): lock the rate, price your orders against it, and settle every transaction at the locked rate.
A rate lock is a held rate — not a quote. A quote is a firm price for a single trade that you accept within seconds. A lock is a rate frozen for hours and reusable across many transactions. It ends at its expiresAt — or occasionally earlier if the market moves sharply — and you’re notified by webhook either way.
The lifecycle is create a lock → reuse it across checkouts → it expires or is cancelled → fetch a new one.
Create a lock
Create a rate lock for a currency pair. You supply only the pair — the base currency you price in and the quote currency you collect in:
We return the lock with its frozen rate and expiry:
No amount is involved at this stage — a lock is a rate, not a trade. The rate, markup, and expiry are fixed when the lock is created and stay constant for its lifetime.
How a lock is priced
A lock is priced as a mid-market reference rate plus a markup, and you see the whole breakdown:
The markup is configured on your account per currency pair, so you can choose how much of the spread to pass on to your payers. Talk to us if you want it adjusted.
Reuse across checkout sessions
Pass the rateLockId when you create a checkout session. You price the session in the lock’s base currency, and the payer is charged the equivalent in the quote currency at the lock’s allInRate. The session currency must match the lock’s base currency, and the session’s collection currency (defaulted from the payer’s location if you don’t set it) must match the lock’s quote currency.
The lock is verified twice: when you attach it at session creation, and again when a payment is initiated. If it is still valid, the payment proceeds at the locked rate. If it is unknown, expired, or cancelled — or its currencies don’t match the session — the call is rejected with 422 and you fetch a new lock. A transaction that has already executed is always honoured at its locked rate — cancellation only blocks new use.
Validity and expiry
Every lock has a validity window, returned as expiresAt — valid for hours rather than the seconds of a single quote. A lock at or past its expiresAt is no longer usable, even if you haven’t yet fetched it to see the change — always check expiresAt, or read the lock with Get a rate lock. When the window elapses we also notify you with a rate_lock.expired webhook, so you don’t need to rely on polling. Once a lock expires, fetch a new one.
Rolling to a new lock before expiry
You can hold up to two active locks per currency pair at once, so you can fetch the next lock before the current one expires and avoid a price cliff for shoppers already browsing. Fetch the replacement shortly before expiry (about an hour ahead is typical): keep pricing existing browsing sessions off the current lock until it expires, and send new browsing sessions to the new lock. Each checkout session keeps the lock it was priced against — read it back with Get a checkout session as fxCommit.quoteId, with fxCommit.provider set to rate_lock — and any transaction already in progress is honoured at its locked rate regardless of which lock expires.
If you create a lock while two are already active for the pair, we return the freshest of the two (200) rather than minting a third.
Drift cancellation
A lock can occasionally end before its expiresAt. If the market rate moves sharply beyond the lock’s tolerance — in either direction — the lock is cancelled early, and we notify you with a rate_lock.cancelled webhook. (Bounding drift this way is what makes an hours-long locked window possible in the first place.)
Treat the webhook as your signal to re-lock: it carries no new rate, so call Create a rate lock and price new sessions off the fresh lock. Shoppers mid-payment are unaffected — a transaction already in progress settles at its locked rate; cancellation only blocks new use.
A cancellation is permanent. A lock cancelled by drift stays cancelled even if the market moves back.
Cancel a lock
You can also cancel an active lock yourself before it expires — for example, if you re-price your catalogue and no longer need it. Cancel a rate lock with its rateLockId:
The lock moves to cancelled (with cancellationReason: requested) and can no longer be used for new checkouts, but stays readable. As with drift, cancellation is permanent and does not affect any transaction already in progress, which is always honoured at its locked rate. The call is idempotent: cancelling a lock that has already expired or been cancelled returns its current state.
No webhook is sent for a cancellation you request yourself — rate_lock.cancelled fires only when we cancel a lock due to market drift.
Reconciliation
Every transaction attempt carries an immutable snapshot of the rate it was charged at — the same FX snapshot fields as any converted transaction: collection ({ amount, currency } the payer was debited), fxMidRate, fxAllInRate, fxMarkupBps, and fxFee. Read them back per transaction with Retrieve Payment Status.
Because a lock’s rate is frozen, every transaction priced by the same lock reconciles at the same fxAllInRate — unlike ACC’s default per-session rate, where each session gets its own quote. To group transactions by lock, match the session’s fxCommit.quoteId to the rlk_ id you created.
Locks vs quotes
Use a rate lock when you price in one currency and collect in another and want one rate to hold across many checkouts. Use a quote when you want to execute a single one-off trade at a firm rate. Merchants who don’t bring a lock to checkout fall back to ACC’s default per-session rate automatically.