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

# Pagination and query parameters

> Understanding pagination, filtering, ordering, and query parameters in the Perch Core API

## Pagination

By default, all index (i.e., GET /resources) calls to our API will be paginated. To paginate the API, you can use the `limit` and `offset` query parameters.

### Basic pagination

```http theme={null}
GET /firms/abc123/leads?limit=20&offset=40
```

This would return page 3 with 20 records per page (records 41-60).

### Default behavior

* **Default limit**: 20 records
* **Maximum limit**: 100 records
* **Default offset**: 0
* **Default order**: `['createdAt', 'DESC']` (newest first)

### Pagination response

Paginated responses include metadata to help with navigation:

```json theme={null}
{
  "data": [
    // ... array of resources
  ],
  "meta": {
    "total": 150,
    "count": 20,
    "offset": 40,
    "limit": 20
  }
}
```

## Query parameters

### Ordering

You can customize the sort order using the `order` parameter:

```http theme={null}
GET /firms/abc123/leads?order=[["email", "ASC"]]
GET /firms/abc123/leads?order=[["createdAt", "DESC"], ["email", "ASC"]]
```

**Supported directions:**

* `ASC` - Ascending order
* `DESC` - Descending order

### Filtering

Use the `filter` parameter to restrict results:

```http theme={null}
GET /firms/abc123/leads?filter={"email": "john.doe@example.com"}
GET /firms/abc123/leads?filter={"firstName": "John", "lastName": "Doe"}
```

<Note>
  Depending on your HTTP client, you may need to URL encode the JSON filter object.
</Note>

### Scopes

The Perch API uses scopes to simplify common or complicated queries:

```http theme={null}
GET /leads/abc123?scope=withDeleted
GET /firms/abc123/leads?scope=withDeleted&include=Plans
```

**Common scopes:**

* `withDeleted` - Include soft-deleted records
* `active` - Only active (non-deleted) records

<Warning>
  Not all scopes are available for each resource. Check the specific endpoint documentation for supported scopes.
</Warning>

### Including related data

Use the `include` parameter to join related resources:

```http theme={null}
GET /firms/abc123/leads?include=Plans
GET /leads/abc123?include=Plans.ClientProfiles
```

This reduces the number of API calls needed to get complete data.

## Soft deletion

Many resources in our system support soft-deletion, which we call "paranoid" resources. This means that records will not be removed from the database, but instead get a timestamp value set to the record's **`deletedAt`** column.

### Working with soft-deleted records

By default, deleted records are not returned:

```http theme={null}
GET /firms/abc123/leads
// Returns only active leads
```

To include deleted records, use the `withDeleted` scope:

```http theme={null}
GET /firms/abc123/leads?scope=withDeleted
// Returns both active and deleted leads
```

### Lead conversion and soft deletion

When a lead converts to a client profile:

1. The `convertedAt` timestamp is set
2. The `deletedAt` timestamp is set (automatically archived)
3. Any associated plans are created and linked

To track converted leads, always use the `withDeleted` scope:

```http theme={null}
GET /leads/abc123?scope=withDeleted
```

## Best practices

1. **Use appropriate page sizes** - Start with the default (20) and adjust based on your needs
2. **Implement client-side caching** - Cache results to reduce API calls
3. **Use filters efficiently** - Filter on the server side rather than fetching all records
4. **Include related data when needed** - Use `include` to reduce round trips
5. **Handle large datasets** - Use pagination rather than trying to fetch all records at once
6. **Combine parameters effectively** - Use filtering, ordering, and pagination together

## Advanced pagination patterns

### Cursor-based pagination alternative

While Perch uses offset-based pagination, you can simulate cursor-based pagination using ordering and filtering:

```javascript theme={null}
// Get first page
let lastCreatedAt = null;
const firstPage = await fetch('/leads?order=[["createdAt","DESC"]]&limit=20');

// Get next page using the last item's timestamp
if (firstPage.data.length > 0) {
  lastCreatedAt = firstPage.data[firstPage.data.length - 1].createdAt;
  const nextPage = await fetch(
    `/leads?order=[["createdAt","DESC"]]&limit=20&filter={"createdAt":{"$lt":"${lastCreatedAt}"}}`
  );
}
```

### Efficient lead fetching example

```javascript theme={null}
// Fetch recent converted leads with their plans
const response = await fetch(
  `/firms/${firmId}/leads?` + new URLSearchParams({
    scope: 'withDeleted',
    include: 'Plans',
    filter: JSON.stringify({ convertedAt: { $ne: null } }),
    order: JSON.stringify([['convertedAt', 'DESC']]),
    limit: 50
  }), {
    headers: { 'X-API-Key': apiKey }
  }
);

const { data: leads, meta } = await response.json();
console.log(`Found ${meta.total} converted leads`);
```

### Working with large datasets

```javascript theme={null}
async function fetchAllLeads(firmId) {
  let allLeads = [];
  let offset = 0;
  const limit = 100; // Use maximum page size for efficiency
  
  while (true) {
    const response = await fetch(
      `/firms/${firmId}/leads?limit=${limit}&offset=${offset}&scope=withDeleted`
    );
    const { data: leads, meta } = await response.json();
    
    allLeads = allLeads.concat(leads);
    
    // Check if we've fetched all records
    if (leads.length < limit || offset + limit >= meta.total) {
      break;
    }
    
    offset += limit;
    
    // Add delay to respect rate limits
    await new Promise(resolve => setTimeout(resolve, 100));
  }
  
  return allLeads;
}
```
