Pagination
List endpoints return paginated results. Two methods are supported.
Cursor pagination (recommended)
Follow the next_page_url link in each response. The URL contains an opaque cursor parameter that efficiently resumes from where the previous page left off.
GET /v2/bookings?property_ids=123&limit=20
→ {
"items": [...],
"next_page_url": "/v2/bookings?limit=20&cursor=eyJ2IjoxLC...",
"limit": 20,
"offset": 0
}
Follow the link to get the next page. When next_page_url is absent from the response, there are no more pages.
Advantages of cursor pagination:
- Consistent performance regardless of how deep you paginate
- No skipped or duplicated items when data changes between pages
- Just follow the link — no need to calculate offsets
Important: Cursors are tied to your current query filters. If you change property_ids, since_utc, or other filter parameters, start from page 1 (no cursor). The server detects filter changes and automatically resets to page 1.
Offset pagination (backward compatible)
You can also paginate using offset and limit parameters directly:
GET /v2/bookings?property_ids=123&limit=20&offset=0 → page 1
GET /v2/bookings?property_ids=123&limit=20&offset=20 → page 2
GET /v2/bookings?property_ids=123&limit=20&offset=40 → page 3
This method is supported for backward compatibility. Cursor pagination is preferred for new integrations.
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
limit |
integer | 20 | Items per page (1–100) |
offset |
integer | 0 | Number of items to skip |
cursor |
string | — | Opaque cursor from next_page_url (takes precedence over offset) |
Sort order
All list endpoints return items sorted by ID descending (newest first). This order is fixed and cannot be changed via query parameters.
Response format
{
"items": [
{ "id": 357821, ... },
{ "id": 357820, ... }
],
"next_page_url": "/v2/bookings?limit=2&cursor=eyJ2IjoxLC...",
"limit": 2,
"offset": 0
}
| Field | Description |
|---|---|
items |
Array of records for the current page. Absent when the query matched no records. |
next_page_url |
URL to fetch the next page. Absent when this is the last page. |
limit |
The page size used |
offset |
The offset used (0 when using cursor) |
Example: fetching all records
url = "/v2/bookings?since_utc=2024-01-01&limit=100"
while url:
response = api.get(url)
data = response.json()
for item in data.get("items", []):
process(item)
url = data.get("next_page_url")
Conditional Requests (ETags)
All v2 API GET endpoints return an ETag header. Use it with If-None-Match to avoid re-downloading unchanged data.
How it works
First, make a normal request:
GET /v2/bookings?property_ids=123&limit=20
→ 200 OK
→ ETag: W/"6:100:639186027650000000:4c429ee7"
→ { "items": [...], "next_page_url": "..." }
On subsequent requests, send the ETag back:
GET /v2/bookings?property_ids=123&limit=20
If-None-Match: W/"6:100:639186027650000000:4c429ee7"
→ 304 Not Modified (no body, no bandwidth)
If the data changed since your last request, you get a fresh 200 with a new ETag.
What triggers a new ETag
Any create, update, or delete on the entity type for your account invalidates the list ETag. For single-entity endpoints (e.g. GET /v2/bookings/{id}), the ETag is tied to that specific entity's last modification.
Rules
- ETags are per-page — page 1 and page 2 of the same query have different ETags
- ETags are per-filter — changing query parameters produces a different ETag
- ETags are only returned on
GETresponses —POST,PATCH, andDELETEdo not include them - Store the ETag per request URL. Send it back on the next identical request.
Supported endpoints
| Endpoint | List ETag | Single ETag |
|---|---|---|
/v2/bookings |
Yes | Yes |
/v2/guests |
Yes | Yes |
/v2/properties |
Yes | Yes |
/v2/inquiries |
Yes | Yes |
/v2/quotes |
Yes | Yes |
/v2/payments |
Yes | Yes |
/v2/refunds |
Yes | Yes |
/v2/deposits |
Yes | Yes |
/v2/reviews |
Yes | Yes |
Example: polling workflow
etag = None
while True:
headers = {"If-None-Match": etag} if etag else {}
response = api.get("/v2/bookings?since_utc=2024-01-01", headers=headers)
if response.status == 304:
# Nothing changed, skip processing
sleep(60)
continue
etag = response.headers["ETag"]
process(response.json())
sleep(60)
Combining ETags with Pagination
ETags and pagination work together. Each page has its own ETag, so you can cache individual pages. For most polling use cases, checking just the first page with ETags is sufficient — if nothing changed, the 304 tells you immediately without iterating all pages.