Skip to main content

Pagination & filtering

Lists are returned by POST /{resource}/search, never by a bare GET /{resource}. Search keeps the query in the request body (so it can be rich and structured) and returns results in one uniform cursor envelope.

The envelope

Every list response has the same shape:

{
"data": [ /* the records for this page */ ],
"meta": {
"cursor": { "next": "eyJvIjoyNX0", "has_more": true },
"total": 13658
}
}
  • data — the records on this page.
  • meta.cursor.next — an opaque string. Pass it back as cursor to get the next page. null when there are no more pages.
  • meta.cursor.has_moretrue if another page exists.
  • meta.total — total matches. May be omitted for large or expensive queries, so never assume it's present; rely on has_more to drive your loop.

Cursors are opaque. Don't parse, store long-term, or construct them — they encode internal paging state and may change shape between versions.

Paging loop

Keep calling until has_more is false, threading next into cursor:

import os, requests

URL = "https://papi.trendev.in/v1/contacts/search"
HEADERS = {
"Authorization": f"Bearer {os.environ['PROSPECTCONNECT_TOKEN']}",
"Version": "2026-06-01",
}

cursor, all_contacts = None, []
while True:
body = {"limit": 100}
if cursor:
body["cursor"] = cursor
page = requests.post(URL, headers=HEADERS, json=body, timeout=30).json()
all_contacts.extend(page["data"])
if not page["meta"]["cursor"]["has_more"]:
break
cursor = page["meta"]["cursor"]["next"]

print(f"fetched {len(all_contacts)} contacts")

limit ranges 1–100 (default 25). Larger pages mean fewer round-trips.

Sorting

Control ordering with order_by + sort_direction:

{ "order_by": "lead_score", "sort_direction": "desc", "limit": 50 }
  • sort_direction is asc or desc (default desc).
  • Common order_by values: created_at, updated_at, name, lead_score, last_contacted_at. Each resource's search operation in the API Reference lists what it supports.

Filtering

filters is an array of structured conditions. Each condition is { field, operator, value } (or values for multi-value operators):

{
"search_text": "acme",
"filters": [
{ "field": "lifecycle_stage_id", "operator": "eq", "value": "stage_lead" },
{ "field": "owner_id", "operator": "in", "values": ["usr_1", "usr_2"] },
{ "field": "lead_score", "operator": "gte", "value": 50 }
],
"order_by": "lead_score",
"sort_direction": "desc",
"limit": 50
}

Multiple filters are combined with AND. search_text is a separate full-text match across common fields (name, email, phone, …) and can be used alone or alongside filters.

Operators

OperatorMeaningUses
eq / neqequals / not equalsvalue
in / ninin / not in a setvalues
containssubstring / membershipvalue
starts_withprefix matchvalue
gt / gte / lt / ltenumeric / date comparisonsvalue
betweeninclusive rangevalues: [low, high]
is_set / is_not_setfield present / absent

Projection

Pass fields to return only the attributes you need — smaller payloads, faster responses:

{ "fields": ["id", "first_name", "last_name", "email"], "limit": 100 }

Validation

Bad paging input is rejected before it reaches the data layer: limit out of range, an unknown operator, or a malformed cursor returns a 422 validation error describing exactly which field was wrong.