{
	"openapi": "3.0.3",
	"info": {
		"title": "Rate Capture API",
		"version": "1.0.0",
		"description": "The Rate Capture API lets you capture rates from your own systems. You start a run for a list of properties, and Rate Capture checks their room rates and availability for every check-in date over the next 365 days. When the run has finished you can download every room and rate it found, or ask for the cheapest rate per property and day.\n\nIt is for teams who want these rates inside their own tools, such as a revenue system, a data warehouse or a scheduled script, rather than downloading them from the web app.\n\n**Times and dates.** Every time and date in the API is UTC, with one exception: a run's check-in window starts on today's date in the run's time zone. That is your company's time zone (Europe/London unless changed in the web app), or the one you send as `timeZone` when you start the run.\n\nNew to the API? Start with the [guides](../), which cover getting a key, a first run and reading the results."
	},
	"servers": [
		{
			"url": "/v1",
			"description": "This host (Beacon Pro is HTTPS only)"
		}
	],
	"security": [
		{
			"bearerAuth": []
		}
	],
	"tags": [
		{
			"name": "Credits",
			"description": "Each run costs one credit per property, charged as soon as the run is accepted (the 202 answer already shows `creditsCharged`). Retrying failed check-in dates is free. Credits are bought in the Beacon Pro web app; the API lets you check your balance."
		},
		{
			"name": "Reports",
			"description": "A report is the list of properties to check, with the currency and stay length. You only need to create reports here if you want to set one up first and start it later; otherwise **Start a run** creates the report and starts it in one call. A report can be run only once, so to check the same properties again, create a new report or start a new run. Listing and reading reports also shows ones made in the web app, with a key that can see all company runs."
		},
		{
			"name": "Runs",
			"description": "A run is one capture of a report. Rate Capture checks every property for each of the 365 check-in dates starting on today's date in the run's time zone, at the report's stay length and in its currency. A run usually takes minutes rather than seconds, so start it, then check its progress until `complete` is true.\n\nA queued run cannot be stopped, and there are no recurring runs: start each run yourself, for example from a scheduled job. Each run can carry your own `reference`, which is how you find it again, and how the Lighthouse-compatible API finds its rates."
		},
		{
			"name": "Results",
			"description": "There are two ways to read a run. **List a run's results** gives every room and rate it found, row by row, and works while the run is still going. **Get the cheapest rate per day** picks one rate per property and check-in date from a finished run, with filters such as guests and meal plan.\n\nOnce you have stored the results, delete them from Beacon with **Delete a run's results**. Your company may also have a retention period after which rows are deleted anyway."
		}
	],
	"x-tagGroups": [
		{
			"name": "Start runs",
			"tags": ["Credits", "Reports", "Runs"]
		},
		{
			"name": "Read the rates",
			"tags": ["Results"]
		}
	],
	"paths": {
		"/credits": {
			"get": {
				"operationId": "getCredits",
				"tags": ["Credits"],
				"summary": "Get your credit balance",
				"description": "Shows how many credits your company has left, so you can check there is enough before you start a run. A run costs one credit per property, charged as soon as the run is accepted (the 202 answer already shows `creditsCharged`).\n\n- Works with a read key.\n- Retrying failed check-in dates is free.\n- Credits are bought in the Beacon Pro web app, not through the API.",
				"responses": {
					"200": {
						"description": "Your company's credit balance.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Credits"
								},
								"examples": {
									"normal": {
										"summary": "A company that pays per run",
										"value": {
											"companyId": 418,
											"balance": 240,
											"unlimited": false
										}
									},
									"unlimited": {
										"summary": "A company with unlimited credits",
										"value": {
											"companyId": 418,
											"balance": 0,
											"unlimited": true
										}
									}
								}
							}
						}
					},
					"401": {
						"$ref": "#/components/responses/Unauthorized"
					},
					"403": {
						"$ref": "#/components/responses/Forbidden"
					},
					"429": {
						"$ref": "#/components/responses/RateLimited"
					},
					"500": {
						"$ref": "#/components/responses/InternalError"
					}
				}
			}
		},
		"/reports": {
			"get": {
				"operationId": "listReports",
				"tags": ["Reports"],
				"summary": "List reports",
				"description": "Lists the reports your key can see, newest first, each with the progress of its run. Use it to find a report you created earlier, or to see at a glance which reports have been run.\n\n- Works with a read key.\n- A key with `own` visibility sees only the reports it created. A key with `company` visibility sees every report in the company, including those made in the web app.\n- Reports come in pages of up to 200. Pass the `nextCursor` of one page as `cursor` to get the next. `nextCursor` is null on the last page.",
				"parameters": [
					{
						"$ref": "#/components/parameters/ListLimit"
					},
					{
						"$ref": "#/components/parameters/Cursor"
					}
				],
				"responses": {
					"200": {
						"description": "A page of reports, newest first.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ReportList"
								},
								"example": {
									"reports": [
										{
											"id": 5120,
											"name": "Otterby competitors",
											"currency": "GBP",
											"nights": 1,
											"createdAt": "2026-10-05T07:00:04Z",
											"origin": "api",
											"reference": null,
											"properties": [
												{
													"id": 30411,
													"name": "The Larkspur Hotel",
													"url": "https://www.booking.com/hotel/gb/the-larkspur-otterby.html",
													"source": "booking",
													"position": 0
												},
												{
													"id": 30412,
													"name": "Quillfield House",
													"url": "https://www.booking.com/hotel/gb/quillfield-house-otterby.html",
													"source": "booking",
													"position": 1
												},
												{
													"id": 30413,
													"name": "Marrow Lane Inn",
													"url": "https://www.booking.com/hotel/gb/marrow-lane-inn-otterby.html",
													"source": "booking",
													"position": 2
												}
											],
											"latestRun": {
												"id": 7731,
												"status": "completed",
												"complete": true,
												"requestsTotal": 1095,
												"done": 1080,
												"soldOut": 11,
												"failed": 4,
												"past": 0,
												"restricted": 0,
												"remaining": 0,
												"startedAt": "2026-10-05T07:00:04Z",
												"finishedAt": "2026-10-05T07:52:41Z"
											}
										}
									],
									"nextCursor": null
								}
							}
						}
					},
					"401": {
						"$ref": "#/components/responses/Unauthorized"
					},
					"403": {
						"$ref": "#/components/responses/Forbidden"
					},
					"422": {
						"$ref": "#/components/responses/InvalidQuery"
					},
					"429": {
						"$ref": "#/components/responses/RateLimited"
					},
					"500": {
						"$ref": "#/components/responses/InternalError"
					}
				}
			},
			"post": {
				"operationId": "createReport",
				"tags": ["Reports"],
				"summary": "Create a report",
				"description": "Saves a list of properties, with a currency and a stay length, without starting a run. Use it when you want to set a report up now and start it later with **Start a report's run**. Most integrations can skip this and use **Start a run**, which does both in one call.\n\n- Needs a `read_write` key.\n- Costs nothing. Credits are charged only when the report's run is accepted.\n- Up to 50 properties by default; your company's own figure is in the web app under Company > API. A URL listed twice is kept once.\n- Send an `Idempotency-Key` so that a retry after a timeout cannot create a second report.",
				"parameters": [
					{
						"$ref": "#/components/parameters/IdempotencyKey"
					}
				],
				"requestBody": {
					"required": true,
					"content": {
						"application/json": {
							"schema": {
								"$ref": "#/components/schemas/CreateReportRequest"
							},
							"example": {
								"name": "Otterby weekend competitors",
								"currency": "GBP",
								"nights": 2,
								"properties": [
									{
										"url": "https://www.booking.com/hotel/gb/the-larkspur-otterby.html",
										"name": "The Larkspur Hotel"
									},
									{
										"url": "https://www.booking.com/hotel/gb/quillfield-house-otterby.html",
										"name": "Quillfield House"
									},
									{
										"url": "https://www.booking.com/hotel/gb/marrow-lane-inn-otterby.html",
										"name": "Marrow Lane Inn"
									}
								]
							}
						}
					}
				},
				"responses": {
					"201": {
						"description": "The new report. `latestRun` is null because it has not been run.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Report"
								},
								"example": {
									"id": 5134,
									"name": "Otterby weekend competitors",
									"currency": "GBP",
									"nights": 2,
									"createdAt": "2026-10-05T09:02:47Z",
									"origin": "api",
									"reference": null,
									"properties": [
										{
											"id": 30452,
											"name": "The Larkspur Hotel",
											"url": "https://www.booking.com/hotel/gb/the-larkspur-otterby.html",
											"source": "booking",
											"position": 0
										},
										{
											"id": 30453,
											"name": "Quillfield House",
											"url": "https://www.booking.com/hotel/gb/quillfield-house-otterby.html",
											"source": "booking",
											"position": 1
										},
										{
											"id": 30454,
											"name": "Marrow Lane Inn",
											"url": "https://www.booking.com/hotel/gb/marrow-lane-inn-otterby.html",
											"source": "booking",
											"position": 2
										}
									],
									"latestRun": null
								}
							}
						}
					},
					"401": {
						"$ref": "#/components/responses/Unauthorized"
					},
					"403": {
						"$ref": "#/components/responses/Forbidden"
					},
					"409": {
						"$ref": "#/components/responses/IdempotencyInProgress"
					},
					"422": {
						"$ref": "#/components/responses/ValidationFailed"
					},
					"429": {
						"$ref": "#/components/responses/RateLimited"
					},
					"500": {
						"$ref": "#/components/responses/InternalError"
					}
				}
			}
		},
		"/reports/{id}": {
			"get": {
				"operationId": "getReport",
				"tags": ["Reports"],
				"summary": "Get a report",
				"description": "Returns one report, with its properties and the progress of its run. Use it to check what a report contains, or whether it has been run yet.\n\n- Works with a read key.\n- A report that does not exist, or that your key cannot see, answers 404 `not_found`.",
				"parameters": [
					{
						"$ref": "#/components/parameters/ReportId"
					}
				],
				"responses": {
					"200": {
						"description": "The report.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Report"
								},
								"example": {
									"id": 5120,
									"name": "Otterby competitors",
									"currency": "GBP",
									"nights": 1,
									"createdAt": "2026-10-05T07:00:04Z",
									"origin": "api",
									"reference": null,
									"properties": [
										{
											"id": 30411,
											"name": "The Larkspur Hotel",
											"url": "https://www.booking.com/hotel/gb/the-larkspur-otterby.html",
											"source": "booking",
											"position": 0
										},
										{
											"id": 30412,
											"name": "Quillfield House",
											"url": "https://www.booking.com/hotel/gb/quillfield-house-otterby.html",
											"source": "booking",
											"position": 1
										},
										{
											"id": 30413,
											"name": "Marrow Lane Inn",
											"url": "https://www.booking.com/hotel/gb/marrow-lane-inn-otterby.html",
											"source": "booking",
											"position": 2
										}
									],
									"latestRun": {
										"id": 7731,
										"status": "completed",
										"complete": true,
										"requestsTotal": 1095,
										"done": 1080,
										"soldOut": 11,
										"failed": 4,
										"past": 0,
										"restricted": 0,
										"remaining": 0,
										"startedAt": "2026-10-05T07:00:04Z",
										"finishedAt": "2026-10-05T07:52:41Z"
									}
								}
							}
						}
					},
					"401": {
						"$ref": "#/components/responses/Unauthorized"
					},
					"403": {
						"$ref": "#/components/responses/Forbidden"
					},
					"404": {
						"$ref": "#/components/responses/NotFound"
					},
					"429": {
						"$ref": "#/components/responses/RateLimited"
					},
					"500": {
						"$ref": "#/components/responses/InternalError"
					}
				}
			}
		},
		"/reports/{id}/runs": {
			"post": {
				"operationId": "startReportRun",
				"tags": ["Reports"],
				"summary": "Start a report's run",
				"description": "Starts the run for a report you created with **Create a report**. The run checks every property in the report for 365 check-in dates from today's date in the run's time zone, at the report's stay length and in its currency.\n\n- Needs a `read_write` key.\n- Charges one credit per property as soon as the run is accepted: the 202 answer already shows `creditsCharged`.\n- The body is optional. Its one field, `timeZone`, is the run's IANA time zone, such as `Europe/Berlin`: the window starts on today's date in it. Leave it out, or send null, for your company's time zone (Europe/London unless changed in the web app). A name that is not in the IANA list answers 422 `validation_failed` with `details.timeZone`.\n- A report runs once. Starting it again answers 409 `report_already_run`. To check the same properties again, create a new report or use **Start a run**.\n- If your company already has as many queued or running runs as it may (5 by default), the answer is 429 `too_many_active_runs`. That answer is not stored against the `Idempotency-Key`, so you can send the same request again once one of your runs has finished.\n- Checks happen in this order: 404 `not_found`, 409 `report_already_run`, 429 `too_many_active_runs`, 402 `insufficient_credits`.",
				"parameters": [
					{
						"$ref": "#/components/parameters/ReportId"
					},
					{
						"$ref": "#/components/parameters/IdempotencyKey"
					}
				],
				"requestBody": {
					"required": false,
					"content": {
						"application/json": {
							"schema": {
								"$ref": "#/components/schemas/StartRunRequest"
							},
							"example": {
								"timeZone": "Europe/Berlin"
							}
						}
					}
				},
				"responses": {
					"202": {
						"description": "The new run, with status `queued` and `reference` null.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Run"
								},
								"example": {
									"id": 7745,
									"reportId": 5134,
									"reference": null,
									"status": "queued",
									"complete": false,
									"origin": "api",
									"requestsTotal": 1095,
									"done": 0,
									"soldOut": 0,
									"failed": 0,
									"past": 0,
									"restricted": 0,
									"remaining": 1095,
									"creditsCharged": 3,
									"dateFrom": "2026-10-05",
									"dateTo": "2027-10-04",
									"timeZone": "Europe/London",
									"startedAt": "2026-10-05T09:14:22Z",
									"finishedAt": null,
									"lastRetriedAt": null,
									"resultsPurgedAt": null
								}
							}
						}
					},
					"401": {
						"$ref": "#/components/responses/Unauthorized"
					},
					"402": {
						"$ref": "#/components/responses/InsufficientCredits"
					},
					"403": {
						"$ref": "#/components/responses/Forbidden"
					},
					"404": {
						"$ref": "#/components/responses/NotFound"
					},
					"409": {
						"description": "The report already has a run (`report_already_run`), or a request with the same `Idempotency-Key` is still being handled (`idempotency_in_progress`).",
						"headers": {
							"Retry-After": {
								"$ref": "#/components/headers/RetryAfter"
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorResponse"
								},
								"examples": {
									"report_already_run": {
										"summary": "The report has already been run",
										"value": {
											"error": {
												"code": "report_already_run",
												"message": "This report already has a run; create a new report to run again."
											}
										}
									},
									"idempotency_in_progress": {
										"summary": "The first request is still being handled",
										"value": {
											"error": {
												"code": "idempotency_in_progress",
												"message": "A request with this Idempotency-Key is still being processed; try again shortly.",
												"details": {
													"retryAfterSeconds": 5
												}
											}
										}
									}
								}
							}
						}
					},
					"422": {
						"$ref": "#/components/responses/StartRunRejected"
					},
					"429": {
						"$ref": "#/components/responses/RateLimitedOrRunLimit"
					},
					"500": {
						"$ref": "#/components/responses/InternalError"
					}
				}
			}
		},
		"/runs": {
			"post": {
				"operationId": "createRun",
				"tags": ["Runs"],
				"summary": "Start a run",
				"description": "Creates a report and starts its run in one call, tagged with your own reference. This is the call most integrations need: send the properties to check and a reference you will recognise, then follow the run until it is complete.\n\n- Needs a `read_write` key.\n- Charges one credit per property as soon as the run is accepted: the 202 answer already shows `creditsCharged`.\n- The body is the same as for **Create a report**, plus a required `reference` and an optional `timeZone`. The run's window starts on today's date in that time zone; left out, your company's is used (Europe/London unless changed in the web app). A name that is not in the IANA list answers 422 `validation_failed` with `details.timeZone`.\n- The reference does not have to be unique. Sending it again starts another report and run, and **List runs** with that `reference` returns both. The Lighthouse-compatible API reads the newest completed run for a reference whose rows have not been deleted.\n- Send an `Idempotency-Key` so that a retry after a timeout cannot start, or charge for, a second run.\n- A refused start (402 or 429) leaves no report behind and charges nothing.\n- If your company already has as many queued or running runs as it may (5 by default), the answer is 429 `too_many_active_runs`. Send the request again once one of your runs has finished.",
				"parameters": [
					{
						"$ref": "#/components/parameters/IdempotencyKey"
					}
				],
				"requestBody": {
					"required": true,
					"content": {
						"application/json": {
							"schema": {
								"$ref": "#/components/schemas/CreateRunRequest"
							},
							"example": {
								"reference": "larkspur-otterby",
								"name": "Otterby competitors",
								"currency": "GBP",
								"nights": 1,
								"properties": [
									{
										"url": "https://www.booking.com/hotel/gb/the-larkspur-otterby.html",
										"name": "The Larkspur Hotel"
									},
									{
										"url": "https://www.booking.com/hotel/gb/quillfield-house-otterby.html",
										"name": "Quillfield House"
									},
									{
										"url": "https://www.booking.com/hotel/gb/marrow-lane-inn-otterby.html",
										"name": "Marrow Lane Inn"
									}
								]
							}
						}
					}
				},
				"responses": {
					"202": {
						"description": "The new run, with status `queued`. Keep its `id` to follow its progress.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Run"
								},
								"example": {
									"id": 7731,
									"reportId": 5120,
									"reference": "larkspur-otterby",
									"status": "queued",
									"complete": false,
									"origin": "api",
									"requestsTotal": 1095,
									"done": 0,
									"soldOut": 0,
									"failed": 0,
									"past": 0,
									"restricted": 0,
									"remaining": 1095,
									"creditsCharged": 3,
									"dateFrom": "2026-10-05",
									"dateTo": "2027-10-04",
									"timeZone": "Europe/London",
									"startedAt": "2026-10-05T07:00:04Z",
									"finishedAt": null,
									"lastRetriedAt": null,
									"resultsPurgedAt": null
								}
							}
						}
					},
					"401": {
						"$ref": "#/components/responses/Unauthorized"
					},
					"402": {
						"$ref": "#/components/responses/InsufficientCredits"
					},
					"403": {
						"$ref": "#/components/responses/Forbidden"
					},
					"409": {
						"$ref": "#/components/responses/IdempotencyInProgress"
					},
					"422": {
						"$ref": "#/components/responses/ValidationFailed"
					},
					"429": {
						"$ref": "#/components/responses/RateLimitedOrRunLimit"
					},
					"500": {
						"$ref": "#/components/responses/InternalError"
					}
				}
			},
			"get": {
				"operationId": "listRuns",
				"tags": ["Runs"],
				"summary": "List runs",
				"description": "Lists the runs your key can see, newest first, with optional filters. Use it to find runs by your own reference, or to check the progress of several runs in one request instead of one request per run.\n\n- Works with a read key.\n- When you use more than one filter, a run must match all of them.\n- A reference with several runs returns all of them, newest first, so you can pick the one you need.\n- Each run includes `remaining`, so one request can show how far along many runs are.\n- Runs come in pages of up to 200. Pass the `nextCursor` of one page as `cursor` to get the next. `nextCursor` is null on the last page.",
				"parameters": [
					{
						"name": "reference",
						"in": "query",
						"required": false,
						"description": "Only runs with this reference. Send one reference, or up to 100 separated by commas. More than 100 answers 422 `invalid_reference`.",
						"schema": {
							"type": "string"
						},
						"example": "larkspur-otterby,quillfield-otterby"
					},
					{
						"name": "status",
						"in": "query",
						"required": false,
						"description": "Only runs with this status. Anything else answers 422 `invalid_status`.",
						"schema": {
							"type": "string",
							"enum": ["queued", "running", "completed"]
						}
					},
					{
						"name": "from",
						"in": "query",
						"required": false,
						"description": "Only runs started on or after this date (YYYY-MM-DD), by the UTC date of `startedAt`. A badly formed date answers 422 `invalid_date`.",
						"schema": {
							"type": "string",
							"format": "date"
						},
						"example": "2026-10-01"
					},
					{
						"name": "to",
						"in": "query",
						"required": false,
						"description": "Only runs started on or before this date (YYYY-MM-DD), by the UTC date of `startedAt`. A badly formed date answers 422 `invalid_date`.",
						"schema": {
							"type": "string",
							"format": "date"
						},
						"example": "2026-10-31"
					},
					{
						"$ref": "#/components/parameters/ListLimit"
					},
					{
						"$ref": "#/components/parameters/Cursor"
					}
				],
				"responses": {
					"200": {
						"description": "A page of runs, newest first.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/RunList"
								},
								"example": {
									"runs": [
										{
											"id": 7731,
											"reportId": 5120,
											"reference": "larkspur-otterby",
											"status": "completed",
											"complete": true,
											"origin": "api",
											"requestsTotal": 1095,
											"done": 1080,
											"soldOut": 11,
											"failed": 4,
											"past": 0,
											"restricted": 0,
											"remaining": 0,
											"creditsCharged": 3,
											"dateFrom": "2026-10-05",
											"dateTo": "2027-10-04",
											"timeZone": "Europe/London",
											"startedAt": "2026-10-05T07:00:04Z",
											"finishedAt": "2026-10-05T07:52:41Z",
											"lastRetriedAt": null,
											"resultsPurgedAt": null
										}
									],
									"nextCursor": null
								}
							}
						}
					},
					"401": {
						"$ref": "#/components/responses/Unauthorized"
					},
					"403": {
						"$ref": "#/components/responses/Forbidden"
					},
					"422": {
						"$ref": "#/components/responses/InvalidQuery"
					},
					"429": {
						"$ref": "#/components/responses/RateLimited"
					},
					"500": {
						"$ref": "#/components/responses/InternalError"
					}
				}
			}
		},
		"/runs/{id}": {
			"get": {
				"operationId": "getRun",
				"tags": ["Runs"],
				"summary": "Get a run",
				"description": "Shows one run's status and progress. Check it until `complete` is true, then read the results.\n\n- Works with a read key.\n- A run covers 365 check-in dates for each property and usually takes minutes, not seconds. Check no more than once a minute.\n- To follow several runs at once, use **List runs** instead.\n- The run keeps answering after its results have been deleted. Only `resultsPurgedAt` changes.",
				"parameters": [
					{
						"$ref": "#/components/parameters/RunId"
					}
				],
				"responses": {
					"200": {
						"description": "The run.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/Run"
								},
								"examples": {
									"running": {
										"summary": "A run still in progress",
										"value": {
											"id": 7731,
											"reportId": 5120,
											"reference": "larkspur-otterby",
											"status": "running",
											"complete": false,
											"origin": "api",
											"requestsTotal": 1095,
											"done": 512,
											"soldOut": 6,
											"failed": 1,
											"past": 0,
											"restricted": 0,
											"remaining": 576,
											"creditsCharged": 3,
											"dateFrom": "2026-10-05",
											"dateTo": "2027-10-04",
											"timeZone": "Europe/London",
											"startedAt": "2026-10-05T07:00:04Z",
											"finishedAt": null,
											"lastRetriedAt": null,
											"resultsPurgedAt": null
										}
									},
									"completed": {
										"summary": "A completed run with 4 failed check-in dates",
										"value": {
											"id": 7731,
											"reportId": 5120,
											"reference": "larkspur-otterby",
											"status": "completed",
											"complete": true,
											"origin": "api",
											"requestsTotal": 1095,
											"done": 1080,
											"soldOut": 11,
											"failed": 4,
											"past": 0,
											"restricted": 0,
											"remaining": 0,
											"creditsCharged": 3,
											"dateFrom": "2026-10-05",
											"dateTo": "2027-10-04",
											"timeZone": "Europe/London",
											"startedAt": "2026-10-05T07:00:04Z",
											"finishedAt": "2026-10-05T07:52:41Z",
											"lastRetriedAt": null,
											"resultsPurgedAt": null
										}
									},
									"purged": {
										"summary": "A completed run whose results were deleted",
										"value": {
											"id": 7731,
											"reportId": 5120,
											"reference": "larkspur-otterby",
											"status": "completed",
											"complete": true,
											"origin": "api",
											"requestsTotal": 1095,
											"done": 1080,
											"soldOut": 11,
											"failed": 4,
											"past": 0,
											"restricted": 0,
											"remaining": 0,
											"creditsCharged": 3,
											"dateFrom": "2026-10-05",
											"dateTo": "2027-10-04",
											"timeZone": "Europe/London",
											"startedAt": "2026-10-05T07:00:04Z",
											"finishedAt": "2026-10-05T07:52:41Z",
											"lastRetriedAt": null,
											"resultsPurgedAt": "2026-10-05T08:10:09Z"
										}
									}
								}
							}
						}
					},
					"401": {
						"$ref": "#/components/responses/Unauthorized"
					},
					"403": {
						"$ref": "#/components/responses/Forbidden"
					},
					"404": {
						"$ref": "#/components/responses/NotFound"
					},
					"429": {
						"$ref": "#/components/responses/RateLimited"
					},
					"500": {
						"$ref": "#/components/responses/InternalError"
					}
				}
			}
		},
		"/runs/{id}/retry": {
			"post": {
				"operationId": "retryRun",
				"tags": ["Runs"],
				"summary": "Retry failed check-in dates",
				"description": "Tries again to fetch the check-in dates that a finished run could not fetch. Use it when a completed run shows `failed` above 0 and you want a fuller set of results.\n\n- Needs a `read_write` key.\n- Free: no credits are charged.\n- There is no request body.\n- The run goes back to `running`, and completes again when the retried dates are done.\n- After a retry, wait 10 minutes before retrying the same run again. The retry button in the web app shares this wait; Retrying again sooner answers 409 `retry_cooldown`.\n- A run with nothing failed answers 200 with `requeued` 0 and is left unchanged. No wait starts.\n- Checks happen in this order: 404 `not_found`, 409 `results_purged`, 409 `run_not_completed`, 409 `retry_cooldown`.",
				"parameters": [
					{
						"$ref": "#/components/parameters/RunId"
					},
					{
						"$ref": "#/components/parameters/IdempotencyKey"
					}
				],
				"responses": {
					"200": {
						"description": "The run, and how many check-in dates were queued again.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/RetryResult"
								},
								"examples": {
									"retried": {
										"summary": "Four failed check-in dates queued again",
										"value": {
											"run": {
												"id": 7731,
												"reportId": 5120,
												"reference": "larkspur-otterby",
												"status": "running",
												"complete": false,
												"origin": "api",
												"requestsTotal": 1095,
												"done": 1080,
												"soldOut": 11,
												"failed": 0,
												"past": 0,
												"restricted": 0,
												"remaining": 4,
												"creditsCharged": 3,
												"dateFrom": "2026-10-05",
												"dateTo": "2027-10-04",
												"timeZone": "Europe/London",
												"startedAt": "2026-10-05T07:00:04Z",
												"finishedAt": null,
												"lastRetriedAt": "2026-10-05T08:03:55Z",
												"resultsPurgedAt": null
											},
											"requeued": 4
										}
									},
									"nothing": {
										"summary": "Nothing had failed",
										"value": {
											"run": {
												"id": 7790,
												"reportId": 5161,
												"reference": "larkspur-otterby",
												"status": "completed",
												"complete": true,
												"origin": "api",
												"requestsTotal": 1095,
												"done": 1084,
												"soldOut": 11,
												"failed": 0,
												"past": 0,
												"restricted": 0,
												"remaining": 0,
												"creditsCharged": 3,
												"dateFrom": "2026-10-12",
												"dateTo": "2027-10-11",
												"timeZone": "Europe/London",
												"startedAt": "2026-10-12T07:00:03Z",
												"finishedAt": "2026-10-12T07:49:10Z",
												"lastRetriedAt": null,
												"resultsPurgedAt": null
											},
											"requeued": 0
										}
									}
								}
							}
						}
					},
					"401": {
						"$ref": "#/components/responses/Unauthorized"
					},
					"403": {
						"$ref": "#/components/responses/Forbidden"
					},
					"404": {
						"$ref": "#/components/responses/NotFound"
					},
					"409": {
						"description": "The run cannot be retried now. `code` is `results_purged` (its results were deleted), `run_not_completed` (it is still queued or running), `retry_cooldown` (it was retried less than 10 minutes ago) or `idempotency_in_progress`.",
						"headers": {
							"Retry-After": {
								"$ref": "#/components/headers/RetryAfter"
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorResponse"
								},
								"examples": {
									"retry_cooldown": {
										"summary": "Retried less than 10 minutes ago",
										"value": {
											"error": {
												"code": "retry_cooldown",
												"message": "This run was retried 3 minutes ago. Please wait 10 minutes between retries.",
												"details": {
													"retryAfterSeconds": 412
												}
											}
										}
									},
									"run_not_completed": {
										"summary": "The run has not finished",
										"value": {
											"error": {
												"code": "run_not_completed",
												"message": "The run has not finished; failed check-in dates can be retried once it has."
											}
										}
									},
									"results_purged": {
										"summary": "The results were deleted",
										"value": {
											"error": {
												"code": "results_purged",
												"message": "The rows of this run have been deleted, so its failed check-in dates cannot be retried.",
												"details": {
													"purgedAt": "2026-10-05T08:10:09Z"
												}
											}
										}
									},
									"idempotency_in_progress": {
										"summary": "The first request is still being handled",
										"value": {
											"error": {
												"code": "idempotency_in_progress",
												"message": "A request with this Idempotency-Key is still being processed; try again shortly.",
												"details": {
													"retryAfterSeconds": 5
												}
											}
										}
									}
								}
							}
						}
					},
					"422": {
						"$ref": "#/components/responses/IdempotencyRejected"
					},
					"429": {
						"$ref": "#/components/responses/RateLimited"
					},
					"500": {
						"$ref": "#/components/responses/InternalError"
					}
				}
			}
		},
		"/runs/{id}/results": {
			"get": {
				"operationId": "getRunResults",
				"tags": ["Results"],
				"summary": "List a run's results",
				"description": "Returns every room and rate the run found: one row per room and rate, plus a placeholder row for each property and check-in date that had no rooms. Use it when you want the full detail, for example to load into your own database.\n\n- Works with a read key.\n- You can read rows while the run is still going. `complete` tells you whether more rows will come.\n- A placeholder row has the property and date, an `availability` that says why there are no rooms, and null in every room field.\n- Rows come in check-in date order, then property, room and price.\n- By default the answer is JSON, in pages of 500 rows (up to 2,000 with `limit`). Pass the `nextCursor` of one page as `cursor` to get the next. `nextCursor` is null on the last page.\n- `format=ndjson` sends the whole run in one answer with no paging, as `application/x-ndjson`: one JSON object per line. The first line is the run, with `\"kind\": \"run\"` added; every line after it is one row. This takes a whole run in one request. `limit` is ignored.\n- Once the results have been deleted, the answer is 410 `results_purged`.",
				"parameters": [
					{
						"$ref": "#/components/parameters/RunId"
					},
					{
						"name": "limit",
						"in": "query",
						"required": false,
						"description": "Rows per page, 1 to 2,000. Anything else answers 422 `invalid_limit`. Ignored with `format=ndjson`.",
						"schema": {
							"type": "integer",
							"minimum": 1,
							"maximum": 2000,
							"default": 500
						}
					},
					{
						"$ref": "#/components/parameters/Cursor"
					},
					{
						"name": "format",
						"in": "query",
						"required": false,
						"description": "`json` for pages of rows, or `ndjson` for the whole run in one stream.",
						"schema": {
							"type": "string",
							"enum": ["json", "ndjson"],
							"default": "json"
						}
					}
				],
				"responses": {
					"200": {
						"description": "A page of rows (JSON), or the whole run (ndjson).",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ResultsPage"
								},
								"example": {
									"run": {
										"id": 7731,
										"reportId": 5120,
										"reference": "larkspur-otterby",
										"status": "completed",
										"complete": true,
										"origin": "api",
										"requestsTotal": 1095,
										"done": 1080,
										"soldOut": 11,
										"failed": 4,
										"past": 0,
										"restricted": 0,
										"remaining": 0,
										"creditsCharged": 3,
										"dateFrom": "2026-10-05",
										"dateTo": "2027-10-04",
										"timeZone": "Europe/London",
										"startedAt": "2026-10-05T07:00:04Z",
										"finishedAt": "2026-10-05T07:52:41Z",
										"lastRetriedAt": null,
										"resultsPurgedAt": null
									},
									"complete": true,
									"rows": [
										{
											"propertyId": 30413,
											"property": "Marrow Lane Inn",
											"propertyUrl": "https://www.booking.com/hotel/gb/marrow-lane-inn-otterby.html",
											"source": "booking",
											"checkIn": "2026-10-05",
											"availability": "sold out",
											"room": null,
											"roomId": null,
											"rateId": null,
											"occupancy": null,
											"price": null,
											"originalPrice": null,
											"currency": null,
											"breakfastIncluded": null,
											"mealType": null,
											"freeCancellation": null,
											"nonRefundable": null,
											"payNow": null,
											"onSale": null,
											"hasOffer": null,
											"partnerOffer": null,
											"limitedAvailability": null,
											"roomsLeft": null,
											"cheapestOfDay": null,
											"cheapestOwnRate": null,
											"undercutsDirect": null,
											"undercutBy": null,
											"geniusPrice": null,
											"excludedReason": null,
											"conditions": null
										},
										{
											"propertyId": 30412,
											"property": "Quillfield House",
											"propertyUrl": "https://www.booking.com/hotel/gb/quillfield-house-otterby.html",
											"source": "booking",
											"checkIn": "2026-10-05",
											"availability": "available",
											"room": "Double Room",
											"roomId": "room-1",
											"rateId": "rate-1",
											"occupancy": 2,
											"price": 104.6,
											"originalPrice": null,
											"currency": "GBP",
											"breakfastIncluded": false,
											"mealType": "room_only",
											"freeCancellation": true,
											"nonRefundable": false,
											"payNow": false,
											"onSale": false,
											"hasOffer": false,
											"partnerOffer": false,
											"limitedAvailability": false,
											"roomsLeft": null,
											"cheapestOfDay": true,
											"cheapestOwnRate": true,
											"undercutsDirect": false,
											"undercutBy": null,
											"geniusPrice": false,
											"excludedReason": null,
											"conditions": null
										},
										{
											"propertyId": 30411,
											"property": "The Larkspur Hotel",
											"propertyUrl": "https://www.booking.com/hotel/gb/the-larkspur-otterby.html",
											"source": "booking",
											"checkIn": "2026-10-05",
											"availability": "available",
											"room": "Superior Double Room",
											"roomId": "room-2",
											"rateId": "rate-4",
											"occupancy": 2,
											"price": 112.5,
											"originalPrice": 125.5,
											"currency": "GBP",
											"breakfastIncluded": true,
											"mealType": "breakfast",
											"freeCancellation": false,
											"nonRefundable": true,
											"payNow": true,
											"onSale": true,
											"hasOffer": true,
											"partnerOffer": false,
											"limitedAvailability": true,
											"roomsLeft": 2,
											"cheapestOfDay": false,
											"cheapestOwnRate": false,
											"undercutsDirect": false,
											"undercutBy": null,
											"geniusPrice": false,
											"excludedReason": null,
											"conditions": null
										},
										{
											"propertyId": 30411,
											"property": "The Larkspur Hotel",
											"propertyUrl": "https://www.booking.com/hotel/gb/the-larkspur-otterby.html",
											"source": "booking",
											"checkIn": "2026-10-05",
											"availability": "available",
											"room": "Twin Room",
											"roomId": "room-1",
											"rateId": "rate-1",
											"occupancy": 2,
											"price": 98.4,
											"originalPrice": null,
											"currency": "GBP",
											"breakfastIncluded": false,
											"mealType": "room_only",
											"freeCancellation": true,
											"nonRefundable": false,
											"payNow": false,
											"onSale": false,
											"hasOffer": false,
											"partnerOffer": false,
											"limitedAvailability": false,
											"roomsLeft": null,
											"cheapestOfDay": true,
											"cheapestOwnRate": true,
											"undercutsDirect": false,
											"undercutBy": null,
											"geniusPrice": false,
											"excludedReason": null,
											"conditions": "Free cancellation until 12:00 on 5 October."
										}
									],
									"nextCursor": "NzczMXxbIjIwMjYtMTAtMDUiLCJUaGUgTGFya3NwdXIgSG90ZWwiLCJUd2luIFJvb20iLCI5OC40MCIsMCw5MTIwNDRd"
								}
							},
							"application/x-ndjson": {
								"schema": {
									"type": "string",
									"description": "One JSON object per line. The first line is the run, with `\"kind\": \"run\"` added. Every line after it is one result row."
								},
								"example": "{\"kind\":\"run\",\"id\":7731,\"reportId\":5120,\"reference\":\"larkspur-otterby\",\"status\":\"completed\",\"complete\":true,\"origin\":\"api\",\"requestsTotal\":1095,\"done\":1080,\"soldOut\":11,\"failed\":4,\"past\":0,\"restricted\":0,\"remaining\":0,\"creditsCharged\":3,\"dateFrom\":\"2026-10-05\",\"dateTo\":\"2027-10-04\",\"startedAt\":\"2026-10-05T07:00:04Z\",\"finishedAt\":\"2026-10-05T07:52:41Z\",\"lastRetriedAt\":null,\"resultsPurgedAt\":null}\n{\"propertyId\":30413,\"property\":\"Marrow Lane Inn\",\"propertyUrl\":\"https://www.booking.com/hotel/gb/marrow-lane-inn-otterby.html\",\"source\":\"booking\",\"checkIn\":\"2026-10-05\",\"availability\":\"sold out\",\"room\":null,\"roomId\":null,\"rateId\":null,\"occupancy\":null,\"price\":null,\"originalPrice\":null,\"currency\":null,\"breakfastIncluded\":null,\"mealType\":null,\"freeCancellation\":null,\"nonRefundable\":null,\"payNow\":null,\"onSale\":null,\"hasOffer\":null,\"partnerOffer\":null,\"limitedAvailability\":null,\"roomsLeft\":null,\"cheapestOfDay\":null,\"cheapestOwnRate\":null,\"undercutsDirect\":null,\"undercutBy\":null,\"geniusPrice\":null,\"excludedReason\":null,\"conditions\":null}\n{\"propertyId\":30412,\"property\":\"Quillfield House\",\"propertyUrl\":\"https://www.booking.com/hotel/gb/quillfield-house-otterby.html\",\"source\":\"booking\",\"checkIn\":\"2026-10-05\",\"availability\":\"available\",\"room\":\"Double Room\",\"roomId\":\"room-1\",\"rateId\":\"rate-1\",\"occupancy\":2,\"price\":104.6,\"originalPrice\":null,\"currency\":\"GBP\",\"breakfastIncluded\":false,\"mealType\":\"room_only\",\"freeCancellation\":true,\"nonRefundable\":false,\"payNow\":false,\"onSale\":false,\"hasOffer\":false,\"partnerOffer\":false,\"limitedAvailability\":false,\"roomsLeft\":null,\"cheapestOfDay\":true,\"cheapestOwnRate\":true,\"undercutsDirect\":false,\"undercutBy\":null,\"geniusPrice\":false,\"excludedReason\":null,\"conditions\":null}\n{\"propertyId\":30411,\"property\":\"The Larkspur Hotel\",\"propertyUrl\":\"https://www.booking.com/hotel/gb/the-larkspur-otterby.html\",\"source\":\"booking\",\"checkIn\":\"2026-10-05\",\"availability\":\"available\",\"room\":\"Superior Double Room\",\"roomId\":\"room-2\",\"rateId\":\"rate-4\",\"occupancy\":2,\"price\":112.5,\"originalPrice\":125.5,\"currency\":\"GBP\",\"breakfastIncluded\":true,\"mealType\":\"breakfast\",\"freeCancellation\":false,\"nonRefundable\":true,\"payNow\":true,\"onSale\":true,\"hasOffer\":true,\"partnerOffer\":false,\"limitedAvailability\":true,\"roomsLeft\":2,\"cheapestOfDay\":false,\"cheapestOwnRate\":false,\"undercutsDirect\":false,\"undercutBy\":null,\"geniusPrice\":false,\"excludedReason\":null,\"conditions\":null}\n{\"propertyId\":30411,\"property\":\"The Larkspur Hotel\",\"propertyUrl\":\"https://www.booking.com/hotel/gb/the-larkspur-otterby.html\",\"source\":\"booking\",\"checkIn\":\"2026-10-05\",\"availability\":\"available\",\"room\":\"Twin Room\",\"roomId\":\"room-1\",\"rateId\":\"rate-1\",\"occupancy\":2,\"price\":98.4,\"originalPrice\":null,\"currency\":\"GBP\",\"breakfastIncluded\":false,\"mealType\":\"room_only\",\"freeCancellation\":true,\"nonRefundable\":false,\"payNow\":false,\"onSale\":false,\"hasOffer\":false,\"partnerOffer\":false,\"limitedAvailability\":false,\"roomsLeft\":null,\"cheapestOfDay\":true,\"cheapestOwnRate\":true,\"undercutsDirect\":false,\"undercutBy\":null,\"geniusPrice\":false,\"excludedReason\":null,\"conditions\":\"Free cancellation until 12:00 on 5 October.\"}\n"
							}
						}
					},
					"401": {
						"$ref": "#/components/responses/Unauthorized"
					},
					"403": {
						"$ref": "#/components/responses/Forbidden"
					},
					"404": {
						"$ref": "#/components/responses/NotFound"
					},
					"410": {
						"$ref": "#/components/responses/ResultsPurged"
					},
					"422": {
						"$ref": "#/components/responses/InvalidQuery"
					},
					"429": {
						"$ref": "#/components/responses/RateLimited"
					},
					"500": {
						"$ref": "#/components/responses/InternalError"
					}
				}
			},
			"delete": {
				"operationId": "acknowledgeRunResults",
				"tags": ["Results"],
				"summary": "Delete a run's results",
				"description": "Tells Beacon you have stored a finished run's results, so they can be deleted. Use it once you have saved the rows in your own system.\n\n- Needs a `read_write` key and a completed run. A run that is still queued or running answers 409 `run_not_completed`.\n- Only the rows are deleted. The run, its counters and the credits it cost stay, and the run's `resultsPurgedAt` is set.\n- Your company may also have a retention period after which results are deleted anyway. This call deletes them sooner.\n- Deleting again is safe: it answers 200 with `deletedRows` 0 and the same `resultsPurgedAt`.\n- Afterwards, **List a run's results** and **Get the cheapest rate per day** answer 410 `results_purged`, and **Retry failed check-in dates** answers 409 `results_purged`.\n- Send an `Idempotency-Key` so that a retry after a timeout is handled safely.",
				"parameters": [
					{
						"$ref": "#/components/parameters/RunId"
					},
					{
						"$ref": "#/components/parameters/IdempotencyKey"
					}
				],
				"responses": {
					"200": {
						"description": "The run, with `resultsPurgedAt` set, and how many rows were deleted.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/DeleteResults"
								},
								"examples": {
									"deleted": {
										"summary": "Rows deleted",
										"value": {
											"run": {
												"id": 7731,
												"reportId": 5120,
												"reference": "larkspur-otterby",
												"status": "completed",
												"complete": true,
												"origin": "api",
												"requestsTotal": 1095,
												"done": 1080,
												"soldOut": 11,
												"failed": 4,
												"past": 0,
												"restricted": 0,
												"remaining": 0,
												"creditsCharged": 3,
												"dateFrom": "2026-10-05",
												"dateTo": "2027-10-04",
												"timeZone": "Europe/London",
												"startedAt": "2026-10-05T07:00:04Z",
												"finishedAt": "2026-10-05T07:52:41Z",
												"lastRetriedAt": null,
												"resultsPurgedAt": "2026-10-05T08:10:09Z"
											},
											"deletedRows": 14031
										}
									},
									"repeat": {
										"summary": "Already deleted",
										"value": {
											"run": {
												"id": 7731,
												"reportId": 5120,
												"reference": "larkspur-otterby",
												"status": "completed",
												"complete": true,
												"origin": "api",
												"requestsTotal": 1095,
												"done": 1080,
												"soldOut": 11,
												"failed": 4,
												"past": 0,
												"restricted": 0,
												"remaining": 0,
												"creditsCharged": 3,
												"dateFrom": "2026-10-05",
												"dateTo": "2027-10-04",
												"timeZone": "Europe/London",
												"startedAt": "2026-10-05T07:00:04Z",
												"finishedAt": "2026-10-05T07:52:41Z",
												"lastRetriedAt": null,
												"resultsPurgedAt": "2026-10-05T08:10:09Z"
											},
											"deletedRows": 0
										}
									}
								}
							}
						}
					},
					"401": {
						"$ref": "#/components/responses/Unauthorized"
					},
					"403": {
						"$ref": "#/components/responses/Forbidden"
					},
					"404": {
						"$ref": "#/components/responses/NotFound"
					},
					"409": {
						"description": "The run has not finished (`run_not_completed`), or a request with the same `Idempotency-Key` is still being handled (`idempotency_in_progress`).",
						"headers": {
							"Retry-After": {
								"$ref": "#/components/headers/RetryAfter"
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorResponse"
								},
								"examples": {
									"run_not_completed": {
										"summary": "The run has not finished",
										"value": {
											"error": {
												"code": "run_not_completed",
												"message": "The run has not finished; its results can be deleted once it has."
											}
										}
									},
									"idempotency_in_progress": {
										"summary": "The first request is still being handled",
										"value": {
											"error": {
												"code": "idempotency_in_progress",
												"message": "A request with this Idempotency-Key is still being processed; try again shortly.",
												"details": {
													"retryAfterSeconds": 5
												}
											}
										}
									}
								}
							}
						}
					},
					"422": {
						"$ref": "#/components/responses/IdempotencyRejected"
					},
					"429": {
						"$ref": "#/components/responses/RateLimited"
					},
					"500": {
						"$ref": "#/components/responses/InternalError"
					}
				}
			}
		},
		"/runs/{id}/rates": {
			"get": {
				"operationId": "getRunRates",
				"tags": ["Results"],
				"summary": "Get the cheapest rate per day",
				"description": "Picks one rate per property and check-in date from a finished run, so you get a single price for each day rather than every room. Use it to compare properties day by day; set `perRoomType=1` to get one rate per room type instead, so you can compare like with like.\n\n- Works with a read key.\n- The run must be completed.\n- Rates are chosen by the same rules as the Lighthouse-compatible rates endpoint, but with Beacon's field names.\n- Checks happen in this order: 404 `not_found`, 410 `results_purged`, 409 `run_not_completed`, 400 `invalid_filter`.\n\n**How a rate is chosen**\n\nFor each property and check-in date, Beacon narrows the rows in this order. The first step that leaves nothing sets the `message`, and the record has null in every rate field.\n\n1. `los` is not the report's stay length, `ota` is not the property's source, or the date is not in the run: `general.missing`.\n2. The date was sold out: `rates.soldout`. The property needs a stay of at least N nights from that date and the run's stay length is shorter: `rates.restrictionlosN`, for example `rates.restrictionlos4`.\n3. The date could not be fetched, had passed, or has not been fetched yet: `general.missing`.\n4. Rows with an `excludedReason` are dropped. If none are left: `general.missing`.\n5. Rooms for fewer than `persons` guests are dropped. If none are left: `rates.no_person.N`.\n6. `mealType` other than `any` keeps only rows of exactly that meal plan. If none are left: `rates.meal.missing.<name>`, with the name you asked for (for example `rates.meal.missing.half_board`).\n7. `bar=true` keeps only the public flexible rate. If none are left: `rates.no_bar`.\n8. `roomType` keeps rooms of that type. If none are left: `rates.roomtypes.unavailable`.\n\nThe cheapest remaining row is chosen. `bestFlex` is true when that rate can be cancelled free of charge and no cheaper free-cancellation rate for the same property and date also passes the `persons`, `mealType` and `bar` filters. (`roomType` does not narrow this comparison.)\n\n**Caching**\n\nA 200 answer carries an `ETag`, `Cache-Control: private, max-age=60` and `Vary: Authorization, X-Oi-Authorization`. Your own client may reuse the body for 60 seconds; a shared cache never may. After that, send the tag back in `If-None-Match`. While the run, the report's properties and your filters are unchanged, the answer is 304 Not Modified with no body, so keep the body you have. A retry of the run, a change to the report's properties, different filters or a new Beacon response format all give a new tag and a full 200. Errors are never cached and never answer 304.",
				"parameters": [
					{
						"$ref": "#/components/parameters/RunId"
					},
					{
						"name": "los",
						"in": "query",
						"required": false,
						"description": "Stay length in nights, 1 to 45. Only the report's own stay length (`nights`) has rates; any other value gives `general.missing` on every date.",
						"schema": {
							"type": "integer",
							"minimum": 1,
							"maximum": 45,
							"default": 1
						}
					},
					{
						"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
						}
					},
					{
						"name": "persons",
						"in": "query",
						"required": false,
						"description": "Number of guests, 1 to 20. Keeps rooms that take at least this many. Rooms for fewer guests are dropped.",
						"schema": {
							"type": "integer",
							"minimum": 1,
							"maximum": 20,
							"default": 2
						}
					},
					{
						"name": "mealType",
						"in": "query",
						"required": false,
						"description": "Meal plan, by name. `any` (the default) keeps every rate. `room_only`, `breakfast`, `half_board`, `full_board`, `all_inclusive`, `breakfast_lunch`, `lunch_dinner`, `dinner` and `lunch` each keep only rates of exactly that meal plan, so `room_only` no longer includes half board, full board or all-inclusive rates. The legacy Lighthouse codes are still accepted and mean the same as their name: `0` any, `1` breakfast, `2` half_board, `3` full_board, `4` all_inclusive, `5` room_only. The four plans `breakfast_lunch`, `lunch_dinner`, `dinner` and `lunch` have no Lighthouse filter code, so filter for them by name; the Lighthouse-compatible API reports them in `mealTypeIncluded` as `2` (`breakfast_lunch`, `lunch_dinner`) or `0` (`dinner`, `lunch`), for compatibility only. The answer's `filters.mealType` always echoes the name.",
						"schema": {
							"type": "string",
							"enum": [
								"any",
								"room_only",
								"breakfast",
								"half_board",
								"full_board",
								"all_inclusive",
								"breakfast_lunch",
								"lunch_dinner",
								"dinner",
								"lunch",
								"0",
								"1",
								"2",
								"3",
								"4",
								"5"
							],
							"default": "any"
						}
					},
					{
						"name": "roomType",
						"in": "query",
						"required": false,
						"description": "Keep only this room type. This is Beacon's classification of the room from its name.",
						"schema": {
							"type": "string",
							"enum": ["suite", "apartment", "studio", "family", "triple", "twin", "double", "single", "dormitory", "standard"]
						}
					},
					{
						"name": "fromDate",
						"in": "query",
						"required": false,
						"description": "First check-in date (YYYY-MM-DD). Defaults to today in the run's time zone (`timeZone`).",
						"schema": {
							"type": "string",
							"format": "date"
						},
						"example": "2026-10-05"
					},
					{
						"name": "shopLength",
						"in": "query",
						"required": false,
						"description": "Number of check-in dates from `fromDate`, 1 to 365. Defaults to every date up to the run's `dateTo`.",
						"schema": {
							"type": "integer",
							"minimum": 1,
							"maximum": 365
						}
					},
					{
						"name": "ota",
						"in": "query",
						"required": false,
						"description": "The source to read rates from, as a code of lower-case letters, digits and underscores, up to 32 characters. Beacon Pro captures `bookingdotcom`, `travelodge` and `premierinn`. 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"
						}
					},
					{
						"name": "compsetIds",
						"in": "query",
						"required": false,
						"description": "Which competitor set to read. Only `1` is supported: every property in the run.",
						"schema": {
							"type": "string",
							"enum": ["1"],
							"default": "1"
						}
					},
					{
						"name": "perRoomType",
						"in": "query",
						"required": false,
						"description": "`1` gives one rate per property, check-in date and room type, instead of one per property and check-in date.",
						"schema": {
							"type": "integer",
							"enum": [0, 1],
							"default": 0
						}
					},
					{
						"name": "If-None-Match",
						"in": "header",
						"required": false,
						"description": "The `ETag` of a rates answer you already hold. While it is still current, the answer is 304 with no body. The comparison is weak: a `W/` prefix or a `-gzip` suffix still matches, and `*` matches any tag.",
						"schema": {
							"type": "string"
						}
					}
				],
				"responses": {
					"200": {
						"description": "The chosen rates, and the filters as applied.",
						"headers": {
							"ETag": {
								"$ref": "#/components/headers/ETag"
							},
							"Cache-Control": {
								"$ref": "#/components/headers/CacheControl"
							},
							"Vary": {
								"$ref": "#/components/headers/Vary"
							}
						},
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/RunRates"
								},
								"example": {
									"run": {
										"id": 7731,
										"reference": "larkspur-otterby"
									},
									"filters": {
										"los": 1,
										"bar": false,
										"persons": 2,
										"mealType": "any",
										"roomType": null,
										"fromDate": "2026-10-05",
										"shopLength": null,
										"ota": "bookingdotcom",
										"compsetIds": 1,
										"perRoomType": false
									},
									"rates": [
										{
											"hotelId": 9182,
											"propertyId": 30411,
											"hotelName": "The Larkspur Hotel",
											"checkIn": "2026-10-05",
											"fetchedAt": "2026-10-05T07:00:19Z",
											"nights": 1,
											"currency": "GBP",
											"price": 98.4,
											"originalPrice": null,
											"roomName": "Twin Room",
											"roomType": "twin",
											"maxOccupancy": 2,
											"breakfastIncluded": false,
											"mealType": "room_only",
											"freeCancellation": true,
											"nonRefundable": false,
											"bestFlex": true,
											"message": null
										},
										{
											"hotelId": 9183,
											"propertyId": 30412,
											"hotelName": "Quillfield House",
											"checkIn": "2026-10-05",
											"fetchedAt": "2026-10-05T07:00:23Z",
											"nights": 1,
											"currency": "GBP",
											"price": 104.6,
											"originalPrice": null,
											"roomName": "Double Room",
											"roomType": "double",
											"maxOccupancy": 2,
											"breakfastIncluded": false,
											"mealType": "room_only",
											"freeCancellation": true,
											"nonRefundable": false,
											"bestFlex": true,
											"message": null
										},
										{
											"hotelId": 9184,
											"propertyId": 30413,
											"hotelName": "Marrow Lane Inn",
											"checkIn": "2026-10-05",
											"fetchedAt": "2026-10-05T07:00:27Z",
											"nights": 1,
											"currency": "GBP",
											"price": null,
											"originalPrice": null,
											"roomName": null,
											"roomType": null,
											"maxOccupancy": null,
											"breakfastIncluded": null,
											"mealType": null,
											"freeCancellation": null,
											"nonRefundable": null,
											"bestFlex": null,
											"message": "rates.soldout"
										}
									]
								}
							}
						}
					},
					"304": {
						"description": "Not Modified: `If-None-Match` named the current tag. There is no body; keep the one you have.",
						"headers": {
							"ETag": {
								"$ref": "#/components/headers/ETag"
							},
							"Cache-Control": {
								"$ref": "#/components/headers/CacheControl"
							},
							"Vary": {
								"$ref": "#/components/headers/Vary"
							}
						}
					},
					"400": {
						"description": "A filter is out of range (`invalid_filter`). The message names the filter.",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorResponse"
								},
								"examples": {
									"los": {
										"summary": "Stay length out of range",
										"value": {
											"error": {
												"code": "invalid_filter",
												"message": "los must be a whole number from 1 to 45."
											}
										}
									},
									"mealType": {
										"summary": "Unknown meal plan",
										"value": {
											"error": {
												"code": "invalid_filter",
												"message": "mealType must be one of any, room_only, breakfast, half_board, full_board, all_inclusive, breakfast_lunch, lunch_dinner, dinner, lunch, or a Lighthouse code 0-5."
											}
										}
									},
									"compsetIds": {
										"summary": "Unsupported competitor set",
										"value": {
											"error": {
												"code": "invalid_filter",
												"message": "compsetIds only supports 1 (every competitor of the subscription)."
											}
										}
									},
									"perRoomType": {
										"summary": "Bad perRoomType",
										"value": {
											"error": {
												"code": "invalid_filter",
												"message": "perRoomType must be 0 or 1."
											}
										}
									}
								}
							}
						}
					},
					"401": {
						"$ref": "#/components/responses/Unauthorized"
					},
					"403": {
						"$ref": "#/components/responses/Forbidden"
					},
					"404": {
						"$ref": "#/components/responses/NotFound"
					},
					"409": {
						"description": "The run is still queued or running (`run_not_completed`).",
						"content": {
							"application/json": {
								"schema": {
									"$ref": "#/components/schemas/ErrorResponse"
								},
								"examples": {
									"run_not_completed": {
										"summary": "The run has not finished",
										"value": {
											"error": {
												"code": "run_not_completed",
												"message": "The run has not finished, so it has no rates to select yet."
											}
										}
									}
								}
							}
						}
					},
					"410": {
						"$ref": "#/components/responses/ResultsPurged"
					},
					"429": {
						"$ref": "#/components/responses/RateLimited"
					},
					"500": {
						"$ref": "#/components/responses/InternalError"
					}
				}
			}
		}
	},
	"components": {
		"securitySchemes": {
			"bearerAuth": {
				"type": "http",
				"scheme": "bearer",
				"bearerFormat": "bp_<8 characters>_<40 characters>",
				"description": "Every request needs an API key, sent as `Authorization: Bearer <key>`. A key looks like `bp_`, then 8 lower-case letters or digits (its prefix, shown in the app), then `_` and 40 more characters.\n\n**Getting a key.** A company owner or admin creates keys in the Beacon Pro web app, under Company > API. The full key is shown once, when it is created. Beacon stores only a hash of it, so a lost key cannot be shown again: revoke it and create another.\n\n**Scope.** Each key has one of two scopes:\n\n- `read` (in the app: \"Read\"): every GET request, including the Lighthouse-compatible API.\n- `read_write` (in the app: \"Read and write\"): everything a read key can do, plus POST and DELETE requests. This is the scope that starts runs, which spends credits. A write request with a read key is refused with 403 `scope_read_only`.\n\n**Visibility.** Each key also has a visibility, which decides which reports and runs it can see:\n\n- `own` (in the app: \"Its own runs\"; the default): only the reports and runs this key created.\n- `company` (in the app: \"All company runs\"): every report and run of the company, including those made in the web app.\n\nA report or run the key cannot see answers 404 `not_found`, exactly as if it did not exist, and never appears in a list. A key never sees another company's reports or runs.\n\n**Allowed IPs.** A key can be limited to a list of IPv4 addresses or ranges, such as `203.0.113.7, 10.0.0.0/8`. Leave it blank to accept any address. The list is set when the key is created and cannot be changed afterwards: to change it, create a new key and revoke the old one. A request from any other address is refused with 403 `ip_not_allowed`.\n\n**Expiry.** When you create a key you choose when it expires: never (the default), or after 1, 7, 30 or 90 days. A key with an expiry stops working at 23:59:59 UTC on its last day, which is the chosen number of days after the UTC date it was created on. The app shows that date. After it, requests with the key get 401 `key_expired`.\n\n**Other rules.**\n\n- A key acts as the person who created it. If that person or the company is deactivated, the key stops working (401 `user_disabled`).\n- A company can hold up to 50 live keys, unless Beacon has changed that limit for your company. Revoked and expired keys do not count. At the limit, the app refuses a new key until you revoke one.\n- An expired key is not listed in the app, and a request with it still gets 401 `key_expired`.\n- A revoked key is deleted 90 days after it was revoked, and an expired key 90 days after it expired.\n- The `X-Oi-Authorization: <key>` header is also accepted, for tools written for Lighthouse. If both headers are sent, `Authorization: Bearer` is used.\n- `GET /v1/openapi.json` needs no key. Neither does an address that doesn't exist (404) or a Lighthouse endpoint Beacon doesn't provide (501).\n\n**The order of the checks.** The first check that fails gives the answer:\n\n1. 401 `missing_token`: no key in either header.\n2. 429 `rate_limited`: too many unrecognised keys from your address (20 an hour).\n3. 401 `invalid_token`: the key is not recognised.\n4. 401 `key_revoked`.\n5. 401 `key_expired`.\n6. 401 `user_disabled`.\n7. 403 `ip_not_allowed`.\n8. 403 `scope_read_only`: a write with a read key.\n9. 429 `rate_limited`: over the key's per-minute limit."
			}
		},
		"parameters": {
			"ReportId": {
				"name": "id",
				"in": "path",
				"required": true,
				"description": "The report's id.",
				"schema": {
					"type": "integer",
					"minimum": 1
				},
				"example": 5120
			},
			"RunId": {
				"name": "id",
				"in": "path",
				"required": true,
				"description": "The run's id.",
				"schema": {
					"type": "integer",
					"minimum": 1
				},
				"example": 7731
			},
			"ListLimit": {
				"name": "limit",
				"in": "query",
				"required": false,
				"description": "How many to return per page, 1 to 200. Anything else answers 422 `invalid_limit`.",
				"schema": {
					"type": "integer",
					"minimum": 1,
					"maximum": 200,
					"default": 50
				}
			},
			"Cursor": {
				"name": "cursor",
				"in": "query",
				"required": false,
				"description": "The `nextCursor` from the previous page of this list. Pass it back unchanged; do not build or edit one. A cursor from another list, or from another run, answers 422 `invalid_cursor`.",
				"schema": {
					"type": "string"
				}
			},
			"IdempotencyKey": {
				"name": "Idempotency-Key",
				"in": "header",
				"required": false,
				"description": "A value you choose, unique for each action you mean to do once, so that a retry after a timeout never creates or charges twice. Up to 200 characters; longer answers 422 `invalid_idempotency_key`.\n\n- A repeat with the same key, path and body gets exactly the first answer again, byte for byte, and nothing is done twice.\n- The same key with a different path or body answers 422 `idempotency_mismatch`.\n- A repeat that arrives while the first request is still being handled answers 409 `idempotency_in_progress`, with `Retry-After: 5`. A first request that never finished is given up after 60 seconds, and the key can then be used again.\n- The answers kept for repeats are every 2xx, 402 `insufficient_credits` and 409 `report_already_run`. Other answers are not kept, so you can send the same key again once you have fixed the cause (for example after 429 `too_many_active_runs`, or after 409 `run_not_completed`, `retry_cooldown` or `results_purged`).\n- Keys belong to your company and are forgotten after 24 hours.",
				"schema": {
					"type": "string",
					"maxLength": 200
				},
				"example": "larkspur-otterby-2026-10-05"
			}
		},
		"headers": {
			"WWWAuthenticate": {
				"description": "Sent with every 401, and always `Bearer`.",
				"schema": {
					"type": "string"
				},
				"example": "Bearer"
			},
			"RetryAfter": {
				"description": "Seconds to wait before trying again. Sent with 429 `rate_limited`, and with 409 `idempotency_in_progress` (always 5). Not sent with 429 `too_many_active_runs`.",
				"schema": {
					"type": "integer"
				},
				"example": 60
			},
			"ETag": {
				"description": "The tag of this answer. Send it back in `If-None-Match`. It changes when the run is retried, the report's properties change, the filters differ or Beacon changes the response format.",
				"schema": {
					"type": "string"
				},
				"example": "\"5b1f0c2e9a7d\""
			},
			"CacheControl": {
				"description": "Your own client may reuse the body for 60 seconds; a shared cache never may. Errors are always `no-store`.",
				"schema": {
					"type": "string"
				},
				"example": "private, max-age=60"
			},
			"Vary": {
				"description": "The answer depends on the key sent.",
				"schema": {
					"type": "string"
				},
				"example": "Authorization, X-Oi-Authorization"
			}
		},
		"responses": {
			"Unauthorized": {
				"description": "The key was not accepted. `code` is `missing_token` (no key sent), `invalid_token` (not a key Beacon issued), `key_revoked`, `key_expired`, or `user_disabled` (the person who created the key, or the company, is no longer active).",
				"headers": {
					"WWW-Authenticate": {
						"$ref": "#/components/headers/WWWAuthenticate"
					}
				},
				"content": {
					"application/json": {
						"schema": {
							"$ref": "#/components/schemas/ErrorResponse"
						},
						"examples": {
							"missing_token": {
								"summary": "No key sent",
								"value": {
									"error": {
										"code": "missing_token",
										"message": "Send your API key as \"Authorization: Bearer <key>\" (or \"X-Oi-Authorization: <key>\")."
									}
								}
							},
							"invalid_token": {
								"summary": "Key not recognised",
								"value": {
									"error": {
										"code": "invalid_token",
										"message": "The API key is not recognised."
									}
								}
							},
							"key_revoked": {
								"summary": "Key revoked",
								"value": {
									"error": {
										"code": "key_revoked",
										"message": "This API key has been revoked."
									}
								}
							},
							"key_expired": {
								"summary": "Key expired",
								"value": {
									"error": {
										"code": "key_expired",
										"message": "This API key has expired."
									}
								}
							},
							"user_disabled": {
								"summary": "Account not active",
								"value": {
									"error": {
										"code": "user_disabled",
										"message": "The account this API key belongs to is not active."
									}
								}
							}
						}
					}
				}
			},
			"Forbidden": {
				"description": "The key is valid but may not do this. `code` is `ip_not_allowed` (the request came from an address outside the key's allowed IPs) or `scope_read_only` (a write request with a read key).",
				"content": {
					"application/json": {
						"schema": {
							"$ref": "#/components/schemas/ErrorResponse"
						},
						"examples": {
							"ip_not_allowed": {
								"summary": "Address not allowed",
								"value": {
									"error": {
										"code": "ip_not_allowed",
										"message": "This API key may not be used from this address."
									}
								}
							},
							"scope_read_only": {
								"summary": "Read key on a write request",
								"value": {
									"error": {
										"code": "scope_read_only",
										"message": "This API key is read-only."
									}
								}
							}
						}
					}
				}
			},
			"NotFound": {
				"description": "No such report or run, or one your key cannot see (another company's, or, for a key with `own` visibility, one this key did not create). The two cases look the same on purpose.",
				"content": {
					"application/json": {
						"schema": {
							"$ref": "#/components/schemas/ErrorResponse"
						},
						"examples": {
							"run": {
								"summary": "No such run",
								"value": {
									"error": {
										"code": "not_found",
										"message": "No such run."
									}
								}
							},
							"report": {
								"summary": "No such report",
								"value": {
									"error": {
										"code": "not_found",
										"message": "No such report."
									}
								}
							}
						}
					}
				}
			},
			"InsufficientCredits": {
				"description": "Your company does not have enough credits for the run, which costs one credit per property. Nothing was created or charged. A company with unlimited credits never gets this answer. It is kept against the `Idempotency-Key`, so a repeat gets the same 402.",
				"content": {
					"application/json": {
						"schema": {
							"$ref": "#/components/schemas/ErrorResponse"
						},
						"examples": {
							"insufficient_credits": {
								"summary": "Not enough credits",
								"value": {
									"error": {
										"code": "insufficient_credits",
										"message": "Not enough credits to run this report: 3 needed, 1 available.",
										"details": {
											"balance": 1,
											"required": 3
										}
									}
								}
							}
						}
					}
				}
			},
			"ValidationFailed": {
				"description": "The request was refused and nothing was created. `code` is one of:\n\n- `validation_failed`: a field is wrong. `details` maps each field to a list of messages.\n- `invalid_json`: the body is not valid JSON, or not a JSON object.\n- `idempotency_mismatch`: this `Idempotency-Key` was already used with a different path or body.\n- `invalid_idempotency_key`: the `Idempotency-Key` is longer than 200 characters.",
				"content": {
					"application/json": {
						"schema": {
							"$ref": "#/components/schemas/ErrorResponse"
						},
						"examples": {
							"validation_failed": {
								"summary": "Fields are wrong",
								"value": {
									"error": {
										"code": "validation_failed",
										"message": "The report was not created; see details.",
										"details": {
											"name": ["Enter a report name."],
											"currency": ["Choose a currency from the list."],
											"nights": ["Choose a stay length of 1 to 7 nights."],
											"properties": ["Add at least one property."]
										}
									}
								}
							},
							"bad_url": {
								"summary": "A property URL is not accepted",
								"value": {
									"error": {
										"code": "validation_failed",
										"message": "The run was not started; see details.",
										"details": {
											"properties": ["Invalid url provided. Please try again."]
										}
									}
								}
							},
							"bad_reference": {
								"summary": "The reference contains a comma",
								"value": {
									"error": {
										"code": "validation_failed",
										"message": "The run was not started; see details.",
										"details": {
											"reference": ["reference must not contain a comma."]
										}
									}
								}
							},
							"idempotency_mismatch": {
								"summary": "Idempotency-Key reused",
								"value": {
									"error": {
										"code": "idempotency_mismatch",
										"message": "This Idempotency-Key was already used for a different request (another endpoint or another body)."
									}
								}
							}
						}
					}
				}
			},
			"IdempotencyRejected": {
				"description": "The `Idempotency-Key` was refused. `code` is `idempotency_mismatch` (already used with a different path or body) or `invalid_idempotency_key` (longer than 200 characters).",
				"content": {
					"application/json": {
						"schema": {
							"$ref": "#/components/schemas/ErrorResponse"
						},
						"examples": {
							"idempotency_mismatch": {
								"summary": "Idempotency-Key reused",
								"value": {
									"error": {
										"code": "idempotency_mismatch",
										"message": "This Idempotency-Key was already used for a different request (another endpoint or another body)."
									}
								}
							}
						}
					}
				}
			},
			"StartRunRejected": {
				"description": "The run was not started and nothing was charged. `code` is one of:\n\n- `validation_failed`: `timeZone` is not a name in the IANA list. `details.timeZone` has the message.\n- `invalid_json`: the body is not valid JSON, or not a JSON object.\n- `idempotency_mismatch`: this `Idempotency-Key` was already used with a different path or body.\n- `invalid_idempotency_key`: the `Idempotency-Key` is longer than 200 characters.",
				"content": {
					"application/json": {
						"schema": {
							"$ref": "#/components/schemas/ErrorResponse"
						},
						"examples": {
							"validation_failed": {
								"summary": "timeZone is not an IANA name",
								"value": {
									"error": {
										"code": "validation_failed",
										"message": "The run was not started; see details.",
										"details": {
											"timeZone": ["timeZone must be an IANA time zone name, such as Europe/London."]
										}
									}
								}
							},
							"invalid_json": {
								"summary": "The body is not JSON",
								"value": {
									"error": {
										"code": "invalid_json",
										"message": "The request body is not valid JSON."
									}
								}
							},
							"idempotency_mismatch": {
								"summary": "Idempotency-Key reused",
								"value": {
									"error": {
										"code": "idempotency_mismatch",
										"message": "This Idempotency-Key was already used for a different request (another endpoint or another body)."
									}
								}
							}
						}
					}
				}
			},
			"InvalidQuery": {
				"description": "A query parameter is wrong. `code` is `invalid_limit`, `invalid_cursor`, `invalid_reference` (more than 100 references), `invalid_status` or `invalid_date`.",
				"content": {
					"application/json": {
						"schema": {
							"$ref": "#/components/schemas/ErrorResponse"
						},
						"examples": {
							"invalid_limit": {
								"summary": "limit out of range",
								"value": {
									"error": {
										"code": "invalid_limit",
										"message": "limit must be a whole number from 1 to 2000."
									}
								}
							},
							"invalid_cursor": {
								"summary": "cursor from somewhere else",
								"value": {
									"error": {
										"code": "invalid_cursor",
										"message": "The cursor is not one this run gave."
									}
								}
							}
						}
					}
				}
			},
			"IdempotencyInProgress": {
				"description": "A request with the same `Idempotency-Key` is still being handled (`idempotency_in_progress`). Wait for `Retry-After` seconds and send it again.",
				"headers": {
					"Retry-After": {
						"$ref": "#/components/headers/RetryAfter"
					}
				},
				"content": {
					"application/json": {
						"schema": {
							"$ref": "#/components/schemas/ErrorResponse"
						},
						"examples": {
							"idempotency_in_progress": {
								"summary": "The first request is still being handled",
								"value": {
									"error": {
										"code": "idempotency_in_progress",
										"message": "A request with this Idempotency-Key is still being processed; try again shortly.",
										"details": {
											"retryAfterSeconds": 5
										}
									}
								}
							}
						}
					}
				}
			},
			"ResultsPurged": {
				"description": "The run's results were deleted, either with **Delete a run's results** or by your company's retention period (`results_purged`). `details.purgedAt` says when. The run itself still answers.",
				"content": {
					"application/json": {
						"schema": {
							"$ref": "#/components/schemas/ErrorResponse"
						},
						"examples": {
							"results_purged": {
								"summary": "Results deleted",
								"value": {
									"error": {
										"code": "results_purged",
										"message": "The rows of this run have been deleted.",
										"details": {
											"purgedAt": "2026-10-05T08:10:09Z"
										}
									}
								}
							}
						}
					}
				}
			},
			"RateLimited": {
				"description": "Too many requests (`rate_limited`). Wait for the number of seconds in `Retry-After`, then try again.\n\n| Limit | Default | When you go over it |\n|---|---|---|\n| Reads per key (GET requests, and every Lighthouse-compatible request) | 600 a minute | 429 `rate_limited`, `Retry-After: 60` |\n| Writes per key (POST and DELETE requests) | 60 a minute | 429 `rate_limited`, `Retry-After: 60` |\n| Unrecognised keys from one IP address | 20 an hour | 429 `rate_limited`, `Retry-After: 3600` |\n| Queued or running runs per company | 5 at once | 429 `too_many_active_runs`, with `details.active` and `details.limit` |\n| Live keys per company | 50 | The web app will not create another key |\n\nReads and writes are counted separately, per key, per calendar minute, so frequent progress checks never use up your writes. `Retry-After` (in seconds) is the only rate-limit header; there are no remaining-count headers.\n\nThe figures in the table are the defaults. Beacon can set different limits for your company, and the web app shows your company's own figures under Company > API. To ask for a higher limit, contact Beacon.",
				"headers": {
					"Retry-After": {
						"$ref": "#/components/headers/RetryAfter"
					}
				},
				"content": {
					"application/json": {
						"schema": {
							"$ref": "#/components/schemas/ErrorResponse"
						},
						"examples": {
							"per_key": {
								"summary": "Over the per-minute limit for this key",
								"value": {
									"error": {
										"code": "rate_limited",
										"message": "Too many requests for this API key. Wait a minute and try again."
									}
								}
							},
							"per_address": {
								"summary": "Too many unrecognised keys from your address",
								"value": {
									"error": {
										"code": "rate_limited",
										"message": "Too many attempts from this connection. Wait an hour and try again."
									}
								}
							}
						}
					}
				}
			},
			"RateLimitedOrRunLimit": {
				"description": "Either too many requests (`rate_limited`, with `Retry-After`), or your company already has as many queued or running runs as it may (`too_many_active_runs`, 5 by default). `too_many_active_runs` has no `Retry-After` header: start the run again when one of yours has finished. It is not stored against the `Idempotency-Key`, so you can resend the same request.\n\n| Limit | Default | When you go over it |\n|---|---|---|\n| Reads per key (GET requests, and every Lighthouse-compatible request) | 600 a minute | 429 `rate_limited`, `Retry-After: 60` |\n| Writes per key (POST and DELETE requests) | 60 a minute | 429 `rate_limited`, `Retry-After: 60` |\n| Unrecognised keys from one IP address | 20 an hour | 429 `rate_limited`, `Retry-After: 3600` |\n| Queued or running runs per company | 5 at once | 429 `too_many_active_runs`, with `details.active` and `details.limit` |\n| Live keys per company | 50 | The web app will not create another key |\n\nThe figures in the table are the defaults. Beacon can set different limits for your company, and the web app shows your company's own figures under Company > API.",
				"headers": {
					"Retry-After": {
						"$ref": "#/components/headers/RetryAfter"
					}
				},
				"content": {
					"application/json": {
						"schema": {
							"$ref": "#/components/schemas/ErrorResponse"
						},
						"examples": {
							"too_many_active_runs": {
								"summary": "Company run limit reached",
								"value": {
									"error": {
										"code": "too_many_active_runs",
										"message": "The company already has as many queued or running runs as it may; start this one when one has finished.",
										"details": {
											"active": 5,
											"limit": 5
										}
									}
								}
							},
							"rate_limited": {
								"summary": "Over the per-minute limit for this key",
								"value": {
									"error": {
										"code": "rate_limited",
										"message": "Too many requests for this API key. Wait a minute and try again."
									}
								}
							}
						}
					}
				}
			},
			"InternalError": {
				"description": "Something went wrong on Beacon's side (`internal_error`), and it has been logged. It is safe to try again; for a POST or DELETE, send the same `Idempotency-Key`.",
				"content": {
					"application/json": {
						"schema": {
							"$ref": "#/components/schemas/ErrorResponse"
						}
					}
				}
			}
		},
		"schemas": {
			"ErrorResponse": {
				"type": "object",
				"description": "Every error from this API has this shape. Branch on `code`, not on `message`: messages are written for people and may be reworded.\n\nCodes by status: 400 `invalid_filter`; 401 `missing_token`, `invalid_token`, `key_revoked`, `key_expired`, `user_disabled`; 402 `insufficient_credits`; 403 `ip_not_allowed`, `scope_read_only`; 404 `not_found` (also for an address under /v1 that does not exist); 405 `method_not_allowed` (the `Allow` header lists the methods the path accepts); 409 `idempotency_in_progress`, `report_already_run`, `run_not_completed`, `retry_cooldown`, `results_purged`; 410 `results_purged`; 422 `validation_failed`, `invalid_json`, `invalid_limit`, `invalid_cursor`, `invalid_status`, `invalid_date`, `invalid_reference`, `invalid_idempotency_key`, `idempotency_mismatch`; 429 `rate_limited`, `too_many_active_runs`; 500 `internal_error`.",
				"required": ["error"],
				"properties": {
					"error": {
						"type": "object",
						"required": ["code", "message"],
						"properties": {
							"code": {
								"type": "string",
								"description": "A stable code your code can rely on.",
								"example": "run_not_completed"
							},
							"message": {
								"type": "string",
								"description": "An explanation for a person. It may be reworded.",
								"example": "The run has not finished, so it has no rates to select yet."
							},
							"details": {
								"type": "object",
								"additionalProperties": true,
								"description": "Extra facts, sent only with some codes and left out otherwise: `validation_failed` (a list of messages for each field), `insufficient_credits` (`balance`, `required`), `too_many_active_runs` (`active`, `limit`), `results_purged` (`purgedAt`), `retry_cooldown` and `idempotency_in_progress` (`retryAfterSeconds`)."
							}
						}
					}
				}
			},
			"Credits": {
				"type": "object",
				"required": ["companyId", "balance", "unlimited"],
				"properties": {
					"companyId": {
						"type": "integer",
						"description": "Your company's id.",
						"example": 418
					},
					"balance": {
						"type": "integer",
						"description": "Credits available now.",
						"example": 240
					},
					"unlimited": {
						"type": "boolean",
						"description": "True when Beacon has set your company to unlimited credits. Runs are then not charged (each still adds a zero-credit line to your credit history) and 402 `insufficient_credits` never happens. `balance` is still your real balance, and does not change while this is true.",
						"example": false
					}
				}
			},
			"PropertyRequest": {
				"type": "object",
				"required": ["url"],
				"properties": {
					"url": {
						"type": "string",
						"description": "The property's own page on a supported source. Accepted formats, by source:\n\n- `booking`: `https://www.booking.com/hotel/<country>/<property>.html`\n- `travelodge`: `https://www.travelodge.co.uk/hotels/<id>/<property>`\n- `premierinn`: `https://www.premierinn.com/gb/en/hotels/<path>.html`",
						"example": "https://www.booking.com/hotel/gb/the-larkspur-otterby.html"
					},
					"name": {
						"type": "string",
						"maxLength": 255,
						"description": "A display name, up to 255 characters. Without one, the property is named from its URL.",
						"example": "The Larkspur Hotel"
					}
				}
			},
			"CreateReportRequest": {
				"type": "object",
				"required": ["name", "currency", "nights", "properties"],
				"properties": {
					"name": {
						"type": "string",
						"minLength": 1,
						"maxLength": 100,
						"description": "Your name for the report, 1 to 100 characters.",
						"example": "Otterby competitors"
					},
					"currency": {
						"type": "string",
						"enum": [
							"AED",
							"AUD",
							"BGN",
							"BRL",
							"CAD",
							"CHF",
							"CNY",
							"CZK",
							"DKK",
							"EUR",
							"GBP",
							"HKD",
							"HUF",
							"IDR",
							"ILS",
							"INR",
							"JPY",
							"KRW",
							"MXN",
							"MYR",
							"NOK",
							"NZD",
							"PLN",
							"RON",
							"SEK",
							"SGD",
							"THB",
							"TRY",
							"USD",
							"ZAR"
						],
						"description": "The currency prices are captured in, as an ISO 4217 code.",
						"example": "GBP"
					},
					"nights": {
						"type": "integer",
						"minimum": 1,
						"maximum": 7,
						"description": "Stay length in nights, 1 to 7. Every price is for a stay of this many nights.",
						"example": 1
					},
					"properties": {
						"type": "array",
						"minItems": 1,
						"description": "The properties to check, 1 to 50 by default (your company's own figure is in the web app under Company > API), in the order you want them. A URL listed twice is kept once. The Lighthouse-compatible API treats the first property as your own and the others as its competitors.",
						"items": {
							"$ref": "#/components/schemas/PropertyRequest"
						}
					}
				}
			},
			"CreateRunRequest": {
				"description": "The **Create a report** body, plus your `reference`.",
				"allOf": [
					{
						"$ref": "#/components/schemas/CreateReportRequest"
					},
					{
						"type": "object",
						"required": ["reference"],
						"properties": {
							"reference": {
								"type": "string",
								"minLength": 1,
								"maxLength": 128,
								"pattern": "^[^,]+$",
								"description": "Your own label for what this run is about, such as a property or a client: 1 to 128 characters, with no comma. Use it to find the run with **List runs**. It does not have to be unique. The Lighthouse-compatible API uses it as the `subscriptionId`, and reads the newest completed run with that reference whose rows have not been deleted.",
								"example": "larkspur-otterby"
							},
							"timeZone": {
								"type": "string",
								"nullable": true,
								"description": "The run's IANA time zone, spelled as in the IANA list, such as `Europe/Berlin`. The run's window starts on today's date in it, and past check-in dates are judged by the same zone's date. Left out or null: your company's time zone (Europe/London unless changed in the web app). A name that is not in the IANA list answers 422 `validation_failed` with `details.timeZone`. All other times and dates are UTC.",
								"example": "Europe/Berlin"
							}
						}
					}
				]
			},
			"StartRunRequest": {
				"type": "object",
				"description": "The optional body of **Start a report's run**.",
				"properties": {
					"timeZone": {
						"type": "string",
						"nullable": true,
						"description": "The run's IANA time zone, spelled as in the IANA list, such as `Europe/Berlin`. The run's window starts on today's date in it, and past check-in dates are judged by the same zone's date. Left out or null: your company's time zone (Europe/London unless changed in the web app). A name that is not in the IANA list answers 422 `validation_failed` with `details.timeZone`. All other times and dates are UTC.",
						"example": "Europe/Berlin"
					}
				}
			},
			"Run": {
				"type": "object",
				"description": "One capture of a report. Times are UTC. A field with no value is null, never missing.",
				"required": [
					"id",
					"reportId",
					"reference",
					"status",
					"complete",
					"origin",
					"requestsTotal",
					"done",
					"soldOut",
					"failed",
					"past",
					"restricted",
					"remaining",
					"creditsCharged",
					"dateFrom",
					"dateTo",
					"timeZone",
					"startedAt",
					"finishedAt",
					"lastRetriedAt",
					"resultsPurgedAt"
				],
				"properties": {
					"id": {
						"type": "integer",
						"description": "The run's id."
					},
					"reportId": {
						"type": "integer",
						"description": "The report this run belongs to."
					},
					"reference": {
						"type": "string",
						"nullable": true,
						"maxLength": 128,
						"description": "Your reference, as sent to **Start a run**. Null for a run started any other way. Not unique."
					},
					"status": {
						"type": "string",
						"enum": ["queued", "running", "completed"],
						"description": "`queued` (waiting to start), `running` or `completed`."
					},
					"complete": {
						"type": "boolean",
						"description": "True once `status` is `completed`. If `failed` is above 0 on a completed run, its results are partial."
					},
					"origin": {
						"type": "string",
						"enum": ["web", "api"],
						"description": "Where the run was started: `api`, or `web` (the Beacon Pro web app)."
					},
					"requestsTotal": {
						"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. You can try them again with **Retry failed check-in dates**."
					},
					"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: the stay length is shorter than the property's minimum, so no rate can be booked. The check-in date is reported with `rates.restrictionlosN` and availability `min stay N nights`, where N is the minimum number of nights."
					},
					"remaining": {
						"type": "integer",
						"description": "Check-in dates still to fetch: `requestsTotal` minus `done`, `soldOut`, `failed`, `past` and `restricted`. Zero when nothing is left."
					},
					"creditsCharged": {
						"type": "integer",
						"description": "Credits charged when the run was accepted. 0 for a company with unlimited credits."
					},
					"dateFrom": {
						"type": "string",
						"format": "date",
						"description": "First check-in date the run covers: today's date in the run's time zone (`timeZone`) when the run started. The window lasts 365 days."
					},
					"dateTo": {
						"type": "string",
						"format": "date",
						"description": "Last check-in date the run covers (included): 364 days after `dateFrom`."
					},
					"timeZone": {
						"type": "string",
						"description": "The run's IANA time zone, such as `Europe/London`: the one sent when the run was started, or your company's time zone at that moment. `dateFrom` is today's date in it when the run started, and past check-in dates are judged by the same zone's date. All other times and dates are UTC.",
						"example": "Europe/London"
					},
					"startedAt": {
						"type": "string",
						"format": "date-time",
						"description": "When the run was accepted (UTC). It is set as soon as the run is created, so a queued run has it too."
					},
					"finishedAt": {
						"type": "string",
						"format": "date-time",
						"nullable": true,
						"description": "When the run completed. Null until then."
					},
					"lastRetriedAt": {
						"type": "string",
						"format": "date-time",
						"nullable": true,
						"description": "When failed check-in dates were last retried, with this API or in the web app. Null if never."
					},
					"resultsPurgedAt": {
						"type": "string",
						"format": "date-time",
						"nullable": true,
						"description": "When the run's results were deleted, by **Delete a run's results** or by your company's retention period. From then on, reading results or rates answers 410 `results_purged`."
					}
				},
				"example": {
					"id": 7731,
					"reportId": 5120,
					"reference": "larkspur-otterby",
					"status": "completed",
					"complete": true,
					"origin": "api",
					"requestsTotal": 1095,
					"done": 1080,
					"soldOut": 11,
					"failed": 4,
					"past": 0,
					"restricted": 0,
					"remaining": 0,
					"creditsCharged": 3,
					"dateFrom": "2026-10-05",
					"dateTo": "2027-10-04",
					"timeZone": "Europe/London",
					"startedAt": "2026-10-05T07:00:04Z",
					"finishedAt": "2026-10-05T07:52:41Z",
					"lastRetriedAt": null,
					"resultsPurgedAt": null
				}
			},
			"RunList": {
				"type": "object",
				"required": ["runs", "nextCursor"],
				"properties": {
					"runs": {
						"type": "array",
						"items": {
							"$ref": "#/components/schemas/Run"
						}
					},
					"nextCursor": {
						"type": "string",
						"nullable": true,
						"description": "Pass this as `cursor` to get the next page. Null on the last page."
					}
				}
			},
			"ReportProperty": {
				"type": "object",
				"description": "A property in a report.",
				"required": ["id", "name", "url", "source", "position", "profile"],
				"properties": {
					"id": {
						"type": "integer",
						"description": "The property's id within this report. Results use it as `propertyId`. The same property in another report has a different id; use `hotelId` in rates to follow one property across reports."
					},
					"name": {
						"type": "string",
						"description": "The property's display name."
					},
					"url": {
						"type": "string",
						"description": "The property's page."
					},
					"source": {
						"type": "string",
						"enum": ["booking", "travelodge", "premierinn"],
						"description": "The source the property is checked on."
					},
					"position": {
						"type": "integer",
						"description": "Its place in the report, from 0 for the first property."
					},
					"profile": {
						"allOf": [
							{
								"$ref": "#/components/schemas/PropertyProfile"
							}
						],
						"nullable": true,
						"description": "What is known about the hotel, or null until the property has been checked."
					}
				}
			},
			"PropertyProfile": {
				"type": "object",
				"description": "What is known about the hotel. Every field is null until known.",
				"required": ["stars", "reviewScore", "reviewCount", "reviewScale", "latitude", "longitude", "city", "postcode", "countryCode", "propertyType", "brand", "checkInFrom", "checkInUntil", "checkOutFrom", "checkOutUntil", "breakfastPrice", "breakfastCurrency", "cityTaxIncluded", "facilities", "observedAt"],
				"properties": {
					"stars": {
						"type": "integer",
						"nullable": true,
						"description": "The star rating, 1 to 5. Null when unknown."
					},
					"reviewScore": {
						"type": "number",
						"nullable": true,
						"description": "The guest review score, out of `reviewScale`. Null when unknown."
					},
					"reviewCount": {
						"type": "integer",
						"nullable": true,
						"description": "How many reviews the score is based on. Null when unknown."
					},
					"reviewScale": {
						"type": "integer",
						"nullable": true,
						"description": "The top of the review score: 10 for Booking.com, 5 for Travelodge and Premier Inn. Null when unknown."
					},
					"latitude": {
						"type": "number",
						"nullable": true,
						"description": "Latitude in degrees. Null when unknown."
					},
					"longitude": {
						"type": "number",
						"nullable": true,
						"description": "Longitude in degrees. Null when unknown."
					},
					"city": {
						"type": "string",
						"nullable": true,
						"description": "The city or town. Null when unknown. Filled for Booking.com, Travelodge and Premier Inn."
					},
					"postcode": {
						"type": "string",
						"nullable": true,
						"description": "The postcode. Null when unknown."
					},
					"countryCode": {
						"type": "string",
						"nullable": true,
						"description": "The two-letter country code, for example `GB`. Null when unknown."
					},
					"propertyType": {
						"type": "string",
						"nullable": true,
						"description": "The kind of property, for example `hotel`. Null when unknown."
					},
					"brand": {
						"type": "string",
						"nullable": true,
						"description": "The brand. The vocabulary depends on the source. Premier Inn: the code `PI`, `PID`, `HUB` or `ZIP`. Travelodge: `Travelodge` or `Travelodge PLUS`. Booking.com: the brand name, for example `Hilton Hotels & Resorts` or `easyHotel`. Null when unknown."
					},
					"checkInFrom": {
						"type": "string",
						"nullable": true,
						"description": "The earliest check-in time, as HH:MM. Filled for Booking.com, Premier Inn and Travelodge. Null when unknown."
					},
					"checkInUntil": {
						"type": "string",
						"nullable": true,
						"description": "The latest check-in time, as HH:MM. Booking.com only; Premier Inn and Travelodge do not state it. Null when unknown."
					},
					"checkOutFrom": {
						"type": "string",
						"nullable": true,
						"description": "The earliest check-out time, as HH:MM. Booking.com only; Premier Inn and Travelodge do not state it. Null when unknown."
					},
					"checkOutUntil": {
						"type": "string",
						"nullable": true,
						"description": "The latest check-out time, as HH:MM. Filled for Booking.com, Premier Inn and Travelodge. Null when unknown."
					},
					"breakfastPrice": {
						"type": "number",
						"nullable": true,
						"description": "The price of breakfast per person, in `breakfastCurrency`. Null when unknown."
					},
					"breakfastCurrency": {
						"type": "string",
						"nullable": true,
						"description": "The currency of `breakfastPrice`, as a three-letter code. Null when unknown."
					},
					"cityTaxIncluded": {
						"type": "boolean",
						"nullable": true,
						"description": "True when the prices the source shows include city tax. Null when unknown."
					},
					"facilities": {
						"type": "array",
						"nullable": true,
						"items": {
							"type": "string"
						},
						"description": "The facilities the property lists, as names. Null when unknown."
					},
					"observedAt": {
						"type": "string",
						"format": "date-time",
						"description": "When the profile was last updated, in UTC (ISO 8601)."
					}
				}
			},
			"ReportLatestRun": {
				"type": "object",
				"nullable": true,
				"description": "A summary of the report's run, with the same meanings as in the run. Null before the report has been run.",
				"properties": {
					"id": {
						"type": "integer",
						"description": "The run's id."
					},
					"status": {
						"type": "string",
						"enum": ["queued", "running", "completed"],
						"description": "`queued` (waiting to start), `running` or `completed`."
					},
					"complete": {
						"type": "boolean",
						"description": "True once `status` is `completed`."
					},
					"requestsTotal": {
						"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. You can try them again with **Retry failed check-in dates**."
					},
					"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: the stay length is shorter than the property's minimum, so no rate can be booked. The check-in date is reported with `rates.restrictionlosN` and availability `min stay N nights`, where N is the minimum number of nights."
					},
					"remaining": {
						"type": "integer",
						"description": "Check-in dates still to fetch: `requestsTotal` minus `done`, `soldOut`, `failed`, `past` and `restricted`. Zero when nothing is left."
					},
					"startedAt": {
						"type": "string",
						"format": "date-time",
						"nullable": true,
						"description": "When the run was started (UTC)."
					},
					"finishedAt": {
						"type": "string",
						"format": "date-time",
						"nullable": true,
						"description": "When the run completed (UTC). Null until then."
					}
				}
			},
			"Report": {
				"type": "object",
				"description": "A list of properties to check, with the currency and stay length. A report is run once; its run is the capture. A field with no value is null, never missing.",
				"required": ["id", "name", "currency", "nights", "createdAt", "origin", "reference", "properties", "latestRun"],
				"properties": {
					"id": {
						"type": "integer",
						"description": "The report's id."
					},
					"name": {
						"type": "string",
						"description": "The report's name, 1 to 100 characters."
					},
					"currency": {
						"type": "string",
						"description": "The currency prices are captured in, as an ISO 4217 code."
					},
					"nights": {
						"type": "integer",
						"description": "Stay length in nights, 1 to 7."
					},
					"createdAt": {
						"type": "string",
						"format": "date-time",
						"description": "When the report was created (UTC)."
					},
					"origin": {
						"type": "string",
						"enum": ["web", "api"],
						"description": "Where the report was created: `api`, or `web` (the Beacon Pro web app)."
					},
					"reference": {
						"type": "string",
						"nullable": true,
						"description": "Always null. References belong to runs, not reports."
					},
					"properties": {
						"type": "array",
						"description": "The report's properties, in report order.",
						"items": {
							"$ref": "#/components/schemas/ReportProperty"
						}
					},
					"latestRun": {
						"$ref": "#/components/schemas/ReportLatestRun"
					}
				}
			},
			"ReportList": {
				"type": "object",
				"required": ["reports", "nextCursor"],
				"properties": {
					"reports": {
						"type": "array",
						"items": {
							"$ref": "#/components/schemas/Report"
						}
					},
					"nextCursor": {
						"type": "string",
						"nullable": true,
						"description": "Pass this as `cursor` to get the next page. Null on the last page."
					}
				}
			},
			"ResultRow": {
				"type": "object",
				"description": "One room and rate found by the run, or a placeholder for a property and check-in date with no rooms. On a placeholder row every room field is null. Fields that a source does not report are null too.",
				"required": [
					"propertyId",
					"property",
					"propertyUrl",
					"source",
					"checkIn",
					"availability",
					"room",
					"roomId",
					"rateId",
					"occupancy",
					"price",
					"originalPrice",
					"currency",
					"breakfastIncluded",
					"mealType",
					"freeCancellation",
					"nonRefundable",
					"payNow",
					"onSale",
					"hasOffer",
					"partnerOffer",
					"limitedAvailability",
					"roomsLeft",
					"cheapestOfDay",
					"cheapestOwnRate",
					"undercutsDirect",
					"undercutBy",
					"geniusPrice",
					"excludedReason",
					"conditions",
					"cancellationDeadline",
					"cancellationDeadlineTime",
					"flexibility",
					"reschedulable"
				],
				"properties": {
					"propertyId": {
						"type": "integer",
						"description": "The property's id in the report (`properties[].id`)."
					},
					"property": {
						"type": "string",
						"description": "The property's name."
					},
					"propertyUrl": {
						"type": "string",
						"description": "The property's page, as stored in the report."
					},
					"source": {
						"type": "string",
						"description": "The source the property is checked on: `booking`, `travelodge` or `premierinn`."
					},
					"checkIn": {
						"type": "string",
						"format": "date",
						"description": "Check-in date. The stay lasts the report's stay length (`nights`)."
					},
					"availability": {
						"type": "string",
						"description": "One of `available`, `sold out`, `not fetched`, `past`, `not fetched yet`, `min stay N nights` or `min stay`. `available` on a row with a room and price. On a placeholder row: `sold out`, `not fetched` (the date could not be fetched), `past` (the date had passed before it was fetched), `not fetched yet` (still to come), or `min stay N nights` (the property needs a stay of at least N nights from that date; N is the number of nights, for example `min stay 4 nights`; `min stay` alone when the number is not known). The values contain spaces."
					},
					"room": {
						"type": "string",
						"nullable": true,
						"description": "The room's name as the source shows it. Some Premier Inn rooms are priced by their room class; `conditions` then says \"Derived price\". These rooms are real and bookable."
					},
					"roomId": {
						"type": "string",
						"nullable": true,
						"description": "The source's own id for the room."
					},
					"rateId": {
						"type": "string",
						"nullable": true,
						"description": "The source's own id for the rate."
					},
					"occupancy": {
						"type": "integer",
						"nullable": true,
						"description": "The most guests the room takes. For Travelodge it is the room's bed capacity, so a family room takes 3 or 4."
					},
					"price": {
						"type": "number",
						"nullable": true,
						"description": "The price for the whole stay (all nights), in `currency`. Booking.com can show a members-only preview price instead of the public one. Then `price` is that member price, `geniusPrice` is true and `originalPrice` holds the public price."
					},
					"originalPrice": {
						"type": "number",
						"nullable": true,
						"description": "The struck-through price, when the source shows one before a discount. Null when the source shows none. On a Booking.com members-only preview price, it is the public price."
					},
					"currency": {
						"type": "string",
						"nullable": true,
						"description": "Currency of the prices: the report's currency."
					},
					"breakfastIncluded": {
						"type": "boolean",
						"nullable": true,
						"description": "True when breakfast is included."
					},
					"mealType": {
						"type": "string",
						"nullable": true,
						"enum": ["room_only", "breakfast", "half_board", "full_board", "all_inclusive", "breakfast_lunch", "lunch_dinner", "dinner", "lunch", null],
						"description": "The meal plan of the rate. Null on a placeholder row or when there is no rate. `breakfastIncluded` is set independently, so a half board or full board rate can still show `breakfastIncluded` false; read `mealType` for the meal plan."
					},
					"freeCancellation": {
						"type": "boolean",
						"nullable": true,
						"description": "True when the booking can be cancelled free of charge. A rate refunded by Booking.com rather than the property is false here, although its flexibility is flexible and it has a cancellationDeadline."
					},
					"nonRefundable": {
						"type": "boolean",
						"nullable": true,
						"description": "True when the rate cannot be refunded."
					},
					"payNow": {
						"type": "boolean",
						"nullable": true,
						"description": "True when the source says the rate is paid at the time of booking."
					},
					"onSale": {
						"type": "boolean",
						"nullable": true,
						"description": "True when the source shows the rate as on sale."
					},
					"hasOffer": {
						"type": "boolean",
						"nullable": true,
						"description": "True when the source shows a deal or offer with the rate."
					},
					"partnerOffer": {
						"type": "boolean",
						"nullable": true,
						"description": "True when the rate is offered by a partner of the source rather than by the property itself."
					},
					"limitedAvailability": {
						"type": "boolean",
						"nullable": true,
						"description": "True when the source says few rooms are left at this rate."
					},
					"roomsLeft": {
						"type": "integer",
						"nullable": true,
						"description": "Rooms left, when the source gives a number. Travelodge: 1 to 5, and a higher count comes back as null. Booking.com: the number the source shows. Premier Inn: the site's own count, often large."
					},
					"cheapestOfDay": {
						"type": "boolean",
						"nullable": true,
						"description": "True on the cheapest rate for this property and check-in date. Every row tied at the lowest price is true."
					},
					"cheapestOwnRate": {
						"type": "boolean",
						"nullable": true,
						"description": "True on the property's own cheapest rate for the date, the one partner offers are compared with."
					},
					"undercutsDirect": {
						"type": "boolean",
						"nullable": true,
						"description": "True when a partner offer is cheaper than the property's own cheapest rate."
					},
					"undercutBy": {
						"type": "number",
						"nullable": true,
						"description": "The price difference between a partner offer and the property's own cheapest rate, in `currency`."
					},
					"geniusPrice": {
						"type": "boolean",
						"nullable": true,
						"description": "True when Booking.com shows a Genius member discount for the rate. On a members-only preview price, `price` is the member price and `originalPrice` the public price; otherwise `price` is the public price. `bar=true` leaves those member prices out."
					},
					"excludedReason": {
						"type": "string",
						"nullable": true,
						"description": "Why this row is left out of rate comparisons, such as a rate that needs a larger group. Null on normal rows."
					},
					"conditions": {
						"type": "string",
						"nullable": true,
						"description": "The rate's conditions, when the source gives them."
					},
					"cancellationDeadline": {
						"type": "string",
						"format": "date",
						"nullable": true,
						"description": "The date free cancellation ends, as YYYY-MM-DD. When the source gives a date only, free cancellation ends when that day starts (00:00). Null when the source does not give one."
					},
					"cancellationDeadlineTime": {
						"type": "string",
						"nullable": true,
						"description": "The time of day on that date, as 24-hour `HH:MM` on every source, for example `12:00` or `14:00`. Null when the source does not give one."
					},
					"flexibility": {
						"type": "string",
						"nullable": true,
						"enum": ["flexible", "semi_flexible", "non_flexible", null],
						"description": "How flexible the rate is: `flexible` (can be cancelled free of charge), `semi_flexible` (the source's middle option, such as a rate that can be cancelled free of charge only until a set number of days before arrival) or `non_flexible` (cannot be cancelled free of charge). It sits next to `freeCancellation` and `nonRefundable` and never changes them. Null when the source does not say."
					},
					"reschedulable": {
						"type": "boolean",
						"nullable": true,
						"description": "True when the source says the stay can be moved to other dates if plans change (a change fee still counts). False when it says it cannot be moved, or says nothing. Null only where there is no rate."
					}
				}
			},
			"ResultsPage": {
				"type": "object",
				"required": ["run", "complete", "rows", "nextCursor"],
				"properties": {
					"run": {
						"$ref": "#/components/schemas/Run"
					},
					"complete": {
						"type": "boolean",
						"description": "True when the run has finished, so no more rows will come."
					},
					"rows": {
						"type": "array",
						"items": {
							"$ref": "#/components/schemas/ResultRow"
						}
					},
					"nextCursor": {
						"type": "string",
						"nullable": true,
						"description": "Pass this as `cursor` to get the next page. Null on the last page."
					}
				}
			},
			"DeleteResults": {
				"type": "object",
				"required": ["run", "deletedRows"],
				"properties": {
					"run": {
						"$ref": "#/components/schemas/Run"
					},
					"deletedRows": {
						"type": "integer",
						"description": "How many rows were deleted. 0 when they had already been deleted."
					}
				}
			},
			"RetryResult": {
				"type": "object",
				"required": ["run", "requeued"],
				"properties": {
					"run": {
						"$ref": "#/components/schemas/Run"
					},
					"requeued": {
						"type": "integer",
						"description": "How many failed check-in dates were queued to be fetched again. 0 when nothing had failed."
					}
				}
			},
			"Rate": {
				"type": "object",
				"description": "The rate chosen for one property and check-in date (and room type, with `perRoomType=1`). When there is no rate, `message` says why and every rate field is null.",
				"required": [
					"hotelId",
					"propertyId",
					"hotelName",
					"checkIn",
					"fetchedAt",
					"nights",
					"currency",
					"price",
					"originalPrice",
					"roomName",
					"roomType",
					"maxOccupancy",
					"breakfastIncluded",
					"mealType",
					"freeCancellation",
					"nonRefundable",
					"bestFlex",
					"message",
					"cancellationDeadline",
					"cancellationDeadlineTime",
					"flexibility",
					"reschedulable"
				],
				"properties": {
					"hotelId": {
						"type": "integer",
						"nullable": true,
						"description": "Beacon's stable id for the property. The Lighthouse-compatible API uses the same id. It is not the same as `propertyId`."
					},
					"propertyId": {
						"type": "integer",
						"description": "The property's id in the report (`properties[].id`)."
					},
					"hotelName": {
						"type": "string",
						"description": "The property's name."
					},
					"checkIn": {
						"type": "string",
						"format": "date",
						"description": "Check-in date."
					},
					"fetchedAt": {
						"type": "string",
						"format": "date-time",
						"nullable": true,
						"description": "When this date was fetched (UTC). Null when it never was."
					},
					"nights": {
						"type": "integer",
						"description": "Stay length in nights."
					},
					"currency": {
						"type": "string",
						"nullable": true,
						"description": "Currency of the prices: the report's currency."
					},
					"price": {
						"type": "number",
						"nullable": true,
						"description": "The chosen price for the whole stay. Null when `message` is set. It can be a Booking.com members-only preview price. Set `bar` to true to leave those out."
					},
					"originalPrice": {
						"type": "number",
						"nullable": true,
						"description": "The struck-through price, when the source shows one. Null when the source shows none or `message` is set."
					},
					"roomName": {
						"type": "string",
						"nullable": true,
						"description": "The room's name as the source shows it. Some Premier Inn rooms are priced by their room class; `conditions` then says \"Derived price\". These rooms are real and bookable."
					},
					"roomType": {
						"type": "string",
						"nullable": true,
						"description": "Beacon's classification of the room from its name, using the same values as the `roomType` filter."
					},
					"maxOccupancy": {
						"type": "integer",
						"nullable": true,
						"description": "The most guests the room takes. For Travelodge it is the room's bed capacity, so a family room takes 3 or 4."
					},
					"breakfastIncluded": {
						"type": "boolean",
						"nullable": true,
						"description": "True when breakfast is included."
					},
					"mealType": {
						"type": "string",
						"nullable": true,
						"enum": ["room_only", "breakfast", "half_board", "full_board", "all_inclusive", "breakfast_lunch", "lunch_dinner", "dinner", "lunch", null],
						"description": "The meal plan of the rate. Null on a placeholder row or when there is no rate. `breakfastIncluded` is set independently, so a half board or full board rate can still show `breakfastIncluded` false; read `mealType` for the meal plan."
					},
					"freeCancellation": {
						"type": "boolean",
						"nullable": true,
						"description": "True when the booking can be cancelled free of charge. A rate refunded by Booking.com rather than the property is false here, although its flexibility is flexible and it has a cancellationDeadline."
					},
					"nonRefundable": {
						"type": "boolean",
						"nullable": true,
						"description": "True when the rate cannot be refunded."
					},
					"bestFlex": {
						"type": "boolean",
						"nullable": true,
						"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."
					},
					"message": {
						"type": "string",
						"nullable": true,
						"description": "Null 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.<name>` (the `mealType` name you asked for, such as `rates.meal.missing.half_board`) or `rates.roomtypes.unavailable` (you sent a `roomType` and no room of that type has a rate on that date). See \"How a rate is chosen\". `rates.restrictionlosN` is Beacon's code in the same style: 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.",
						"example": "rates.soldout"
					},
					"cancellationDeadline": {
						"type": "string",
						"format": "date",
						"nullable": true,
						"description": "The date free cancellation ends, as YYYY-MM-DD. When the source gives a date only, free cancellation ends when that day starts (00:00). Null when the source does not give one."
					},
					"cancellationDeadlineTime": {
						"type": "string",
						"nullable": true,
						"description": "The time of day on that date, as 24-hour `HH:MM` on every source, for example `12:00` or `14:00`. Null when the source does not give one."
					},
					"flexibility": {
						"type": "string",
						"nullable": true,
						"enum": ["flexible", "semi_flexible", "non_flexible", null],
						"description": "How flexible the rate is: `flexible` (can be cancelled free of charge), `semi_flexible` (the source's middle option, such as a rate that can be cancelled free of charge only until a set number of days before arrival) or `non_flexible` (cannot be cancelled free of charge). It sits next to `freeCancellation` and `nonRefundable` and never changes them. Null when the source does not say."
					},
					"reschedulable": {
						"type": "boolean",
						"nullable": true,
						"description": "True when the source says the stay can be moved to other dates if plans change (a change fee still counts). False when it says it cannot be moved, or says nothing. Null only where there is no rate."
					}
				}
			},
			"RateFilters": {
				"type": "object",
				"description": "The filters as applied, defaults included. Note that `compsetIds` comes back as a number and `perRoomType` as true or false.",
				"properties": {
					"los": {
						"type": "integer"
					},
					"bar": {
						"type": "boolean"
					},
					"persons": {
						"type": "integer"
					},
					"mealType": {
						"type": "string",
						"description": "The meal plan filter as a name, such as `any` or `half_board`. A legacy numeric code in the request is echoed as its name."
					},
					"roomType": {
						"type": "string",
						"nullable": true
					},
					"fromDate": {
						"type": "string",
						"format": "date"
					},
					"shopLength": {
						"type": "integer",
						"nullable": true,
						"description": "Null when not sent."
					},
					"ota": {
						"type": "string"
					},
					"compsetIds": {
						"type": "integer"
					},
					"perRoomType": {
						"type": "boolean"
					}
				}
			},
			"RunRates": {
				"type": "object",
				"required": ["run", "filters", "rates"],
				"properties": {
					"run": {
						"type": "object",
						"description": "The run the rates come from.",
						"properties": {
							"id": {
								"type": "integer",
								"description": "The run's id."
							},
							"reference": {
								"type": "string",
								"nullable": true,
								"description": "The run's reference, if it has one."
							}
						}
					},
					"filters": {
						"$ref": "#/components/schemas/RateFilters"
					},
					"rates": {
						"type": "array",
						"items": {
							"$ref": "#/components/schemas/Rate"
						}
					}
				}
			}
		}
	}
}
