Travel payments: keep bookings, refunds and settlement connected
A successful payment does not mean every part of a trip is confirmed or settled. Track customer charges, supplier payments and refunds against the booking they belong to.
A paid: true field cannot describe the financial state of a travel booking. It does not tell you whether the hotel confirmed, whether the supplier has been paid or whether a refund reached the traveller.
Those events can happen at different times. A payment can succeed while a booking fails. A supplier can approve a refund before returning the funds. Finance needs to see each step rather than infer it from one status.
The model needs a shared reference for each booking component and every related movement of money. The travel order lifecycle provides that structure; this note follows collection, settlement, refunds and reconciliation through it.
Track booking, payment, settlement and refunds separately
Split the boolean into the questions it was hiding:
- Collection: did we successfully collect or authorise customer funds?
- Order: which travel components are confirmed, pending or failed?
- Settlement: which supplier obligation is due, through which rail and currency?
- Accounting: how do all movements map to revenue, cost, liability and refund positions?
These influence each other. They are not interchangeable. A successful card charge does not prove a supplier confirmed. A confirmed booking does not prove the supplier has been paid. A supplier-approved refund does not mean the customer sees the money yet.
Link every payment to its booking component
Identifier lineage is the design decision that everything else rests on. Every financial event should trace back through one hierarchy:
trip → order → component → supplier obligation → payment movement
Customer collections, virtual-card authorisations, bank transfers, fees, FX adjustments, refunds and chargebacks all carry enough reference data to rejoin that chain.
Do not key on the supplier's reference or a free-text descriptor. Supplier references arrive late, change format between releases and sometimes cover several components at once. Your own immutable identifiers anchor the system; external references are indexed attributes, useful for lookup, never load-bearing.
OnArrival's Payments product is organised around a travel ledger that follows money from offer through booking, segment and ancillary. Build your own if you prefer. The principle does not change: money inherits the structure of the trip, or finance reverse-engineers it every month.
A practical state model
Use explicit state machines. One per question.
| Object | Example states | Critical question |
|---|---|---|
| Customer payment | created, authorized, captured, failed, reversed, refunded | What did the customer’s payment method do? |
| Booking component | pending, confirmed, changed, cancelled, failed | What travel service currently exists? |
| Supplier payable | not due, scheduled, issued, accepted, failed, netted | What do we owe the supplier? |
| Refund | requested, supplier-approved, funded, submitted, completed, failed | Where is the return of funds now? |
| Ledger entry | pending, posted, reversed | Has finance recorded the economic event? |
Transitions are event-driven, timestamped and idempotent. Store why a transition happened and which command or external message caused it. A state machine that cannot explain its own last hop is a status field with extra steps.
Split supplier settlement under one customer payment
Take a bundle: flight, hotel, activity. The customer sees one charge. The supplier side needs an airline settlement path, a hotel payment on a different rail and a third schedule for the activity operator. Margin belongs partly to each component. A cancellation touches one branch.
Collect it as one authorisation for the total, then capture per component as each supplier confirms. That is the single-auth, staged-capture pattern the travel cart essay derives from the commit saga. Here is why it matters for money rather than for inventory: each capture is born knowing which component it pays for. Capture the total in one movement and you throw that away at the exact joint where customer money meets supplier money. Nobody recovers it later. They approximate it, in a spreadsheet, in month four.
The settlement plan is therefore generated from confirmed order components, never from the cart total. Each payable needs:
- Supplier and booking-component identity
- Amount and settlement currency
- Due date or release condition
- Selected rail
- FX rate and source where relevant
- Current position, retries and external reference
- Links to adjustments or refund netting
Virtual cards, industry settlement systems and bank transfers have different timing and failure modes. Keep those differences visible while linking every payment to the booking component it belongs to. The ledger provides the common record.
FX is order data, not a display concern
When display, collection and supplier settlement currencies differ, the exchange rate moves both customer experience and margin. Record the rate used at quote, any lock or expiry, the rate applied at capture or settlement, and the resulting variance.
Never recompute a historical amount at today's rate. A reporting query that multiplies stored foreign amounts by a live rate looks correct in staging and returns a different answer every morning in production. The order needs an immutable monetary snapshot: currency, precision, price components, rate context and timestamps.
When a booking changes, write adjustment entries. Do not rewrite the past. Finance has to be able to state the difference between what was promised and what happened.
Keep the original charge when recording a refund
A refund references the original payment and the specific cancelled component. It does not delete the sale, and it does not mutate a capture into a smaller fictional one. Mutating the capture is the tempting shortcut because the totals agree instantly. It also destroys the only record of what the customer agreed to pay.
A refund flow has to answer:
- What is the customer entitled to under the accepted rules?
- Has the supplier approved or returned the corresponding value?
- Is the business advancing funds before supplier recovery?
- Which fees or penalties remain?
- Which payment method receives the refund?
- What ledger entries reverse revenue, cost or liability?
- Can part of the supplier position be netted against a future payable?
A refund is a first-class object that points backwards. For the cancelled hotel in the diagram above:
{
"refund_id": "ref_204",
"order_id": "ord_7ac1",
"component_id": "cmp_hotel_2",
"original_capture": "cap_c2",
"customer_amount": { "currency": "INR", "value": 17600 },
"supplier_recovery": {
"payable_id": "spl_02",
"expected": { "currency": "USD", "value": 168.75 },
"state": "supplier_approved",
"settlement": "net_against_next_payable"
},
"fx_context": "rate snapshot from cap_c2, never today's rate",
"advanced_before_recovery": true,
"ledger_entries": ["reverse_revenue", "reverse_supplier_cost", "recognize_recovery_receivable"]
}
advanced_before_recovery is the field finance argues about, and they are right to. Refunding the traveller before the bedbank returns the USD is usually the correct product decision: nobody should wait thirty days on a supplier's settlement calendar. But that float is a real position. It belongs on the books as a receivable, not netted quietly into a number that looks fine only while volume is small.
Reconcile booking and payment records every day
Month-end should confirm what the system already knew. If month-end is where you discover things, you do not have reconciliation, you have an audit.
Reconcile at several levels:
- Gateway or bank movement to payment object
- Payment object to customer order
- Supplier payout to payable and booking component
- Refund movement to refund object and original collection
- Ledger balances to external statements and safeguarded or operating accounts as applicable
Type the exception queues by cause: missing reference, amount mismatch, currency mismatch, unexpected duplicate, overdue supplier acceptance. One generic unmatched bucket converts a specific product defect into a growing pile that nobody owns and everyone learns to scroll past.
The bundle from the settlement tree above crosses at least four external statements over its life, and each line has exactly one internal record it must match.
| External statement line | Amount | Should match | Outcome |
|---|---|---|---|
Gateway payout, batch PG-8841 | +INR 52,300 | Payment pay_9f2: captures C1, C2, C3 | Matched at capture level |
| Virtual card clearing, airline | -EUR 316.00 | Payable spl_01 for component C1 | Matched; INR 12.40 funding-rate variance posted as an adjustment |
| Bank debit, bedbank | -USD 171.00 | Payable spl_02, scheduled at USD 168.75 | Exception: amount mismatch, typed supplier-overcharge, owner assigned |
Card refund, ref RF-204 | -INR 17,600 | Refund ref_204 against capture C2 | Matched to the original collection; USD 168.75 recovery receivable stays open |
Rows one, two and four confirm the system. Row three is the point. The mismatch is caught the day the statement arrives, typed, owned and sized at USD 2.25, instead of joining an unexplained write-off eight months later when nobody can name the booking it came from.
Show the same payment state to product, support and finance
The itinerary says "cancelled", finance says "refund submitted", the bank says "processing". All three are true at once. They are not contradictions to resolve; they are three views of one timeline, and the product's job is to connect and translate them.
Support gets read access to the financial timeline and never to raw payment credentials. Finance gets component and supplier context without filing a support ticket. The customer gets the part that affects them: amount, destination, current stage, next expected event. The post-booking trust guide covers how those operational states become customer language.
The same model earns its keep during incidents. When a payout rail fails, operators can name the affected obligations instead of treating every booking as suspect. When capture succeeds and confirmation does not, recovery or reversal becomes a decision someone makes, not a race between two retry loops.
A partial refund needs more than one status
Consider an illustrative trip paid for in one currency: a flight costs INR 6,000 and a hotel costs INR 4,000. The customer pays INR 10,000. The hotel is then cancelled with a full refund, while the flight remains booked. These figures are sample amounts and assume no fees or currency conversion.
The order should show the flight as confirmed and the hotel as cancelled. The financial record should separately show the original charge, the hotel refund due, any supplier funds still pending and the refund sent to the customer. A cancelled hotel does not establish that the customer already has the money.
Link the INR 4,000 refund to the hotel's original charge and supplier reference. Support can then explain the outstanding step, while finance reconciles it against the supplier and payment records. Keep the original entries and add the refund history rather than replacing the original total.
Before you launch
Ask:
- Can every cent be traced to an order and component?
- Are collection, booking, settlement and refund states separate?
- Are commands idempotent across retries and webhooks?
- Are price and FX snapshots immutable?
- Can partial changes create adjustments without rewriting history?
- Does supplier settlement start from confirmed obligations?
- Can finance explain a customer refund from a single timeline?
- Are reconciliation exceptions typed and owned?
- Can support see financial status without seeing sensitive credentials?
- Do customer-facing labels reflect what the system actually knows?
Ten no answers is a rebuild waiting for a trigger. One customer action creates many movements. They only ever tell one story if they were born knowing which part of the trip they belong to.