API v2 · Rates
Agent brief

Direct prices, cached or live.

Prices from each hotel's own booking engine, from cache now or read live as a job.

POST /v2/rates

Cached: $0.002 / hotel.

Umiltà 3614 Nov – 16 Nov · 2 adults · engine SYNXIS
freshness: cached · max_age_hours: 24 · 200 straight away
Superior RoomNon-refundable, breakfast included
€962.00direct · observed_at 09:30Z · cache
Third-party channelEnterprise, under contract
third_party

freshness: cached answers immediately from prices we've already seen, within max_age_hours. freshness: live reads the hotel's booking engine again and returns a job; the result has the same shape as the cached response.

You pay per hotel that comes back with a price. Hotels we can't price (unknown, unsupported, unresolved) are free.

A stay without rooms is one room for two adults. max_age_hours is how old a cached price may be; anything above 168 is treated as 168.

Free keys can read cached rates for up to 100 hotels a call. Live and batch rates need Build or above; on Free they answer 403 forbidden with details.reason: "tier_required".

Rates

Channels

direct is the hotel's own booking engine, the price behind its Book direct button. An offer's channel is direct, third_party or expedia; no offers carry expedia yet.

Third-party channel observations are available to Enterprise customers under contract. Contact sales.

Rates

Sold out and warnings

When the hotel's engine says a stay is sold out, the offer comes back with availability: sold_out, total: "0" and, when the engine gave no price at all, currency: "XXX", the ISO 4217 code for no currency. Treat a total of 0 as "not bookable", never as free; check availability before showing a price.

warnings[] lists what the response couldn't do: channel_unavailable with channel: "third_party" when that channel couldn't be served, currency_not_converted when prices stay in the hotel's currency, and live_* codes when a live read fell short. Warnings don't change the price.

Rates

Rates for a stay

POST/v2/rates
Mode
Sync · 200; job · 202 when freshness is live
Price
$0.002–$0.040 / hotel
SKU
rates.direct.cached, rates.direct.live
Auth
Scope rates

Needs the rates scope. freshness: cached is synchronous and bills rates.direct.cached for each hotel that comes back observed, or stale within max_age_hours; freshness: live answers 202 with a rates.live job, needs the Build plan or higher, and bills rates.direct.live for each hotel it observes. channel: third_party is Enterprise-only (403 forbidden, details.reason: enterprise_only otherwise). Honours Idempotency-Key when it creates a job.

Billing

