Skip to content

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:

  1. Set your time range once at the beginning
  2. Keep the same range for all pagination requests
  3. 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.