Cursor-based pagination returns a small, ordered slice of results plus an opaque token you pass back to get the next slice. Compared to offset-based pagination, cursors are typically more consistent and performant under concurrent writes and large datasets.
Use the cursor returned by the API as-is. Do not build or parse cursors on the client.
How it works
- Request parameters
page_size — number of items per page. Typical range is 1–100 with default 10. Some endpoints may enforce different bounds; check the API Reference for specifics.
cursor — pass the string returned in paging.next_cursor from the previous response to load the next page.
- Response fields
paging.next_cursor — present only when there is more data to fetch. If it’s missing or empty, you’ve reached the end.
Keep all filters and sort parameters identical for every page in a pagination sequence. Changing them mid-stream can lead to duplicates or gaps.
Example: Validators list
First page (no cursor):
curl --request POST \
--url https://beaconcha.in/api/v2/ethereum/validators \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"chain": "mainnet",
"page_size": 10,
"validator": {
"validator_identifiers": [
1
]
}
}
'
Response (excerpt):
{
"data": [
{ /* validator item */ },
{ /* validator item */ },
{ /* validator item */ }
],
"paging": {
"next_cursor": "eyJfc2FtcGxlX2N1cnNvciI6MTIzNDU2Nzg5MH0" // truncated
}
}
Next page (use paging.next_cursor from the previous response):
curl --request POST \
--url https://beaconcha.in/api/v2/ethereum/validators \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"chain": "mainnet",
"page_size": 10,
"cursor": "eyJfc2FtcGxlX2N1cnNvciI6MTIzNDU2Nzg5MH0",
"validator": {
"validator_identifiers": [
1
]
}
}
'
Response (excerpt):
{
"data": [ /* next 3 validators */ ],
"paging": {
"next_cursor": "eyJfc2FtcGxlX2N1cnNvciI6OTg3NjU0MzIxMH0" // if more data remains
}
}
Some endpoints require additional filters (e.g., validator selectors or time ranges). Include the same filters on every paginated request. Refer to the API Reference for each endpoint’s required and optional parameters.
Python
TypeScript
JavaScript
import requests
from typing import Iterator, Any
API_KEY = "<YOUR_API_KEY>"
BASE_URL = "https://beaconcha.in"
def paginate_all(endpoint: str, payload: dict, page_size: int = 100) -> Iterator[dict]:
"""
Generator that yields all items across all pages.
Args:
endpoint: API endpoint path
payload: Request payload (without cursor)
page_size: Items per page (max 100)
Yields:
Individual items from each page
"""
cursor = None
payload = {**payload, "page_size": page_size}
while True:
if cursor:
payload["cursor"] = cursor
response = requests.post(
f"{BASE_URL}{endpoint}",
headers={
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json"
},
json=payload
)
response.raise_for_status()
result = response.json()
# Yield each item
for item in result.get("data", []):
yield item
# Check for next page
cursor = result.get("paging", {}).get("next_cursor")
if not cursor:
break # No more pages
# Usage: Get all rewards for validators
all_rewards = list(paginate_all(
"/api/v2/ethereum/validators/rewards-list",
{
"chain": "mainnet",
"validator": {"validator_identifiers": [1, 2, 3]},
"epoch": 347566
}
))
print(f"Retrieved {len(all_rewards)} reward records")
const API_KEY = "<YOUR_API_KEY>";
const BASE_URL = "https://beaconcha.in";
interface PaginatedResponse<T> {
data: T[];
paging: { next_cursor?: string };
}
async function* paginateAll<T>(
endpoint: string,
payload: Record<string, unknown>,
pageSize: number = 100
): AsyncGenerator<T> {
let cursor: string | undefined;
const requestPayload = { ...payload, page_size: pageSize };
while (true) {
if (cursor) {
requestPayload.cursor = cursor;
}
const response = await fetch(`${BASE_URL}${endpoint}`, {
method: "POST",
headers: {
"Authorization": `Bearer ${API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify(requestPayload)
});
if (!response.ok) {
const error = await response.json();
throw new Error(`API error ${response.status}: ${error.error}`);
}
const result: PaginatedResponse<T> = await response.json();
// Yield each item
for (const item of result.data) {
yield item;
}
// Check for next page
cursor = result.paging?.next_cursor;
if (!cursor) break;
}
}
// Usage: Get all rewards for validators
async function getAllRewards() {
const rewards = [];
for await (const reward of paginateAll(
"/api/v2/ethereum/validators/rewards-list",
{
chain: "mainnet",
validator: { validator_identifiers: [1, 2, 3] },
epoch: 347566
}
)) {
rewards.push(reward);
}
console.log(`Retrieved ${rewards.length} reward records`);
return rewards;
}
const API_KEY = "<YOUR_API_KEY>";
const BASE_URL = "https://beaconcha.in";
async function* paginateAll(endpoint, payload, pageSize = 100) {
let cursor = undefined;
const requestPayload = { ...payload, page_size: pageSize };
while (true) {
if (cursor) {
requestPayload.cursor = cursor;
}
const response = await fetch(`${BASE_URL}${endpoint}`, {
method: "POST",
headers: {
"Authorization": `Bearer ${API_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify(requestPayload)
});
if (!response.ok) {
const error = await response.json();
throw new Error(`API error ${response.status}: ${error.error}`);
}
const result = await response.json();
// Yield each item
for (const item of result.data) {
yield item;
}
// Check for next page
cursor = result.paging?.next_cursor;
if (!cursor) break;
}
}
// Usage: Get all rewards for validators
async function getAllRewards() {
const rewards = [];
for await (const reward of paginateAll(
"/api/v2/ethereum/validators/rewards-list",
{
chain: "mainnet",
validator: { validator_identifiers: [1, 2, 3] },
epoch: 347566
}
)) {
rewards.push(reward);
}
console.log(`Retrieved ${rewards.length} reward records`);
return rewards;
}
Endpoint-specific limits
While many endpoints accept page_size values between 1 and 100 (default 10), some may enforce different limits or include additional paging fields. Always check the API Reference for the exact bounds and response schema of each endpoint.
If you need a stable snapshot across many pages, keep your filters constant and paginate in one continuous session.