Billed per hotel that comes back observed, or stale within max_age_hours. unknown, unsupported and unresolved are free.

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
hotels
REQUIRED
object[]Up to 100 hotels, each a hotel_id or an inline identifier resolved as POST /v2/hotels/resolve would; a hotel listed twice is billed once.
stay
REQUIRED
StayDates and rooms of a stay, up to 30 nights and 9 rooms.
channelstringdirect (the hotel's own booking engine, the default) or third_party (Enterprise only). One of direct, third_party. Default direct.
freshnessstringcached answers now from stored prices; live reads the booking engines and answers 202 with a job (Build plan or higher). One of cached, live. Default cached.
max_age_hoursintegerOldest stored price to accept on cached, in hours; values above 168 (7 days) are treated as 168. Default 24. 1–720.
currencystring | nullPreferred ISO 4217 currency; prices are never converted, and a currency_not_converted warning says when some differ.

Example

Request
curl -X POST 'https://api.vistalink.com/v2/rates' \
  -H "Authorization: Bearer $VISTALINK_API_KEY" \
  -H 'Idempotency-Key: 4f1c2b8e-booking-ada-rossi' \
  -H "Content-Type: application/json" \
  -d '{
  "hotels": [
    "43d0be71-5f3c-4c8e-9a52-1d7e2b6f0a19",
    {
      "website_url": "https://umilta36.com"
    }
  ],
  "stay": {
    "check_in": "2026-11-14",
    "check_out": "2026-11-16",
    "rooms": [
      {
        "adults": 2,
        "children_ages": []
      }
    ]
  },
  "channel": "direct",
  "freshness": "cached",
  "max_age_hours": 24,
  "currency": "EUR"
}'
Response
{
  "billing": {
    "amount": "0.002",
    "currency": "USD",
    "sku": "rates.direct.cached",
    "units": 1
  },
  "request_id": "req_8f2c1a9b3d4e5f60",
  "results": [
    {
      "channel": "direct",
      "engine": "SYNXIS",
      "hotel_id": "43d0be71-5f3c-4c8e-9a52-1d7e2b6f0a19",
      "input": 0,
      "observed_at": "2026-10-07T09:30:00Z",
      "rooms": [
        {
          "listing_key": "engine_room:1234567",
          "name": "Superior Room",
          "offers": [
            {
              "availability": "available",
              "board": "breakfast",
              "channel": "direct",
              "currency": "EUR",
              "engine": "SYNXIS",
              "excluded_charges": [],
              "free_cancellation": false,
              "includes_taxes": true,
              "listing_key": "engine_room:1234567",
              "nightly_avg": "481.00",
              "observed_at": "2026-10-07T09:30:00Z",
              "occupancy": {
                "adults": 2,
                "children_ages": []
              },
              "offer_id": "off_t3c8e1a9b4f2d7e6a5c0b",
              "rate_name": "Non-refundable, breakfast included",
              "room_id": "5b1e2f3a-0c4d-4e5f-8a9b-1c2d3e4f5a6b",
              "room_name": "Superior Room",
              "total": "962.00",
              "url": "https://umilta36.com/book?checkin=2026-11-14&checkout=2026-11-16"
            }
          ],
          "room_id": "5b1e2f3a-0c4d-4e5f-8a9b-1c2d3e4f5a6b"
        }
      ],
      "status": "observed"
    }
  ],
  "summary": {
    "observed": 1,
    "stale": 0,
    "unknown": 0,
    "unresolved": 0,
    "unsupported": 0
  }
}

Response 200

Successful Response

FieldTypeDescription
request_id
REQUIRED
stringOpaque id of this request, also sent as the X-Request-Id header; quote it when contacting support.
billingBilling | nullWhat a billed call cost. Present on every billed response; billing is null on free ones.
results
REQUIRED
RateResult[]One result per entry in hotels[], in request order.
results[].hotel_idstring (uuid) | nullThe hotel's id; null when the input did not resolve.
results[].input
REQUIRED
integerIndex of the hotel in the request's hotels[].
results[].status
REQUIRED
stringobserved (recent prices), stale (older but within max_age_hours), unknown (no prices for this stay), unsupported (no readable booking engine, or the channel is not enabled) or unresolved (the input did not resolve to an indexed hotel). One of observed, stale, unknown, unsupported, unresolved.
results[].channelstring | nullThe channel read, as requested. One of direct, third_party.
results[].enginestring | nullCode of the hotel's booking engine on the direct channel; null on the third-party channel and when unknown.
results[].observed_atstring (date-time) | nullWhen the newest price was observed, RFC 3339 UTC; null unless observed or stale.
results[].roomsRateRoom[]Prices per room, cheapest room first; empty unless observed or stale.
results[].rooms[].room_idstring (uuid) | nullCatalogue room id when the source's room is matched to one; null otherwise.
results[].rooms[].namestring | nullRoom name as the source shows it.
results[].rooms[].listing_keystring | nullThe source's own key for the room.
results[].rooms[].offersOffer[]The cheapest offer per rate plan, cheapest first, sold-out last.
summaryobjectCount of results per status.
summary.observedintegerResults with recent prices. Default 0.
summary.staleintegerResults with older prices within max_age_hours. Default 0.
summary.unknownintegerResults with no prices for the stay. Default 0.
summary.unsupportedintegerResults whose hotel cannot be read on this channel. Default 0.
summary.unresolvedintegerInputs that did not resolve to an indexed hotel. Default 0.
warningsWarning_[]Things the call could not fully do, such as currency_not_converted, channel_unavailable, live_unavailable or live_partial.

