API v2 · Rooms
Agent brief

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.

POST /v2/rooms/reconcilemin_confidence 0.5 · $0.25 / reconciliation
On Booking.com bookingOn the hotel’s site siteIn your list partner_pms
Superior RoomSuperior Double or Twin Room0.93Superior Double RoomSuperior King
Prestige RoomPrestige Double Room0.88Prestige RoomPrestige King
Deluxe ApartmentApartment with Terrace0.79Deluxe Apartment—
shared[]Classic Double Roomsells Superior Room and Prestige Room under one name
unmatchednearest Deluxe Apartment at 0.41, under 0.5Family Suite

Hotels 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.

Rooms

Reconcile room types across sources

POST/v2/rooms/reconcile
Mode
Job · 202; sync · 200 when every source has kind: rooms and hotel_id is known
Price
$0.08–$0.25 / reconciliation
SKU
rooms.reconcile.urls, rooms.reconcile.supplied
Auth
Scope reconcile

Needs 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.

Billing

rooms.reconcile.urls when any source is a URL, else rooms.reconcile.supplied. A repeat within 30 days bills "0".

Parameters

NameInTypeDescription
Idempotency-KeyheaderstringAny unique string. Same key and same body returns the original response; same key and a different body is a 409.

Request body

FieldTypeDescription
hotel_idstring (uuid) | nullThe hotel whose rooms to match; wins over identifiers when both are set.
identifiersHotelIdentifier | nullA 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).
sources
REQUIRED
(UrlSource | RoomsSource)[]Between 1 and 8 sources to match: channels named by URL, or rooms you supply. At most 8.
sources[].kind
REQUIRED
stringbooking_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[].urlstring (uri)Public http(s) URL of the hotel on that channel.
sources[].labelstringName for this source in the result, lower case letters, digits and underscores, for example partner_pms.
sources[].roomsSuppliedRoom[]Up to 500 rooms (1,000 across all sources).
sources[].rooms[].key
REQUIRED
stringYour key for the room; it comes back as the listing_key of this room's listing.
sources[].rooms[].name
REQUIRED
stringRoom name in your system.
sources[].rooms[].size_m2number | nullRoom size in square metres. 0–10000.
sources[].rooms[].descriptionstring | nullRoom description, up to 5,000 characters.
sources[].rooms[].max_occupancyinteger | nullMost guests the room can hold. 1–50.
sources[].rooms[].bed_configSuppliedBed[]Beds in the room. At most 10.
sources[].rooms[].bed_config[].type
REQUIRED
stringBed type, for example king, double, twin or sofa bed.
sources[].rooms[].bed_config[].countintegerNumber of beds of this type, 1 to 20. Default 1.
sources[].rooms[].imagesstring (uri)[]Up to 30 public http(s) image URLs of the room, used for image matching.
min_confidencenumberLowest 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

Request
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
}'
Response
{
  "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

FieldTypeDescription
request_idstring | nullId of the request; null inside a job result.
hotel_idstring (uuid) | nullThe hotel matched; null only when supplied-rooms-only sources named no known hotel.
reconciliation_id
REQUIRED
stringId of this reconciliation, rec_ plus hex; the same inputs give the same id.
version
REQUIRED
stringMatcher version, for example reconcile-v1.0.
roomsReconciledRoom[]Rooms matched across sources.
rooms[].canonical
REQUIRED
objectThe room as one record.
rooms[].canonical.room_idstring | nullCatalogue room id when the group includes a third-party channel listing; null otherwise.
rooms[].canonical.namestring | nullRoom name, from the third-party channel listing when there is one, else from the first listing in the group.
rooms[].canonical.size_m2number | nullRoom size in square metres; null when no source gives it.
rooms[].canonical.occupancyinteger | nullMost guests the room holds; null when no source gives it.
rooms[].canonical.bed_configBedConfig[]Beds in the room; empty when no source gives them in a structured form.
rooms[].canonical.bed_config[].type
REQUIRED
stringBed type as the source names it, for example king, double, twin or sofa bed.
rooms[].canonical.bed_config[].countintegerNumber of beds of this type. Default 1.
rooms[].listingsReconcileListing[]Every listing of this room, one or more per source.
rooms[].listings[].source
REQUIRED
stringSource label: booking, site, expedia, or the label of rooms you supplied.
rooms[].listings[].namestring | nullRoom name on that source.
rooms[].listings[].listing_keystring | nullThe listing's key: room_type:<id> or room:<id> on channels, the site's room key, or your key for supplied rooms.
rooms[].listings[].urlstring (uri) | nullPage of the listing, or the source URL you sent; null when neither is known.
rooms[].confidence
REQUIRED
numberConfidence of the match from 0 to 1.
rooms[].basisstring[]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[].conflictsReconcileConflict[]Facts the listings disagree on.
rooms[].conflicts[].field
REQUIRED
stringThe fact in dispute: size_m2, max_occupancy or bed_config.
rooms[].conflicts[].values
REQUIRED
objectEach source's value, keyed by source label; bed values are lists of bed types.
unmatchedmap of UnmatchedListing[]Listings that matched nothing, keyed by source label.
sharedSharedListing[]Listings that sell several rooms under one name.
shared[].source
REQUIRED
stringSource label of the listing.
shared[].namestring | nullRoom name on that source.
shared[].listing_keystring | nullThe listing's key.
shared[].coversstring[]Names of the rooms it covers.
billingBilling | nullWhat a billed call cost. Present on every billed response; billing is null on free ones.

Response 202

Job accepted

FieldTypeDescription
request_idstring | nullId of the request that returned this job; null in lists and webhook payloads.
job_id
REQUIRED
stringJob id, job_ plus 12 letters and digits.
kind
REQUIRED
stringrates.live (from POST /v2/rates or rates=refresh), rates.batch or rooms.reconcile. One of rates.live, rates.batch, rooms.reconcile.
status
REQUIRED
stringqueued, running, then succeeded, failed or canceled. One of queued, running, succeeded, failed, canceled.
progressobjectUnits of work done out of the total.
progress.doneintegerUnits of work finished. Default 0.
progress.totalintegerUnits of work in the job: hotels for rates.live, hotel and stay pairs for rates.batch, 1 for rooms.reconcile. Default 1.
created_at
REQUIRED
string (date-time)When the job was created, RFC 3339 UTC; jobs expire 7 days later.
updated_at
REQUIRED
string (date-time)When the job last changed, RFC 3339 UTC.
estimated_secondsinteger | nullRough time the job was expected to take when it was created, in seconds.
resultobject | nullThe 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_urlstring (uri) | nullrates.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.
errorobject | nullWhy the job failed; null unless status is failed.
error.code
REQUIRED
stringError code, one of the API error codes, usually upstream_error, unavailable (the job stopped and its held credit was returned) or bad_request.
error.message
REQUIRED
stringExplanation in English.
billingBilling | nullWhat a billed call cost. Present on every billed response; billing is null on free ones.

Errors

StatusCodeWhen
400bad_requestValidation errors carry details.fields[].
401unauthorizedNo key, or the key is unknown or revoked.
402insufficient_creditNot enough credit for the call. Checked before any work starts, so nothing was billed.
403forbiddenMissing scope, or details.reason tier_required: this needs the Build plan or higher.
404not_foundURL sources need a hotel that resolves.
409conflictThe Idempotency-Key was reused with a different body, or the resource is in a state that forbids the change.
429rate_limitedOver the per-minute limit for the tier. Wait Retry-After seconds.
502upstream_errorThe matcher failed; retry later.
503unavailableWe're temporarily unable to serve the call. Retry with backoff.