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.

Pagination parameters

All collection endpoints support the following query parameters:

ParameterTypeDefaultDescription
limitinteger25Number of items to return (1-100)
startingAfterstring-Cursor for forward pagination
endingBeforestring-Cursor for backward pagination
orderBystringcreatedAtField to sort by
sortstringdescSort direction (asc or desc)

How it works

  1. Initial Request: Make a request without pagination parameters to get the first page
  2. Get Next Page: Use the id of the last item as startingAfter
  3. Get Previous Page: Use the id of the first item as endingBefore
  4. Check for More: If the returned items equal the limit, more pages may exist

Basic pagination example

# 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...
  }
}

4. Optimize for performance

  • 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;
  }
}

Infinite scroll implementation

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:

  1. Check if filters are too restrictive
  2. Verify the cursor ID exists and belongs to the same resource type
  3. Ensure you’re not mixing startingAfter and endingBefore

Performance issues

For better performance:

  1. Use larger page sizes (up to 100)
  2. Limit the fields returned with select[]
  3. Use count endpoint separately instead of fetching all data
  4. Consider caching results for static data