API Pagination
Grizzly REST APIs use cursor-based pagination for endpoints that return large datasets.
How It Works
When making requests, pagination parameters are provided in the querystring, and the pagination details are returned in a pagination property on the body.
Request Parameters (Query String)
Pagination parameters are sent in the query string, regardless of HTTP method:
| Parameter | Type | Description | Default |
|---|---|---|---|
limit |
integer | Page size (1-10,000) | 100 |
cursor |
string | Cursor for next page | - |
Response Format
All paginated endpoints return:
{
"data": [ /* Array of results */ ],
"pagination": {
"limit": 100,
"count": 100,
"hasMore": true,
"nextCursor": "eyJjcmVhdGVkQXQiOjE3MDIzNDU2..."
}
}
| Field | Type | Description |
|---|---|---|
data |
array | Results for current page |
pagination.limit |
integer | Page size used |
pagination.count |
integer | Number of items in current page |
pagination.hasMore |
boolean | Whether more pages exist |
pagination.nextCursor |
string | Cursor for next page (if hasMore is true) |
GET Endpoints
For GET requests, all parameters go in the query string:
Example: List API Keys
First Page:
GET /auth/apikey?limit=50
Authorization: Bearer apikey-xxxxx
Response:
{
"data": [
{
"id": "key-123",
"accountId": "acc-456",
"active": true
}
// ... 49 more items
],
"pagination": {
"limit": 50,
"count": 50,
"hasMore": true,
"nextCursor": "eyJjcmVhdGVkQXQiOjE3MDIzNDU2Nzg5MDEsImlkIjoia2V5LTEyMyJ9"
}
}
Next Page:
GET /auth/apikey?limit=50&cursor=eyJjcmVhdGVkQXQiOjE3MDIzNDU2Nzg5MDEsImlkIjoia2V5LTEyMyJ9
Authorization: Bearer apikey-xxxxx
POST Endpoints
For POST endpoints with complex filters:
- Pagination params → Query string (
?limit=100&cursor=xyz) - Filter params → Request body (JSON)
Example: Search Activities
First Page:
POST /auth/search/activity?limit=100
Authorization: Bearer apikey-xxxxx
Content-Type: application/json
{
"action": "encrypt",
"ring": "production",
"start": 1702300000000,
"end": 1702400000000
}
Response:
{
"data": [
{
"id": "activity-789",
"keyid": "key-123",
"createdAt": 1702345678901,
"props": {
"action": "encrypt",
"ring": "production"
}
}
// ... 99 more items
],
"pagination": {
"limit": 100,
"count": 100,
"hasMore": true,
"nextCursor": "eyJjcmVhdGVkQXQiOjE3MDIzNDU2Nzg5MDEsImlkIjoiYWN0aXZpdHktNzg5In0"
}
}
Next Page:
POST /auth/search/activity?limit=100&cursor=eyJjcmVhdGVkQXQiOjE3MDIzNDU2Nzg5MDEsImlkIjoiYWN0aXZpdHktNzg5In0
Authorization: Bearer apikey-xxxxx
Content-Type: application/json
{
"action": "encrypt",
"ring": "production",
"start": 1702300000000,
"end": 1702400000000
}
Keep Filters Consistent
You must send the same filter parameters (request body) for all pagination requests. Changing filters mid-pagination will produce incorrect results.
Best Practices
1. Store the Cursor Opaquely
Cursors are opaque tokens. Do not parse or modify them.
// Good
let cursor = response.pagination.nextCursor
fetch(`/auth/apikey?cursor=${encodeURIComponent(cursor)}`)
// Bad - Don't parse or modify cursors
let cursorData = JSON.parse(atob(cursor)) // Don't do this!
2. Check hasMore Before Requesting Next Page
if (response.pagination.hasMore) {
// Fetch next page
const nextCursor = response.pagination.nextCursor
await fetchNextPage(nextCursor)
}
3. Handle Last Page
When hasMore is false, you've reached the end:
{
"data": [ /* Last 25 items */ ],
"pagination": {
"limit": 100,
"count": 25,
"hasMore": false
// No nextCursor
}
}
4. Keep Filters Consistent (POST Endpoints)
For POST endpoints using the Re-POST pattern, maintain the same request body:
const filters = {
action: "encrypt",
ring: "production",
start: 1702300000000,
end: 1702400000000
}
// First page
let response = await fetch('/auth/search/activity?limit=100', {
method: 'POST',
body: JSON.stringify(filters) // Same filters
})
// Next page
response = await fetch(`/auth/search/activity?limit=100&cursor=${cursor}`, {
method: 'POST',
body: JSON.stringify(filters) // Same filters!
})
Don't Change Filters Mid-Pagination
Changing filter criteria between pagination requests will produce incorrect or incomplete results. Always use the same filters for an entire pagination sequence.
Time Range Filters
When using time-based filters (start, end) with pagination:
- Set your time range once at the beginning
- Keep the same range for all pagination requests
- Cursors work within the time range you specified
Example:
// Define time range
const filters = {
start: Date.now() - (24 * 60 * 60 * 1000), // Last 24 hours
end: Date.now()
}
// Page 1
let response = await fetch('/auth/search/activity?limit=100', {
method: 'POST',
body: JSON.stringify(filters)
})
// Page 2 - Same time range!
response = await fetch(`/auth/search/activity?limit=100&cursor=${cursor}`, {
method: 'POST',
body: JSON.stringify(filters) // Don't change start/end
})
Pagination Limits
| Limit Type | Value |
|---|---|
| Default page size | 100 items |
| Maximum page size | 10,000 items |
| Minimum page size | 1 item |
Requesting more than 10,000 items:
# Will be capped at 10,000
GET /auth/apikey?limit=50000
# Use pagination instead
GET /auth/apikey?limit=10000
# Then follow nextCursor for additional pages
Client Implementation Examples
JavaScript/TypeScript
async function getAllActivities(filters: object): Promise<Activity[]> {
const allActivities: Activity[] = []
let cursor: string | undefined = undefined
const limit = 1000
do {
const url = cursor
? `/auth/search/activity?limit=${limit}&cursor=${encodeURIComponent(cursor)}`
: `/auth/search/activity?limit=${limit}`
const response = await fetch(url, {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(filters)
})
const data = await response.json()
allActivities.push(...data.data)
if (data.pagination.hasMore) {
cursor = data.pagination.nextCursor
} else {
cursor = undefined
}
} while (cursor)
return allActivities
}
Python
def get_all_activities(api_key: str, filters: dict) -> list:
all_activities = []
cursor = None
limit = 1000
while True:
url = f'/auth/search/activity?limit={limit}'
if cursor:
url += f'&cursor={cursor}'
response = requests.post(
url,
headers={
'Authorization': f'Bearer {api_key}',
'Content-Type': 'application/json'
},
json=filters
)
data = response.json()
all_activities.extend(data['data'])
if not data['pagination']['hasMore']:
break
cursor = data['pagination']['nextCursor']
return all_activities
Troubleshooting
"Invalid cursor" Error
Problem: You receive an error about an invalid cursor.
Solutions: - Ensure you're not modifying the cursor token - Check that you're properly URL-encoding the cursor - Verify the cursor hasn't expired (cursors are valid for the lifetime of the filter criteria)
Empty Results Mid-Pagination
Problem: You get empty results when you know more data exists.
Causes:
- You changed filter parameters between requests
- The time range filter (start/end) was modified mid-pagination
Solution: Use the same filter parameters for the entire pagination sequence.