Skip to main content
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

# 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:
{
  "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,modellimit ≥ 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.

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

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",
});

Where to next?