Pagination and rate limits#
Place searches use forward-only cursor pagination. A response contains:
{
"page": {
"count": 25,
"hasMore": true,
"nextCursor": "eyJhZnRlciI6…"
}
}
Pass nextCursor unchanged in the next request. A cursor is bound to its
country, city, text, lodging type, WhatsApp, and verification filters. Changing
a filter while reusing the cursor returns 400.
const endpoint = new URL('https://www.hotelovia.com/api/v1/places')
endpoint.searchParams.set('country', 'es')
endpoint.searchParams.set('city', 'madrid')
endpoint.searchParams.set('lodgingType', 'hotel')
endpoint.searchParams.set('limit', '100')
let cursor
do {
if (cursor) endpoint.searchParams.set('cursor', cursor)
const response = await fetch(endpoint, {
headers: { 'X-API-Key': process.env.HOTELOVIA_API_KEY },
})
if (response.status === 429) {
const retryAfter = Number(response.headers.get('Retry-After') || 1)
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000))
continue
}
if (!response.ok) throw new Error(`Hotelovia API ${response.status}`)
const page = await response.json()
await persistPlaces(page.places)
cursor = page.page.nextCursor || undefined
} while (cursor)
Cursor rules#
- Treat cursors as opaque; their encoding can change.
- Do not persist them as durable record identifiers.
- Stop when
nextCursorisnull, regardless ofcount. - A deleted or newly published record can shift a later traversal; deduplicate
durable imports by place
id.
Rate limits#
The default authenticated limit is 300 requests per 60 seconds per API key. Read the response headers instead of hard-coding that number: Hotelovia may assign a different limit to a key. The limiter is shared across concurrent API instances.