> For the complete documentation index, see [llms.txt](https://docs.okup.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.okup.ai/okupai-api/basics/pagination.md).

# Pagination

How pagination works

List endpoints use offset-based pagination.

* **Parameters**
  * **limit**: number of items to return (`/data/bookings`: default 100, max 1000; `/data/pricing`: default 1000, max 5000)
  * **offset**: number of items to skip (default: 0)
* **Response fields**
  * **total**: total items available after filters
  * **limit**: the limit you requested/applied
  * **offset**: the offset you requested/applied
  * **has\_more**: whether another page exists
  * **next\_offset**: the next offset, or `null` on the final page
  * **items**: the current page of results
* **Paging logic**
  * Request `next_offset` while `has_more` is `true`
  * You’ve reached the end when `has_more` is `false`
  * `GET /data/bookings` is ordered by `reservation_start` (asc), then `booking_date` (asc)

#### Example request

{% tabs %}
{% tab title="JavaScript" %}

```js
// Server-side (Node 18+). Don’t expose API keys in the browser.
(async () => {
  const BASE_URL = 'https://api.okup.ai';
  const API_KEY = process.env.OKUPAI_API_KEY;

  const res = await fetch(`${BASE_URL}/data/bookings?from=2025-01-01&to=2025-01-31&limit=200&offset=0`, {
    headers: { 'X-API-Key': API_KEY },
  });
  if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);
  const data = await res.json();
  console.log(data);
})();
```

{% endtab %}

{% tab title="Python" %}

```python
import os, requests

BASE_URL = "https://api.okup.ai"
API_KEY = os.environ["OKUPAI_API_KEY"]

r = requests.get(
    f"{BASE_URL}/data/bookings",
    headers={"X-API-Key": API_KEY},
    params={"from": "2025-01-01", "to": "2025-01-31", "limit": 200, "offset": 0},
    timeout=30,
)
r.raise_for_status()
print(r.json())
```

{% endtab %}

{% tab title="cURL" %}

```bash
curl -sS \
  -H "X-API-Key: $OKUPAI_API_KEY" \
  "https://api.okup.ai/data/bookings?from=2025-01-01&to=2025-01-31&limit=200&offset=0"
```

{% endtab %}
{% endtabs %}

#### Iterate pages

{% tabs %}
{% tab title="JavaScript" %}

```js
const BASE_URL = 'https://api.okup.ai';
const API_KEY = process.env.OKUPAI_API_KEY;

async function fetchAllBookings({ from, to, pageSize = 500 }) {
  let offset = 0;
  const all = [];

  while (true) {
    const res = await fetch(`${BASE_URL}/data/bookings?from=${from}&to=${to}&limit=${pageSize}&offset=${offset}`, {
      headers: { 'X-API-Key': API_KEY },
    });
    if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);

    const data = await res.json();
    all.push(...data.items);

    if (!data.has_more) break;
    offset = data.next_offset;
  }
  return all;
}

// Usage:
fetchAllBookings({ from: '2025-01-01', to: '2025-01-31', pageSize: 500 }).then(console.log);
```

{% endtab %}

{% tab title="Python" %}

```python
import os, requests

BASE_URL = "https://api.okup.ai"
API_KEY = os.environ["OKUPAI_API_KEY"]

def fetch_all_bookings(from_date: str, to_date: str, page_size: int = 500):
    offset = 0
    all_items = []

    while True:
        r = requests.get(
            f"{BASE_URL}/data/bookings",
            headers={"X-API-Key": API_KEY},
            params={"from": from_date, "to": to_date, "limit": page_size, "offset": offset},
            timeout=30,
        )
        r.raise_for_status()
        data = r.json()
        all_items.extend(data.get("items", []))

        if not data.get("has_more"):
            break
        offset = data["next_offset"]

    return all_items

# Usage:
print(fetch_all_bookings("2025-01-01", "2025-01-31", 500))
```

{% endtab %}

{% tab title="cURL" %}

```bash
# Example: paginate with a simple offset loop (adjust page size and range as needed)
PAGE_SIZE=500
for OFFSET in $(seq 0 $PAGE_SIZE 5000); do
  echo "Fetching offset=$OFFSET"
  curl -sS -H "X-API-Key: $OKUPAI_API_KEY" \
    "https://api.okup.ai/data/bookings?from=2025-01-01&to=2025-01-31&limit=$PAGE_SIZE&offset=$OFFSET"
  echo # newline between pages
done
```

{% endtab %}
{% endtabs %}

## Incremental synchronization policy

Repeatedly downloading all historical data is slow, creates avoidable load, and increases the chance that a long sync fails partway through. Delta synchronization transfers only rows that changed and is safer to retry.

{% hint style="warning" %}
For API keys that existed before this policy was introduced, broad booking and pricing scans become unavailable on **26 October 2026 at 00:00 UTC**. New API keys receive a 30-day migration period. Treat the `enforcement_at` value returned by the API as authoritative for your key.
{% endhint %}

Before your deadline, broad requests still succeed but include:

```http
Deprecation: true
Sunset: Mon, 26 Oct 2026 00:00:00 GMT
Link: <https://docs.okup.ai/okupai-api/basics/pagination>; rel="deprecation"
```

The JSON body also contains an additive warning:

```json
{
  "warnings": [
    {
      "code": "full_sync_deprecated",
      "message": "Repeated full synchronization will stop being supported after 2026-10-26.",
      "required_parameter": "updated_since",
      "docs_url": "https://docs.okup.ai/okupai-api/basics/pagination",
      "enforcement_at": "2026-10-26T00:00:00Z"
    }
  ]
}
```

Existing response fields are unchanged. Pricing warnings use `changed_since` as `required_parameter`.

### Recommended delta workflow

1. Complete one bootstrap import and upsert every page locally.
2. Save the highest `updated_at` from bookings and `changed_at` from pricing only after all pages complete successfully.
3. On the next run, request:
   * `/data/bookings?updated_since=<ISO-8601 timestamp>`
   * `/data/pricing?changed_since=<ISO-8601 timestamp>`
4. Use a small overlap (for example, one second), and upsert by stable identifiers so retries and boundary timestamps are safe.
5. Continue requesting `next_offset` while `has_more` is `true`.
6. Send the previous response's `ETag` in `If-None-Match` for the exact same URL. A `304 Not Modified` response has no JSON body.

Advance your saved timestamp only after every page succeeds. Run one sync job per OkupAI account. As a starting point, poll pricing every **5 minutes** and bookings every **15 minutes**.

### Initial or recovery bootstrap

Use bootstrap mode only for an initial import or disaster recovery:

```bash
curl -sS \
  -H "X-API-Key: $OKUPAI_API_KEY" \
  "https://api.okup.ai/data/bookings?sync_mode=bootstrap&limit=1000&offset=0"
```

The first request with `sync_mode=bootstrap&offset=0` opens a **30-minute window** for that API key. During the window:

* include `sync_mode=bootstrap` on every page;
* follow `next_offset` until `has_more` is `false`;
* you may page through both bookings and pricing;
* do not combine bootstrap mode with `updated_since` or `changed_since`.

Only one bootstrap may start per API key in any 24-hour period. Starting another after the 30-minute window but before the cooldown ends returns `429 Too Many Requests`; wait for `Retry-After`. A nonzero offset without an active window returns `422`—when eligible, restart at `offset=0`.

### Bounded operational reads

These focused requests remain available without delta or bootstrap mode:

* bookings with both `from` and `to`, for a window of at most 90 days;
* pricing with `okupai_property_id`, `from`, and `to`, for a window of at most 90 days.

These exemptions are for operational lookups, not repeated account-wide synchronization.

### Enforcement errors

An unbounded request after your deadline returns:

```http
HTTP/1.1 422 Unprocessable Entity
X-Error-Code: incremental_sync_required
```

```json
{
  "error": "Broad synchronization now requires updated_since or sync_mode=bootstrap.",
  "code": "incremental_sync_required",
  "required_parameter": "updated_since",
  "bootstrap_parameter": "sync_mode=bootstrap",
  "docs_url": "https://docs.okup.ai/okupai-api/basics/pagination",
  "enforcement_at": "2026-10-26T00:00:00Z"
}
```

A bootstrap started during its cooldown returns:

```http
HTTP/1.1 429 Too Many Requests
Retry-After: 84540
X-Error-Code: bootstrap_cooldown
```

```json
{
  "error": "A bootstrap may only be started once per API key every 24 hours.",
  "code": "bootstrap_cooldown",
  "docs_url": "https://docs.okup.ai/okupai-api/basics/pagination",
  "bootstrap_window_minutes": 30,
  "bootstrap_available_at": "2026-10-27T00:00:00Z"
}
```

If the API returns another `429`, wait for `Retry-After` and add random jitter before retrying.

{% hint style="warning" %}
An ETag belongs to the exact request URL, including filters, pagination, and date range. Do not reuse it for a different URL.
{% endhint %}
