When you sell in one currency and pay in another: protecting margin from FX drift
Rates jump, the rouble wobbles, the euro moves on its own logic. If you lock a price for a customer today and pay the supplier 90 days later — who carries the FX risk?
Tourism is a business with a long gap between "sold" and "paid out". The customer pays today, the hotel gets paid 30 days before check-in, the guide is paid after the event, the restaurant is invoiced post-fact. In between, FX rates can drift 5%, 10%, on a bad day 15%.
If you've got different currencies on different sides of the deal, your margin evaporates without your participation.
Where the problem actually hides
The most common case: you sell tours in EUR (or USD), but pay most costs in the local currency — RUB, KGS, GEL, KZT. Simple example.
In April a customer buys a tour for €2,500. Planned margin 25% — €1,875 goes to suppliers, €625 stays with you. At sale time that looks like, say, 167,500 RUB in supplier payouts at a 67 RUB/EUR rate.
In July, when you actually pay the same suppliers, the rate is 75. The same €1,875 now costs 140,625 RUB. Looks like a windfall.
But the other side of the same coin. Suppose your customer pays partially in RUB at the spot rate, while your invoice was in EUR. If the rate goes the other way — you earn €420 instead of €625. Margin moved 30% and you didn't move a finger.
This isn't theory. It comes up every season in operator finance circles.
What "locking the rate" means
In banking, an FX lock is a forward contract: the bank guarantees a future date's rate. In tour-operator life it's simpler: you record the rate this specific booking is calculated at, and you don't recalculate that booking ever, no matter what happens to the spot market.
This works both ways. If the rate moves in your favour, you earn more than plan. If it moves against you, you see exactly how much margin you're losing in advance, and decide what to do: renegotiate with the supplier, push the dates, raise the price for future customers.
The key is that you don't conflate two things: one booking under the old rate, another booking under the new rate. Reprocessing all of history "at today's rate" is hell in Excel and an absolute lie in the management report.
Where to source rates
This is important — don't make up an "average market" rate. You need an external source you can point to in customer contracts and management reports.
For the Russian market, the standard is the Central Bank (CBR). The rate is taken on the payment date or the lock date, and that line lives in the audit log: "on this date the rate was that, because the CBR said so."
For the eurozone the equivalent is the ECB. For the US, the Federal Reserve. For the UK, the Bank of England. Your business case is yours, the rule is the same: the rate has to be verifiable from a third source.
Risky alternative — sourcing the rate from the supplier's site. Technically convenient because the supplier will calculate the same way, but legally fragile: if the supplier updates their rate retroactively, you also retroactively get different numbers in your books. Not somewhere you want to be.
What needs to live in the system
A minimum useful implementation looks like this.
Rate table with date and source. Every day a fresh snapshot lands from CBR and ECB. No "averages" — point-in-time rates with the source named in a column.
A lock on the booking or quote. When you issue a booking (or an official quote), the system records: this deal works with locked rate X, source Y, date Z. That lock never changes.
A separate lock for quotes. Between "sent quote to customer" and "customer agreed", several days pass. The rate can drift in those days. If you sent a EUR price and the customer pays at the spot rate a week later, you have to decide upfront: lock at quote time (we carry the risk) or recalculate at payment time (the customer carries it). Without an explicit decision — both carry it and both are unhappy.
Audit log on every recalculation. If somebody does recalculate an old booking, the log should record: who, when, what reason. Not bureaucracy — so that six months later in a customer dispute or a tax audit, you can explain why the invoice has the number it has.
How it's wired in INITE Travel
In Phase 8 we shipped FxRate (daily pull from CBR and ECB via cron) and FxLock (frozen on quote OR booking, never both). Conversion in lib/fx.ts has three outcomes: forward, inverse via the reciprocal pair, and null when no bridge exists between currencies — null is more honest than a fabricated number.
And anything payment-related goes through our inite-billing-service. Stripe, Tinkoff, YooKassa, SBP — the choice is on the billing side, not on the app side. We don't tie the integration to any specific rail provider, because in 2026 a Russian tour operator's payment-rail menu can change three times in a year.
If you're rolling your own stack — the simplest thing you can do today: stand up a daily_fx_rate table with two sources and a booking_fx_lock table that pins one currency to another. From there you can talk about margin analytics without end-of-season surprises.