The FirstQuadrant API uses cursor-based pagination to efficiently navigate through large collections of resources. This approach provides consistent results even when data is being modified.
All collection endpoints support the following query parameters:
Parameter Type Default Description limit
integer 25 Number of items to return (1-100) startingAfter
string - Cursor for forward pagination endingBefore
string - Cursor for backward pagination orderBy
string createdAt
Field to sort by sort
string desc
Sort direction (asc
or desc
)
How it works
Initial Request : Make a request without pagination parameters to get the first page
Get Next Page : Use the id
of the last item as startingAfter
Get Previous Page : Use the id
of the first item as endingBefore
Check for More : If the returned items equal the limit, more pages may exist
cURL
JavaScript
Python
TypeScript
# Get first page of contacts
curl "https://api.us.firstquadrant.ai/v5/contacts?limit=10" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "FirstQuadrant-Organization-ID: org_YOUR_ORG_ID"
# Get next page using the last contact's ID
curl "https://api.us.firstquadrant.ai/v5/contacts?limit=10&startingAfter=con_abc123" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "FirstQuadrant-Organization-ID: org_YOUR_ORG_ID"
Sorting results
You can sort results by any field that’s included in the response:
# Sort by email address in ascending order
curl "https://api.us.firstquadrant.ai/v5/contacts?orderBy=email&sort=asc" \
-H "Authorization: Bearer YOUR_API_KEY"
# Sort by last activity date (most recent first)
curl "https://api.us.firstquadrant.ai/v5/contacts?orderBy=lastActivityAt&sort=desc" \
-H "Authorization: Bearer YOUR_API_KEY"
# Sort by custom property
curl "https://api.us.firstquadrant.ai/v5/contacts?orderBy=customProperties.score&sort=desc" \
-H "Authorization: Bearer YOUR_API_KEY"
Getting total count
To get the total number of items without fetching all data, use the count endpoint:
# Get total number of contacts
curl "https://api.us.firstquadrant.ai/v5/contacts/count" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "FirstQuadrant-Organization-ID: org_YOUR_ORG_ID"
# Response
{
"count" : 1234
}
You can also apply filters to the count endpoint:
# Count contacts with a specific tag
curl "https://api.us.firstquadrant.ai/v5/contacts/count?tags=customer" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "FirstQuadrant-Organization-ID: org_YOUR_ORG_ID"
Combining with filters
Pagination works seamlessly with filtering and search:
# Paginate through filtered results
curl "https://api.us.firstquadrant.ai/v5/contacts?query=john&limit=20&startingAfter=con_abc123" \
-H "Authorization: Bearer YOUR_API_KEY"
# Paginate with advanced filters
curl "https://api.us.firstquadrant.ai/v5/[email protected] &limit=50" \
-H "Authorization: Bearer YOUR_API_KEY"
Best practices
1. Choose appropriate page sizes
Use smaller limits (10-25) for real-time UI updates
Use larger limits (50-100) for batch processing
Maximum limit is 100 items per request
2. Handle edge cases
// Handle empty results
if ( page . length === 0 ) {
console . log ( 'No more results' );
break ;
}
// Handle deleted items
try {
const page = await fetchPage ( cursor );
} catch ( error ) {
if ( error . code === 'not_found' ) {
// Item used as cursor was deleted, start over
cursor = null ;
continue ;
}
}
3. Implement progress tracking
async function exportContactsWithProgress () {
// First, get the total count
const { count } = await fetchCount ();
let processed = 0 ;
for await ( const page of paginateContacts ()) {
processed += page . length ;
console . log ( `Progress: ${ processed } / ${ count } ( ${ Math . round (( processed / count ) * 100 ) } %)` );
// Process page...
}
}
Use field selection to reduce payload size
Process pages in parallel when order doesn’t matter
Cache results when appropriate
// Fetch only required fields
const params = new URLSearchParams ({
limit: "100" ,
"select[]" : [ "id" , "email" , "firstName" , "lastName" ],
});
// Process multiple pages in parallel
const pagePromises = [];
for ( let i = 0 ; i < 5 ; i ++ ) {
pagePromises . push ( fetchPage ( cursors [ i ]));
}
const pages = await Promise . all ( pagePromises );
Common patterns
Bidirectional navigation
class PaginationState {
constructor () {
this . currentPage = [];
this . prevCursor = null ;
this . nextCursor = null ;
}
async loadNext () {
const params = {
limit: 25 ,
... ( this . nextCursor && { startingAfter: this . nextCursor }),
};
const page = await fetchContacts ( params );
if ( page . length > 0 ) {
this . prevCursor = page [ 0 ]. id ;
this . nextCursor = page [ page . length - 1 ]. id ;
this . currentPage = page ;
}
return page ;
}
async loadPrev () {
const params = {
limit: 25 ,
endingBefore: this . prevCursor ,
};
const page = await fetchContacts ( params );
if ( page . length > 0 ) {
this . prevCursor = page [ 0 ]. id ;
this . nextCursor = page [ page . length - 1 ]. id ;
this . currentPage = page ;
}
return page ;
}
}
class InfiniteScroll {
constructor ( container , fetchFn ) {
this . container = container ;
this . fetchFn = fetchFn ;
this . cursor = null ;
this . loading = false ;
this . hasMore = true ;
this . observeLastItem ();
}
async loadMore () {
if ( this . loading || ! this . hasMore ) return ;
this . loading = true ;
const items = await this . fetchFn ( this . cursor );
if ( items . length > 0 ) {
this . cursor = items [ items . length - 1 ]. id ;
this . renderItems ( items );
this . hasMore = items . length === 50 ;
} else {
this . hasMore = false ;
}
this . loading = false ;
}
observeLastItem () {
const observer = new IntersectionObserver (( entries ) => {
if ( entries [ 0 ]. isIntersecting ) {
this . loadMore ();
}
});
// Observe the last item in the container
const lastItem = this . container . lastElementChild ;
if ( lastItem ) observer . observe ( lastItem );
}
}
Troubleshooting
No results returned
If you’re not getting expected results:
Check if filters are too restrictive
Verify the cursor ID exists and belongs to the same resource type
Ensure you’re not mixing startingAfter
and endingBefore
For better performance:
Use larger page sizes (up to 100)
Limit the fields returned with select[]
Use count endpoint separately instead of fetching all data
Consider caching results for static data