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_more — true 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.prospectconnect.ai/v1/contacts/search"
HEADERS = {
"Authorization": f"Bearer {os.environ['PROSPECTCONNECT_TOKEN']}",
"Version": "v1",
}

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.