> ## Documentation Index
> Fetch the complete documentation index at: https://docs.firstquadrant.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Pagination

> Learn how to navigate through large datasets using cursor-based pagination

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:

| 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

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

<CodeGroup>
  ```bash cURL theme={null}
  # 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"
  ```

  ```javascript JavaScript theme={null}
  async function getAllContacts() {
    const contacts = [];
    let hasMore = true;
    let lastId = null;

    while (hasMore) {
      const params = new URLSearchParams({
        limit: "50",
        ...(lastId && { startingAfter: lastId }),
      });

      const response = await fetch(`https://api.us.firstquadrant.ai/v5/contacts?${params}`, {
        headers: {
          Authorization: "Bearer YOUR_API_KEY",
          "FirstQuadrant-Organization-ID": "org_YOUR_ORG_ID",
        },
      });

      const page = await response.json();
      contacts.push(...page);

      // Check if there are more pages
      hasMore = page.length === 50;
      if (hasMore) {
        lastId = page[page.length - 1].id;
      }
    }

    return contacts;
  }
  ```

  ```python Python theme={null}
  def get_all_contacts(api_key, org_id):
      contacts = []
      has_more = True
      starting_after = None

      while has_more:
          params = {
              'limit': 50
          }
          if starting_after:
              params['startingAfter'] = starting_after

          response = requests.get(
              'https://api.us.firstquadrant.ai/v5/contacts',
              params=params,
              headers={
                  'Authorization': f'Bearer {api_key}',
                  'FirstQuadrant-Organization-ID': org_id
              }
          )

          page = response.json()
          contacts.extend(page)

          # Check if there are more pages
          has_more = len(page) == 50
          if has_more:
              starting_after = page[-1]['id']

      return contacts
  ```

  ```typescript TypeScript theme={null}
  interface PaginationParams {
    limit?: number;
    startingAfter?: string;
    endingBefore?: string;
    orderBy?: string;
    sort?: "asc" | "desc";
  }

  async function* paginateContacts(params: PaginationParams = {}): AsyncGenerator<Contact[]> {
    let hasMore = true;
    let cursor: string | undefined;

    while (hasMore) {
      const searchParams = new URLSearchParams({
        limit: String(params.limit || 50),
        ...(cursor && { startingAfter: cursor }),
        ...(params.orderBy && { orderBy: params.orderBy }),
        ...(params.sort && { sort: params.sort }),
      });

      const response = await fetch(`https://api.us.firstquadrant.ai/v5/contacts?${searchParams}`, {
        headers: {
          Authorization: "Bearer YOUR_API_KEY",
          "FirstQuadrant-Organization-ID": "org_YOUR_ORG_ID",
        },
      });

      const page: Contact[] = await response.json();
      yield page;

      hasMore = page.length === (params.limit || 50);
      if (hasMore && page.length > 0) {
        cursor = page[page.length - 1].id;
      }
    }
  }

  // Usage
  for await (const page of paginateContacts({ limit: 25 })) {
    console.log(`Processing ${page.length} contacts`);
    // Process each page
  }
  ```
</CodeGroup>

## Sorting results

You can sort results by any field that's included in the response:

```bash theme={null}
# 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:

```bash theme={null}
# 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:

```bash theme={null}
# 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:

```bash theme={null}
# 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/contacts?filter.email.contains=@company.com&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

```javascript theme={null}
// 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

```javascript theme={null}
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

```javascript theme={null}
// 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

```javascript theme={null}
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

```javascript theme={null}
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
