Pagination — Grep API v2

Pagination

Cursor-based pagination on /research, /research/{job_id_or_slug}/timeline, and /billing/transactions. CursorPage shape, why keyset, and how to walk pages.

Where it applies

Three v2 endpoints return paged results. All share the same CursorPage[T] envelope.

EndpointReturns
GET /api/v2/researchYour tenant's research jobs, newest first.
GET /api/v2/research/{job_id_or_slug}/timelineA job's execution events, newest first.
GET /api/v2/billing/transactionsCredit ledger entries, newest first.

CursorPage[T] shape

Every paged response is a CursorPage[T] envelope: the page items, an opaque next_cursor token, and a has_more flag.

json
{
  "items": [
    { "job_id": "...", "status": "completed", "created_at": "..." },
    ...
  ],
  "next_cursor": "eyJjcmVhdGVkX2F0IjoiMjAyNi0wNC0yN1QxMDoxNTowMFoiLCJpZCI6IjU1MGU4NDAwLi4uIn0",
  "has_more": true
}

First page

Omit the cursor parameter. Pass limit if you want a smaller page; each endpoint documents its maximum.

curl
curl "https://api.grep.ai/api/v2/research?limit=50" \
  -H "Authorization: Bearer $GREP_API_KEY"

Next page

Pass next_cursor from the previous response as cursor. Stop when has_more is false.

curl
curl "https://api.grep.ai/api/v2/research?limit=50&cursor=$NEXT_CURSOR" \
  -H "Authorization: Bearer $GREP_API_KEY"

Why keyset, not offset?

Cursors encode a (created_at, id) pair as opaque base64. This is keyset pagination — and it's stable in ways offset pagination is not.

  • Inserts at the head of the list don't shift later pages. With offset, page 2 would suddenly include the row that page 1 just showed you.
  • Performance is constant regardless of how deep you walk. Offset pagination scans every skipped row.
  • Cursors are opaque — never parse them client-side. The encoding may change between API versions.

Walking all pages

A simple generator that yields every job across the full history.

python
import os
import httpx

API_KEY = os.environ["GREP_API_KEY"]
BASE = "https://api.grep.ai/api/v2"

def all_jobs():
    cursor = None
    while True:
        params = {"limit": 100}
        if cursor:
            params["cursor"] = cursor
        r = httpx.get(
            f"{BASE}/research",
            params=params,
            headers={"Authorization": f"Bearer {API_KEY}"},
            timeout=30,
        )
        r.raise_for_status()
        page = r.json()
        for job in page["items"]:
            yield job
        if not page["has_more"]:
            return
        cursor = page["next_cursor"]

for job in all_jobs():
    print(job["job_id"], job["status"])