> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.pivotal.app/llms.txt.
> For full documentation content, see https://docs.pivotal.app/llms-full.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.pivotal.app/_mcp/server.

# Pagination

Every list endpoint in the Pivotal API uses cursor pagination. The pattern is the same shape across customers, contacts, and onboardings:

```http
GET /api/v1/customers?limit=20&cursor=2026-05-20T14:08:11.000Z
```

Response:

```json
{
  "object": "list",
  "data": [ /* 0–20 items */ ],
  "has_more": true,
  "next_cursor": "2026-05-19T22:41:03.000Z"
}
```

Pass `response.next_cursor` straight back as the next request's `cursor`. Stop when `has_more` is `false`.

## Parameters

| Param    | Type              | Default | Max | Notes                                                          |
| -------- | ----------------- | ------- | --- | -------------------------------------------------------------- |
| `limit`  | integer           | 20      | 100 | Page size. Larger = fewer round trips, more work per response. |
| `cursor` | ISO 8601 datetime | none    | —   | Returns items strictly older than this `created_at`.           |

`cursor` must validate as an ISO 8601 datetime. Garbage in returns a 400:

```json
{
  "error": {
    "type": "invalid_request_error",
    "code": "invalid_string",
    "message": "Invalid datetime",
    "field": "cursor"
  }
}
```

## Walking a list end to end

```typescript
async function* walkCustomers() {
  let cursor: string | undefined;
  while (true) {
    const url = new URL("https://my.pivotal.app/api/v1/customers");
    url.searchParams.set("limit", "100");
    if (cursor) url.searchParams.set("cursor", cursor);

    const res = await fetch(url, {
      headers: { Authorization: `Bearer ${process.env.PIVOTAL_API_KEY}` },
    });
    const page = await res.json();
    for (const c of page.data) yield c;

    if (!page.has_more) return;
    cursor = page.next_cursor;
  }
}

for await (const customer of walkCustomers()) {
  console.log(customer.display_id, customer.name);
}
```

Python:

```python
def walk_customers():
    cursor = None
    while True:
        params = {"limit": 100}
        if cursor:
            params["cursor"] = cursor
        res = requests.get(
            "https://my.pivotal.app/api/v1/customers",
            params=params,
            headers={"Authorization": f"Bearer {os.environ['PIVOTAL_API_KEY']}"},
        )
        page = res.json()
        for c in page["data"]:
            yield c
        if not page["has_more"]:
            return
        cursor = page["next_cursor"]
```

## Filters and pagination interact

Filters like `?status=active` apply on the server, then pagination cuts the filtered result. The cursor still walks `created_at` descending — you don't get drift from cursoring through a moving filter as long as the filter is stable for the duration of the walk.

If you mutate records mid-walk (e.g. flipping `status` from `onboarding` to `active` while paginating `?status=onboarding`), expect to either skip or duplicate the affected rows depending on the timing. Snapshot in your client if that's a problem.

## What we deliberately don't do

* **No total count.** Counting at scale gets expensive. If you need a UI element that says "245 customers", fetch the count separately (a future `/customers/count` endpoint will land — for now, walk the list once and cache).
* **No offset/limit.** Offsets get slower as the offset grows and behave badly with concurrent inserts. Cursors are stable and constant-cost.
* **No `before` cursor.** Lists return newest first; if you need older-first, reverse the page in your client.

## Edge cases

* An **empty page** comes back as `{ object: "list", data: [], has_more: false, next_cursor: null }`.
* The **last page** has `has_more: false` even if it returned `limit` items — trust the flag, not the count.
* **Soft-deleted rows never appear** in lists. They're still gettable by id if you know it (`GET /customers/{id}` will 404 — soft-deleted is treated as gone). If you need to read deletes, surface them through the customer's timeline.