Pagination & Conditional Requests (ETags)

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 GET responses — POST, PATCH, and DELETE do 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.