{
	"openapi": "3.0.3",
	"info": {
		"title": "Lighthouse-compatible API",
		"version": "1.0.0",
		"description": "These endpoints answer like Lighthouse's v3 API, so an existing Lighthouse integration can switch to Beacon Pro with minimal changes: point it at this base URL and send a Beacon key in the `X-Oi-Authorization` header. The supported endpoints are `/hotels`, `/rates` and `/roomtyperates`, with the same parameter names, field names and CSV option, and `/liveupdate`, a live shop that starts a fresh capture of a subscription and lets you follow it.\n\nThe rates come from runs started through the Rate Capture API. A Lighthouse subscription is a run reference: start a run with **Start a run** (`POST /v1/runs`) and a `reference`, and once it has completed, that reference works as your `subscriptionId`. Beacon does not run captures on a schedule, so start a new run, or a live shop (`POST /liveupdate`), whenever you want fresher rates; the newest completed run for a reference whose rows have not been deleted, among the runs your key can see, is the one served here.\n\nOther Lighthouse v3 endpoints are not available and answer 501 `not_supported`. A path outside `/lighthouse/v3/`, and the bare base path, answer 404 `endpoint_not_found`. A trailing slash on a supported path is accepted.\n\nErrors carry Lighthouse's `message` and an `errors` list. Every answer that follows an accepted key also carries `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`. The [guides](../) explain how to switch, step by step."
	},
	"servers": [
		{
			"url": "https://api.pro.getbeacon.cloud/lighthouse/v3",
			"description": "Beacon Pro, Lighthouse-compatible (HTTPS only)"
		}
	],
	"security": [
		{
			"oiAuthorization": []
		},
		{
			"bearerAuth": []
		}
	],
	"tags": [
		{
			"name": "Hotels",
			"description": "Find the subscriptions your key can read. Each subscription is a run reference: the run's first property is your property, and the rest are its competitors."
		},
		{
			"name": "Rates",
			"description": "Read the chosen rate for each property and arrival date of a subscription, as JSON or CSV. These are read-only and need only a `read` key; to get new rates, start a new run through the Rate Capture API."
		},
		{
			"name": "Live shop",
			"description": "Start a fresh capture of a subscription and follow it. A live shop is an ordinary Rate Capture run of a copy of the subscription's report, charged and limited exactly as **Start a run** is. Once it has completed, `/rates` and `/roomtyperates` serve it, being the newest completed run of the reference."
		}
	],
	"paths": {
		"/hotels": {
			"get": {
				"operationId": "listHotels",
				"tags": ["Hotels"],
				"summary": "List subscriptions",
				"description": "Lists every subscription your key can read, with its property and competitors, in the same shape as Lighthouse's `/hotels` endpoint. Use it to find the `subscriptionId` values to pass to the rates endpoints, and the property ids that appear in their answers.\n\n- One entry per run reference your key can read, ordered by reference.\n- The run's first property is the subscription's own property (`id`, `name`). The others are its `competitors`, all in one competitor set: id `1`, named \"All\".\n- `subscription_id` is the run reference: a number when the reference is all digits, a string otherwise.\n- `id` values are Beacon's stable property ids, not your Lighthouse ids.\n- `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.\n- `otas` lists the sources of the run's properties: `bookingdotcom`, `travelodge` and `premierinn`, never `branddotcom`.\n- Without `page`, every subscription is listed and there is no `meta`, as in Lighthouse. With `page`, the list is paged by `per_page` (100 unless you ask otherwise) and `meta` is included. A `page` past the last one answers 400 `invalid_page`.\n- With `format=csv` the answer is `text/csv`. A text cell that starts with `=`, `+`, `-` or `@` gets a leading `'` so a spreadsheet does not run it as a formula; numbers are never changed. The CSV has Lighthouse's 10 columns and one row for each competitor of each subscription. A subscription with no competitors is one row with the competitor and compset cells empty.",
				"parameters": [
					{
						"name": "page",
						"in": "query",
						"required": false,
						"description": "Page number, 1 or more. Leave it out to list every subscription with no `meta`. Anything else answers 400, and so does a page past the last one.",
						"schema": {
							"type": "integer",
							"minimum": 1
						}
					},
					{
						"name": "per_page",
						"in": "query",
						"required": false,
						"description": "Subscriptions per page, 1 to 100. Anything else answers 400.",
						"schema": {
							"type": "integer",
							"minimum": 1,
							"maximum": 100,
							"default": 100
						}
					},
					{
						"$ref": "#/components/parameters/Format"
					}
				],
				"responses": {
					"200": {
						"description": "The subscriptions, or one page of them when `page` is sent.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/HotelList"
								},
								"example": {
									"hotels": [
										{
											"id": 9190,
											"name": "The Old Tannery Rooms",
											"stars": null,
											"brand_code": "",
											"subscription_id": 40217,
											"competitors": [
												{
													"id": 9191,
													"name": "Saltmarsh Lodge",
													"stars": null
												}
											],
											"competitor_sets": [
												{
													"id": 1,
													"name": "All",
													"competitor_ids": [9191]
												}
											],
											"subscribed_features": [
												{
													"id": "rates",
													"otas": ["bookingdotcom"]
												}
											]
										},
										{
											"id": 9182,
											"name": "The Larkspur Hotel",
											"stars": null,
											"brand_code": "",
											"subscription_id": "larkspur-otterby",
											"competitors": [
												{
													"id": 9183,
													"name": "Quillfield House",
													"stars": null
												},
												{
													"id": 9184,
													"name": "Marrow Lane Inn",
													"stars": null
												}
											],
											"competitor_sets": [
												{
													"id": 1,
													"name": "All",
													"competitor_ids": [9183, 9184]
												}
											],
											"subscribed_features": [
												{
													"id": "rates",
													"otas": ["bookingdotcom"]
												}
											]
										}
									]
								}
							},
							"text/csv": {
								"schema": {
									"type": "string",
									"description": "Header `HotelID,HotelName,HotelStars,CompetitorID,CompetitorName,CompetitorStars,CompsetID,CompsetName,SubscriptionID,BrandCode`, then one row for each competitor of each subscription."
								},
								"example": "HotelID,HotelName,HotelStars,CompetitorID,CompetitorName,CompetitorStars,CompsetID,CompsetName,SubscriptionID,BrandCode\n9190,\"The Old Tannery Rooms\",,9191,\"Saltmarsh Lodge\",,1,All,40217,\n9182,\"The Larkspur Hotel\",,9183,\"Quillfield House\",,1,All,larkspur-otterby,\n9182,\"The Larkspur Hotel\",,9184,\"Marrow Lane Inn\",,1,All,larkspur-otterby,\n"
							}
						},
						"headers": {
							"X-RateLimit-Limit": {
								"$ref": "#/components/headers/XRateLimitLimit"
							},
							"X-RateLimit-Remaining": {
								"$ref": "#/components/headers/XRateLimitRemaining"
							},
							"X-RateLimit-Reset": {
								"$ref": "#/components/headers/XRateLimitReset"
							}
						}
					},
					"400": {
						"$ref": "#/components/responses/BadRequest"
					},
					"401": {
						"$ref": "#/components/responses/Unauthorized"
					},
					"403": {
						"$ref": "#/components/responses/Forbidden"
					},
					"405": {
						"$ref": "#/components/responses/MethodNotAllowed"
					},
					"429": {
						"$ref": "#/components/responses/RateLimited"
					},
					"500": {
						"$ref": "#/components/responses/InternalError"
					}
				}
			}
		},
		"/rates": {
			"get": {
				"operationId": "getRates",
				"tags": ["Rates"],
				"summary": "Get rates for a subscription",
				"description": "Returns one rate per property and arrival date for a subscription, in the same shape as Lighthouse's rates endpoint, CSV included. Use it in place of your existing Lighthouse rates call: change the base URL and the key, and keep your parameters, except as noted below.\n\n- `subscriptionId` is a run reference. It reads the newest completed run with that reference whose results have not been deleted, among the runs your key can see. A newer run with the same reference replaces it here as soon as it completes.\n- Rates are chosen exactly as by **Get the cheapest rate per day** in the Rate Capture API, with the same filters and messages, except that `roomType` uses Lighthouse's own values (see the `roomType` parameter).\n- `los` is 1 to 45. Only the run's own stay length has rates. Any other `los` gives `general.missing` on every date.\n- Prices are in the report's currency. `currency` must be that currency, or the answer is 400, because Beacon does not convert. `changeDays` is accepted and ignored, and change history is not returned.\n- `ota` takes `bookingdotcom`, `travelodge`, `premierinn` or `branddotcom`, which covers the Travelodge and Premier Inn sites.\n- `compsetIds` takes a comma-separated list such as `-1,1`. A list that holds `1` covers every property in the run. A list without `1` answers 400.\n- With `shopLength`, every date from `fromDate` is answered, and a date outside the run is a `general.missing` record.\n- When there is no rate, `value` is `0`, `cancellable` and `best_flex` are `false`, `roomName` and `roomType` are empty text, `maxPersons` is the `persons` you asked for, `mealTypeIncluded` is `0`, and `message` says why. On a row with a rate, `message` is empty text. Only the three tax flags are null.\n- A `subscriptionId` that matches nothing your key can read is not an error. The answer is 200 with `{\"rates\":[],\"message\":\"general.missing\"}`, or the CSV header line alone.\n- With `format=csv` the answer is `text/csv`. Booleans are `true` or `false`, and null and empty text are an empty cell. A text cell that starts with `=`, `+`, `-` or `@` gets a leading `'` so a spreadsheet does not run it as a formula; numbers, negative ones included, are never changed.",
				"parameters": [
					{
						"$ref": "#/components/parameters/SubscriptionId"
					},
					{
						"$ref": "#/components/parameters/Los"
					},
					{
						"$ref": "#/components/parameters/Bar"
					},
					{
						"$ref": "#/components/parameters/Persons"
					},
					{
						"$ref": "#/components/parameters/MealType"
					},
					{
						"$ref": "#/components/parameters/RoomType"
					},
					{
						"$ref": "#/components/parameters/FromDate"
					},
					{
						"$ref": "#/components/parameters/ShopLength"
					},
					{
						"$ref": "#/components/parameters/Ota"
					},
					{
						"$ref": "#/components/parameters/CompsetIds"
					},
					{
						"$ref": "#/components/parameters/Currency"
					},
					{
						"$ref": "#/components/parameters/ChangeDays"
					},
					{
						"$ref": "#/components/parameters/Format"
					}
				],
				"responses": {
					"200": {
						"description": "One rate per property and arrival date.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/RateList"
								},
								"examples": {
									"rates": {
										"summary": "Rates for a subscription",
										"value": {
											"rates": [
												{
													"hotelId": 9182,
													"hotelName": "The Larkspur Hotel",
													"extractDateTime": "2026-10-05T07:00:19",
													"arrivalDate": "2026-10-05",
													"los": 1,
													"value": 98.4,
													"currency": "GBP",
													"shopCurrency": "GBP",
													"cancellable": true,
													"best_flex": true,
													"vatIncl": null,
													"cityTaxIncl": null,
													"otherTaxesIncl": null,
													"roomName": "Twin Room",
													"roomType": "standard",
													"maxPersons": 2,
													"mealTypeIncluded": 5,
													"message": ""
												},
												{
													"hotelId": 9183,
													"hotelName": "Quillfield House",
													"extractDateTime": "2026-10-05T07:00:23",
													"arrivalDate": "2026-10-05",
													"los": 1,
													"value": 104.6,
													"currency": "GBP",
													"shopCurrency": "GBP",
													"cancellable": true,
													"best_flex": true,
													"vatIncl": null,
													"cityTaxIncl": null,
													"otherTaxesIncl": null,
													"roomName": "Double Room",
													"roomType": "standard",
													"maxPersons": 2,
													"mealTypeIncluded": 5,
													"message": ""
												},
												{
													"hotelId": 9184,
													"hotelName": "Marrow Lane Inn",
													"extractDateTime": "2026-10-05T07:00:27",
													"arrivalDate": "2026-10-05",
													"los": 1,
													"value": 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"
												}
											]
										}
									},
									"unmatched": {
										"summary": "A subscriptionId that matches nothing",
										"value": {
											"rates": [],
											"message": "general.missing"
										}
									}
								}
							},
							"text/csv": {
								"schema": {
									"type": "string",
									"description": "A header line, then one row per rate."
								},
								"example": "HotelID,HotelName,ExtractDateTime,ArrivalDate,Los,Value,Currency,Cancellable,Best_Flex,VatIncl,CityTaxIncl,OtherTaxesIncl,RoomName,RoomType,MaxPersons,MealTypeIncluded,Message\n9182,\"The Larkspur Hotel\",2026-10-05T07:00:19,2026-10-05,1,98.4,GBP,true,true,,,,\"Twin Room\",standard,2,5,\n9183,\"Quillfield House\",2026-10-05T07:00:23,2026-10-05,1,104.6,GBP,true,true,,,,\"Double Room\",standard,2,5,\n9184,\"Marrow Lane Inn\",2026-10-05T07:00:27,2026-10-05,1,0,GBP,false,false,,,,,,2,0,rates.soldout\n"
							}
						},
						"headers": {
							"X-RateLimit-Limit": {
								"$ref": "#/components/headers/XRateLimitLimit"
							},
							"X-RateLimit-Remaining": {
								"$ref": "#/components/headers/XRateLimitRemaining"
							},
							"X-RateLimit-Reset": {
								"$ref": "#/components/headers/XRateLimitReset"
							}
						}
					},
					"400": {
						"$ref": "#/components/responses/BadRequest"
					},
					"401": {
						"$ref": "#/components/responses/Unauthorized"
					},
					"403": {
						"$ref": "#/components/responses/Forbidden"
					},
					"405": {
						"$ref": "#/components/responses/MethodNotAllowed"
					},
					"429": {
						"$ref": "#/components/responses/RateLimited"
					},
					"500": {
						"$ref": "#/components/responses/InternalError"
					}
				}
			}
		},
		"/roomtyperates": {
			"get": {
				"operationId": "getRoomTypeRates",
				"tags": ["Rates"],
				"summary": "Get rates per room type",
				"description": "Works like **Get rates for a subscription**, but returns one rate per property, arrival date and room type, so you can compare doubles with doubles. Use it in place of Lighthouse's room type rates call.\n\n- Same parameters, fields and CSV header as `/rates`. `best_flex` is judged within each room type.\n- For each property and date, room types come in alphabetical order.\n- With `format=csv` the answer is `text/csv`. Booleans are `true` or `false` and null and empty text are an empty cell. A text cell that starts with `=`, `+`, `-` or `@` gets a leading `'` so a spreadsheet does not run it as a formula; numbers, negative ones included, are never changed.",
				"parameters": [
					{
						"$ref": "#/components/parameters/SubscriptionId"
					},
					{
						"$ref": "#/components/parameters/Los"
					},
					{
						"$ref": "#/components/parameters/Bar"
					},
					{
						"$ref": "#/components/parameters/Persons"
					},
					{
						"$ref": "#/components/parameters/MealType"
					},
					{
						"$ref": "#/components/parameters/RoomType"
					},
					{
						"$ref": "#/components/parameters/FromDate"
					},
					{
						"$ref": "#/components/parameters/ShopLength"
					},
					{
						"$ref": "#/components/parameters/Ota"
					},
					{
						"$ref": "#/components/parameters/CompsetIds"
					},
					{
						"$ref": "#/components/parameters/Currency"
					},
					{
						"$ref": "#/components/parameters/ChangeDays"
					},
					{
						"$ref": "#/components/parameters/Format"
					}
				],
				"responses": {
					"200": {
						"description": "One rate per property, arrival date and room type.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/RateList"
								},
								"example": {
									"rates": [
										{
											"hotelId": 9182,
											"hotelName": "The Larkspur Hotel",
											"extractDateTime": "2026-10-05T07:00:19",
											"arrivalDate": "2026-10-05",
											"los": 1,
											"value": 112.5,
											"currency": "GBP",
											"shopCurrency": "GBP",
											"cancellable": false,
											"best_flex": false,
											"vatIncl": null,
											"cityTaxIncl": null,
											"otherTaxesIncl": null,
											"roomName": "Superior Double Room",
											"roomType": "premium",
											"maxPersons": 2,
											"mealTypeIncluded": 1,
											"message": ""
										},
										{
											"hotelId": 9182,
											"hotelName": "The Larkspur Hotel",
											"extractDateTime": "2026-10-05T07:00:19",
											"arrivalDate": "2026-10-05",
											"los": 1,
											"value": 98.4,
											"currency": "GBP",
											"shopCurrency": "GBP",
											"cancellable": true,
											"best_flex": true,
											"vatIncl": null,
											"cityTaxIncl": null,
											"otherTaxesIncl": null,
											"roomName": "Twin Room",
											"roomType": "standard",
											"maxPersons": 2,
											"mealTypeIncluded": 5,
											"message": ""
										}
									]
								}
							},
							"text/csv": {
								"schema": {
									"type": "string",
									"description": "A header line, then one row per rate."
								},
								"example": "HotelID,HotelName,ExtractDateTime,ArrivalDate,Los,Value,Currency,Cancellable,Best_Flex,VatIncl,CityTaxIncl,OtherTaxesIncl,RoomName,RoomType,MaxPersons,MealTypeIncluded,Message\n9182,\"The Larkspur Hotel\",2026-10-05T07:00:19,2026-10-05,1,112.5,GBP,false,false,,,,\"Superior Double Room\",premium,2,1,\n9182,\"The Larkspur Hotel\",2026-10-05T07:00:19,2026-10-05,1,98.4,GBP,true,true,,,,\"Twin Room\",standard,2,5,\n"
							}
						},
						"headers": {
							"X-RateLimit-Limit": {
								"$ref": "#/components/headers/XRateLimitLimit"
							},
							"X-RateLimit-Remaining": {
								"$ref": "#/components/headers/XRateLimitRemaining"
							},
							"X-RateLimit-Reset": {
								"$ref": "#/components/headers/XRateLimitReset"
							}
						}
					},
					"400": {
						"$ref": "#/components/responses/BadRequest"
					},
					"401": {
						"$ref": "#/components/responses/Unauthorized"
					},
					"403": {
						"$ref": "#/components/responses/Forbidden"
					},
					"405": {
						"$ref": "#/components/responses/MethodNotAllowed"
					},
					"429": {
						"$ref": "#/components/responses/RateLimited"
					},
					"500": {
						"$ref": "#/components/responses/InternalError"
					}
				}
			}
		},
		"/liveupdate": {
			"post": {
				"operationId": "startLiveUpdate",
				"tags": ["Live shop"],
				"summary": "Start a live shop",
				"description": "Starts a fresh capture of a subscription. Beacon copies the report of the newest run of that reference your key can see, whatever its status: the same name, currency, stay length and properties, the same reference and the same time zone. It then starts the copy exactly as **Start a run** (`POST /v1/runs`) does, so the charge, the limit on runs in progress and every refusal are the same.\n\n- Needs a `read_write` key. A `read` key answers 403 `scope_read_only`.\n- Send `subscriptionId` in a JSON body, or as a query parameter. If both are sent, the body wins. A number is read as its digits.\n- The answer is 202 with the live shop and its `statusUrl`. Follow it with **Read a live shop** until `status` is `completed`, then read the rates from `/rates` as usual.\n- Send an `Idempotency-Key` header to make a retry safe: a repeat with the same key and the same `subscriptionId` gives the first answer again, byte for byte, and charges nothing again. The same key with another `subscriptionId` answers 422 `idempotency_mismatch`.",
				"parameters": [
					{
						"name": "subscriptionId",
						"in": "query",
						"required": false,
						"description": "The subscription to capture again: a run reference, 1 to 128 characters. Used when the JSON body has no `subscriptionId`.",
						"schema": {
							"type": "string"
						},
						"example": "40217"
					},
					{
						"name": "Idempotency-Key",
						"in": "header",
						"required": false,
						"description": "Any string of your choosing, up to 200 characters, to make a retry safe. A repeat with the same key and `subscriptionId` gives the first answer again and charges nothing again.",
						"schema": {
							"type": "string",
							"maxLength": 200
						}
					}
				],
				"requestBody": {
					"required": false,
					"content": {
						"application/json": {
							"schema": {
								"type": "object",
								"properties": {
									"subscriptionId": {
										"description": "The subscription to capture again: a run reference, 1 to 128 characters. A number is read as its digits.",
										"oneOf": [
											{
												"type": "integer"
											},
											{
												"type": "string"
											}
										]
									}
								}
							},
							"example": {
								"subscriptionId": 40217
							}
						}
					}
				},
				"responses": {
					"202": {
						"description": "The live shop was accepted and charged. `statusUrl` is where to follow it.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/LiveUpdate"
								},
								"example": {
									"liveupdateId": 1844,
									"subscriptionId": 40217,
									"status": "queued",
									"progress": {
										"total": 2920,
										"done": 0,
										"soldOut": 0,
										"failed": 0,
										"past": 0,
										"restricted": 0,
										"remaining": 2920
									},
									"requestedAt": "2026-10-15T08:30:00Z",
									"completedAt": null,
									"creditsCharged": 8,
									"statusUrl": "/lighthouse/v3/liveupdate/1844"
								}
							}
						}
					},
					"401": {
						"$ref": "#/components/responses/Unauthorized"
					},
					"402": {
						"description": "The company does not have enough credits for the run. Nothing was charged and no run was started.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								},
								"examples": {
									"credits": {
										"summary": "Not enough credits",
										"value": {
											"message": "Not enough credits to run this report: 8 needed, 3 available.",
											"errors": [
												{
													"status": 402,
													"code": "insufficient_credits",
													"title": "Payment required",
													"detail": "Not enough credits to run this report: 8 needed, 3 available."
												}
											]
										}
									}
								}
							}
						}
					},
					"403": {
						"description": "The key is read-only, or the request came from an address outside the key's allowed IPs.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								},
								"examples": {
									"scope": {
										"summary": "Read-only key",
										"value": {
											"message": "This API key is read-only.",
											"errors": [
												{
													"status": 403,
													"code": "scope_read_only",
													"title": "Permission denied",
													"detail": "This API key is read-only."
												}
											]
										}
									},
									"ip": {
										"summary": "Address not allowed",
										"value": {
											"message": "This API key may not be used from this address.",
											"errors": [
												{
													"status": 403,
													"code": "ip_not_allowed",
													"title": "Permission denied",
													"detail": "This API key may not be used from this address."
												}
											]
										}
									}
								}
							}
						}
					},
					"404": {
						"description": "Your key can see no run with this reference.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								},
								"examples": {
									"subscription": {
										"summary": "Unknown subscription",
										"value": {
											"message": "No such subscription.",
											"errors": [
												{
													"status": 404,
													"code": "not_found",
													"title": "Not found",
													"detail": "No such subscription."
												}
											]
										}
									}
								}
							}
						}
					},
					"405": {
						"description": "This path answers POST only; the status of a live shop is read with GET at its `statusUrl`. The `Allow` header says so.",
						"headers": {
							"Allow": {
								"$ref": "#/components/headers/Allow"
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								},
								"examples": {
									"method": {
										"summary": "Not a POST",
										"value": {
											"message": "This path answers POST only.",
											"errors": [
												{
													"status": 405,
													"code": "method_not_allowed",
													"title": "Method not allowed",
													"detail": "This path answers POST only."
												}
											]
										}
									}
								}
							}
						}
					},
					"409": {
						"description": "A request with the same `Idempotency-Key` is still being processed. Try again shortly.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								},
								"examples": {
									"in_progress": {
										"summary": "Same key still in progress",
										"value": {
											"message": "A request with this Idempotency-Key is still being processed; try again shortly.",
											"errors": [
												{
													"status": 409,
													"code": "idempotency_in_progress",
													"title": "Error",
													"detail": "A request with this Idempotency-Key is still being processed; try again shortly."
												}
											]
										}
									}
								}
							}
						}
					},
					"422": {
						"description": "`subscriptionId` is missing, empty or longer than 128 characters, or the `Idempotency-Key` was already used for another request.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								},
								"examples": {
									"subscriptionId": {
										"summary": "No subscriptionId",
										"value": {
											"message": "subscriptionId is required: the reference of a run, 1 to 128 characters.",
											"errors": [
												{
													"status": 422,
													"code": "validation_failed",
													"title": "Error",
													"detail": "subscriptionId is required: the reference of a run, 1 to 128 characters."
												}
											]
										}
									},
									"idempotency": {
										"summary": "Key used for another request",
										"value": {
											"message": "This Idempotency-Key was already used for a different request (another endpoint or another body).",
											"errors": [
												{
													"status": 422,
													"code": "idempotency_mismatch",
													"title": "Error",
													"detail": "This Idempotency-Key was already used for a different request (another endpoint or another body)."
												}
											]
										}
									}
								}
							}
						}
					},
					"429": {
						"description": "The company already has as many runs queued or running as it may (nothing was charged; start the live shop again when one has finished), or the key is over its request limit (wait for the number of seconds in `Retry-After`).",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								},
								"examples": {
									"active_runs": {
										"summary": "Too many runs in progress",
										"value": {
											"message": "The company already has as many queued or running runs as it may; start this one when one has finished.",
											"errors": [
												{
													"status": 429,
													"code": "too_many_active_runs",
													"title": "Too many requests",
													"detail": "The company already has as many queued or running runs as it may; start this one when one has finished."
												}
											]
										}
									},
									"per_key": {
										"summary": "Over the per-minute limit for this key",
										"value": {
											"message": "Too many requests for this API key. Wait a minute and try again.",
											"errors": [
												{
													"status": 429,
													"code": "rate_limited",
													"title": "Too many requests",
													"detail": "Too many requests for this API key. Wait a minute and try again."
												}
											]
										}
									}
								}
							}
						}
					},
					"500": {
						"$ref": "#/components/responses/InternalError"
					}
				}
			}
		},
		"/liveupdate/{id}": {
			"get": {
				"operationId": "getLiveUpdate",
				"tags": ["Live shop"],
				"summary": "Read a live shop",
				"description": "How far a live shop has got: its status and how many check-in dates are done. Any run your key can see can be read here, not only live shops. A `read` key is enough. The answer is never cached, as it moves while the run is capturing. Once `status` is `completed`, `/rates?subscriptionId=` serves the new rates.",
				"parameters": [
					{
						"name": "id",
						"in": "path",
						"required": true,
						"description": "The `liveupdateId` that **Start a live shop** answered.",
						"schema": {
							"type": "integer"
						},
						"example": 1844
					}
				],
				"responses": {
					"200": {
						"description": "The live shop.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/LiveUpdate"
								},
								"example": {
									"liveupdateId": 1844,
									"subscriptionId": 40217,
									"status": "running",
									"progress": {
										"total": 2920,
										"done": 1310,
										"soldOut": 84,
										"failed": 2,
										"past": 0,
										"restricted": 0,
										"remaining": 1524
									},
									"requestedAt": "2026-10-15T08:30:00Z",
									"completedAt": null,
									"creditsCharged": 8
								}
							}
						}
					},
					"401": {
						"$ref": "#/components/responses/Unauthorized"
					},
					"403": {
						"$ref": "#/components/responses/Forbidden"
					},
					"404": {
						"description": "There is no run with this id that your key can see.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								},
								"examples": {
									"id": {
										"summary": "Unknown live shop",
										"value": {
											"message": "No such live update.",
											"errors": [
												{
													"status": 404,
													"code": "not_found",
													"title": "Not found",
													"detail": "No such live update."
												}
											]
										}
									}
								}
							}
						}
					},
					"405": {
						"description": "This path answers GET only. The `Allow` header says so.",
						"headers": {
							"Allow": {
								"$ref": "#/components/headers/Allow"
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Error"
								},
								"examples": {
									"method": {
										"summary": "Not a GET",
										"value": {
											"message": "This path answers GET only.",
											"errors": [
												{
													"status": 405,
													"code": "method_not_allowed",
													"title": "Method not allowed",
													"detail": "This path answers GET only."
												}
											]
										}
									}
								}
							}
						}
					},
					"429": {
						"$ref": "#/components/responses/RateLimited"
					},
					"500": {
						"$ref": "#/components/responses/InternalError"
					}
				}
			}
		},
		"/{endpoint}": {
			"get": {
				"operationId": "notSupported",
				"tags": ["Hotels"],
				"summary": "Any other endpoint",
				"description": "Every other Lighthouse v3 endpoint answers 501 with a message that names the endpoint you asked for and the four that are supported. No key is needed to get this answer. A path outside `/lighthouse/v3/`, and the bare `/lighthouse/v3/`, answer 404 `endpoint_not_found`.",
				"security": [],
				"parameters": [
					{
						"name": "endpoint",
						"in": "path",
						"required": true,
						"description": "A Lighthouse v3 endpoint other than `hotels`, `rates`, `roomtyperates` and `liveupdate`.",
						"schema": {
							"type": "string"
						},
						"example": "parity"
					}
				],
				"responses": {
					"404": {
						"$ref": "#/components/responses/EndpointNotFound"
					},
					"501": {
						"$ref": "#/components/responses/NotSupported"
					}
				}
			}
		}
	},
	"components": {
		"securitySchemes": {
			"oiAuthorization": {
				"type": "apiKey",
				"in": "header",
				"name": "X-Oi-Authorization",
				"description": "Send a Beacon Pro API key in the `X-Oi-Authorization` header, the same header your Lighthouse client already uses: `X-Oi-Authorization: bp_<prefix>_<secret>`. A `Bearer ` prefix inside this header is tolerated.\n\nA key with the `read` scope is enough for every endpoint here except **Start a live shop** (`POST /liveupdate`), which starts a charged run and so needs a `read_write` key. Keys are created by a company owner or admin in the Beacon Pro web app, under Company > API. A key sees either only the runs it started, or every run in the company, depending on the visibility chosen when it was created."
			},
			"bearerAuth": {
				"type": "http",
				"scheme": "bearer",
				"bearerFormat": "bp_<8 characters>_<40 characters>",
				"description": "The same key can be sent as `Authorization: Bearer <key>` instead, as in the Rate Capture API. If both headers are sent, `Authorization: Bearer` is used."
			}
		},
		"parameters": {
			"SubscriptionId": {
				"name": "subscriptionId",
				"in": "query",
				"required": true,
				"description": "The subscription to read: a run reference, as sent to **Start a run** in the Rate Capture API. Missing answers 400 \"subscriptionId is required\".",
				"schema": {
					"type": "string"
				},
				"example": "larkspur-otterby"
			},
			"Los": {
				"name": "los",
				"in": "query",
				"required": false,
				"description": "Stay length in nights, 1 to 45. Only the run's own stay length has rates; any other value gives `general.missing` on every date.",
				"schema": {
					"type": "integer",
					"minimum": 1,
					"maximum": 45,
					"default": 1
				}
			},
			"Bar": {
				"name": "bar",
				"in": "query",
				"required": false,
				"description": "`true` keeps only the public flexible rate: free cancellation, and not a members-only or partner offer. Accepts `true`/`false` or `1`/`0`.",
				"schema": {
					"type": "boolean",
					"default": false
				}
			},
			"Persons": {
				"name": "persons",
				"in": "query",
				"required": false,
				"description": "Number of guests, 1 to 20. Keeps rooms that take at least this many.",
				"schema": {
					"type": "integer",
					"minimum": 1,
					"maximum": 20,
					"default": 2
				}
			},
			"MealType": {
				"name": "mealType",
				"in": "query",
				"required": false,
				"description": "Meal plan: `0` any, `1` breakfast, `2` half board, `3` full board, `4` all inclusive, `5` room only. Each number from 1 to 5 keeps only rates of exactly that meal plan, so `5` no longer includes half board, full board or all-inclusive rates. `2` keeps only half board rates: a `breakfast_lunch` rate is reported as `2` in `mealTypeIncluded` but is not returned by this filter. A plan with none of its rates on that date gives `rates.meal.missing.N`, where `N` is the number you asked for.",
				"schema": {
					"type": "integer",
					"enum": [0, 1, 2, 3, 4, 5],
					"default": 0
				}
			},
			"RoomType": {
				"name": "roomType",
				"in": "query",
				"required": false,
				"description": "Keep only this room type. Values outside this list answer 400. The values are `parkandfly`, `dormitory`, `sharedbathroom`, `camper`, `villa`, `bungalow`, `suite`, `apartment`, `family`, `premium`, `budget`, `single` and `standard`. This is Beacon's classification of the room from its name. Lighthouse sets room types per hotel in its app, so a room can get a different type here. When no room of this type has a rate on a date, `message` is `rates.roomtypes.unavailable`.",
				"schema": {
					"type": "string",
					"enum": [
						"parkandfly",
						"dormitory",
						"sharedbathroom",
						"camper",
						"villa",
						"bungalow",
						"suite",
						"apartment",
						"family",
						"premium",
						"budget",
						"single",
						"standard"
					]
				}
			},
			"FromDate": {
				"name": "fromDate",
				"in": "query",
				"required": false,
				"description": "First arrival date (YYYY-MM-DD). Defaults to today in the run's time zone (the `timeZone` it was started with).",
				"schema": {
					"type": "string",
					"format": "date"
				},
				"example": "2026-10-05"
			},
			"ShopLength": {
				"name": "shopLength",
				"in": "query",
				"required": false,
				"description": "Number of arrival dates from `fromDate`, 1 to 365. Defaults to every date up to the end of the run. Every date in the window is answered, and a date outside the run is a `general.missing` record.",
				"schema": {
					"type": "integer",
					"minimum": 1,
					"maximum": 365
				}
			},
			"Ota": {
				"name": "ota",
				"in": "query",
				"required": false,
				"description": "The source to read rates from: `bookingdotcom`, `travelodge`, `premierinn` or `branddotcom`, which covers the Travelodge and Premier Inn sites. A property on a different source from the one asked for gives `general.missing`.",
				"schema": {
					"type": "string",
					"pattern": "^[a-z0-9_]{1,32}$",
					"default": "bookingdotcom"
				}
			},
			"CompsetIds": {
				"name": "compsetIds",
				"in": "query",
				"required": false,
				"description": "A comma-separated list of compset ids, such as `-1,1`. Only set `1` exists, and it holds every property in the run. A list that holds `1` is answered as set `1`. A list without `1` answers 400.",
				"schema": {
					"type": "string",
					"default": "1",
					"example": "-1,1"
				}
			},
			"Currency": {
				"name": "currency",
				"in": "query",
				"required": false,
				"description": "The currency of the subscription, such as `GBP`. Beacon does not convert, so any other currency answers 400. Read `currency` on each rate.",
				"schema": {
					"type": "string"
				}
			},
			"ChangeDays": {
				"name": "changeDays",
				"in": "query",
				"required": false,
				"description": "Accepted and ignored. Change history is not returned.",
				"schema": {
					"type": "string"
				}
			},
			"Format": {
				"name": "format",
				"in": "query",
				"required": false,
				"description": "`json` or `csv`. Anything else answers 400.",
				"schema": {
					"type": "string",
					"enum": ["json", "csv"],
					"default": "json"
				}
			}
		},
		"headers": {
			"WWWAuthenticate": {
				"description": "Sent with every 401, and always `Bearer`.",
				"schema": {
					"type": "string"
				},
				"example": "Bearer"
			},
			"RetryAfter": {
				"description": "Seconds to wait before trying again.",
				"schema": {
					"type": "integer"
				},
				"example": 60
			},
			"Allow": {
				"description": "The methods this path accepts.",
				"schema": {
					"type": "string"
				},
				"example": "GET"
			},
			"XRateLimitLimit": {
				"description": "Reads allowed a minute for this key: 600, unless Beacon has raised it.",
				"schema": {
					"type": "integer"
				},
				"example": 600
			},
			"XRateLimitRemaining": {
				"description": "Reads left in the current minute. `0` on a 429.",
				"schema": {
					"type": "integer"
				},
				"example": 598
			},
			"XRateLimitReset": {
				"description": "Seconds until the current minute ends and the count restarts.",
				"schema": {
					"type": "integer"
				},
				"example": 41
			}
		},
		"responses": {
			"BadRequest": {
				"description": "A parameter is missing, out of range or, for `currency`, not the subscription's. The message names it.",
				"content": {
					"application/json": {
						"schema": {
							"$ref": "#/components/schemas/Error"
						},
						"examples": {
							"subscriptionId": {
								"summary": "No subscriptionId",
								"value": {
									"message": "subscriptionId is required",
									"errors": [
										{
											"status": 400,
											"code": "invalid_filter",
											"title": "Bad request",
											"detail": "subscriptionId is required"
										}
									]
								}
							},
							"los": {
								"summary": "Stay length out of range",
								"value": {
									"message": "los must be a whole number from 1 to 45.",
									"errors": [
										{
											"status": 400,
											"code": "invalid_filter",
											"title": "Bad request",
											"detail": "los must be a whole number from 1 to 45."
										}
									]
								}
							},
							"compsetIds": {
								"summary": "A compset list without 1",
								"value": {
									"message": "compsetIds only supports 1 (every competitor of the subscription).",
									"errors": [
										{
											"status": 400,
											"code": "invalid_filter",
											"title": "Bad request",
											"detail": "compsetIds only supports 1 (every competitor of the subscription)."
										}
									]
								}
							},
							"format": {
								"summary": "Unknown format",
								"value": {
									"message": "format must be json or csv.",
									"errors": [
										{
											"status": 400,
											"code": "invalid_format",
											"title": "Bad request",
											"detail": "format must be json or csv."
										}
									]
								}
							},
							"page": {
								"summary": "Bad page (hotels)",
								"value": {
									"message": "page must be a whole number of 1 or more.",
									"errors": [
										{
											"status": 400,
											"code": "invalid_page",
											"title": "Bad request",
											"detail": "page must be a whole number of 1 or more."
										}
									]
								}
							},
							"per_page": {
								"summary": "Bad per_page (hotels)",
								"value": {
									"message": "per_page must be a whole number from 1 to 100.",
									"errors": [
										{
											"status": 400,
											"code": "invalid_per_page",
											"title": "Bad request",
											"detail": "per_page must be a whole number from 1 to 100."
										}
									]
								}
							},
							"currency": {
								"summary": "Currency other than the subscription's",
								"value": {
									"message": "Beacon does not convert currencies; this subscription is priced in GBP",
									"errors": [
										{
											"status": 400,
											"code": "invalid_filter",
											"title": "Bad request",
											"detail": "Beacon does not convert currencies; this subscription is priced in GBP"
										}
									]
								}
							},
							"page_end": {
								"summary": "Page past the end (hotels)",
								"value": {
									"message": "page is past the last page (1).",
									"errors": [
										{
											"status": 400,
											"code": "invalid_page",
											"title": "Bad request",
											"detail": "page is past the last page (1)."
										}
									]
								}
							}
						}
					}
				},
				"headers": {
					"X-RateLimit-Limit": {
						"$ref": "#/components/headers/XRateLimitLimit"
					},
					"X-RateLimit-Remaining": {
						"$ref": "#/components/headers/XRateLimitRemaining"
					},
					"X-RateLimit-Reset": {
						"$ref": "#/components/headers/XRateLimitReset"
					}
				}
			},
			"Unauthorized": {
				"description": "The key was not accepted: none was sent, it is not one Beacon issued, or it was revoked, has expired, or belongs to a person or company that is no longer active.",
				"headers": {
					"WWW-Authenticate": {
						"$ref": "#/components/headers/WWWAuthenticate"
					}
				},
				"content": {
					"application/json": {
						"schema": {
							"$ref": "#/components/schemas/Error"
						},
						"examples": {
							"missing": {
								"summary": "No key sent",
								"value": {
									"message": "Send your API key as \"Authorization: Bearer <key>\" (or \"X-Oi-Authorization: <key>\").",
									"errors": [
										{
											"status": 401,
											"code": "missing_token",
											"title": "Unauthorized",
											"detail": "Send your API key as \"Authorization: Bearer <key>\" (or \"X-Oi-Authorization: <key>\")."
										}
									]
								}
							},
							"invalid": {
								"summary": "Key not recognised",
								"value": {
									"message": "The API key is not recognised.",
									"errors": [
										{
											"status": 401,
											"code": "invalid_token",
											"title": "Unauthorized",
											"detail": "The API key is not recognised."
										}
									]
								}
							},
							"revoked": {
								"summary": "Key revoked",
								"value": {
									"message": "This API key has been revoked.",
									"errors": [
										{
											"status": 401,
											"code": "key_revoked",
											"title": "Unauthorized",
											"detail": "This API key has been revoked."
										}
									]
								}
							}
						}
					}
				}
			},
			"Forbidden": {
				"description": "The request came from an address outside the key's allowed IPs.",
				"content": {
					"application/json": {
						"schema": {
							"$ref": "#/components/schemas/Error"
						},
						"examples": {
							"ip": {
								"summary": "Address not allowed",
								"value": {
									"message": "This API key may not be used from this address.",
									"errors": [
										{
											"status": 403,
											"code": "ip_not_allowed",
											"title": "Permission denied",
											"detail": "This API key may not be used from this address."
										}
									]
								}
							}
						}
					}
				}
			},
			"RateLimited": {
				"description": "Too many requests. Every request here counts as a read: 600 a minute per key, unless Beacon has raised it. Separately, 20 unrecognised keys an hour from one IP address are allowed. Wait for the number of seconds in `Retry-After`, then try again.",
				"headers": {
					"Retry-After": {
						"$ref": "#/components/headers/RetryAfter"
					},
					"X-RateLimit-Limit": {
						"$ref": "#/components/headers/XRateLimitLimit"
					},
					"X-RateLimit-Remaining": {
						"$ref": "#/components/headers/XRateLimitRemaining"
					},
					"X-RateLimit-Reset": {
						"$ref": "#/components/headers/XRateLimitReset"
					}
				},
				"content": {
					"application/json": {
						"schema": {
							"$ref": "#/components/schemas/Error"
						},
						"examples": {
							"per_key": {
								"summary": "Over the per-minute limit for this key",
								"value": {
									"message": "Too many requests for this API key. Wait a minute and try again.",
									"errors": [
										{
											"status": 429,
											"code": "rate_limited",
											"title": "Too many requests",
											"detail": "Too many requests for this API key. Wait a minute and try again."
										}
									]
								}
							}
						}
					}
				}
			},
			"MethodNotAllowed": {
				"description": "These endpoints answer GET only. The `Allow` header says so.",
				"headers": {
					"Allow": {
						"$ref": "#/components/headers/Allow"
					}
				},
				"content": {
					"application/json": {
						"schema": {
							"$ref": "#/components/schemas/Error"
						},
						"examples": {
							"method": {
								"summary": "Not a GET",
								"value": {
									"message": "This path answers GET only.",
									"errors": [
										{
											"status": 405,
											"code": "method_not_allowed",
											"title": "Method not allowed",
											"detail": "This path answers GET only."
										}
									]
								}
							}
						}
					}
				}
			},
			"InternalError": {
				"description": "Something went wrong on Beacon's side, and it has been logged. It is safe to try again.",
				"content": {
					"application/json": {
						"schema": {
							"$ref": "#/components/schemas/Error"
						},
						"examples": {
							"internal": {
								"summary": "Server error",
								"value": {
									"message": "Something went wrong. The problem has been logged; please try again.",
									"errors": [
										{
											"status": 500,
											"code": "internal_error",
											"title": "Internal server error",
											"detail": "Something went wrong. The problem has been logged; please try again."
										}
									]
								}
							}
						}
					}
				}
			},
			"NotSupported": {
				"description": "Beacon Pro does not have this Lighthouse endpoint. Only `hotels`, `rates` and `roomtyperates` are supported.",
				"content": {
					"application/json": {
						"schema": {
							"$ref": "#/components/schemas/Error"
						},
						"examples": {
							"endpoint": {
								"summary": "Endpoint not supported",
								"value": {
									"message": "Not supported by Beacon Pro: /lighthouse/v3/parity. Supported: hotels, rates, roomtyperates.",
									"errors": [
										{
											"status": 501,
											"code": "not_supported",
											"title": "Not implemented",
											"detail": "Not supported by Beacon Pro: /lighthouse/v3/parity. Supported: hotels, rates, roomtyperates."
										}
									]
								}
							}
						}
					}
				}
			},
			"EndpointNotFound": {
				"description": "The path is not under `/lighthouse/v3/`, or it is the bare base path with no endpoint.",
				"content": {
					"application/json": {
						"schema": {
							"$ref": "#/components/schemas/Error"
						},
						"examples": {
							"path": {
								"summary": "No such endpoint",
								"value": {
									"message": "No such Lighthouse endpoint. Beacon Pro serves /lighthouse/v3/ only: hotels, rates, roomtyperates.",
									"errors": [
										{
											"status": 404,
											"code": "endpoint_not_found",
											"title": "Not found",
											"detail": "No such Lighthouse endpoint. Beacon Pro serves /lighthouse/v3/ only: hotels, rates, roomtyperates."
										}
									]
								}
							}
						}
					}
				}
			}
		},
		"schemas": {
			"Error": {
				"type": "object",
				"description": "Errors keep their HTTP status and use Lighthouse's shape: a `message` and an `errors` list with one entry. The message is the same text as `errors[0].detail`, and it is the same message the Rate Capture API would give.",
				"required": ["message", "errors"],
				"properties": {
					"message": {
						"type": "string",
						"example": "subscriptionId is required"
					},
					"errors": {
						"type": "array",
						"description": "One entry for the error.",
						"items": {
							"type": "object",
							"required": ["status", "code", "title", "detail"],
							"properties": {
								"status": {
									"type": "integer",
									"description": "The HTTP status.",
									"example": 400
								},
								"code": {
									"type": "string",
									"description": "Beacon's error code, such as `invalid_filter`.",
									"example": "invalid_filter"
								},
								"title": {
									"type": "string",
									"description": "A short title for the status.",
									"example": "Bad request"
								},
								"detail": {
									"type": "string",
									"description": "The same text as `message`.",
									"example": "subscriptionId is required"
								}
							}
						}
					}
				}
			},
			"Competitor": {
				"type": "object",
				"required": ["id", "name", "stars"],
				"properties": {
					"id": {
						"type": "integer",
						"description": "Beacon's stable id for the property."
					},
					"name": {
						"type": "string",
						"description": "The property's name."
					},
					"stars": {
						"type": "integer",
						"nullable": true,
						"description": "The property's star rating from its profile when known, otherwise null."
					}
				}
			},
			"CompetitorSet": {
				"type": "object",
				"required": ["id", "name", "competitor_ids"],
				"properties": {
					"id": {
						"type": "integer",
						"description": "Always `1`.",
						"example": 1
					},
					"name": {
						"type": "string",
						"description": "Always \"All\".",
						"example": "All"
					},
					"competitor_ids": {
						"type": "array",
						"items": {
							"type": "integer"
						},
						"description": "Every competitor in the run."
					}
				}
			},
			"SubscribedFeature": {
				"type": "object",
				"required": ["id", "otas"],
				"properties": {
					"id": {
						"type": "string",
						"description": "The feature, such as `rates`.",
						"example": "rates"
					},
					"otas": {
						"type": "array",
						"items": {
							"type": "string"
						},
						"description": "The sources of the run's properties, such as `bookingdotcom`."
					}
				}
			},
			"Hotel": {
				"type": "object",
				"description": "One subscription: the run's first property, with the rest of the run as its competitors.",
				"required": ["id", "name", "stars", "brand_code", "subscription_id", "competitors", "competitor_sets", "subscribed_features"],
				"properties": {
					"id": {
						"type": "integer",
						"description": "Beacon's stable id for the subscription's own property (the run's first property)."
					},
					"name": {
						"type": "string",
						"description": "The property's name."
					},
					"stars": {
						"type": "integer",
						"nullable": true,
						"description": "The property's star rating from its profile when known, otherwise null."
					},
					"brand_code": {
						"type": "string",
						"description": "Premier Inn brand code from the property profile: `PI`, `PID`, `HUB` or `ZIP`. Empty for every other property, and when the profile has no brand."
					},
					"subscription_id": {
						"description": "The run reference to pass as `subscriptionId`. A number when the reference is all digits, a string otherwise.",
						"oneOf": [
							{
								"type": "integer"
							},
							{
								"type": "string"
							}
						]
					},
					"competitors": {
						"type": "array",
						"items": {
							"$ref": "#/components/schemas/Competitor"
						},
						"description": "The run's other properties."
					},
					"competitor_sets": {
						"type": "array",
						"items": {
							"$ref": "#/components/schemas/CompetitorSet"
						},
						"description": "One set, id `1` \"All\", holding every competitor."
					},
					"subscribed_features": {
						"type": "array",
						"items": {
							"$ref": "#/components/schemas/SubscribedFeature"
						},
						"description": "One entry, `rates`, listing the run's sources."
					}
				}
			},
			"HotelList": {
				"type": "object",
				"required": ["hotels"],
				"properties": {
					"hotels": {
						"type": "array",
						"items": {
							"$ref": "#/components/schemas/Hotel"
						}
					},
					"meta": {
						"type": "object",
						"required": ["page", "per_page", "total_pages", "total_results"],
						"properties": {
							"page": {
								"type": "integer"
							},
							"per_page": {
								"type": "integer"
							},
							"total_pages": {
								"type": "integer"
							},
							"total_results": {
								"type": "integer"
							}
						}
					}
				},
				"description": "`meta` is only present when the request sent `page`."
			},
			"Rate": {
				"type": "object",
				"description": "The rate chosen for one property and arrival date (and room type, on `/roomtyperates`). When there is no rate, `message` says why, `value` is `0`, `cancellable` and `best_flex` are `false`, `roomName` and `roomType` are empty text, and `mealTypeIncluded` is `0`. Only the three tax flags are null.",
				"required": [
					"hotelId",
					"hotelName",
					"extractDateTime",
					"arrivalDate",
					"los",
					"value",
					"currency",
					"shopCurrency",
					"cancellable",
					"best_flex",
					"vatIncl",
					"cityTaxIncl",
					"otherTaxesIncl",
					"roomName",
					"roomType",
					"maxPersons",
					"mealTypeIncluded",
					"message"
				],
				"properties": {
					"hotelId": {
						"type": "integer",
						"nullable": true,
						"description": "Beacon's stable id for the property, as in `/hotels`."
					},
					"hotelName": {
						"type": "string",
						"description": "The property's name."
					},
					"extractDateTime": {
						"type": "string",
						"description": "When this date was fetched, in UTC, written without a `Z` as Lighthouse does (`YYYY-MM-DDTHH:MM:SS`). Never null: a date that was never fetched shows the time the run finished.",
						"example": "2026-10-05T07:00:19"
					},
					"arrivalDate": {
						"type": "string",
						"format": "date",
						"description": "Arrival (check-in) date."
					},
					"los": {
						"type": "integer",
						"description": "Stay length in nights."
					},
					"value": {
						"type": "number",
						"description": "The chosen price for the whole stay. `0` when `message` is set."
					},
					"currency": {
						"type": "string",
						"description": "The currency of the chosen rate, or the report's currency when there is none."
					},
					"shopCurrency": {
						"type": "string",
						"description": "The same as `currency`."
					},
					"cancellable": {
						"type": "boolean",
						"description": "True when the booking can be cancelled free of charge. `false` when `message` is set."
					},
					"best_flex": {
						"type": "boolean",
						"description": "True when this rate can be cancelled free of charge and is the cheapest such rate for the property and date, after the `persons`, `mealType` and `bar` filters. On `/roomtyperates` and with a `roomType` filter, it is judged within the room type. `false` when `message` is set."
					},
					"vatIncl": {
						"type": "boolean",
						"nullable": true,
						"description": "Always null: tax is not captured."
					},
					"cityTaxIncl": {
						"type": "boolean",
						"nullable": true,
						"description": "`true` on Premier Inn rates, whose price includes city tax. Null for every other source."
					},
					"otherTaxesIncl": {
						"type": "boolean",
						"nullable": true,
						"description": "Always null: tax is not captured."
					},
					"roomName": {
						"type": "string",
						"description": "The room's name as the source shows it. Empty text when `message` is set."
					},
					"roomType": {
						"type": "string",
						"description": "The room type read from the room's name, using Lighthouse's values, the same as the `roomType` filter (see that parameter for how names map to types). Empty text when `message` is set."
					},
					"maxPersons": {
						"type": "integer",
						"description": "The most guests the room takes. When `message` is set, the `persons` you asked for."
					},
					"mealTypeIncluded": {
						"type": "integer",
						"description": "The Lighthouse code of the rate's meal plan: `1` breakfast, `2` half board, `3` full board, `4` all inclusive, `5` room only. A `breakfast_lunch` rate is reported as `2`, because Lighthouse has no lunch category and bundles it with half board. A `lunch_dinner` rate is also reported as `2` (two meals = half board), and `dinner` and `lunch` are reported as `0` ('no meal type included'); those three are for Lighthouse compatibility only, so the `mealType=2` filter still means exactly half board and `mealType=0` still means any. `0` when there is no rate."
					},
					"message": {
						"type": "string",
						"description": "Empty text when there is a rate. Otherwise why there is none: `rates.soldout`, `rates.restrictionlosN`, `general.missing`, `rates.no_bar`, `rates.no_person.N`, `rates.meal.missing.N` or `rates.roomtypes.unavailable`. `rates.roomtypes.unavailable` means you sent a `roomType` and no room of that type has a rate on that date. `rates.restrictionlosN` is Lighthouse's code: 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` for a minimum stay of 4 nights. A minimum stay with no number of nights is `general.missing`. `general.missing` also answers a date the run does not cover when `shopLength` is sent."
					}
				}
			},
			"RateList": {
				"type": "object",
				"required": ["rates"],
				"properties": {
					"rates": {
						"type": "array",
						"items": {
							"$ref": "#/components/schemas/Rate"
						}
					},
					"message": {
						"type": "string",
						"description": "Only when the `subscriptionId` matched nothing your key can read: `general.missing`, with an empty `rates` list.",
						"example": "general.missing"
					}
				}
			},
			"LiveUpdate": {
				"type": "object",
				"description": "A live shop: a Rate Capture run of a copy of the subscription's report.",
				"required": [
					"liveupdateId",
					"subscriptionId",
					"status",
					"progress",
					"requestedAt",
					"completedAt",
					"creditsCharged"
				],
				"properties": {
					"liveupdateId": {
						"type": "integer",
						"description": "The live shop's id: the id of its run, the same as in the Rate Capture API."
					},
					"subscriptionId": {
						"description": "The run reference. A number when the reference is all digits, a string otherwise.",
						"oneOf": [
							{
								"type": "integer"
							},
							{
								"type": "string"
							}
						]
					},
					"status": {
						"type": "string",
						"enum": ["queued", "running", "completed"],
						"description": "`queued` (waiting to start), `running` or `completed`."
					},
					"progress": {
						"type": "object",
						"description": "Check-in dates, counted as in the Rate Capture API's run.",
						"required": [
							"total",
							"done",
							"soldOut",
							"failed",
							"past",
							"restricted",
							"remaining"
						],
						"properties": {
							"total": {
								"type": "integer",
								"description": "How many check-in dates the run fetches in total: the number of properties times 365."
							},
							"done": {
								"type": "integer",
								"description": "Check-in dates fetched that had rooms."
							},
							"soldOut": {
								"type": "integer",
								"description": "Check-in dates fetched that were sold out."
							},
							"failed": {
								"type": "integer",
								"description": "Check-in dates that could not be fetched."
							},
							"past": {
								"type": "integer",
								"description": "Check-in dates that had already passed, by today's date in the run's time zone, before they were fetched."
							},
							"restricted": {
								"type": "integer",
								"description": "Check-in dates the property needs a longer minimum stay for, so no rate can be booked."
							},
							"remaining": {
								"type": "integer",
								"description": "Check-in dates still to fetch. Zero when nothing is left."
							}
						}
					},
					"requestedAt": {
						"type": "string",
						"format": "date-time",
						"description": "When the live shop was accepted (UTC)."
					},
					"completedAt": {
						"type": "string",
						"format": "date-time",
						"nullable": true,
						"description": "When it completed (UTC). Null until then."
					},
					"creditsCharged": {
						"type": "integer",
						"description": "Credits charged when the live shop was accepted. 0 for a company with unlimited credits."
					},
					"statusUrl": {
						"type": "string",
						"description": "Only in the answer to **Start a live shop**: the path to read it at."
					}
				}
			}
		}
	}
}