Response 202

Live 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.
409conflictIdempotency-Key reused with another body.
429rate_limitedOver the per-minute limit for the tier. Wait Retry-After seconds.
503unavailableWe're temporarily unable to serve the call. Retry with backoff.
Rates

Rates for many hotels and stays

POST/v2/rates/batch
Mode
Job · 202
Price
$0.002–$0.040 / hotel
SKU
rates.direct.cached, rates.direct.live, rates.batch
Auth
Scope rates

Always a job: answers 202 with a rates.batch job; needs the rates scope and the Build plan or higher. Billed at the POST /v2/rates unit prices for each hotel and stay that comes back priced, 25% off when the batch has 1,000 hotels or more. The result is NDJSON at result_url, one line per hotel and stay in the results[] shape of POST /v2/rates with its stay, plus a summary. The 10 cheapest available offers of each line can be booked with POST /v2/bookings for 30 minutes; re-quote any other offer with POST /v2/rates. Honours Idempotency-Key.

Billing

Same unit prices as POST /v2/rates, 25% off when the batch has 1,000 hotels or more.

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
hotels
REQUIRED
object[]Up to 10,000 hotels, each a hotel_id or an inline identifier.
stays
REQUIRED
Stay[]Up to 10 stays; every hotel is priced for every stay.
channelstringdirect (the default) or third_party (Enterprise only). One of direct, third_party. Default direct.
freshnessstringcached reads stored prices; live reads the booking engines. One of cached, live. Default cached.
max_age_hoursintegerOldest stored price to accept on cached, in hours; values above 168 are treated as 168. Default 24. 1–720.
currencystring | nullPreferred ISO 4217 currency; prices are never converted.

Example

Request
curl -X POST 'https://api.vistalink.com/v2/rates/batch' \
  -H "Authorization: Bearer $VISTALINK_API_KEY" \
  -H 'Idempotency-Key: 4f1c2b8e-booking-ada-rossi' \
  -H "Content-Type: application/json" \
  -d '{
  "hotels": [
    "43d0be71-5f3c-4c8e-9a52-1d7e2b6f0a19"
  ],
  "stays": [
    {
      "check_in": "2026-11-14",
      "check_out": "2026-11-16",
      "rooms": [
        {
          "adults": 2,
          "children_ages": []
        }
      ]
    }
  ],
  "channel": "direct",
  "freshness": "cached",
  "currency": "EUR"
}'
Response
202
{
  "created_at": "2026-10-07T09:30:00Z",
  "estimated_seconds": 25,
  "job_id": "job_K8s2Lq9Wx4Np",
  "kind": "rates.live",
  "progress": {
    "done": 0,
    "total": 1
  },
  "status": "running",
  "updated_at": "2026-10-07T09:30:00Z"
}

Response 202

Successful Response

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.
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.
503unavailableWe're temporarily unable to serve the call. Retry with backoff.
Objects

Shared objects.

Objects that appear in more than one place. Billing is on the Billing page and ErrorBody under Errors & limits.

Stay

Dates and rooms of a stay, up to 30 nights and 9 rooms.

FieldTypeDescription
check_in
REQUIRED
string (date)Arrival date, YYYY-MM-DD.
check_out
REQUIRED
string (date)Departure date, YYYY-MM-DD, after check_in and at most 30 nights later.
roomsRoomOccupancy[]One entry per room, 1 to 9; defaults to one room for two adults. At most 9.
rooms[].adultsintegerAdults in this room, 1 to 16. Default 2.
rooms[].children_agesinteger[]Age of each child in this room, 0 to 17, one entry per child; empty for adults only. At most 8.

Offer

One bookable price for one room and rate, as observed on a channel.

