What is inside the amount a guest pays, what Booking adds on top, and what sits outside it entirely - with the field holding each, and the one column to reconcile against.
Describes build 1.0.173 of rl1987/booking-api-scraper, deployed 2026-10-29. The column set last changed 2026-10-29. Earlier builds can return a different one.
Three questions get asked about this data more than any other, all of them arithmetic:
“Are included charges already inside
total? Which excludedtotal_amountvalues must be added exactly once? Does missing or emptyexcludedmean zero extras, or incomplete evidence?”
The answers are below the diagram, but the diagram is the answer: the mistake these guard against is vertical, so the fields are stacked in the order the money is.
rate level - mode `rooms` or `rates`, with Include extra charges on
┌──────────────────────────────────────────┐
│ undiscounted rate │ price.before_discount
└──────────────────────────────────────────┘
− deal / campaign price.discount
− mobile-only cut price.mobile_discount
(what a desktop browser shows: price.without_mobile_discount)
┌──────────────────────────────────────────┐
│ net, before any tax or fee │ price.base
├──────────────────────────────────────────┤
│ + charges ALREADY INSIDE the quote │ extra_charges.included[]
│ (always 0 on a plain search row) │ extra_charges.includedTotal
╞══════════════════════════════════════════╡
│ = DISPLAY PRICE, what Booking shows │ price.total (= price.book)
╞══════════════════════════════════════════╡
│ + charges BOOKING ADDS ON TOP │ extra_charges.excluded[]
│ VAT, city tax, resort / service fee │ extra_charges.excludedTotal
╞══════════════════════════════════════════╡
│ = ALL-IN, what the guest owes Booking │ price.total_with_charges
└──────────────────────────────────────────┘ ← reconcile against THIS one
Reconcile against price.total_with_charges, always. Not because total is
wrong — on a room-list rate it is exactly the figure on the app’s price sheet —
but because it is only sometimes right. Where Booking excludes tax from its
display price, total understates what the guest pays; where the tax is already
inside, the two columns are the same number. One column is correct on both
paths, so reading it removes the branch.
total_with_charges = total + excludedTotal, added exactly once. Do not sum
details[] by hand: inclusion on a search-row charge line is forced to
excluded by construction, and summing only the lines you think are excluded is
what once dropped an entire VAT line on every property in a VAT-charging
country.
One difference between the two paths, and it is the only one that matters here:
includedTotal is 0 on a plain search row and can be non-zero at rate
level, because inclusion there comes from Booking’s own inclusion_type per
item rather than from our normalisation.
Two different things, and conflating them is how a floor gets read as a bill.
Evidence gaps — money that may exist and cannot be quantified
| what | where it says so |
|---|---|
| More charges Booking will not quantify, so the all-in is a floor | extra_charges.chargesCompleteness = "incalculable" |
| A quantified amount beyond the itemised lines | chargesCompleteness = "extra_charges", with extra_charges.chargesAmount |
| Inclusion Booking never stated — not “already paid for” | extra_charges.unknown[], extra_charges.unknownTotal |
excludedTotal: 0 means “nothing further to pay” only under
chargesCompleteness: "all_included". Under incalculable the same zero
means Booking declined to say, and a rate can itemise VAT, a tourism fee and a
service charge and still be incalculable — the mode is about charges beyond
the itemised ones.
Money that is really owed, just not to Booking
| what | field |
|---|---|
| Refundable damage deposit, taken at the desk, in the property’s currency | details.damageDeposit (carries its own currency) |
| Cot or extra bed, paid at the property | childAndBedPolicy.ageBands[] |
| A meal bought separately | products[].policies.meal_plan.offers[], with breakfastPrice / breakfastCurrency on the row |
| Anything booked on arrival | not on this surface |
booking ─────────── dueDate ─────────── check-in ──────── stay
│ │ │
amountDueNow the rest falls pay_at_property
null = no stage due (never the settles here
stated, NOT 0 cancellation date)
policies.payment.types is one of pay_now, pay_later, pay_at_property.
policies.payment.choosable is true where the guest picks and false where
the rate requires it.
Two fields read wrong more often than the rest, which is why timing is drawn separately from amount:
amountDueNow is null, not 0, when a rate states no payment stage.
Treating null as zero turns every such rate into a false “pays nothing at
booking”.dueDate is not the cancellation deadline. It is absent on some
no_prepayment_until rates and has no fallback — both obvious candidates are
the cancellation date, which is a different thing.