> ## Documentation Index
> Fetch the complete documentation index at: https://docs.aurous-labs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Usage pagination

> How the GET /v1/usage cursor walk works — and the known edge case when limit is smaller than the per-bucket group count.

`GET /v1/usage` is a forward-only cursor pagination — pass `limit` for the page size and use the response's `next_page` token to walk forward. The cursor encodes the **query fingerprint** so filters cannot drift between pages.

## Basic walk

```bash theme={null}
# Page 1
curl 'https://api.aurous-labs.com/v1/usage?start_time=2026-04-01T00:00:00Z&end_time=2026-05-01T00:00:00Z&bucket_width=1d&group_by=type&limit=10' \
  -H "X-Api-Key: $AUROUS_API_KEY"

# Response:
# {
#   "object": "list",
#   "data": [/* 10 buckets */],
#   "has_more": true,
#   "next_page": "eyJ2IjoxLCJxZiI6IjlmYW..."
# }

# Page 2 — pass the cursor in `page_token`
curl 'https://api.aurous-labs.com/v1/usage?page_token=eyJ2IjoxLCJxZiI6IjlmYW...' \
  -H "X-Api-Key: $AUROUS_API_KEY"
```

When `page_token` is provided, you do NOT need to re-send `start_time`, `end_time`, `bucket_width`, `group_by`, or filter parameters — they're encoded in the cursor. In fact, sending them with **different values** mismatches the fingerprint and returns:

```json theme={null}
{
  "error": {
    "type": "invalid_request",
    "code": "invalid_page_token",
    "message": "page_token fingerprint mismatch — query parameters drifted between pages."
  }
}
```

This is intentional. A cursor that silently re-resolves params (e.g. resolves a new `now` for `end_time` on page 2) would return different data than page 1 sees — silent skew. The fingerprint check is Stripe-grade and we don't relax it.

## Cursor lifetime

* Tokens expire 24 hours after the page-1 response that minted them. Walking pages slower than 24 hours apart returns `400 invalid_page_token` with `token_expired` detail.
* Tokens are scoped to your team — using a cursor from team A's response against team B's API key returns `400 invalid_page_token`.

## Cursor opacity

The `next_page` value is base64url-encoded JSON with version + query fingerprint + last-bucket-start + expiry. Don't parse it yourself — treat it as opaque. Future versions of the platform may add fields or change the encoding entirely.

## Known edge case — `limit` smaller than per-bucket group count

The cursor encodes `last_bucket_start` (the timestamp of the last bucket returned on the current page). If a bucket has N groups and your `limit` is less than N:

* Page 1 returns the bucket with the first `limit` groups; `has_more: true`; `next_page` encodes that bucket's `bucket_start`
* Page 2 — using the cursor — starts AFTER `bucket_start`, so the remaining groups in that bucket are LOST

In practice this matters when:

* Your `group_by` is multi-dimensional (e.g. `group_by=type,model` could produce 8 groups per bucket: 4 types × 2 models)
* Your `limit` is unusually small (\< the maximum expected groups-per-bucket)

**Recommended workaround**: set `limit` ≥ the max possible groups per bucket. For a single `group_by` like `type` (≤ 4 groups), `limit ≥ 4` is safe. For multi-dim `group_by`, multiply: `group_by=type,model` → `limit ≥ 16` is safe up to 8 models. The defaults (`limit=100`) are generous enough that most production callers never hit this.

The proper fix is to paginate at the **bucket level** (return whole buckets, never split a bucket's groups across pages). It's on the v1.1 roadmap — see the [pagination plan in launch-week followups](https://github.com/MayaTech88/OnlyGold/blob/preprod/docs/superpowers/plans/2026-05-20-llm-launch-week-followups.md).

## Stable ordering

Within a response, buckets are ordered by `bucket_start` ascending (oldest first). Within a bucket, groups are ordered by total credit charge descending (highest spend first), then by `dimensions` alpha-sort tie-break.

The order is stable across page walks for a given fingerprint — paging forward through a 1000-row window deterministically returns the same row order whether you do it in one shot or in five 200-row pages.

## Sample code

<CodeGroup>
  ```typescript Node.js theme={null}
  async function fetchAllBuckets(params: Record<string, string>): Promise<unknown[]> {
    const buckets: unknown[] = [];
    let pageToken: string | undefined = undefined;
    for (;;) {
      const url = new URL("https://api.aurous-labs.com/v1/usage");
      if (pageToken) {
        url.searchParams.set("page_token", pageToken);
      } else {
        for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
      }
      const res = await fetch(url, { headers: { "X-Api-Key": process.env.AUROUS_API_KEY! } });
      const body = await res.json() as { data: unknown[]; has_more: boolean; next_page: string | null };
      buckets.push(...body.data);
      if (!body.has_more || !body.next_page) break;
      pageToken = body.next_page;
    }
    return buckets;
  }

  const all = await fetchAllBuckets({
    start_time: "2026-04-01T00:00:00Z",
    end_time: "2026-05-01T00:00:00Z",
    bucket_width: "1d",
    group_by: "type",
    limit: "500",
  });
  ```

  ```python Python theme={null}
  def fetch_all_buckets(params: dict) -> list:
      buckets = []
      next_page = None
      while True:
          query = {"page_token": next_page} if next_page else params
          res = requests.get(
              "https://api.aurous-labs.com/v1/usage",
              params=query,
              headers={"X-Api-Key": AUROUS_API_KEY},
          ).json()
          buckets.extend(res["data"])
          if not res.get("has_more") or not res.get("next_page"):
              break
          next_page = res["next_page"]
      return buckets

  all_buckets = fetch_all_buckets({
      "start_time": "2026-04-01T00:00:00Z",
      "end_time": "2026-05-01T00:00:00Z",
      "bucket_width": "1d",
      "group_by": "type",
      "limit": "500",
  })
  ```
</CodeGroup>

## Where to next?

* [Usage overview](/api-reference/usage/overview) — the full query surface
* [Usage event stream](/api-reference/usage/event-stream) — per-event detail rows
* [`GET /v1/usage`](/api-reference/openapi#tag/account) — endpoint reference
