Skip to content

Comparing rates by source

roost rates answers a narrower question than roost search: not “what’s available in this city” but “for this one property, what does each booking source want to charge.”

Terminal window
roost rates "Sakura Cross Hotel" --location kyoto --nights 3 --json

rates takes two things: a property identifier as the argument, and a required --location option. Every other stay flag (--check-in, --check-out, --nights, --adults, --children, --currency, --brand) works the same way it does on search.

Rates are date-and-place scoped, not globally unique. Under the hood, rates re-runs the same availability query search would run and then looks for a property that matches your identifier inside those results — there’s no standalone “look up this property by id” endpoint. Without a location, roost has no query to run and nowhere to look.

Run search for the same city and stay first, then feed the id (or the name) into rates:

Terminal window
roost search kyoto --nights 3 --json
{
"schemaVersion": "1",
"properties": [
{
"id": "ChIJ...xyz",
"name": "Sakura Cross Hotel",
"perNight": { "amount": 142.0, "currency": "USD" },
"total": { "amount": 426.0, "currency": "USD" }
}
]
}

Both of these work as the rates argument:

Terminal window
roost rates "ChIJ...xyz" --location kyoto --nights 3 --json
roost rates "Sakura Cross Hotel" --location kyoto --nights 3 --json

The id comes from the property’s Google entity token, which isn’t always present on a given card. The name is always there, so it’s the identifier to reach for when you’re scripting from a human-readable list rather than piping straight from search --json.

If nothing matches, rates exits 5 (NOT_FOUND) rather than returning an empty result — the property genuinely wasn’t in that search’s result set, so the fix is to re-run search and confirm the id or name you’re passing.

What the google backend can and cannot answer here

Section titled “What the google backend can and cannot answer here”

The default google backend does not have a real per-source list to give you. Here’s why: the property page it would need to scrape client-renders its rate matrix, and that matrix arrives over an internal RPC — not from HTML in the initial response. That RPC sits outside roost’s robots-respecting allowlist (ALLOWED_PATH_PREFIXES only covers /travel/hotels/), so roost declines to call it rather than reverse-engineer a path Google hasn’t opened up.

Instead, on google, rates answers from the same search-results page search already fetched — the property’s own price, not a comparison across OTAs:

Terminal window
roost rates "Sakura Cross Hotel" --location kyoto --nights 3 --json
{
"schemaVersion": "1",
"scope": {
"backend": "google",
"brandsFilter": [],
"partial": true,
"note": "google backend: per-source rates are best-effort from the search surface; use --backend serpapi for the full source list"
},
"rates": {
"property": { "id": "ChIJ...xyz", "name": "Sakura Cross Hotel" },
"sources": [],
"perNight": { "amount": 142.0, "currency": "USD" },
"total": { "amount": 426.0, "currency": "USD" },
"taxesIncluded": false
}
}

sources is always an empty array on this backend, and scope.partial is true with a note explaining exactly why. This is declared, not silent — an agent reading scope knows it got the property’s headline rate and nothing to compare it against, rather than assuming it received a full breakdown that happens to be short.

To see brand-direct pricing against every OTA carrying the property, opt into the serpapi backend:

Terminal window
roost rates "Sakura Cross Hotel" --location kyoto --nights 3 --backend serpapi --json
{
"schemaVersion": "1",
"scope": {
"backend": "serpapi",
"brandsFilter": [],
"partial": false,
"note": "lead-in rates from Google Hotels metasearch; not a booking guarantee — confirm on the property's own site"
},
"rates": {
"property": { "id": "12345", "name": "Sakura Cross Hotel" },
"sources": [
{
"source": "Booking.com",
"perNight": { "amount": 138.0, "currency": "USD" },
"total": { "amount": 414.0, "currency": "USD" },
"freeCancellation": true
},
{
"source": "Hotels.com",
"perNight": { "amount": 145.0, "currency": "USD" },
"total": { "amount": 435.0, "currency": "USD" },
"freeCancellation": false
}
],
"perNight": { "amount": 138.0, "currency": "USD" },
"total": { "amount": 414.0, "currency": "USD" },
"taxesIncluded": false
}
}

serpapi is opt-in and needs an API key — set it up with roost auth login. See Choosing a backend for the full tradeoff, and Authentication for how the key is resolved.

Each entry is one booking source’s offer for the same property and stay:

FieldMeaning
sourceThe OTA or channel name (e.g. "Booking.com", "Hotels.com")
perNight{amount, currency} for one night at this source
total{amount, currency} for the full stay at this source
freeCancellationWhether this specific source’s rate is cancellable

Money is always the {amount, currency} object — never a bare number — on both perNight and total, and the two are never interchangeable: perNight is a per-night rate, total is the full-stay price for the nights you asked for. To find the cheapest source for a stay, sort sources by total.amount rather than perNight.amount if nights isn’t 1.