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

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:
{
  "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:
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:
GET /firms/abc123/leads?filter={"email": "john.doe@example.com"}
GET /firms/abc123/leads?filter={"firstName": "John", "lastName": "Doe"}
Depending on your HTTP client, you may need to URL encode the JSON filter object.

Scopes

The Perch API uses scopes to simplify common or complicated queries:
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
Not all scopes are available for each resource. Check the specific endpoint documentation for supported scopes.
Use the include parameter to join related resources:
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:
GET /firms/abc123/leads
// Returns only active leads
To include deleted records, use the withDeleted scope:
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:
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:
// 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

// 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

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