Rate Ranger watches what a hotel charges after your user has already booked it, and emails them if the rate drops far enough to be worth rebooking. This page is the contract for doing that programmatically.
Do not drive the web form. It is protected by a CAPTCHA that issues no token to an automated browser, so a browser-driving agent will fill the form correctly and then fail at the last step with nothing to click. Use the API below instead.
Submit a booking
POST https://www.rateranger.io/api/agent/bookingscurl -X POST https://www.rateranger.io/api/agent/bookings \
-H 'Content-Type: application/json' \
-d '{
"email": "traveller@example.com",
"agent": "my-assistant/1.0",
"bookings": [{
"hotel_name": "Novotel Bali Ubud Resort",
"city": "Ubud",
"country": "ID",
"check_in_date": "2027-07-14",
"check_out_date": "2027-07-17",
"total_price": 7428070,
"currency": "IDR",
"room_type": "Superior Room",
"num_guests": 2,
"num_rooms": 1,
"booking_platform": "direct"
}]
}'Response, 202 Accepted:
{
"status": "pending_confirmation",
"message": "1 booking(s) recorded. We have emailed the traveller a single link to
confirm. Nothing is monitored and no price is checked until they click it.",
"confirmation_required": true,
"results": [
{
"index": 0,
"status": "pending_confirmation",
"booking_id": "5c96e96b-0d7e-4b5d-96f8-db003bc3e919",
"read_token": "..."
}
]
}The traveller has to confirm. Tell them so.
A successful submission monitors nothing. We email the traveller one link. Until they click it, no price is checked and no alert can fire. If they never click, everything we hold is deleted after 7 days.
So do not tell your user their booking is being tracked. Tell them to check their inbox. Then confirm it yourself with the read endpoint before you say anything stronger.
This exists because the endpoint takes an email address from whoever calls it. If submission alone started the service, anyone could sign any address up for mail it never asked for. The confirmation step is what makes an open API safe to leave open, and it is why you do not need a key to use it.
Fields
| Field | Required | Notes |
|---|---|---|
email | Yes | The traveller's address. One per call. This is where the confirmation and any later alerts go. |
bookings | Yes | 1 to 10 per call. |
hotel_name | Yes | As the property is normally listed. Closer to the official name is better; we match it against live hotel data. |
city | Yes | City, not district. |
country | Yes | ISO 3166-1 alpha-2, for example JP or ID.
See the note below: this one matters more than it looks. |
check_in_date, check_out_date | Yes | YYYY-MM-DD. Check-in must be in the future and check-out
after it. |
total_price | Yes | The whole cost of the stay, not a nightly rate. Decimals are fine. |
currency | No | ISO 4217, defaults to USD. Must be one we support; a bad
one is rejected by name. |
room_type | No | Free text. |
num_guests, num_rooms | No | Default 2 and 1. |
booking_platform | No | booking.com, expedia, hotels.com,
agoda, trip.com, priceline,
direct, or other. |
agent | No | Name yourself. It is logged, never shown to the traveller, and helps us work out who to talk to if an integration starts sending malformed batches. |
Always send country. It decides which rate source
can find the hotel. A booking without one falls to a fallback source that
cannot look up a hotel id, and without that id we can never send an alert,
however far the price falls. You almost certainly know the country. Send it.
Check what happened
GET https://www.rateranger.io/api/agent/bookings/{read_token}
Use the read_token from the submission. It reaches that one booking
and nothing else. Poll it to find out whether the traveller confirmed, and later
to see the current rate.
{
"status": "ok",
"booking": {
"booking_id": "5c96e96b-...",
"hotel_name": "Novotel Bali Ubud Resort",
"monitoring_status": "watching",
"confirmed": true,
"booked_price": 7428070.0,
"currency": "IDR",
"last_checked": "2026-08-31T02:06:31+00:00",
"lowest_price": 6900000.0,
"lowest_source": "agoda",
"saving": 528070.0,
"saving_pct": 7.1
}
}
saving is only ever populated when a real alert could fire for that
booking. If we cannot link the traveller to a bookable page for the hotel, or the
drop is too small to be worth an email, it stays null and a
saving_note explains why. We would rather report nothing than a number
your user cannot act on.
Reading the results array
One entry per booking you sent, in the same order, each with its own outcome. One bad date does not reject the rest.
status | Meaning |
|---|---|
pending_confirmation |
Recorded. Waiting on the traveller's email click. |
duplicate |
Already recorded for this address. Sending it again does nothing and does not re-email anyone, so retrying is safe. |
rejected |
We cannot monitor it. reason and message say
why. The common ones are a past check-in, check-out on or before
check-in, and a non-refundable booking. |
rate_limited |
You are out of allowance for today. Everything before it in the batch still went through. |
Non-refundable bookings are refused on purpose. The whole method is cancel the old booking and rebook cheaper. If it cannot be cancelled, an alert costs your user money instead of saving it.
Limits
| Without a key | 10 bookings per day, per IP |
|---|---|
| With a key | 50 bookings per day |
| Per call | 10 bookings |
| Per traveller | 3 confirmation emails per day, across everyone |
Email hello@rateranger.io if you need more
and we will issue a key. Send it as X-RateRanger-Key. An unrecognised
key does not fail the request; it drops you to the anonymous limit.
Use https://www.rateranger.io as the base URL. It is the only
supported host.
Cost
Free, for you and for the traveller. There is no account and no card. Rate Ranger earns an affiliate commission only if the traveller decides to rebook through a link in an alert, and never charges them anything.
Machine-readable
- /openapi.json, OpenAPI 3.1, generated from the running service so it cannot drift from what the API actually accepts.
- /llms.txt, a short description of the whole product.