Which room is which, everywhere.
Match one hotel's rooms across Booking.com, its own site, Expedia and your own list, with a confidence each.
bookingOn the hotel’s site siteIn your list partner_pmsshared[]Classic Double Roomsells Superior Room and Prestige Room under one nameunmatchednearest Deluxe Apartment at 0.41, under 0.5Family SuiteHotels name the same room differently on every channel, and a "Double or Twin" listing can sell two physical rooms. Reconciliation returns one entry per real room with every listing attached, a confidence, the signals behind it and the fields where sources disagree.
It runs as a job when we have to read a URL. When every source is a room list you send and the hotel is known, it answers directly. Identical inputs within 30 days return the stored result for nothing.
Reconciliation needs Build or above; on Free it answers 403 forbidden with details.reason: "tier_required".
Send up to 8 sources. Each is either a URL (kind booking_url, website_url or expedia_url, with url) or a room list you hold (kind: rooms, a label and rooms). URL sources need a hotel that resolves; if it doesn't, the call is a 404 before any work. If the matcher itself fails, it's a 502 and nothing is billed. min_confidence must be above 0. A job made only of your own room lists can finish with hotel_id: null when it named no hotel we know.
Reconcile room types across sources
/v2/rooms/reconcileNeeds the reconcile scope and the Build plan or higher. Answers 200 with the result when every source is kind: rooms and the hotel is known, otherwise 202 with a rooms.reconcile job. Billed once per request, rooms.reconcile.urls when any source is a URL and rooms.reconcile.supplied otherwise; an identical request within 30 days returns the stored result at amount "0". Honours Idempotency-Key.
rooms.reconcile.urls when any source is a URL, else rooms.reconcile.supplied. A repeat within 30 days bills "0".
Parameters
| Name | In | Type | Description |
|---|---|---|---|
Idempotency-Key | header | string | Any unique string. Same key and same body returns the original response; same key and a different body is a 409. |
Request body
| Field | Type | Description |
|---|---|---|
hotel_id | string (uuid) | null | The hotel whose rooms to match; wins over identifiers when both are set. |
identifiers | HotelIdentifier | null | A hotel named by something other than its hotel_id. Send exactly one form: booking_url, website_url, expedia_url, or name with city (and ideally country). |
sourcesREQUIRED | (UrlSource | RoomsSource)[] | Between 1 and 8 sources to match: channels named by URL, or rooms you supply. At most 8. |
sources[].kindREQUIRED | string | booking_url (third-party channel listing), website_url (the hotel's website) or expedia_url; their listings appear under the labels booking, site and expedia. One of booking_url, website_url, expedia_url. Or: Always rooms. Always rooms. |
sources[].url | string (uri) | Public http(s) URL of the hotel on that channel. |
sources[].label | string | Name for this source in the result, lower case letters, digits and underscores, for example partner_pms. |
sources[].rooms | SuppliedRoom[] | Up to 500 rooms (1,000 across all sources). |
sources[].rooms[].keyREQUIRED | string | Your key for the room; it comes back as the listing_key of this room's listing. |
sources[].rooms[].nameREQUIRED | string | Room name in your system. |
sources[].rooms[].size_m2 | number | null | Room size in square metres. 0–10000. |
sources[].rooms[].description | string | null | Room description, up to 5,000 characters. |
sources[].rooms[].max_occupancy | integer | null | Most guests the room can hold. 1–50. |
sources[].rooms[].bed_config | SuppliedBed[] | Beds in the room. At most 10. |
sources[].rooms[].bed_config[].typeREQUIRED | string | Bed type, for example king, double, twin or sofa bed. |
sources[].rooms[].bed_config[].count | integer | Number of beds of this type, 1 to 20. Default 1. |
sources[].rooms[].images | string (uri)[] | Up to 30 public http(s) image URLs of the room, used for image matching. |
min_confidence | number | Lowest match confidence, above 0 and at most 1, for two listings to be grouped as one room; weaker pairs stay in unmatched. Default 0.5. |
Example
curl -X POST 'https://api.vistalink.com/v2/rooms/reconcile' \
-H "Authorization: Bearer $VISTALINK_API_KEY" \
-H 'Idempotency-Key: 4f1c2b8e-booking-ada-rossi' \
-H "Content-Type: application/json" \
-d '{
"hotel_id": "43d0be71-5f3c-4c8e-9a52-1d7e2b6f0a19",
"sources": [
{
"kind": "website_url",
"url": "https://umilta36.com/rooms"
},
{
"kind": "rooms",
"label": "partner_pms",
"rooms": [
{
"key": "SUP-K",
"name": "Superior King",
"size_m2": 28,
"description": "City view",
"max_occupancy": 2,
"bed_config": [
{
"type": "king",
"count": 1
}
],
"images": [
"https://partner.example/sup-k.jpg"
]
}
]
}
],
"min_confidence": 0.5
}'{
"billing": {
"amount": "0.25",
"currency": "USD",
"sku": "rooms.reconcile.urls",
"units": 1
},
"hotel_id": "43d0be71-5f3c-4c8e-9a52-1d7e2b6f0a19",
"reconciliation_id": "rec_9b2e4c7a1f3d5e8b0c6a2d4f",
"rooms": [
{
"basis": [
"name_similarity:0.81",
"size_match",
"image_similarity:0.88"
],
"canonical": {
"bed_config": [
{
"count": 1,
"type": "king"
}
],
"name": "Superior Room",
"occupancy": 2,
"room_id": "5b1e2f3a-0c4d-4e5f-8a9b-1c2d3e4f5a6b",
"size_m2": 28
},
"confidence": 0.93,
"conflicts": [
{
"field": "size_m2",
"values": {
"partner_pms": 28,
"site": 30
}
}
],
"listings": [
{
"listing_key": "site:superior",
"name": "Superior Double Room",
"source": "site",
"url": "https://umilta36.com/rooms/superior"
},
{
"listing_key": "SUP-K",
"name": "Superior King",
"source": "partner_pms"
}
]
}
],
"shared": [],
"unmatched": {
"partner_pms": [],
"site": []
},
"version": "reconcile-v1.0"
}Response 200
Successful Response
| Field | Type | Description |
|---|---|---|
request_id | string | null | Id of the request; null inside a job result. |
hotel_id | string (uuid) | null | The hotel matched; null only when supplied-rooms-only sources named no known hotel. |
reconciliation_idREQUIRED | string | Id of this reconciliation, rec_ plus hex; the same inputs give the same id. |
versionREQUIRED | string | Matcher version, for example reconcile-v1.0. |
rooms | ReconciledRoom[] | Rooms matched across sources. |
rooms[].canonicalREQUIRED | object | The room as one record. |
rooms[].canonical.room_id | string | null | Catalogue room id when the group includes a third-party channel listing; null otherwise. |
rooms[].canonical.name | string | null | Room name, from the third-party channel listing when there is one, else from the first listing in the group. |
rooms[].canonical.size_m2 | number | null | Room size in square metres; null when no source gives it. |
rooms[].canonical.occupancy | integer | null | Most guests the room holds; null when no source gives it. |
rooms[].canonical.bed_config | BedConfig[] | Beds in the room; empty when no source gives them in a structured form. |
rooms[].canonical.bed_config[].typeREQUIRED | string | Bed type as the source names it, for example king, double, twin or sofa bed. |
rooms[].canonical.bed_config[].count | integer | Number of beds of this type. Default 1. |
rooms[].listings | ReconcileListing[] | Every listing of this room, one or more per source. |
rooms[].listings[].sourceREQUIRED | string | Source label: booking, site, expedia, or the label of rooms you supplied. |
rooms[].listings[].name | string | null | Room name on that source. |
rooms[].listings[].listing_key | string | null | The listing's key: room_type:<id> or room:<id> on channels, the site's room key, or your key for supplied rooms. |
rooms[].listings[].url | string (uri) | null | Page of the listing, or the source URL you sent; null when neither is known. |
rooms[].confidenceREQUIRED | number | Confidence of the match from 0 to 1. |
rooms[].basis | string[] | Evidence the match rests on, for example name_exact, name_similarity:0.81, size_match, occupancy_match, beds_overlap:1.00 or image_similarity:0.88. |
rooms[].conflicts | ReconcileConflict[] | Facts the listings disagree on. |
rooms[].conflicts[].fieldREQUIRED | string | The fact in dispute: size_m2, max_occupancy or bed_config. |
rooms[].conflicts[].valuesREQUIRED | object | Each source's value, keyed by source label; bed values are lists of bed types. |
unmatched | map of UnmatchedListing[] | Listings that matched nothing, keyed by source label. |
shared | SharedListing[] | Listings that sell several rooms under one name. |
shared[].sourceREQUIRED | string | Source label of the listing. |
shared[].name | string | null | Room name on that source. |
shared[].listing_key | string | null | The listing's key. |
shared[].covers | string[] | Names of the rooms it covers. |
billing | Billing | null | What a billed call cost. Present on every billed response; billing is null on free ones. |
Response 202
Job accepted
| Field | Type | Description |
|---|---|---|
request_id | string | null | Id of the request that returned this job; null in lists and webhook payloads. |
job_idREQUIRED | string | Job id, job_ plus 12 letters and digits. |
kindREQUIRED | string | rates.live (from POST /v2/rates or rates=refresh), rates.batch or rooms.reconcile. One of rates.live, rates.batch, rooms.reconcile. |
statusREQUIRED | string | queued, running, then succeeded, failed or canceled. One of queued, running, succeeded, failed, canceled. |
progress | object | Units of work done out of the total. |
progress.done | integer | Units of work finished. Default 0. |
progress.total | integer | Units of work in the job: hotels for rates.live, hotel and stay pairs for rates.batch, 1 for rooms.reconcile. Default 1. |
created_atREQUIRED | string (date-time) | When the job was created, RFC 3339 UTC; jobs expire 7 days later. |
updated_atREQUIRED | string (date-time) | When the job last changed, RFC 3339 UTC. |
estimated_seconds | integer | null | Rough time the job was expected to take when it was created, in seconds. |
result | object | null | The outcome once succeeded: the POST /v2/rates response for rates.live, {summary, lines, format, warnings} for rates.batch, the reconcile result for rooms.reconcile. Null otherwise. |
result_url | string (uri) | null | rates.batch only: where to download the NDJSON result, a signed link or GET /v2/jobs/{job_id}/result; null for other kinds and until the job succeeds. |
error | object | null | Why the job failed; null unless status is failed. |
error.codeREQUIRED | string | Error code, one of the API error codes, usually upstream_error, unavailable (the job stopped and its held credit was returned) or bad_request. |
error.messageREQUIRED | string | Explanation in English. |
billing | Billing | null | What a billed call cost. Present on every billed response; billing is null on free ones. |
Errors
| Status | Code | When |
|---|---|---|
| 400 | bad_request | Validation errors carry details.fields[]. |
| 401 | unauthorized | No key, or the key is unknown or revoked. |
| 402 | insufficient_credit | Not enough credit for the call. Checked before any work starts, so nothing was billed. |
| 403 | forbidden | Missing scope, or details.reason tier_required: this needs the Build plan or higher. |
| 404 | not_found | URL sources need a hotel that resolves. |
| 409 | conflict | The Idempotency-Key was reused with a different body, or the resource is in a state that forbids the change. |
| 429 | rate_limited | Over the per-minute limit for the tier. Wait Retry-After seconds. |
| 502 | upstream_error | The matcher failed; retry later. |
| 503 | unavailable | We're temporarily unable to serve the call. Retry with backoff. |