# Pagination

All list endpoints use cursor-based pagination. You walk a result set by
following the `next` URL until `hasMore` is `false`.

## Parameters

| Parameter | Type | Default | Description |
|  --- | --- | --- | --- |
| `limit` | integer | `1000` | Records per page. Range 1–1000. |
| `after` | string | — | Cursor: the ID of the last item from the previous page. Omit on the first request. |


The cursor format depends on the resource:

- **Bookings:** composite ID like `studio_123` or `livestream_456`.
- **Customers, classes, orders, teachers, write logs:** numeric ID like `456`.


The same shape that appears in list responses is what you pass back as
`after` — you don't have to compose anything yourself.

## Response shape

Every paginated response wraps results in the same envelope:


```json
{
  "data": [ /* array of resource objects */ ],
  "hasMore": true,
  "next": "https://api.yogobooking.com/customers?after=456&limit=1000"
}
```

| Field | Type | Description |
|  --- | --- | --- |
| `data` | array | The page of results. |
| `hasMore` | boolean | `true` if more pages exist. |
| `next` | string | Fully qualified URL for the next page. Present only when `hasMore`. |


## How to walk a list

1. Make the first request without `after`.
2. If `hasMore` is `true`, fetch the URL in `next`.
3. Repeat until `hasMore` is `false`.


The `next` URL preserves all your query parameters (filters, expansions,
date ranges) so you don't have to re-attach them.

## Stability under concurrent writes

Cursor pagination keeps your walk consistent even when new records are
created mid-iteration. New items added after your first page won't shift
the items you've already seen.

## Invalid cursors

If you supply a non-existent ID as the `after` cursor, the API returns
`422 Unprocessable Entity`:


```json
{
  "statusCode": 422,
  "message": "Invalid cursor: [resource] not found",
  "error": "Unprocessable Entity"
}
```