Hotel data,
built for agents.

When it comes to accommodation, web search doesn’t cut it - scraped pages, stale rates, scattered images, and no hotel/guest fit. We provide fast and reliable intelligence in white-label format. Free to start.

Get an API key
350,820Properties indexed
47.2MPhotos analyzed
18.4MReviews indexed
14Visual dims
50/moFree tier
01Overview

A search engine that understands hotels the way humans do - through visual aesthetics, guest sentiment from 18.4M reviews, and 14 dimensions of spatial signal - plus a proprietary voice infrastructure that can call a property and book on the guest’s behalf.

Two interfaces, identical capability: an HTTP-Streamable MCP server at api.vistalink.com/mcp that auto-discovers four tools, two read-only resources (hotel://, amenities://catalog), and two prompt templates in any MCP-compatible client; and a REST API at api.vistalink.com with the same shapes for traditional backends.

Search

Structured filters or natural-language. Returns ranked candidates with images, sentiment, geo fit.

Chat

Multi-turn sessions for refinement - “cheaper but with a balcony”, “show me the next five”.

Detail

Full property profile - up to 50 images, rooms with occupancy, full reviews, phone number.

Call

Our voice agent calls the hotel to negotiate or confirm. Sub-280ms latency, full-duplex audio.

02Quick start

Up and running in five minutes.

  1. 01

    Create a key

    Sign in with GitHub or email. Generate vl_test_* for the sandbox or vl_live_* for production. Keys are shown once - store in VISTALINK_API_KEY or a secret manager. Rotate and revoke from the dashboard.

  2. 02

    Pick a transport

    MCP for agents (Claude, Cursor, custom). REST for any HTTP client. Both expose the same four tools with the same response shapes.

  3. 03

    Fire the first request

    The simplest possible call - a chat query with no structured filters - returns a ranked list of hotels with session_id for follow-ups.

  4. 04

    Promote to live

    Swap vl_test_ for vl_live_, attach a card on the Pro tier, and you are at 120 req/min with full booking access.

// claude_desktop_config.json
{
  "mcpServers": {
    "vistalink": {
      "url": "https://api.vistalink.com/mcp",
      "headers": {
        "Authorization": "Bearer vl_live_YOUR_API_KEY"
      }
    }
  }
}
03Authentication

Bearer tokens, scoped narrowly.

Every request carries an Authorization: Bearer vl_* header. Test keys (vl_test_*) hit a sandboxed dataset of 5,000 hotels. Live keys (vl_live_*) hit the full 350,820-property index.

  • One key, one tenant. Generate as many keys as you need - separate ones per project, environment, or deployment.
  • Keys are shown once at creation. Store them in a secret manager or env var; you can revoke and rotate from the dashboard.
  • Per-key rate limits, per-key usage, and per-key revocation. Live and test keys can coexist on the same account.
  • Keys never appear in URLs - only in the Authorization header. Treat them like passwords.
04Plans & rate limits

Free to start. Linear to scale.

 
Free
Enterprise
Price
$0
Custom
Monthly
50 req
Unlimited
Rate
5 / min
500 / min
Environment
Test only
Live + dedicated
Voice tools
SOC 2 / DPA
On request

Headers on every response: X-RateLimit-Remaining, X-Monthly-Quota-Remaining, X-RateLimit-Reset. Treat 429 as a signal, not a failure.

05How MCP works

Wired into any agent.

VistaLink ships an HTTP-Streamable MCP server - no npm package, no local proxy. Drop the URL and a Bearer token into Claude Desktop, Cursor, or any MCP-compatible client and tools, resources, and prompts auto-discover.

// claude_desktop_config.json
{
  "mcpServers": {
    "vistalink": {
      "url": "https://api.vistalink.com/mcp",
      "headers": {
        "Authorization": "Bearer vl_live_YOUR_API_KEY"
      }
    }
  }
}
Set up

Where to put the config

Claude DesktopmacOS
~/Library/Application Support/Claude/claude_desktop_config.json
Claude DesktopWindows
%APPDATA%\Claude\claude_desktop_config.json
Claude DesktopLinux
~/.config/Claude/claude_desktop_config.json
Cursorworkspace root
.cursor/mcp.json
Verify

After editing, restart your client. Ask the model “what tools are available?” - it should list search_hotels, chat_about_hotels, get_hotel_details, and call_hotel.

Security

Set VISTALINK_API_KEY as an environment variable and reference it via $VISTALINK_API_KEY rather than inlining the key. Never commit a live key to source control.

06The four tools

Four tools. Everything you need.

POST
search_hotels

Fast, structured. No LLM. Use when you can extract city/dates/budget cleanly from the user message.

$0.01 / call
POST
chat_about_hotels

Conversational. Multi-turn sessions, vibe matching, follow-ups. Pass guest context for sharper results.

$0.03 / call
GET
get_hotel_details

Full profile. Up to 50 images, all rooms, full reviews, phone number. The data you need before booking.

$0.005 / call
POST
call_hotel

Our voice agent calls the property to negotiate or confirm on the guest’s behalf. Pro tier and above.

$0.50 flat + $0.05/min
07bMCP Resources

Read-only URIs, fetchable.

The MCP server also exposes two read-only resources. Use these when your client prefers URI fetches over tool calls (e.g. to cache hotel profiles or validate amenity codes).

hotel://{hotel_id}

Returns the same payload as get_hotel_details for a specific hotel. Fetch by URI without paying for a tool call when your agent has already retrieved the hotel once and just wants a re-read.

amenities://catalog

Canonical list of amenity codes recognized by search_hotels. Misspelled codes are silently dropped, so validate user-provided amenity language (“hot tub”, “WiFi”, “kid friendly”) against this catalog before passing it in.

07cMCP Prompts

Templates for common flows.

The server also exposes prompt templates that codify recurring agent patterns. Invoke via your MCP client’s prompt loader.

search_wizard

Walks an agent through gathering the information needed for a high-quality search_hotels or chat_about_hotelscall. Useful when the user’s first message is ambiguous and you want focused follow-ups before querying.

comparison_template

Renders a structured comparison brief - the agent fetches get_hotel_details for two or more hotels and presents a side-by-side table covering price, location, amenities, reviews, and recommended fit.

07Agent integration

The whole spec, one paste.

Drop this file into Claude Projects, the system prompt of a custom agent, Cursor’s .cursorrules, or anywhere else you brief an LLM. It encodes every tool, parameter, routing rule, error code, and pricing line your agent needs to integrate VistaLink without a single round trip to documentation.

vistalink-api-instructions.md
v1 · 2026-05-20 · 350 lines · 1,583 words
DROP-IN PROMPT
One file. Every tool, parameter, routing rule, error code, pricing line, and gotcha your agent needs.
# VistaLink API - LLM Integration Brief

Version v1 - Last updated 2026-05-20

VistaLink is a hotel intelligence API for AI agents. 350,820 properties
indexed across visual aesthetics (47.2M analyzed photos), semantic guest
review analysis (18.4M reviews, 18.7M extracted vibe signals), and
contextual matching. Available as an MCP server and a REST API at
https://api.vistalink.com.

> This file is a drop-in system-prompt brief. Paste it into your agent's
> system prompt, Claude Project, Cursor `.cursorrules`, or any other
> place you brief an LLM. It encodes every tool, parameter, error code,
> and gotcha needed to integrate VistaLink without round trips to docs.

---

## Authentication

Every request carries a Bearer token:

```

            ...   
08Tool routing

When to call which tool.

The VistaLink API is only for hotel accommodation. Do not route destination advice, flights, restaurants, or general travel questions to the API - handle those locally with your base model. Below is the routing tree we recommend in the system prompt.

User message
  |
  |-- Finding / searching hotels?
  |     |-- Simple criteria (city + dates / budget / amenities) -----> search_hotels
  |     |
  |     |-- Complex (proximity to POI, vibe, reviews, follow-up) ---> chat_about_hotels
  |
  |-- Specific hotel already shown? ---------------------------------> get_hotel_details
  |
  |-- User wants to phone a hotel? ----------------------------------> call_hotel
  |
  |-- Anything else (greetings, travel advice, non-hotel) -----------> Handle locally
RULE OF THUMB

If you’re tempted to parse more than one subjective criterion into search_hotels parameters, use chat_about_hotelsinstead. It is slower but it understands phrases like “feels like a Wes Anderson set” that filters cannot represent.

09Tool reference

Every parameter, every type.

Each tool below ships with a one-click Copy for LLM button - the markdown reference is self-contained: when to use it, every parameter, an example request, an example response, error modes, gotchas. Paste a single tool into your agent’s system prompt without dragging the rest of the spec along.

search_hotels POST

Structured hotel search. Fast (< 40ms), cheap, no LLM. Returns ranked summary cards with basic info, ratings, pricing.

FieldTypeDescription
citystringCity name, e.g. "Paris"
countrystringISO 3166-1 alpha-2 code (e.g. "FR", "US"). Use to disambiguate city names.
latitudefloatSearch center latitude
longitudefloatSearch center longitude
radius_metersintegerSearch radius in meters
check_instringYYYY-MM-DD
check_outstringYYYY-MM-DD
guestsintegerNumber of guests
roomsintegerNumber of rooms
budget_minfloatMinimum budget per night
budget_maxfloatMaximum budget per night
currencystringCurrency code (default EUR)
amenitiesstringComma-separated codes (e.g. "wifi,pool,spa")
vibestringComma-separated tags (e.g. "romantic,quiet")
hotel_namestringPartial-match hotel name
limitintegerMax results (default 20, max 50)
include_ratesbooleanFetch live room rates (adds ~30-45s)

Response shape:

FieldTypePresenceDescription
hotelsHotelCard[]alwaysRanked hotel cards; empty array when no results
totalintegeralwaysTotal number of hotels matching the query (may exceed hotels.length)
fallback_usedbooleanalwaystrue when the engine widened search criteria to return results
fallback_messagestring | nullconditionalHuman-readable explanation of the fallback applied (e.g. radius widened)
usageUsageInfo | nullconditional{ latency_ms, cost_usd }. Null only on error paths.

chat_about_hotels POST

LLM-powered conversational search. Understands nuance (“a cozy place near the beach that feels like home”), comparisons, and follow-ups. Pass everything you know about the user in context; reuse session_id for multi-turn. Supply user_locationon “near me” queries to skip the GPS-collection round-trip.

FieldTypeReqDescription
messagestringyesThe user's message or question (1-4000 chars)
session_idstring-UUID returned by a prior chat call - continues the conversation
guest_idstring-UUID of a stored guest profile - personalizes results
currencystring-Currency code (default EUR)
localestring-Locale code (e.g. "en-GB", "it-IT")
contextobject-Free-form guest context for personalization (max 100 KB)
clarification_idstring-Echo the id from a prior clarification_pending response
clarification_option_idstring-Echo the chosen option id (use "" for free-text replies)
user_locationobject-GPS coordinates for location-sensitive queries (e.g. "near me"). Skips the supervisor GPS-collection round-trip. Shape: { lat: float, lng: float, accuracy?: float (meters) }

Example context payload:

{
  "travel_style": "luxury",
  "purpose": "honeymoon",
  "loyalty_programs": ["Marriott Bonvoy"],
  "dietary": "vegetarian",
  "accessibility": "wheelchair",
  "past_stays": ["Aman Tokyo", "Four Seasons Bali"],
  "preferred_amenities": ["spa", "pool", "room service"],
  "budget_tendency": "splurge on location, save on extras"
}

Clarification protocol. When the engine needs the user to choose between a few interpretations before continuing, the response carries a clarification_pending object. Echo clarification_id and either clarification_option_id or a free-text message on the next call to resolve it.

{
  "session_id": "5b3e...",
  "message": "I need a quick clarification before searching.",
  "hotels": [],
  "clarification_pending": {
    "clarification_id": "c-abc123",
    "question": "Which venue are you closest to?",
    "options": [
      { "id": "ryman",   "label": "Ryman Auditorium",  "description": "Downtown" },
      { "id": "opry",    "label": "Grand Ole Opry",    "description": "17 km from downtown" },
      { "id": "bluebird","label": "The Bluebird Cafe", "description": "11 km from downtown" }
    ],
    "context_hint": "Computing routes to all three would be slow.",
    "allow_free_text": true,
    "display_mode": "inline"
  }
}

Response shape — all fields:

FieldTypePresenceDescription
session_idstring (UUID)alwaysReuse on all follow-up calls to maintain conversation context
messagestringalwaysAssistant reply text; inline hotel-placement markers are stripped before delivery
hotelsHotelCard[]alwaysMatched hotels in ranked order; empty array when no results
segmentsSegment[]conditionalAlternating text + hotel_ref units for inline card placement. Empty on non-search turns (comparisons, analysis). When empty, render hotels[] after message.
poisobject[]alwaysPoints of interest when proximity was in play; usually empty
routesobject[]alwaysRoute segments when routing was computed; usually empty
referencesReference[]alwaysWeb sources when the engine ran live search; usually empty
clarification_pendingobject | nullconditionalSupervisor needs user to choose an interpretation. Echo clarification_id + chosen option on next call.
usageUsageInfo | nullconditional{ latency_ms, cost_usd }. Null only on error paths where usage could not be computed.

Enriched HotelCard fields:

FieldTypePresenceDescription
hero_image_urlstring | nullconditionalURL of the representative image; null only when images[] is empty
hero_image_labelstring | nullconditionalLabel of the hero image (e.g. "exterior", "lobby")
explanationstring | nullconditionalWhy this hotel was recommended. Present on conversational chat path; absent on structured-search fast path and non-search follow-ups.
explanation_factorsExplanationFactor[]alwaysRanked signals: { factor_type: "review"|"amenity"|"proximity"|"semantic", human_readable: string, contribution: 0–1 }. Always present as an array; empty [] when explanation is null.
poi_distancesRecord<string, number>conditional{ [poi_name]: metres } — straight-line distance to each queried POI. Empty when no POI was in the query.
routes_to_poisobjectconditional{ [poi_name]: { walk_minutes?, walk_meters?, drive_minutes?, drive_meters? } }. Empty when no routing was computed.

Example response:

{
  "session_id": "5b3e9f2a-...",
  "message": "Here's the top match for a romantic boutique Paris hotel with a rooftop bar:",
  "hotels": [
    {
      "hotel_id": "a1b2c3d4-...",
      "name": "Hotel Le Marais",
      "city": "Paris",
      "country": "France",
      "star_rating": 4.0,
      "review_score": 9.1,
      "review_count": 1284,
      "amenities": ["wifi", "rooftop", "spa"],
      "price": { "min": 240, "max": 320, "currency": "EUR" },
      "images": [{ "url": "https://...", "label": "exterior" }],
      "hero_image_url": "https://...",
      "hero_image_label": "exterior",
      "explanation": "Top boutique match in the Marais — rooftop bar, excellent reviews (9.1), 240 EUR/night within budget.",
      "explanation_factors": [
        { "factor_type": "review", "human_readable": "Excellent reviews (9.1/10)", "contribution": 0.45 },
        { "factor_type": "amenity", "human_readable": "Rooftop bar", "contribution": 0.30 },
        { "factor_type": "semantic", "human_readable": "Romantic boutique match", "contribution": 0.25 }
      ],
      "poi_distances": { "Eiffel Tower": 3200 },
      "routes_to_pois": { "Eiffel Tower": { "walk_minutes": 38, "walk_meters": 3200 } }
    }
  ],
  "segments": [
    { "type": "text", "id": "seg-a1b2c3d4", "content": "Here's the top match for a romantic boutique Paris hotel with a rooftop bar:", "hotel_id": null },
    { "type": "hotel_ref", "id": "seg-e5f6a7b8", "content": null, "hotel_id": "a1b2c3d4-..." }
  ],
  "references": [],
  "pois": [],
  "routes": [],
  "usage": { "latency_ms": 9820, "cost_usd": 0.03 }
}

get_hotel_details GET

Full hotel profile - everything search results don’t include. No LLM, direct database lookup. phone_number is needed before any call_hotel request.

FieldTypeReqDescription
hotel_idstringyesThe hotel's unique identifier (from search/chat results)
currencystring-Currency code (default EUR)
localestring-Locale code (e.g. "en-GB"). Affects language of text fields where available.

Response shape:

FieldTypePresenceDescription
hotel_idstringalwaysStable identifier — use for subsequent get_hotel_details or call_hotel calls
namestringalwaysHotel display name
descriptionstring | nullconditionalLong-form property description
citystring | nullconditionalCity name
countrystring | nullconditionalCountry name (display form, e.g. "France")
addressstring | nullconditionalStreet address
star_ratingfloat | nullconditionalOfficial star rating (1–5)
review_scorefloat | nullconditionalAggregated guest review score (0–10)
review_countinteger | nullconditionalNumber of reviews used to compute review_score
latitudefloat | nullconditionalDecimal latitude
longitudefloat | nullconditionalDecimal longitude
website_urlstring | nullconditionalProperty website URL
phone_numberstring | nullconditionalE.164 phone number — required before calling call_hotel
amenitiesstring[]alwaysCanonical amenity codes (e.g. "wifi", "pool", "spa")
imagesHotelImage[]alwaysAll available images with url, label, and category
roomsobject[]alwaysRoom types with offers (price, currency, breakfast, cancellation policy)
priceobject | nullconditionalPrice snapshot { min, max, currency } from booking sources
reviewsReviewHighlight[]alwaysSample guest review snippets; empty when no reviews exist
guest_insightsobject | nullconditional{ highlights: string[], aspects: GuestInsightAspect[] } — aggregated sentiment
hotel_typestring | nullconditionalProperty type, e.g. "hotel", "apartment", "hostel"

call_hotel POST

Initiate a voice call. The AI agent calls the hotel on behalf of the guest to negotiate rates, confirm bookings, or handle cancellations. Pro tier and above. The scenario parameter picks the playbook the agent runs (default hotel_negotiation). Returns a call_id immediately (202 Accepted) — poll /v1/call/{call_id}/status until completed, then fetch /v1/call/{call_id}/results for the transcript and outcome.

FieldTypeReqDescription
hotel_idstringyesThe hotel's unique identifier
phone_numberstringyesE.164 format, e.g. "+390551234567"
hotel_namestringyesName of the hotel
scenariostring-hotel_negotiation (default), hotel_confirm_booking, hotel_cancel_booking
guest_first_namestring-For booking and confirmation scenarios
guest_last_namestring-For booking and confirmation scenarios
guest_emailstring-For confirmation emails
check_in_datestring-YYYY-MM-DD
check_out_datestring-YYYY-MM-DD
number_of_roomsinteger-Number of rooms
number_of_peopleinteger-Number of guests
booking_pricefloat-Current or target booking price
room_typestring-e.g. "deluxe double"
currencystring-Currency for the price
languagestring-Language for the call (it, en, fr, ...)
confirmation_numberstring-Existing booking confirmation (for confirm / cancel)
special_requestsstring-Free-text guest requests ("late check-in")
caller_instructionsstring-Instructions for the AI caller ("try 15% under listed rate")

Initiate response (202 Accepted):

FieldTypePresenceDescription
call_idstringalwaysOpaque identifier — use to poll /v1/call/{call_id}/status and /results
ui_call_idstring | nullconditionalIdentifier used by the voice-agent UI (may differ from call_id)
statusstringalwaysInitial status: dispatching | in_progress | completed | failed
queue_positioninteger | nullconditionalPosition in the call queue (1 = next up)
messagestring | nullconditionalHuman-readable status message from the voice infrastructure

Status response (GET /v1/call/{call_id}/status):

FieldTypePresenceDescription
call_idstringalwaysIdentifier echoed from the initiating request
statusstringalwaysdispatching | in_progress | completed | failed | unknown
ui_call_idstring | nullconditionalVoice-agent UI identifier
updated_atstring | nullconditionalISO 8601 timestamp of the last status change
duration_secondsinteger | nullconditionalElapsed call duration in seconds (set when completed)
transcriptstring | nullconditionalPartial or full call transcript (set when completed)

Results response (GET /v1/call/{call_id}/results— returns 409 if the call has not yet completed):

FieldTypePresenceDescription
call_idstringalwaysIdentifier echoed from the initiating request
statusstringalwayscompleted | failed
structured_outputsobject | nullconditionalScenario-specific outcome, e.g. { negotiated_price, currency, outcome: "success"|"failed" }
transcriptstring | nullconditionalFull call transcript
recording_urlstring | nullconditionalURL of the call recording (MP3/WAV); best-effort — may be null
summarystring | nullconditionalOne-sentence human-readable outcome summary
duration_secondsinteger | nullconditionalTotal call duration in seconds
exchange_countinteger | nullconditionalNumber of conversational turns in the call
cost_usdfloat | nullconditionalActual billed cost for this call
created_atstring | nullconditionalISO 8601 timestamp when the call was created
completed_atstring | nullconditionalISO 8601 timestamp when the call completed
10REST endpoints

Sixteen endpoints. One base URL.

Anything available via MCP is available via REST at https://api.vistalink.com. Same auth header, same response shapes, same rate limits. The REST API additionally exposes guest profile management for booking workflows.

VOICE CALLS — DISPATCHING BY SCENARIO

POST /v1/call is the single entry point for every voice call. The scenario field in the request body selects the playbook the agent follows: hotel_negotiation (default), hotel_confirm_booking, or hotel_cancel_booking. Poll /v1/call/{call_id}/status for state, and /v1/call/{call_id}/results for the transcript and structured outcome.

POST/v1/searchStructured hotel search
POST/v1/chatConversational search
GET/v1/hotels/{hotel_id}Full hotel profile
POST/v1/callInitiate a voice call (scenario in body)
GET/v1/call/{call_id}/statusPoll call status
GET/v1/call/{call_id}/resultsCall results + transcript
POST/v1/guestsCreate guest profile
GET/v1/guests/{guest_id}Get guest profile
PATCH/v1/guests/{guest_id}Update guest profile
DELETE/v1/guests/{guest_id}Delete (GDPR)
POST/v1/keysCreate API key
GET/v1/keysList API keys
DELETE/v1/keys/{prefix}Revoke API key
GET/v1/usageUsage summary
GET/v1/sessions/{session_id}Chat session metadata + history
GET/v1/billing/invoicesInvoice history
11Errors & limits

Status codes, retry behavior.

401Invalid or missing API key. Check the Authorization header.
403Account not in good standing. Visit the dashboard.
404Resource not found. The hotel_id or session_id may have expired.
429Rate limit or monthly quota exceeded. Wait the number of seconds in Retry-After, or use X-RateLimit-Reset (unix ts) for finer backoff.
502Upstream service unavailable. Retry with exponential backoff.
503Database unavailable. Status updates at status.vistalink.com.

All errors return a JSON body: { "error": { "code": "rate_limited", "message": "..." } }. Use the machine-readable code for retry logic; the message field is for humans. On 429, the response also carries a Retry-After header in seconds.

Build something weird.

A travel agent that lives in your terminal. A hotel-vibe Slack bot. A mood-board-to-booking pipeline. Whatever it is - start free, ship in an afternoon.

Get an API key