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.
| Endpoint | Returns |
|---|---|
GET /api/v2/research | Your tenant's research jobs, newest first. |
GET /api/v2/research/{job_id_or_slug}/timeline | A job's execution events, newest first. |
GET /api/v2/billing/transactions | Credit 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"])