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.”
The basic shape
Section titled “The basic shape”roost rates "Sakura Cross Hotel" --location kyoto --nights 3 --jsonrates 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.
Why --location is required
Section titled “Why --location is required”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.
Getting a property id from search first
Section titled “Getting a property id from search first”Run search for the same city and stay first, then feed the id (or the name) into
rates:
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:
roost rates "ChIJ...xyz" --location kyoto --nights 3 --jsonroost rates "Sakura Cross Hotel" --location kyoto --nights 3 --jsonThe 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:
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.
Getting the full per-source breakdown
Section titled “Getting the full per-source breakdown”To see brand-direct pricing against every OTA carrying the property, opt into the serpapi
backend:
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.
Reading the sources[] array
Section titled “Reading the sources[] array”Each entry is one booking source’s offer for the same property and stay:
| Field | Meaning |
|---|---|
source | The 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 |
freeCancellation | Whether 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.
See also
Section titled “See also”- Searching for hotels — get the property id or name in the first place
- Choosing a backend — the full google-vs-serpapi tradeoff
- Output schema — the envelope and money shape in detail
- Read-only by design — why roost won’t call unlisted RPCs