Switching from Lighthouse
If your reports, spreadsheets or code read rates from Lighthouse's v3 API, you can point them at Beacon. Three endpoints answer with the same
filters, field names and rates CSV columns, so in most cases a request changes in two places: the base URL and the token. The
hotelId values in the answers are Beacon's own, so anything you've keyed on Lighthouse's ids needs matching up again.
Beacon can answer the same requests that your Lighthouse reports, spreadsheets and code already make. You change the web address and the key, and they carry on working.
The one new job is starting runs yourself, because Beacon captures rates when you ask for them, not on a schedule. If nobody at your company reads rates from Lighthouse's API, you can skip this page.
These endpoints keep working. Beacon's own /v1 API returns more on every rate, in an easy-to-read shape that suits code and LLMs. See
Moving from Lighthouse to /v1.
What stays the same
- The token goes in the same
X-Oi-Authorizationheader, the one Lighthouse clients already send. - The paths are the same under a new base URL:
/hotels,/ratesand/roomtyperates. -
The filters have the same names:
subscriptionId,ota,los,bar,persons,mealType,roomType,fromDate,shopLength,compsetIdsandformat, with the same defaults forota,los,bar,personsandcompsetIds. - Rates come back with the same fields in the same order, and the rates CSV has exactly the same header line.
-
When there's no rate for a date,
messagesays why with Lighthouse's codes, such asrates.soldout. Beacon doesn't send every Lighthouse code. A night that needs a longer minimum stay givesrates.restrictionlosN, Lighthouse's code, with the minimum number of nights asN. roomTypeuses Lighthouse's own values on these endpoints. How Beacon reads them is in Room types.
Lighthouse terms on Beacon
| In Lighthouse | On Beacon |
|---|---|
Subscription, subscriptionId |
A run reference, which you choose when you start a run |
| The subscription's own property | The first property in the run |
| Competitors | The other properties in the run |
| Competitor set |
One set, 1 "All", holding every competitor. As a rates filter, compsetIds=1 covers every property in the run, your
own included.
|
Hotel ID, hotelId |
Beacon's own property ID, which is different from the Lighthouse one |
| Lighthouse's capture schedule | Your own scheduled job that starts runs |
| API token |
A Beacon read key, in the same
X-Oi-Authorization header
|
You decide when rates are captured
This is the main difference. Lighthouse checks rates for each subscription on its own schedule. Rate Capture captures rates when you start a run. Each run checks every check-in date for the next 365 days.
A Lighthouse subscription is a Beacon run reference. When you start a run with POST /v1/runs, you
give it a reference. /rates?subscriptionId=client-42 then reads the newest completed run with that reference. Start a new run with the
same reference and it takes over as soon as it completes.
So switching means adding one scheduled job that starts runs, as often as you want fresh rates. Each run costs one credit per property.
A company can have 5 runs queued or running at once, unless Beacon has raised your company's limit. Starting another answers
429 too_many_active_runs. If a job has more subscriptions than that, it should start the next run when one finishes.
A subscription reads only runs whose rows are still stored. If you delete a run's results, or your company's retention period removes them, the
subscription reads the newest earlier run with the same reference whose rows still exist. If there isn't one, it answers
general.missing. So if you delete every run's results once you've loaded them, the subscription has nothing to read.
Switch in three steps
-
Create your keys
In Beacon Pro, open Company, then the API tab. Create a Read and write key for the job that starts runs, and a Read key for the reports and tools that read rates. Choose All company runs for the read key so it can see runs started by the other key. See API keys.
-
Start a run for each subscription
Put your own property first and its competitors after it. The first property becomes "the hotel", and the rest become its competitors in competitor set
1.curl -X POST https://api.pro.getbeacon.cloud/v1/runs \ -H "Authorization: Bearer $BEACON_WRITE_KEY" \ -H "Idempotency-Key: 4211-2026-10-05" \ -H "Content-Type: application/json" \ -d '{ "reference": "4211", "name": "Harbour View and competitors", "currency": "GBP", "nights": 1, "properties": [ {"url": "https://www.booking.com/hotel/gb/harbour-view.html", "name": "Harbour View Hotel"}, {"url": "https://www.booking.com/hotel/gb/quayside-inn.html", "name": "Quayside Inn"}, {"url": "https://www.booking.com/hotel/gb/marina-lodge.html", "name": "Marina Lodge"} ] }'import os, requests requests.post( "https://api.pro.getbeacon.cloud/v1/runs", headers={ "Authorization": f"Bearer {os.environ['BEACON_WRITE_KEY']}", "Idempotency-Key": "4211-2026-10-05", }, json={ "reference": "4211", "name": "Harbour View and competitors", "currency": "GBP", "nights": 1, "properties": [ {"url": "https://www.booking.com/hotel/gb/harbour-view.html", "name": "Harbour View Hotel"}, {"url": "https://www.booking.com/hotel/gb/quayside-inn.html", "name": "Quayside Inn"}, {"url": "https://www.booking.com/hotel/gb/marina-lodge.html", "name": "Marina Lodge"}, ], }, ).raise_for_status()
// Node 18 or later, saved as a .mjs file so await works at the top level await fetch("https://api.pro.getbeacon.cloud/v1/runs", { method: "POST", headers: { Authorization: `Bearer ${process.env.BEACON_WRITE_KEY}`, "Idempotency-Key": "4211-2026-10-05", "Content-Type": "application/json", }, body: JSON.stringify({ reference: "4211", name: "Harbour View and competitors", currency: "GBP", nights: 1, properties: [ { url: "https://www.booking.com/hotel/gb/harbour-view.html", name: "Harbour View Hotel" }, { url: "https://www.booking.com/hotel/gb/quayside-inn.html", name: "Quayside Inn" }, { url: "https://www.booking.com/hotel/gb/marina-lodge.html", name: "Marina Lodge" }, ], }), });
Keep your subscription IDsUse your existing Lighthouse subscription ID as the reference, as above. Your stored
subscriptionIdvalues then keep working, and/hotelsreturnssubscription_idas a number, as it did before.A run usually takes minutes rather than seconds. Until the first run for a reference completes,
/ratesfor that subscription answers with an empty list andgeneral.missing. After that, the last completed run keeps answering while a new one is going. -
Point your client at Beacon
Change the base URL and the token. In most cases everything else in the request stays as it is. Values Beacon doesn't support, such as a
compsetIdslist without1, aroomTypeoutside Lighthouse's list, alosover 45 or acurrencyother than the subscription's, answer400. The differences that remain are in Differences from Lighthouse.import os, requests
BASE = "https://api.mylighthouse.com/v3"BASE = "https://api.pro.getbeacon.cloud/lighthouse/v3"TOKEN = os.environ["LIGHTHOUSE_TOKEN"]TOKEN = os.environ["BEACON_READ_KEY"] rates = requests.get( f"{BASE}/rates", headers={"X-Oi-Authorization": TOKEN}, params={"subscriptionId": 4211, "los": 1, "persons": 2}, ).json()["rates"]curl -H "X-Oi-Authorization:
$LIGHTHOUSE_TOKEN$BEACON_READ_KEY" \ "https://api.mylighthouse.com/v3/rates?subscriptionId=4211&format=csvhttps://api.pro.getbeacon.cloud/lighthouse/v3/rates?subscriptionId=4211&format=csv"let Source = Csv.Document( Web.Contents("https://api.mylighthouse.com/v3/rates","https://api.pro.getbeacon.cloud/lighthouse/v3/rates", [ Query = [subscriptionId = "4211", format = "csv"],Headers = [#"X-Oi-Authorization" = LighthouseToken]Headers = [#"X-Oi-Authorization" = BeaconReadKey] ] ), [Delimiter = ",", Encoding = 65001, QuoteStyle = QuoteStyle.Csv] ), Rates = Table.PromoteHeaders(Source) in RatesIn Excel or Power BI, create a parameter called
BeaconReadKeythat holds your read key, and refer to it as above.
Supported endpoints
Base URL:
https://api.pro.getbeacon.cloud/lighthouse/v3. The three that read need only a read key, and count towards the key's limit of 600 reads
a minute, unless Beacon has raised it. Starting a live shop needs a write key, see Live shop.
-
GET/hotelsOne entry for each subscription the key can read, with its competitors and one competitor set,
1"All". Like Lighthouse, it returns every subscription when you leave outpage. Withpage(1 or more) andper_page(default and most 100) it returns that page, withmeta. A page past the last one answers400. - GET/ratesThe lowest rate per property and arrival date for one subscription.
- GET/roomtyperatesThe lowest rate per property, arrival date and room type, room types in alphabetical order.
-
POST/liveupdateStarts a live shop of a subscription: a fresh run, charged like
POST /v1/runs. - GET/liveupdate/{id}How far a live shop has got.
otas in /hotels lists the sites of the run's properties.
Live shop
A live shop asks for fresh rates for one subscription. It is the same thing as starting a run, so you can also use
POST /v1/runs directly, see Moving from Lighthouse to /v1. If your Lighthouse client expects something
different, contact us.
-
Start it.
POST /lighthouse/v3/liveupdatewith a write key and the subscription, as JSON ({"subscriptionId": "client-42"}) or in the query string. If both have it, the body wins. Beacon copies the report of the newest run with that reference that your key can see, whatever its status, and starts the copy. The answer is202. -
Poll it.
GET /lighthouse/v3/liveupdate/{id}withliveupdateIdfrom the start answer, or followstatusUrl. A read key is enough.statusisqueued,runningorcompleted. -
Read the rates. Once
statusiscompleted,GET /rates?subscriptionId=client-42serves the new run, because the newest completed run of a reference is the one a subscription reads.
{
"liveupdateId": 1310,
"subscriptionId": "client-42",
"status": "queued",
"progress": {
"total": 3650,
"done": 0,
"soldOut": 0,
"failed": 0,
"past": 0,
"restricted": 0,
"remaining": 3650
},
"requestedAt": "2026-10-15T09:30:00Z",
"completedAt": null,
"creditsCharged": 10,
"statusUrl": "/lighthouse/v3/liveupdate/1310"
}
progress counts check-in dates the way a run does. requestedAt is when the run started, and completedAt is
null until it has finished. Errors have Lighthouse's shape, message and errors.
-
Credits. A live shop is a normal run: one credit per property, taken when it starts, exactly as for
POST /v1/runs. Nothing is refunded. Without enough credits the answer is402 insufficient_credits. -
Limits. It counts towards the company's runs in progress. Over the limit the answer is
429 too_many_active_runs. -
Retries. Send an
Idempotency-Keyheader, as on/v1. A repeat with the same key and subscription gives the first answer and charges nothing again. -
Other refusals.
404 not_foundfor a subscription or live shop your key can't see,422whensubscriptionIdis missing,403 scope_read_onlyfor a read key on the start, and405for another method.
Filters for rates and room type rates
| Filter | Default | On Beacon |
|---|---|---|
subscriptionId |
Required | The run reference to read. |
ota |
bookingdotcom |
The site the rates come from: bookingdotcom, travelodge, premierinn or branddotcom,
which is the Travelodge and Premier Inn sites. A property on a different site answers general.missing.
|
los |
1 |
Stay length in nights, 1 to 45. Rates exist for the stay length the run captured, which is its nights. |
bar |
false |
true keeps only the public flexible rate: free cancellation, and not a members-only or partner offer. |
persons |
2 |
Keeps rooms whose maximum occupancy is at least this many guests, 1 to 20. It filters rooms; prices are always for 2 adults. |
mealType |
0 |
0 any, 1 breakfast, 2 half board, 3 full board, 4 all inclusive,
5 room only. Each of 1 to 5 keeps only rates of exactly that meal plan, so 5 doesn't include half board, full
board or all inclusive. A date with none of that plan answers rates.meal.missing.N.
|
roomType |
None |
parkandfly, dormitory, sharedbathroom, camper, villa,
bungalow, suite, apartment, family, premium, budget,
single or standard. Any other value answers 400. See Room types.
|
fromDate |
Today in the run's time zone | First arrival date, YYYY-MM-DD. |
shopLength |
To the end of the run | Number of arrival dates, 1 to 365. |
compsetIds |
1 |
1, every property in the run, your own included. A comma-separated list that holds 1, such as -1,1,
is answered as 1. A list without 1 answers 400.
|
currency |
None |
Must be the subscription's currency, else 400. Beacon doesn't convert: prices always come in the currency the run captured,
shown in the currency field.
|
changeDays |
None | Accepted and ignored. Change history is not returned, and changes is never sent. |
format |
json |
json or csv. |
What comes back
- Every field, with its match in the Rate Capture API, is in the field dictionary, and there's a sample CSV.
-
hotelIdis Beacon's ID for the property, not Lighthouse's ID. It is nevernull, and it stays the same in every run and report of your company for the same property URL, so you can key your own tables on it. -
Each rate has the same fields as Lighthouse:
hotelId,hotelName,extractDateTime,arrivalDate,los,value,currency,shopCurrency,cancellable,best_flex, the three tax flags,roomName,roomType,maxPersons,mealTypeIncludedandmessage. -
mealTypeIncluded(theMealTypeIncludedcolumn in CSV) is1Breakfast,2Half board,3Full board,4All-inclusive or5Room only. It is0when there is no rate. Lighthouse has no lunch category, so some rates are reported as the nearest code. A breakfast and lunch rate, and a lunch and dinner rate, are both reported as2(Half board), because each is two meals. A dinner-only or lunch-only rate is reported as0, Lighthouse's "no meal type included". These codes are for compatibility only. Filtering withmealType=2still returns only half board rates, andmealType=0still means any. -
When a date has no rate,
messagegives the reason, and the other fields are filled as Lighthouse fills them:valueis0,cancellableandbest_flexarefalse,roomNameandroomTypeare empty text,maxPersonsis thepersonsyou asked for andmealTypeIncludedis0. When there is a rate,messageis empty text. Checkmessageto tell the two apart. -
extractDateTimeis nevernull. A date that was never fetched shows the time the run finished.currencyandshopCurrencyare the currency of the chosen rate, or the report's currency when there is none. -
vatInclandotherTaxesInclare alwaysnull, because whether a price includes those taxes isn't captured.cityTaxInclistrueon Premier Inn rates, whose price includes city tax, andnullfor every other source. -
The
/hotelsCSV has Lighthouse's 10 columns:HotelID,HotelName,HotelStars,CompetitorID,CompetitorName,CompetitorStars,CompsetID,CompsetName,SubscriptionID,BrandCode, with one row for each competitor in the competitor set. A subscription with no competitors has one row, with the competitor and set cells empty. -
Errors come back as
messageand Lighthouse'serrorslist, each entry withstatus,code,titleanddetail. The codes are on Errors.
{"rates": [
{"hotelId": 57, "hotelName": "Harbour View Hotel", "extractDateTime": "2026-10-05T08:00:33",
"arrivalDate": "2026-10-06", "los": 1, "value": 99.0, "currency": "GBP", "shopCurrency": "GBP",
"cancellable": true, "best_flex": true, "vatIncl": null, "cityTaxIncl": null, "otherTaxesIncl": null,
"roomName": "Superior Double Room", "roomType": "premium", "maxPersons": 2, "mealTypeIncluded": 5, "message": ""},
{"hotelId": 57, "hotelName": "Harbour View Hotel", "extractDateTime": "2026-10-05T08:00:36",
"arrivalDate": "2026-10-07", "los": 1, "value": 0.0, "currency": "GBP", "shopCurrency": "GBP",
"cancellable": false, "best_flex": false, "vatIncl": null, "cityTaxIncl": null, "otherTaxesIncl": null,
"roomName": "", "roomType": "", "maxPersons": 2, "mealTypeIncluded": 0, "message": "rates.soldout"}
]}
Room types
In the Lighthouse app, each hotel sets the type of each room. Beacon classifies each room from its name instead, so the same room can get a different type from the one Lighthouse gives it. Check the types you filter or group by.
The roomType values are parkandfly, dormitory, sharedbathroom, camper,
villa, bungalow, suite, apartment, family, premium,
budget, single and standard. Each is Beacon's classification of the room from its name.
A roomType filter that matches no room with a rate on a date gives rates.roomtypes.unavailable for that date.
Why a date has no rate
Beacon narrows each property's rows for a date in this order. The first step that leaves nothing gives the message.
message |
Why |
|---|---|
general.missing |
The los isn't the run's stay length, the ota isn't the property's site, or the date wasn't in the run. A date in
the window you asked for with shopLength that the run doesn't cover also gives this.
|
rates.soldout |
The property was sold out that date. |
rates.restrictionlosN |
The property needs a stay of at least N nights from that date, and the run's stay length is shorter. For example
rates.restrictionlos4 means a minimum stay of 4 nights. When the site doesn't say how many nights, the message is
general.missing.
|
general.missing |
The date couldn't be checked, was past, or hasn't been checked yet. Or every row was left out of comparisons (excludedReason). |
rates.no_person.N |
No room takes at least persons guests. |
rates.meal.missing.N |
No room left has exactly the meal plan you asked for with
mealType 1 to 5, for example half board (mealType=2). If you asked by a name that has no code of its own, such as
dinner, N is that name.
|
rates.no_bar |
bar=true and there's no public flexible rate. |
rates.roomtypes.unavailable |
No room of the roomType asked for has a rate on that date. |
Other Lighthouse endpoints under the base URL aren't available on Beacon. They answer 501 straight away, even without a key, so your
client finds out at once. The message names the endpoint and the ones that are supported.
Differences from Lighthouse
These are the places where Beacon answers differently from Lighthouse. Check them against what your code relies on.
Most of these differences go away on /v1, which also returns more fields. See Moving from Lighthouse to /v1.
| Topic | Lighthouse | Beacon | What to do |
|---|---|---|---|
| Room types | Set for each room in the Lighthouse app. |
Beacon's classification of the room from its name, such as premium, suite or standard.
|
Check the types you filter or group by against your room names. The types are listed in Room types. |
| Meal codes | mealType asks for rates that include that meal. mealTypeIncluded is 0 to 5. |
mealType matches exactly, so 5 doesn't include half board. Breakfast and lunch, and lunch and dinner, are
reported as 2. Dinner-only and lunch-only are reported as 0. mealType also takes names, such as
breakfast_lunch. A missing plan asked for by a name with no code of its own gives rates.meal.missing.dinner, not a number.
|
If you read 2 as half board, you will also see breakfast and lunch, and lunch and dinner, rates there. If you read 0
as "no meal plan", you will also see dinner-only and lunch-only rates. To tell them apart, filter by name.
|
| Persons | persons sets how many guests the price is for: 1 to 10 on Booking.com, and 2 on other sites. |
persons only filters rooms by their maximum occupancy, 1 to 20. Every price is for 2 adults. maxPersons is the
room's maximum, or the persons you asked for when there is no rate.
|
Don't expect a price to change with persons. Use it to leave out rooms that are too small. |
| Length of stay | Any los from 1 to 45 that the subscription shops. |
A run captures one stay length, its nights. los 1 to 45 is accepted, but any los other than the run's
nights gives general.missing on every date.
|
Start the run with the nights you need, and send the same los. For two stay lengths, start two runs with two references. |
| Hotel ids | Lighthouse's own hotel ids. | Beacon's property ids, in hotelId, id, competitors and competitor_ids. |
Match them to your own records once. An id stays the same for the same property URL in your company. |
| Subscription ids | A number that Lighthouse gives each subscription. |
A run reference that you choose. subscription_id in /hotels is a number when the reference is all digits, and text
otherwise. A reference with a leading zero, such as 007, stays text. A subscriptionId that matches nothing gives
general.missing with an empty list, not an error.
|
Use your Lighthouse subscription id as the reference, so your stored ids keep working. |
| Competitor sets | Several sets for a hotel. |
One set, 1 "All", holding every competitor. compsetIds takes a comma-separated list, such as -1,1,
and answers it as set 1 when the list holds 1. A list without 1 answers 400.
|
Expect every competitor of the run whatever other ids you send. |
| OTA codes | Many, such as bookingdotcom, expedia and branddotcom. |
bookingdotcom (the default), travelodge and premierinn. branddotcom matches the brand
sites, which means Travelodge and Premier Inn properties. Any other code gives general.missing. The otas in
/hotels list bookingdotcom, travelodge and premierinn, never branddotcom.
|
Send ota=branddotcom for a hotel's own site, or the chain's code. |
| Tax flags | vatIncl, cityTaxIncl and otherTaxesIncl are true or false. |
vatIncl and otherTaxesIncl are always null. cityTaxIncl is true on Premier Inn rates and null for every other source. |
Treat the VAT and other-tax status as unknown, and the city tax status as unknown unless the rate is from Premier Inn. |
| Stars and brand | stars and brand_code are filled in /hotels. |
stars and brand_code come from the property's profile when Beacon knows it. stars is null when the profile has none. brand_code is Premier Inn's only (PI, PID, HUB or ZIP) and is empty for every other property. |
Use the values when they are set, and keep your own records for properties where they are empty. |
| A bad key | 403 permission_denied. |
401, for a key that is not recognised, revoked or expired. 403 is only for a request from an address the key
doesn't allow. A request with no key is 401, as in Lighthouse.
|
If your code reacts to 403 for a bad key, react to 401 too. |
| Endpoints that don't exist | 404 endpoint_not_found for an unknown path. |
501 not_supported for a Lighthouse endpoint that Beacon doesn't provide, with a message that names it. It needs no
key. An address that is not under /lighthouse/v3 is 404 endpoint_not_found.
|
Treat 501 as "not available on Beacon". |
| Live shop | A live shop is started and followed with its own calls. |
POST /lighthouse/v3/liveupdate and GET /lighthouse/v3/liveupdate/{id}. A live shop starts a normal run, charged
like POST /v1/runs, and needs a write key.
|
Keep a write key for the job that starts live shops. If your Lighthouse client expects something different, contact us. |
| Endpoints that aren't available | History, markets, reviews, ranking, parities, raw rates, hotel search and more. |
Not available, and they answer 501: /historic/rates, /historic/roomtyperates, /markets,
/reviews, /ranking, /rankingsummaries, /parities, /demand,
/marketinsight, /parityinsight, raw rates and hotel search. There is no OAuth either: you use a key.
changeDays is accepted and ignored, and changes is never sent.
|
Keep those calls on Lighthouse, or leave them out. |
| Rate limits | 20 calls a day for each API and subscription, and 120 a minute. |
600 reads a minute for each key, unless Beacon has raised it. Over it, the answer is 429 with Retry-After. Every
answer to a request with a recognised key has X-RateLimit-Limit, X-RateLimit-Remaining and
X-RateLimit-Reset, which is the seconds left in the current minute.
|
Nothing, unless you send more than 600 requests a minute. See Limits. |
| Currency | Converts the prices to the currency you ask for. | Doesn't convert. A currency that is not the subscription's currency answers 400. |
Leave out currency, or send the one the run was started with. |
| Dates outside the run | A record for every date asked for. |
With shopLength sent, every date from fromDate for that many days gets a record for each hotel, and a date the run
doesn't cover gives general.missing. Without shopLength, you get only the run's own dates from
fromDate.
|
Send shopLength if your code expects one record for each date. |
| best_flex | Judged within each room type on room type rates. |
The same on /roomtyperates, and on /rates when you send roomType. On /rates without
roomType it is judged across all rooms of the property and date.
|
Nothing. It matches Lighthouse. |
| roomType on room type rates | Not a parameter. | Accepted on /roomtyperates as an extra. It keeps one room type. |
Use it if you want one type. Leave it out to match Lighthouse. |
| Order of room types | Not stated. | Alphabetical for each hotel and date, such as budget before standard. |
Don't rely on the order if you read Lighthouse's output by position. |
| CSV numbers | Two decimals, such as 100.10. |
No padding, such as 98.4 or 99. |
Read prices as decimals. |
| CSV row order | Grouped by date. | By hotel, then by date. | Sort the rows if your code expects them by date. |
| CSV formulas | Text is written as it is. |
A text cell that starts with =, +, - or @ gets a leading ', the formula
guard, so a spreadsheet doesn't run it. Numbers are never changed.
|
Strip a leading ' if you load room or hotel names into a database. |
Your numbers won't match exactly
Rate Capture captures the rates a guest is shown online at the moment your run checks each date. The same room on the same date can differ from a figure captured at another time or in another way. Compare a few dates side by side when you switch, and expect small differences rather than identical columns.
Going further than the compatible API
The same runs can be read with the Rate Capture API, which goes beyond the lowest rate:
- Every room and rate plan on every date, with sale badges, rooms left, partner offers that undercut the property's own rate, and whether the price is a members-only preview price. See results.
- Every snapshot you've taken, listed by reference, so you can compare this week with last week for as long as you keep each run's rows.