Quote in Their Currency, Bank in Yours
Three currencies, one booking, and only one field
Here’s a situation that sounds like an edge case and is actually a Tuesday.
A guest in Europe asks for a price. You quote EUR 600 — that’s the number in the email, the number they said yes to, the number they consider the deal. Your business accounts in US dollars, so your books need a dollar figure. Your card processor, for reasons of its own, will settle in dollars or the local currency and flatly refuses euros.
Three currencies, all legitimate, all necessary. Most booking software offers you one field called “currency” and expects you to sort out the rest.
What ours did was worse than offering one field. The manual booking form sent the currency and the amount — EUR, 600 — and the code that saved it kept the number and threw the currency away. A EUR 600 stay was stored as USD 600. No error, no warning. Just a booking quietly worth about a hundred dollars less than it should have been, in a system where everything downstream trusted that figure.
Two pairs, not one field
The fix is conceptually simple and has to be applied ruthlessly, because the temptation to fudge it is everywhere.
A booking now carries two amounts.
The agreed pair is the price the guest said yes to, in the currency they said it in. EUR 600. This is the promise, and it doesn’t move.
The ledger pair is the same booking expressed in your accounting currency. USD, whatever the conversion came to. This is what your books, your stats and your reports read.
Between them sits a locked conversion — the rate, the date, the source, the original agreed total and the moment it was locked. Not a rate looked up fresh each time anyone asks, which would mean the booking was worth something different every morning. One rate, recorded, reusable, stable when you re-save the booking.
And there is a third conversion, which is the one that catches people. The rate used when the booking reaches your accounting software isn’t the same rate as the one used to charge the guest. Accounting has its own currency and its own moment of truth. Three conversions is not over-engineering — it’s the actual number of conversions in the transaction. Software that offers you fewer is hiding one from you.
If you want the ground-floor version of why this gets so tangled, we wrote it up in Multi-Currency Is Breaking Your Spreadsheet and Multi-Currency, Made Simple.
What the guest sees, and what gets charged
The decision we landed on: charge in the currency that actually works, show the guest the currency they agreed in, and show them the conversion.
So the card is charged in your ledger currency, because that’s what your processor will accept. But the guest’s portal shows the agreed price, the converted amount, and the rate between them — and the price they agreed travels with the payment as a field on the checkout, so it appears on their receipt rather than only on your side.
Nobody is surprised at the card machine. That’s the entire objective.
When the rate gets fixed
There’s a genuinely difficult question underneath all this: a booking agreed in March and paid in September spans months of currency movement. Which rate is real?
The answer we chose, having rejected the alternative:
The rate is taken at payment, and trued up when the payment confirms. When a guest pays, Airflow converts what’s still owed in the agreed currency at that day’s rate. When the payment confirms, it records what was actually realised and adjusts the ledger total to match — so your books show the money that genuinely arrived rather than an estimate made months earlier.
The rate locked when the booking was created is the initial estimate, and nothing more.
The rejected alternative was re-rating every unpaid booking daily. It sounds more accurate and is much worse: it rewrites every foreign-currency booking in your account every morning, pokes your accounting sync every time, and means no figure on your dashboard is the same two days running.
The load-bearing consequence, which took us a while to state clearly: the balance is tracked in the agreed currency. The guest owes EUR 600 minus what they’ve paid, expressed in euros. Asking “what’s still owed” in ledger currency is asking a question with a moving answer.
The near-miss, since we’re being honest
The day this shipped, it nearly created a debt that didn’t exist.
A guest had paid USD 689.17 under the previous deployment — before the true-up mechanism existed. When the new code looked for the record of realised payments, it found nothing, concluded nothing had been paid, and asked that guest for the full amount again.
A post-deployment check caught it within minutes and it was corrected by hand. Exactly one booking in the entire system was in the agreed-currency state at the time, so the blast radius was one guest for a few minutes.
The lesson is general enough to be worth stating: when you introduce a new source of truth for an amount owed, backfill every record that already has payments before you deploy — not after. Otherwise the switch itself manufactures a phantom debt. We got away with it because of timing rather than because of care, and the check that caught it existed because the deployment was watched rather than assumed.
The related thing: getting paid at all
Two smaller fixes in the same area, both of which were quietly costing money.
A card payment needs a guest email address. Our processor rejects an empty one, and the function returned a generic “payment initialisation failed” that told nobody anything. Twenty upcoming bookings had no email address on file. Now the guest is asked for their address inline, it saves, and the payment retries — and the processor’s own error message is passed through rather than swallowed, so the next failure explains itself.
Payment schedules follow the total. A schedule is built from the booking total when the booking is created and used not to move when the total changed. If a currency correction changed the total, the schedule still referred to the old one. An all-unpaid schedule now rescales proportionally, and rows already paid are left strictly alone.
Where the commission fits
If you take card payments through the connected-account rail, commission is applied automatically as a fee on the charge itself — 6% on a booking from your own site, 12% on one that came through the marketplace, because discovery is worth something and your own audience isn’t ours to charge for. There is now exactly one place in the code that decides which key, which account and which rate apply, which is the only reason we trust the answer.
Per-account test mode also shipped alongside this. A test booking behaves exactly like a real one — it blocks dates, sends emails, raises an accounting draft, the lot — because the point is confidence that the whole circle closes, not a sanitised rehearsal. You delete the test booking and its draft invoice yourself. The single hard exclusion: a test booking never charges your real card for commission.
That whole path passed end to end on 21 September: two test bookings, both confirmed by the payment provider’s webhook rather than by anyone clicking anything, both raising accounting drafts, at the correct 12% and 6%.
And the honest caveat: that was the test environment’s webhook. The live one has not yet confirmed a real foreign-currency booking unattended. It is designed, built and proven in test — it is not yet proven in anger. We’ll say so when it is.
If Xero is your ledger, the practical companion to this is Multi-Currency Airbnb Income in Xero: Which Rate Goes Where — which rate belongs on the guest invoice, the payment and the Xero entry, and where the FX difference should land.
Related: Multi-Currency, Made Simple, Get Paid in Kenya: M-Pesa Bookings, and See the Number Before You Charge It. Or look at what Airflow does with your money.