FieldTypeDescription
offer_id
REQUIRED
stringOffer id, off_t plus 20 hex characters, specific to your account; book it with POST /v2/bookings within 30 minutes of receiving it.
channel
REQUIRED
stringWhere the price was observed: direct for the hotel's own booking engine, third_party for the Enterprise third-party channel. One of direct, third_party, expedia.
enginestring | nullCode of the hotel's booking engine for direct offers, for example SYNXIS; null on the third-party channel.
room_idstring (uuid) | nullCatalogue room this offer is for, matching rooms[].room_id on hotel detail; null when the source names the room only by its own code or name.
room_namestring | nullRoom name as the source shows it; null when the source does not name the room.
listing_keystring | nullThe source's own key for the room, such as engine_room:SUP or room_type:1234567; null when the source has none.
rate_namestring | nullRate plan name as the source shows it, or one built from the cancellation terms and board, such as Free cancellation, breakfast included.
total
REQUIRED
stringPrice for the whole stay and the whole party, a decimal string in currency; taxes the source quoted separately are added in when their amount is known. "0" when sold out.
currency
REQUIRED
stringISO 4217 currency the price was observed in; never converted.
nightly_avgstring | nulltotal divided by the number of nights, for the whole party rather than per room; null when sold out.
includes_taxesboolean | nullTrue when total includes taxes and fees, false when some are listed in excluded_charges, null when the source does not say.
excluded_chargesExcludedCharge[]Charges payable on top of total; usually empty.
excluded_charges[].kind
REQUIRED
stringWhat the charge is; taxes_and_fees (taxes and fees the source quoted separately) is the only kind produced today.
excluded_charges[].amount
REQUIRED
stringAmount of the charge, a decimal string in currency.
excluded_charges[].currency
REQUIRED
stringISO 4217 currency of amount.
excluded_charges[].perstring | nullWhat amount is charged per: stay (once for the whole stay, the only basis produced today), night, person or person_night. One of night, stay, person, person_night.
excluded_charges[].notestring | nullShort note on how the charge is applied, for example quoted on top of the total.
boardstring | nullMeals included: none, breakfast, half (breakfast and dinner), full (all meals) or all_inclusive; null when the source does not say. One of none, breakfast, half, full, all_inclusive.
free_cancellationboolean | nullTrue when the rate can be canceled free of charge, false when it is non-refundable, null when unknown.
cancellation_deadlinestring (date-time) | nullLast moment to cancel free of charge, RFC 3339; set only on offers read live, null otherwise.
paymentstring | nullWhen the guest pays: prepay at booking or pay_at_hotel at the property; null when the source does not say. One of prepay, pay_at_hotel.
occupancyobject | nullThe party this price is for, totalled across all rooms.
occupancy.adults
REQUIRED
integerAdults in the room.
occupancy.children_agesinteger[]Age of each child in the room; empty for adults only.
availabilitystringavailable when the room can be booked at this price, sold_out when the source showed it as unavailable, unknown when it did not say. One of available, sold_out, unknown. Default unknown.
observed_atstring (date-time) | nullWhen this price was observed, RFC 3339 UTC.
urlstring (uri) | nullPage on the source where this price was observed; null when the source has no linkable page.

Warning

Something the response could not fully do, without failing it.

FieldTypeDescription
code
REQUIRED
stringStable warning code: rates_unavailable (some hotels have no stored rate), rates_need_stay (rates asked for without a stay), channel_unavailable (the third-party channel was left out), constraints_relaxed (no exact match, so some constraints were loosened), semantic_unavailable, unresolved (with clarification policy never), search_id_unavailable (the search cannot be paged or referenced), on chat price_redacted (a price in the reply that is not a direct rate was replaced with "a rate on request"), and on rates currency_unavailable.
messagestring | nullExplanation in English.
channelstring | nullThe channel left out, third_party; set only on channel_unavailable. Always third_party.