API Pagination

Pagination strategies for different APIs and implementations.

Offset Pagination

Basic offset pagination — simple but unstable under concurrent writes
# Page 1
curl -s "https://api.example.com/v1/devices?page=1&per_page=50" | jq .

# Page 2
curl -s "https://api.example.com/v1/devices?page=2&per_page=50" | jq .
Loop all pages — offset-based
page=1
per_page=50
while true; do
    response=$(curl -s "https://api.example.com/v1/devices?page=$page&per_page=$per_page")
    count=$(echo "$response" | jq '.items | length')
    echo "$response" | jq -r '.items[] | [.name, .ip] | @tsv'
    (( count < per_page )) && break
    (( page++ ))
done | column -t
Offset pagination skips or duplicates records when items are inserted/deleted between requests. Use cursor pagination for reliable enumeration.

Cursor Pagination

Cursor-based — opaque token from server, stable under writes
cursor=""
while true; do
    if [[ -z "$cursor" ]]; then
        response=$(curl -s "https://api.example.com/v1/devices?limit=50")
    else
        response=$(curl -s "https://api.example.com/v1/devices?cursor=$cursor&limit=50")
    fi
    echo "$response" | jq -r '.items[] | [.name, .status] | @tsv'
    cursor=$(echo "$response" | jq -r '.next_cursor // empty')
    [[ -z "$cursor" ]] && break
done | column -t
Follow RFC 8288 Link headers — HATEOAS-style
url="https://api.example.com/v1/devices?per_page=50"
while [[ -n "$url" ]]; do
    response=$(curl -sD- "$url")
    headers=$(echo "$response" | sed '/^\r$/q')
    body=$(echo "$response" | sed '1,/^\r$/d')
    echo "$body" | jq -r '.[] | .name'
    url=$(echo "$headers" | grep -oP '(?<=<)[^>]+(?=>; rel="next")')
done

ISE ERS Pagination

ISE ERS uses page/size parameters with SearchResult wrapper
page=1
size=100
total=0
while true; do
    response=$(curl -sk -u "$ISE_USER:$ISE_PASS" \
      -H "Accept: application/json" \
      "https://10.50.1.20:9060/ers/config/endpoint?size=$size&page=$page")

    total=$(echo "$response" | jq '.SearchResult.total')
    echo "$response" | jq -r '.SearchResult.resources[] | [.name, .id] | @tsv'

    fetched=$(( page * size ))
    (( fetched >= total )) && break
    (( page++ ))
done | column -t

GitHub API Pagination (gh CLI)

gh CLI handles pagination automatically with --paginate
# List all repos (auto-paginated)
gh api --paginate /user/repos --jq '.[].full_name'

# List all issues across pages
gh api --paginate repos/owner/repo/issues --jq '.[].title'
Manual pagination with gh — when you need control
page=1
while true; do
    response=$(gh api "repos/owner/repo/issues?page=$page&per_page=100")
    count=$(echo "$response" | jq 'length')
    echo "$response" | jq -r '.[] | [.number, .title] | @tsv'
    (( count < 100 )) && break
    (( page++ ))
done

Meraki API Pagination

Meraki uses Link header with startingAfter token
url="https://api.meraki.com/api/v1/organizations/$ORG_ID/devices?perPage=100"
while [[ -n "$url" ]]; do
    response=$(curl -sD- -H "X-Cisco-Meraki-API-Key: $MERAKI_KEY" "$url")
    headers=$(echo "$response" | sed '/^\r$/q')
    body=$(echo "$response" | sed '1,/^\r$/d')
    echo "$body" | jq -r '.[] | [.name, .model, .networkId] | @tsv'
    url=$(echo "$headers" | grep -oP '(?<=<)[^>]+(?=>; rel=next)' || true)
done | column -t

FastAPI Pagination Pattern

Offset pagination in domus-api
from fastapi import Query

@app.get("/v1/devices")
async def list_devices(
    skip: int = Query(0, ge=0, description="Offset"),
    limit: int = Query(50, ge=1, le=100, description="Page size"),
):
    devices = await db.fetch_devices(skip=skip, limit=limit)
    total = await db.count_devices()
    return {
        "items": devices,
        "total": total,
        "skip": skip,
        "limit": limit,
        "has_more": skip + limit < total,
    }