How to Scrape Viator Listings with an API
Use Viator’s official Partner API to search, retrieve, and synchronize tours and activities without scraping public HTML pages.
Use Viator’s official Partner API v2 to retrieve listings. It returns structured product content, pricing, terms, photos, reviews, availability, and, for eligible partners, booking functions. Public-HTML scraping is a separate activity and is not the supported integration described by Viator’s partner terms.
This guide shows how to obtain partner access, search products, fetch details, build a catalog synchronizer, handle pagination and rate limits, and keep credentials and Viator-unique content protected.
What you need before writing code
- Apply for the Viator partner tier that matches your business.
- Obtain an API key and keep it on a server you control.
- Decide whether your application is an affiliate integration or a merchant integration.
- Implement server-side requests, pagination, retry handling, and a freshness policy for prices and availability.
Affiliate partners receive content and send customers to Viator for checkout. Merchant partners can receive transactional access and take on merchant-of-record responsibilities. Your access tier determines which operations are available.
Viator API endpoints to use
| Task | Endpoint | Use it for |
|---|---|---|
| Search | /products/search |
Structured searches with filters and pagination. |
| Free-text search | /search/freetext |
Finding products from a user’s text query. |
| Product detail | /products/{product-code} |
On-demand details after a user selects a result. |
| Catalog deltas | /products/modified-since |
Initial catalog ingestion followed by incremental updates. |
| Selected-product batch | /products/bulk |
Fetching selected product codes, up to 500 per request; it is not a full-catalog ingestion endpoint. |
Viator identifies /products/modified-since as the endpoint for catalog ingestion. Do not substitute /products/bulk for a synchronization process.
Authentication and request headers
Send your key in the exp-api-key header and request API version 2.0. Use Accept-Language to localize responses. Keep these headers and the key on your server; never place the credential in browser JavaScript, a public repository, or client-side source.
GET /partner-api/products/search
Host: api.viator.com
exp-api-key: YOUR_API_KEY
Accept: application/json
Accept-Language: en-US
Content-Type: application/json
The exact base URL and request schema come from your Partner API documentation and enrollment. Treat the endpoint paths above as the integration map and use the schema supplied for your tier.
Search listings, then fetch details
A practical request path is:
- Search with
/products/searchor/search/freetext. - Store the returned product codes and pagination state.
- Fetch
/products/{product-code}when a user opens a result. - Retrieve current schedules, prices, and availability before displaying a bookable offer.
Certification guidance limits search pages to 50 results and asks partners to control search volume. Preserve the API’s pagination token or offset exactly as returned; do not repeatedly request page one while a user scrolls.
Python example: a server-side search client
import os
import requests
API_KEY = os.environ["VIATOR_API_KEY"]
BASE_URL = os.environ.get("VIATOR_BASE_URL", "https://api.viator.com")
headers = {
"exp-api-key": API_KEY,
"Accept": "application/json",
"Accept-Language": "en-US",
"Content-Type": "application/json",
}
payload = {
"searchTerm": "Rome food tour",
"pagination": {"start": 1, "count": 50}
}
response = requests.post(
f"{BASE_URL}/partner-api/products/search",
headers=headers,
json=payload,
timeout=30,
)
response.raise_for_status()
data = response.json()
for product in data.get("products", []):
print(product.get("productCode"), product.get("title"))
Use the request and response field names from the version of the Partner API enabled for your account. The example demonstrates the server-side pattern, authentication, localization, timeout, and pagination size.
cURL example
curl -X POST "https://api.viator.com/partner-api/products/search" \
-H "exp-api-key: YOUR_API_KEY" \
-H "Accept: application/json" \
-H "Accept-Language: en-US" \
-H "Content-Type: application/json" \
--data '{
"searchTerm": "Rome food tour",
"pagination": {"start": 1, "count": 50}
}'
Node.js example
const apiKey = process.env.VIATOR_API_KEY;
const baseUrl = process.env.VIATOR_BASE_URL || 'https://api.viator.com';
const response = await fetch(`${baseUrl}/partner-api/products/search`, {
method: 'POST',
headers: {
'exp-api-key': apiKey,
'Accept': 'application/json',
'Accept-Language': 'en-US',
'Content-Type': 'application/json'
},
body: JSON.stringify({
searchTerm: 'Rome food tour',
pagination: { start: 1, count: 50 }
})
});
if (!response.ok) {
throw new Error(`Viator request failed: ${response.status}`);
}
const data = await response.json();
for (const product of data.products || []) {
console.log(product.productCode, product.title);
}
Fetch one product on demand
When a visitor selects a search result, request /products/{product-code}. This real-time model minimizes local storage and keeps details close to the source, but it adds latency to the page path and makes retry handling part of the user experience.
curl "https://api.viator.com/partner-api/products/PRODUCT_CODE" \
-H "exp-api-key: YOUR_API_KEY" \
-H "Accept: application/json" \
-H "Accept-Language: en-US"
If the product is already in your synchronized store, you can serve that record according to your freshness policy, then refresh time-sensitive fields before presenting a bookable offer.
Build a local catalog with modified-since
Use an ingestion model when you need fast local search, filtering, analytics, or editorial workflows.
- Run an initial catalog load using the approved ingestion workflow.
- Persist each product code and the source modification cursor or timestamp.
- Poll
/products/modified-sinceon a schedule. Viator describes hourly updates as the normal cadence and permits more frequent polling when needed, subject to limits. - Upsert changed products by product code.
- Mark products that the API reports as inactive or unavailable according to your application rules.
- Record the last successful cursor only after the complete page set is committed.
A resilient synchronizer needs idempotent writes, a retry queue, metrics for missed or failed pages, and a recovery procedure that can resume from the last committed cursor. Keep raw responses when your agreement permits it so that parsing changes can be replayed.
Real-time versus ingestion
| Concern | Real-time product requests | Local ingestion |
|---|---|---|
| Freshness | Latest response at request time. | Depends on your polling interval and successful delta processing. |
| Page latency | Includes Viator network time. | Fast local reads; refresh booking data separately. |
| Search flexibility | Limited to API capabilities. | Local indexes and custom filters. |
| Operations | Simpler storage, harder request-path failures. | More jobs, deduplication, monitoring, and recovery work. |
| Cost control | Traffic drives API calls. | Scheduled calls and storage drive operating cost. |
Rate limits, pagination, and retries
Read RateLimit-Limit, RateLimit-Remaining, and RateLimit-Reset from responses. For HTTP 429, honor Retry-After when present. If an overall-cap response has no rate headers, use exponential backoff with jitter and reduce concurrency.
def retry_delay(response, attempt):
retry_after = response.headers.get("Retry-After")
if retry_after:
return float(retry_after)
# Cap this in production and add random jitter.
return min(60, 2 ** attempt)
Do not retry malformed requests, authentication failures, or permission errors unchanged. Log the status, request correlation information, endpoint, and attempt number without logging the API key.
Prices, availability, and bookings
Catalog content is not a promise that a price or schedule remains valid. Before showing a bookable offer, retrieve the current schedules and prices through the relevant endpoints for your partner tier. Affiliate partners send customers to Viator to complete the purchase. Viator states that affiliate links set a cookie so qualifying transactions can accrue commission; verify eligibility, cookie terms, and access details during enrollment.
Protect credentials and Viator content
- Store the API key in a secret manager or protected environment variable.
- Proxy all requests through your server.
- Do not expose keys in browser bundles, logs, screenshots, or error messages.
- Protect Viator-unique content and review text from search indexing.
- Keep protected content out of client source where practical; Viator recommends blocking external JavaScript for protected content in
robots.txt.
Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| 401 or 403 | Missing, invalid, or unauthorized partner key. | Check the exp-api-key header and confirm the endpoint is enabled for your tier. |
| 429 | Rate or overall-cap limit. | Honor Retry-After, inspect rate headers, lower concurrency, and back off exponentially. |
| Empty next page | Pagination state was discarded or page size exceeded guidance. | Persist the returned pagination state and keep pages at or below 50 results. |
| Stale price | Cached catalog data was treated as live booking data. | Refresh schedules and prices before presenting the offer. |
| Missing products after sync | Cursor advanced before all pages were committed. | Commit each page idempotently and advance the cursor only after the full delta succeeds. |
| Search traffic spikes | Every keystroke creates a request. | Debounce input, cache repeated searches, and cap concurrent requests. |
Performance, reliability, and cost practices
- Debounce free-text search and cache identical queries for a short, documented period.
- Use a bounded worker pool for catalog deltas instead of unbounded parallelism.
- Set connect and read timeouts, retry only transient failures, and expose a stale-data indicator when appropriate.
- Measure request volume, 2xx/4xx/5xx rates, 429 counts, synchronization lag, and product freshness.
- Use bulk only for selected product codes; it supports up to 500 codes per request and is not the catalog-ingestion mechanism.
- Separate catalog refresh cost from user-facing detail requests so traffic spikes do not starve synchronization jobs.
Or skip the browser setup
If your workflow also needs a rendered page image for documentation, QA, or an AI agent, ScreenshotNeo provides a website screenshot API and MCP server. It removes cookie banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and billing result. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.
See the ScreenshotNeo API documentation for the available options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://www.viator.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://www.viator.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://www.viator.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
Every feature is available on every plan. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.
FAQ
Is there a public Viator API key?
Access is partner-tier based. Apply for the appropriate Partner API access and keep the issued key server-side.
Can I scrape Viator’s HTML pages instead?
The supported route described by the partner documentation is the Partner API. HTML scraping is a different activity and is not authorized by the cited partner terms.
How often should I synchronize?
Viator describes hourly updates as normal and permits more frequent polling when needed, subject to rate limits. Choose a cadence based on freshness requirements and monitor lag.
Can I ingest the entire catalog with bulk?
No. Use /products/modified-since for catalog ingestion. Bulk is for selected products and supports up to 500 product codes per request.
Who handles checkout?
Affiliate partners send customers to Viator. Merchant partners with transactional access can handle bookings and merchant-of-record responsibilities according to their agreement.


