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.
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.
Structured filters or natural-language. Returns ranked candidates with images, sentiment, geo fit.
Multi-turn sessions for refinement - “cheaper but with a balcony”, “show me the next five”.
Full property profile - up to 50 images, rooms with occupancy, full reviews, phone number.
Our voice agent calls the hotel to negotiate or confirm. Sub-280ms latency, full-duplex audio.
Up and running in five minutes.
- 01
Create a key
Sign in with GitHub or email. Generate
vl_test_*for the sandbox orvl_live_*for production. Keys are shown once - store inVISTALINK_API_KEYor a secret manager. Rotate and revoke from the dashboard. - 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.
- 03
Fire the first request
The simplest possible call - a chat query with no structured filters - returns a ranked list of hotels with
session_idfor follow-ups. - 04
Promote to live
Swap
vl_test_forvl_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" } } } }
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
Authorizationheader. Treat them like passwords.
Free to start. Linear to scale.
Headers on every response: X-RateLimit-Remaining, X-Monthly-Quota-Remaining, X-RateLimit-Reset. Treat 429 as a signal, not a failure.
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" } } } }
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
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.
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.
Four tools. Everything you need.
search_hotelsFast, structured. No LLM. Use when you can extract city/dates/budget cleanly from the user message.
$0.01 / callchat_about_hotelsConversational. Multi-turn sessions, vibe matching, follow-ups. Pass guest context for sharper results.
$0.03 / callget_hotel_detailsFull profile. Up to 50 images, all rooms, full reviews, phone number. The data you need before booking.
$0.005 / callcall_hotelOur voice agent calls the property to negotiate or confirm on the guest’s behalf. Pro tier and above.
$0.50 flat + $0.05/minRead-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.
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.
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 - 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:
```
... 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
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.
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.
| Field | Type | Description |
|---|---|---|
city | string | City name, e.g. "Paris" |
country | string | ISO 3166-1 alpha-2 code (e.g. "FR", "US"). Use to disambiguate city names. |
latitude | float | Search center latitude |
longitude | float | Search center longitude |
radius_meters | integer | Search radius in meters |
check_in | string | YYYY-MM-DD |
check_out | string | YYYY-MM-DD |
guests | integer | Number of guests |
rooms | integer | Number of rooms |
budget_min | float | Minimum budget per night |
budget_max | float | Maximum budget per night |
currency | string | Currency code (default EUR) |
amenities | string | Comma-separated codes (e.g. "wifi,pool,spa") |
vibe | string | Comma-separated tags (e.g. "romantic,quiet") |
hotel_name | string | Partial-match hotel name |
limit | integer | Max results (default 20, max 50) |
include_rates | boolean | Fetch live room rates (adds ~30-45s) |
Response shape:
| Field | Type | Presence | Description |
|---|---|---|---|
hotels | HotelCard[] | always | Ranked hotel cards; empty array when no results |
total | integer | always | Total number of hotels matching the query (may exceed hotels.length) |
fallback_used | boolean | always | true when the engine widened search criteria to return results |
fallback_message | string | null | conditional | Human-readable explanation of the fallback applied (e.g. radius widened) |
usage | UsageInfo | null | conditional | { 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.
| Field | Type | Req | Description |
|---|---|---|---|
message | string | yes | The user's message or question (1-4000 chars) |
session_id | string | - | UUID returned by a prior chat call - continues the conversation |
guest_id | string | - | UUID of a stored guest profile - personalizes results |
currency | string | - | Currency code (default EUR) |
locale | string | - | Locale code (e.g. "en-GB", "it-IT") |
context | object | - | Free-form guest context for personalization (max 100 KB) |
clarification_id | string | - | Echo the id from a prior clarification_pending response |
clarification_option_id | string | - | Echo the chosen option id (use "" for free-text replies) |
user_location | object | - | 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:
| Field | Type | Presence | Description |
|---|---|---|---|
session_id | string (UUID) | always | Reuse on all follow-up calls to maintain conversation context |
message | string | always | Assistant reply text; inline hotel-placement markers are stripped before delivery |
hotels | HotelCard[] | always | Matched hotels in ranked order; empty array when no results |
segments | Segment[] | conditional | Alternating text + hotel_ref units for inline card placement. Empty on non-search turns (comparisons, analysis). When empty, render hotels[] after message. |
pois | object[] | always | Points of interest when proximity was in play; usually empty |
routes | object[] | always | Route segments when routing was computed; usually empty |
references | Reference[] | always | Web sources when the engine ran live search; usually empty |
clarification_pending | object | null | conditional | Supervisor needs user to choose an interpretation. Echo clarification_id + chosen option on next call. |
usage | UsageInfo | null | conditional | { latency_ms, cost_usd }. Null only on error paths where usage could not be computed. |
Enriched HotelCard fields:
| Field | Type | Presence | Description |
|---|---|---|---|
hero_image_url | string | null | conditional | URL of the representative image; null only when images[] is empty |
hero_image_label | string | null | conditional | Label of the hero image (e.g. "exterior", "lobby") |
explanation | string | null | conditional | Why this hotel was recommended. Present on conversational chat path; absent on structured-search fast path and non-search follow-ups. |
explanation_factors | ExplanationFactor[] | always | Ranked signals: { factor_type: "review"|"amenity"|"proximity"|"semantic", human_readable: string, contribution: 0–1 }. Always present as an array; empty [] when explanation is null. |
poi_distances | Record<string, number> | conditional | { [poi_name]: metres } — straight-line distance to each queried POI. Empty when no POI was in the query. |
routes_to_pois | object | conditional | { [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.
| Field | Type | Req | Description |
|---|---|---|---|
hotel_id | string | yes | The hotel's unique identifier (from search/chat results) |
currency | string | - | Currency code (default EUR) |
locale | string | - | Locale code (e.g. "en-GB"). Affects language of text fields where available. |
Response shape:
| Field | Type | Presence | Description |
|---|---|---|---|
hotel_id | string | always | Stable identifier — use for subsequent get_hotel_details or call_hotel calls |
name | string | always | Hotel display name |
description | string | null | conditional | Long-form property description |
city | string | null | conditional | City name |
country | string | null | conditional | Country name (display form, e.g. "France") |
address | string | null | conditional | Street address |
star_rating | float | null | conditional | Official star rating (1–5) |
review_score | float | null | conditional | Aggregated guest review score (0–10) |
review_count | integer | null | conditional | Number of reviews used to compute review_score |
latitude | float | null | conditional | Decimal latitude |
longitude | float | null | conditional | Decimal longitude |
website_url | string | null | conditional | Property website URL |
phone_number | string | null | conditional | E.164 phone number — required before calling call_hotel |
amenities | string[] | always | Canonical amenity codes (e.g. "wifi", "pool", "spa") |
images | HotelImage[] | always | All available images with url, label, and category |
rooms | object[] | always | Room types with offers (price, currency, breakfast, cancellation policy) |
price | object | null | conditional | Price snapshot { min, max, currency } from booking sources |
reviews | ReviewHighlight[] | always | Sample guest review snippets; empty when no reviews exist |
guest_insights | object | null | conditional | { highlights: string[], aspects: GuestInsightAspect[] } — aggregated sentiment |
hotel_type | string | null | conditional | Property 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.
| Field | Type | Req | Description |
|---|---|---|---|
hotel_id | string | yes | The hotel's unique identifier |
phone_number | string | yes | E.164 format, e.g. "+390551234567" |
hotel_name | string | yes | Name of the hotel |
scenario | string | - | hotel_negotiation (default), hotel_confirm_booking, hotel_cancel_booking |
guest_first_name | string | - | For booking and confirmation scenarios |
guest_last_name | string | - | For booking and confirmation scenarios |
guest_email | string | - | For confirmation emails |
check_in_date | string | - | YYYY-MM-DD |
check_out_date | string | - | YYYY-MM-DD |
number_of_rooms | integer | - | Number of rooms |
number_of_people | integer | - | Number of guests |
booking_price | float | - | Current or target booking price |
room_type | string | - | e.g. "deluxe double" |
currency | string | - | Currency for the price |
language | string | - | Language for the call (it, en, fr, ...) |
confirmation_number | string | - | Existing booking confirmation (for confirm / cancel) |
special_requests | string | - | Free-text guest requests ("late check-in") |
caller_instructions | string | - | Instructions for the AI caller ("try 15% under listed rate") |
Initiate response (202 Accepted):
| Field | Type | Presence | Description |
|---|---|---|---|
call_id | string | always | Opaque identifier — use to poll /v1/call/{call_id}/status and /results |
ui_call_id | string | null | conditional | Identifier used by the voice-agent UI (may differ from call_id) |
status | string | always | Initial status: dispatching | in_progress | completed | failed |
queue_position | integer | null | conditional | Position in the call queue (1 = next up) |
message | string | null | conditional | Human-readable status message from the voice infrastructure |
Status response (GET /v1/call/{call_id}/status):
| Field | Type | Presence | Description |
|---|---|---|---|
call_id | string | always | Identifier echoed from the initiating request |
status | string | always | dispatching | in_progress | completed | failed | unknown |
ui_call_id | string | null | conditional | Voice-agent UI identifier |
updated_at | string | null | conditional | ISO 8601 timestamp of the last status change |
duration_seconds | integer | null | conditional | Elapsed call duration in seconds (set when completed) |
transcript | string | null | conditional | Partial or full call transcript (set when completed) |
Results response (GET /v1/call/{call_id}/results— returns 409 if the call has not yet completed):
| Field | Type | Presence | Description |
|---|---|---|---|
call_id | string | always | Identifier echoed from the initiating request |
status | string | always | completed | failed |
structured_outputs | object | null | conditional | Scenario-specific outcome, e.g. { negotiated_price, currency, outcome: "success"|"failed" } |
transcript | string | null | conditional | Full call transcript |
recording_url | string | null | conditional | URL of the call recording (MP3/WAV); best-effort — may be null |
summary | string | null | conditional | One-sentence human-readable outcome summary |
duration_seconds | integer | null | conditional | Total call duration in seconds |
exchange_count | integer | null | conditional | Number of conversational turns in the call |
cost_usd | float | null | conditional | Actual billed cost for this call |
created_at | string | null | conditional | ISO 8601 timestamp when the call was created |
completed_at | string | null | conditional | ISO 8601 timestamp when the call completed |
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.
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.
/v1/searchStructured hotel search/v1/chatConversational search/v1/hotels/{hotel_id}Full hotel profile/v1/callInitiate a voice call (scenario in body)/v1/call/{call_id}/statusPoll call status/v1/call/{call_id}/resultsCall results + transcript/v1/guestsCreate guest profile/v1/guests/{guest_id}Get guest profile/v1/guests/{guest_id}Update guest profile/v1/guests/{guest_id}Delete (GDPR)/v1/keysCreate API key/v1/keysList API keys/v1/keys/{prefix}Revoke API key/v1/usageUsage summary/v1/sessions/{session_id}Chat session metadata + history/v1/billing/invoicesInvoice historyStatus codes, retry behavior.
Authorization header.hotel_id or session_id may have expired.Retry-After, or use X-RateLimit-Reset (unix ts) for finer backoff.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.