OpenAPI Specification
openapi: 3.2.0
info:
title: Staying Data API
version: 1.0.0
description: 'One accommodation-data API for Airbnb, Booking.com, Vrbo and Google Hotels — availability, search and cross-OTA price comparison in a single unified schema. Every endpoint returns the canonical envelope (§6.3.3); errors use the locked sub-code catalog (§6.4). Failed, empty and blocked calls are never billed; cached results bill at tier (caching is margin, not a discount). Full docs: https://stayingapi.com/docs.'
contact:
name: StayingAPI
url: https://stayingapi.com/docs
license:
name: Proprietary
servers:
- url: https://api.stayingapi.com/v1
description: REST API (v1)
security:
- bearerAuth: []
tags:
- name: Data
description: Accommodation data endpoints
paths:
/search:
get:
tags:
- Data
operationId: searchGet
summary: 'Search properties across platforms (per-result: RATE×results per platform, min 5 credits; airbnb 2×, booking/vrbo 1×, google 15/page).'
parameters:
- name: location
in: query
required: true
schema:
type: string
description: Destination (city, region, address).
- name: checkIn
in: query
required: false
schema:
type: string
description: YYYY-MM-DD.
- name: checkOut
in: query
required: false
schema:
type: string
description: YYYY-MM-DD.
- name: platforms
in: query
required: false
schema:
type: string
description: Comma-separated platforms; defaults to all enabled. `limit` applies per platform.
- name: platform
in: query
required: false
schema:
type: string
description: Alias for `platforms=<value>` when requesting exactly one platform.
- name: adults
in: query
required: false
schema:
type: integer
description: Default 2.
- name: children
in: query
required: false
schema:
type: integer
description: Default 0; requires matching childAges.
- name: rooms
in: query
required: false
schema:
type: integer
description: Default 1.
- name: currency
in: query
required: false
schema:
type: string
description: ISO-4217; default USD.
- name: limit
in: query
required: false
schema:
type: integer
description: '1–40; default 10. Applies per platform: a 3-platform search with limit=5 returns up to 15 results.'
- name: cursor
in: query
required: false
schema:
type: string
description: Opaque pagination cursor.
- name: sort
in: query
required: false
schema:
type: string
description: recommended | price_asc | price_desc | rating_desc.
responses:
'200':
description: Success.
headers:
X-Request-Id:
schema:
type: string
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/SuccessEnvelope'
- type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/Property'
'202':
description: Accepted — async job created; poll pollUrl. Retry-After hints the back-off.
headers:
Retry-After:
schema:
type: integer
description: Seconds to wait before polling.
X-Request-Id:
schema:
type: string
content:
application/json:
schema:
$ref: '#/components/schemas/AsyncAccepted'
'400':
description: Validation error (invalid_request).
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'401':
description: Authentication error (missing/invalid API key).
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'402':
description: Insufficient credits (credit_balance_too_low).
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'403':
description: Permission denied (e.g. email_unverified on live credits; subscription_required on top-ups).
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'429':
description: Rate limited (rate_limit_exceeded); carries Retry-After.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'503':
description: All upstream sources failed or were blocked (no charge).
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
post:
tags:
- Data
operationId: searchPost
summary: Search properties (JSON body form of GET /search).
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties: true
responses:
'200':
description: Success.
headers:
X-Request-Id:
schema:
type: string
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/SuccessEnvelope'
- type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/Property'
'202':
description: Accepted — async job created; poll pollUrl. Retry-After hints the back-off.
headers:
Retry-After:
schema:
type: integer
description: Seconds to wait before polling.
X-Request-Id:
schema:
type: string
content:
application/json:
schema:
$ref: '#/components/schemas/AsyncAccepted'
'400':
description: Validation error (invalid_request).
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'401':
description: Authentication error (missing/invalid API key).
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'402':
description: Insufficient credits (credit_balance_too_low).
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'403':
description: Permission denied (e.g. email_unverified on live credits; subscription_required on top-ups).
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'429':
description: Rate limited (rate_limit_exceeded); carries Retry-After.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'503':
description: All upstream sources failed or were blocked (no charge).
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
/availability:
get:
tags:
- Data
operationId: availability
summary: Nightly availability for a listing (max(5, ⌈days/2⌉) credits by requested date range).
description: 'Provide EXACTLY ONE identification form: listingId, listingIds, or url. None → missing_parameter; conflicting forms → mutually_exclusive_params; a url that is not a well-formed listing URL for the platform → invalid_listing_url.'
parameters:
- name: platform
in: query
required: true
schema:
$ref: '#/components/schemas/Platform'
description: Target platform (must be enabled for availability).
- name: listingId
in: query
required: false
schema:
type: string
description: Platform listing id (one identification form).
- name: listingIds
in: query
required: false
schema:
type: string
description: Comma-separated listing ids (one identification form).
- name: url
in: query
required: false
schema:
type: string
description: Full listing URL for the platform (one identification form).
- name: startDate
in: query
required: true
schema:
type: string
description: YYYY-MM-DD (required).
- name: endDate
in: query
required: true
schema:
type: string
description: YYYY-MM-DD (required).
- name: onlyAvailable
in: query
required: false
schema:
type: boolean
description: Only return available dates.
responses:
'200':
description: Success.
headers:
X-Request-Id:
schema:
type: string
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/SuccessEnvelope'
- type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/Availability'
'202':
description: Accepted — async job created; poll pollUrl. Retry-After hints the back-off.
headers:
Retry-After:
schema:
type: integer
description: Seconds to wait before polling.
X-Request-Id:
schema:
type: string
content:
application/json:
schema:
$ref: '#/components/schemas/AsyncAccepted'
'400':
description: Validation error (invalid_request).
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'401':
description: Authentication error (missing/invalid API key).
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'402':
description: Insufficient credits (credit_balance_too_low).
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'403':
description: Permission denied (e.g. email_unverified on live credits; subscription_required on top-ups).
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'429':
description: Rate limited (rate_limit_exceeded); carries Retry-After.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'503':
description: All upstream sources failed or were blocked (no charge).
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
/listing/{platform}/{id}:
get:
tags:
- Data
operationId: listing
summary: Full normalized listing (3 credits/leg, flat).
parameters:
- name: platform
in: path
required: true
schema:
$ref: '#/components/schemas/Platform'
- name: id
in: path
required: true
schema:
type: string
- name: country
in: query
required: false
schema:
type: string
description: ISO-3166 alpha-2 country hint (booking). Disambiguates a bare booking.com slug (slugs are not globally unique) without the %2F-encoded /hotel/{cc}/{slug} id — e.g. /v1/listing/booking/casa-de-alba?country=co. A bare booking slug with no country returns 400 needs_country.
- name: checkIn
in: query
required: false
schema:
type: string
description: YYYY-MM-DD.
- name: checkOut
in: query
required: false
schema:
type: string
description: YYYY-MM-DD.
- name: currency
in: query
required: false
schema:
type: string
description: ISO-4217; default USD.
responses:
'200':
description: Success.
headers:
X-Request-Id:
schema:
type: string
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/SuccessEnvelope'
- type: object
properties:
data:
$ref: '#/components/schemas/Property'
'202':
description: Accepted — async job created; poll pollUrl. Retry-After hints the back-off.
headers:
Retry-After:
schema:
type: integer
description: Seconds to wait before polling.
X-Request-Id:
schema:
type: string
content:
application/json:
schema:
$ref: '#/components/schemas/AsyncAccepted'
'400':
description: Validation error (invalid_request).
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'401':
description: Authentication error (missing/invalid API key).
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'402':
description: Insufficient credits (credit_balance_too_low).
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'403':
description: Permission denied (e.g. email_unverified on live credits; subscription_required on top-ups).
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'429':
description: Rate limited (rate_limit_exceeded); carries Retry-After.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'503':
description: All upstream sources failed or were blocked (no charge).
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
/price:
get:
tags:
- Data
operationId: price
summary: Live price for a listing + dates (3 credits/leg, flat).
parameters:
- name: platform
in: query
required: true
schema:
$ref: '#/components/schemas/Platform'
description: Target platform (must be enabled for price).
- name: listingId
in: query
required: true
schema:
type: string
description: Platform listing id.
- name: checkIn
in: query
required: true
schema:
type: string
description: YYYY-MM-DD (required).
- name: checkOut
in: query
required: true
schema:
type: string
description: YYYY-MM-DD (required).
- name: currency
in: query
required: false
schema:
type: string
description: ISO-4217; default USD.
responses:
'200':
description: Success.
headers:
X-Request-Id:
schema:
type: string
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/SuccessEnvelope'
- type: object
properties:
data:
$ref: '#/components/schemas/Price'
'202':
description: Accepted — async job created; poll pollUrl. Retry-After hints the back-off.
headers:
Retry-After:
schema:
type: integer
description: Seconds to wait before polling.
X-Request-Id:
schema:
type: string
content:
application/json:
schema:
$ref: '#/components/schemas/AsyncAccepted'
'400':
description: Validation error (invalid_request).
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'401':
description: Authentication error (missing/invalid API key).
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'402':
description: Insufficient credits (credit_balance_too_low).
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'403':
description: Permission denied (e.g. email_unverified on live credits; subscription_required on top-ups).
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'429':
description: Rate limited (rate_limit_exceeded); carries Retry-After.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'503':
description: All upstream sources failed or were blocked (no charge).
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
/price-compare:
get:
tags:
- Data
operationId: priceCompare
summary: Cross-OTA price comparison — via Google (30 credits, flat) or across listing ids you supply (sum of the per-platform price legs).
description: 'TWO modes, one response type.
**Google-aggregator mode** (`name` / `location` / `googleHotelId`): we resolve the property on Google Hotels and report the offers it surfaces. You need no ids. Flat 30 credits.
**Direct fan-out mode** (`listings=`): you supply 2–6 `platform:listingId` pairs for the SAME property and we run one real price call per listing, in parallel, then normalize the comparison ourselves. Each offer carries `platform`, `listingId`, `nightlyPrice`, `nights` and a best-effort `fees` breakdown, and the response is marked `source: ''direct_fanout''`. Billed as the SUM of the per-platform price legs; a leg that fails or returns nothing is charged 0 and is reported in `meta.platformResults` with `meta.partial: true`.
FEE COVERAGE IS UNEVEN and this is deliberate rather than a gap we hide: Airbnb reports cleaning, service and taxes; Booking.com reports taxes only; Vrbo and Google report none. Compare on `totalPrice`, which is present and comparable everywhere. A null fee means we could not read it, NOT that the platform does not charge it.
`min` and `median` cover only the offers quoted in the response `currency`; offers in another currency are still returned but excluded from the summary.'
parameters:
- name: listings
in: query
required: false
schema:
type: string
description: DIRECT MODE. 2–6 comma-separated (or repeated) platform:listingId pairs for the same property, e.g. listings=airbnb:12345,booking:co/casa-de-alba,vrbo:987654. Split on the FIRST colon, so a full listing URL works as the id. Mutually exclusive with name/location/googleHotelId. An id containing a comma must be sent as a repeated param.
- name: name
in: query
required: false
schema:
type: string
description: GOOGLE MODE. Property name (one of name/location/googleHotelId).
- name: location
in: query
required: false
schema:
type: string
description: GOOGLE MODE. Location hint (one of name/location/googleHotelId).
- name: googleHotelId
in: query
required: false
schema:
type: string
description: GOOGLE MODE. Google hotel id (mutually exclusive with name/location).
- name: checkIn
in: query
required: true
schema:
type: string
description: YYYY-MM-DD (required).
- name: checkOut
in: query
required: true
schema:
type: string
description: YYYY-MM-DD (required).
- name: adults
in: query
required: false
schema:
type: integer
description: Default 2.
- name: children
in: query
required: false
schema:
type: integer
description: Default 0.
- name: currency
in: query
required: false
schema:
type: string
description: ISO-4217; default USD.
responses:
'200':
description: Success.
headers:
X-Request-Id:
schema:
type: string
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/SuccessEnvelope'
- type: object
properties:
data:
$ref: '#/components/schemas/PriceCompare'
'202':
description: Accepted — async job created; poll pollUrl. Retry-After hints the back-off.
headers:
Retry-After:
schema:
type: integer
description: Seconds to wait before polling.
X-Request-Id:
schema:
type: string
content:
application/json:
schema:
$ref: '#/components/schemas/AsyncAccepted'
'400':
description: Validation error (invalid_request).
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'401':
description: Authentication error (missing/invalid API key).
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'402':
description: Insufficient credits (credit_balance_too_low).
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'403':
description: Permission denied (e.g. email_unverified on live credits; subscription_required on top-ups).
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'429':
description: Rate limited (rate_limit_exceeded); carries Retry-After.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'503':
description: All upstream sources failed or were blocked (no charge).
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
/reviews:
get:
tags:
- Data
operationId: reviews
summary: 'Listing reviews (per-review: RATE×reviews, min 5 credits; airbnb 5×, booking/vrbo 1×).'
description: Provide EXACTLY ONE of listingId or url. None → missing_parameter; both → mutually_exclusive_params; a bad url → invalid_listing_url. Required params are platform plus either listingId or url. listingId should be the platform-native id from /v1/search `platformListingId`; a `stays_<platform>_<platformListingId>` object id is accepted, normalized, and may supply platform when platform is omitted.
parameters:
- name: platform
in: query
required: true
schema:
$ref: '#/components/schemas/Platform'
description: Target platform (must be enabled for reviews).
- name: listingId
in: query
required: false
schema:
type: string
description: Platform-native id from /v1/search `platformListingId` (one identification form); a `stays_<platform>_<platformListingId>` id is accepted, normalized, and may supply platform when platform is omitted.
- name: url
in: query
required: false
schema:
type: string
description: Full listing URL (one identification form).
- name: limit
in: query
required: false
schema:
type: integer
description: 1–100; default 20.
- name: cursor
in: query
required: false
schema:
type: string
description: Opaque pagination cursor.
- name: language
in: query
required: false
schema:
type: string
description: ISO-639-1 filter.
- name: sort
in: query
required: false
schema:
type: string
description: recent | rating_desc | rating_asc.
responses:
'200':
description: Success.
headers:
X-Request-Id:
schema:
type: string
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/SuccessEnvelope'
- type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/Review'
'202':
description: Accepted — async job created; poll pollUrl. Retry-After hints the back-off.
headers:
Retry-After:
schema:
type: integer
description: Seconds to wait before polling.
X-Request-Id:
schema:
type: string
content:
application/json:
schema:
$ref: '#/components/schemas/AsyncAccepted'
'400':
description: Validation error (invalid_request).
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'401':
description: Authentication error (missing/invalid API key).
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'402':
description: Insufficient credits (credit_balance_too_low).
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'403':
description: Permission denied (e.g. email_unverified on live credits; subscription_required on top-ups).
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'429':
description: Rate limited (rate_limit_exceeded); carries Retry-After.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'503':
description: All upstream sources failed or were blocked (no charge).
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
components:
schemas:
Pagination:
type: object
properties:
limit:
type: integer
cursor:
type:
- string
- 'null'
nextCursor:
type:
- string
- 'null'
description: Opaque base64 cursor for the next page; null on the last page.
hasMore:
type: boolean
PlatformFailureError:
type: object
properties:
type:
type: string
enum:
- invalid_request
- authentication_error
- permission_denied
- not_found
- insufficient_credits
- rate_limited
- upstream_unavailable
- upstream_timeout
- internal_error
code:
type: string
enum:
- missing_parameter
- invalid_parameter
- invalid_date_range
- date_in_past
- child_ages_mismatch
- stay_too_long
- window_too_long
- invalid_listing_url
- invalid_property_type
- invalid_amenity
- invalid_price_range
- invalid_language
- invalid_sort
- invalid_cursor
- limit_out_of_range
- mutually_exclusive_params
- platform_not_enabled
- no_enabled_platform
- needs_country
- missing_api_key
- invalid_api_key
- revoked_api_key
- scope_insufficient
- email_unverified
- subscription_required
- listing_not_found
- job_not_found
- identity_mismatch
- credit_balance_too_low
- rate_limit_exceeded
- all_actors_failed
- actor_blocked
- upstream_degraded
- actor_timeout
- internal_error
message:
type: string
minLength: 1
retryable:
type: boolean
required:
- type
- code
- message
- retryable
Meta:
type: object
description: Canonical response metadata present on every 200 (§6.3.3).
required:
- requestId
- platforms
- cached
- partial
- creditsCharged
- currency
properties:
requestId:
type: string
pattern: ^req_[0-9A-HJKMNP-TV-Z]{26}$
description: 'Correlation id (also the X-Request-Id header): req_ + 26-char Crockford ULID.'
example: req_01J8ZK3QF9ABCDEF0123456789
platforms:
type: array
items:
$ref: '#/components/schemas/Platform'
environment:
type: string
enum:
- live
- test
description: Authenticated key environment used for this response. `test` means deterministic sandbox fixtures, never a live scrape.
sandbox:
type: boolean
description: True when the response came from the deterministic sandbox (`stay_test_` key).
cached:
type: boolean
description: Served fully from cache. Cached results still bill at tier (margin, not discount).
partial:
type: boolean
description: At least one fan-out leg failed; failed legs are charged 0.
creditsCharged:
type: integer
minimum: 0
currency:
type: string
example: USD
pagination:
oneOf:
- $ref: '#/components/schemas/Pagination'
- type: 'null'
platformResults:
type: array
items:
$ref: '#/components/schemas/PlatformResult'
warnings:
type: array
items:
$ref: '#/components/schemas/Warning'
Availability:
type: object
properties:
platform:
type: string
enum:
- airbnb
- booking
- vrbo
- expedia
- hotels
- google
- tripadvisor
listingId:
type: string
minLength: 1
dates:
type: array
items:
type: object
properties:
date:
# --- truncated at 32 KB (53 KB total) ---
# Full source: https://raw.githubusercontent.com/api-evangelist/stayingapi/refs/heads/main/openapi/stayingapi-data-api-openapi.yml