Data API
Pagination
Page through search results using limit, offset, and search_after on the Parcel Data API.
Every POST /data/{dataset}/search response includes a metadata.total count and supports two pagers: offset for simple shallow paging, and search_after (an opaque keyset cursor) for stable, deep paging past the offset ceiling.
Parameters
| Parameter | Type | Default | Constraints |
|---|---|---|---|
limit | integer | 25 | 1 to 100 |
offset | integer | 0 | 0 to 10,000 |
search_after | string | Opaque keyset cursor from the previous response’s metadata.search_after. Pages forward with no offset ceiling. |
Understanding metadata.total
The response metadata object includes:
{ "metadata": { "total": 1000, "total_is_capped": true, "limit": 25, "offset": 0, "search_after": "eyJ2IjoiMjAyNS0xMS0wNCIsImlkIjoiOGExZjNjMmUtMDAwMy00YjdkLTllNWEtMDAwMDAwMDAwMDAzIiwiZiI6Imxhc3Rfc2lnbmFsX2F0IiwibyI6ImRlc2MiLCJlIjoicHJvamVjdCJ9", "credits": 25 }}totalis a count capped at 1,000. When more than 1,000 records match,totalreports1000andtotal_is_cappedistrue(the true count is higher and unknown).search_afteris the cursor for the next page (nullon the last page). See Cursor pagination below.creditsis the number of records returned in this response (each record costs 1 credit).
total_is_capped: true does not mean your search has no results beyond what is shown. It means the total count exceeds the reporting ceiling. Page with offset for the first 10,000 results, and search_after to continue past that.
Paging through results
Increment offset by limit on each request to walk through result pages.
curl -X POST https://api.parcelengineering.com/api/v1/data/projects/search \ -H "Authorization: Bearer pcl_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "filters": { "city": { "value": "Boston" } }, "sort": { "field": "last_signal_at", "order": "desc" }, "limit": 25, "offset": 0 }'curl -X POST https://api.parcelengineering.com/api/v1/data/projects/search \ -H "Authorization: Bearer pcl_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "filters": { "city": { "value": "Boston" } }, "sort": { "field": "last_signal_at", "order": "desc" }, "limit": 25, "offset": 25 }'async function* fetchAllProjects(filters) { const base = 'https://api.parcelengineering.com/api/v1/data/projects/search'; const headers = { Authorization: 'Bearer pcl_your_api_key', 'Content-Type': 'application/json', }; const limit = 100; let offset = 0;
while (true) { const res = await fetch(base, { method: 'POST', headers, body: JSON.stringify({ filters, limit, offset }), }); const { data, metadata } = await res.json(); if (data.length === 0) break; yield data; offset += limit; if (offset >= metadata.total && !metadata.total_is_capped) break; if (data.length < limit) break; if (offset >= 10000) break; // offset ceiling — page deeper with search_after }}
for await (const page of fetchAllProjects({ city: { value: 'Boston' } })) { console.log(page.length, 'projects');}import httpx
def fetch_all_projects(filters): base = "https://api.parcelengineering.com/api/v1/data/projects/search" headers = {"Authorization": "Bearer pcl_your_api_key"} limit = 100 offset = 0
while True: resp = httpx.post( base, headers=headers, json={"filters": filters, "limit": limit, "offset": offset}, ) body = resp.json() page = body["data"] if not page: break yield page offset += limit if len(page) < limit: break if offset >= 10000: # offset ceiling — page deeper with search_after break
for page in fetch_all_projects({"city": {"value": "Boston"}}): print(len(page), "projects")Cursor pagination with search_after
offset is capped at 10,000. To page beyond that ceiling, or to page a large result set stably while the underlying data changes, use the search_after cursor instead of offset.
Every search response returns metadata.search_after. Pass that value back as search_after on the next request to fetch the following page. A null cursor means there are no more pages.
The cursor is bound to the sort it was issued with. Keep sort identical across cursor pages. offset is ignored whenever search_after is present.
curl -X POST https://api.parcelengineering.com/api/v1/data/projects/search \ -H "Authorization: Bearer pcl_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "filters": { "city": { "value": "Boston" } }, "sort": { "field": "last_signal_at", "order": "desc" }, "limit": 100 }'# Response includes: "metadata": { "search_after": "eyJ2IjoiMjAyNS0xMS0wNCIsImlkIjoiOGExZjNjMmUtMDAwMy00YjdkLTllNWEtMDAwMDAwMDAwMDAzIiwiZiI6Imxhc3Rfc2lnbmFsX2F0IiwibyI6ImRlc2MiLCJlIjoicHJvamVjdCJ9", ... }curl -X POST https://api.parcelengineering.com/api/v1/data/projects/search \ -H "Authorization: Bearer pcl_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "filters": { "city": { "value": "Boston" } }, "sort": { "field": "last_signal_at", "order": "desc" }, "limit": 100, "search_after": "eyJ2IjoiMjAyNS0xMS0wNCIsImlkIjoiOGExZjNjMmUtMDAwMy00YjdkLTllNWEtMDAwMDAwMDAwMDAzIiwiZiI6Imxhc3Rfc2lnbmFsX2F0IiwibyI6ImRlc2MiLCJlIjoicHJvamVjdCJ9" }'async function* fetchAllProjects(filters) { const base = 'https://api.parcelengineering.com/api/v1/data/projects/search'; const headers = { Authorization: 'Bearer pcl_your_api_key', 'Content-Type': 'application/json', }; const sort = { field: 'last_signal_at', order: 'desc' }; let searchAfter;
while (true) { const body = { filters, sort, limit: 100 }; if (searchAfter) body.search_after = searchAfter; const res = await fetch(base, { method: 'POST', headers, body: JSON.stringify(body) }); const { data, metadata } = await res.json(); if (data.length === 0) break; yield data; if (!metadata.search_after) break; searchAfter = metadata.search_after; }}Each record in each response costs 1 credit. Paging through large result sets consumes credits quickly. Check remaining credits with GET /data/usage before bulk fetches. The Pro plan allows 250,000 credits per rolling 24 hours.