Pagination
This API uses two pagination models, and which one applies depends on the endpoint. Check this table before writing a paging loop — code written for one will not work against the other.
| Endpoint | Model | Controls |
|---|---|---|
POST /companies/search | Cursor | cursor, limit — default 20, max 100 |
GET /companies/{id}/job-postings | Offset | page (default 1), page_size — default 10, max 50 |
A company's open roles are a small, stable set, so a row cannot shift between pages the way it can in a 13-million-row search. Offset paging is safe there and simpler to consume. Everywhere else, cursors are what keep a page from skipping or repeating rows while the underlying data moves.
Cursor pagination (search)#
POST /companies/search returns results in pages using cursor-based pagination. Instead of asking for a page number, each response hands you an opaque, signed cursor that points at the next page. Treat it as a token: pass it back exactly as received. Cursors are tamper-checked, so an edited or hand-built one is rejected with 422 INVALID_CURSOR rather than quietly returning a different page.
Page size#
limit controls how many companies a search returns per page: 20 by default, 100 at most. Asking for more than 100 is a 422 naming the field rather than a silently truncated page, so a client cannot believe it received 500 rows and receive 100.
Job postings use page_size instead — 10 by default, 50 at most — with page starting at 1. The two endpoints do not share a parameter name or a default, so code written for one needs changing for the other.
How cursors work#
The search response includes a next_cursor field. To fetch the next page, send it back in the request body as cursor. When next_cursor is null, you've reached the end.
// Response shape
{
"items": [ /* companies */ ],
"total": 4213,
"aggregations": { /* facet counts */ },
"next_cursor": "{\"a\":[10,\"0004be64-…\"],\"s\":\"relevance:desc:f\"}"
// null on the last page
}The exact contents differ by search type and may change without notice — never parse or construct one. A lookalike search returns a cursor in a different, signed format again.
Paging through results#
Loop until next_cursor is null, passing the previous cursor each time:
async function* allCompanies(filters, token) {
let cursor = null;
do {
const res = await fetch("https://api.cornect.io/api/v1/companies/search", {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ ...filters, cursor }),
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const page = await res.json();
for (const company of page.items) yield company;
cursor = page.next_cursor;
} while (cursor);
}Best practices#
- Treat the cursor as opaque — don't parse or construct it yourself; its format may change. Cursors are signed, so a modified one is rejected with
422 INVALID_CURSOR. - Keep
sort_byandsort_orderidentical across pages of the same scan. A cursor is only valid for the ordering that produced it; changing the sort mid-scan returns422 CURSOR_SORT_MISMATCH. - Keep your filters identical too — but note this is not enforced. Changing a filter mid-scan is accepted and silently resumes from the old cursor position against the new filter set, which can skip or repeat rows. Restart from the first page instead.
- Respect the 60/min limit on search while paging — for large scans, add backoff (see Rate Limits & Backoff).
- Use
totalto show progress, but page untilnext_cursoris null rather than computing page counts from it.
capped_at_10k: true in the preview and create responses — it does not fail, and it does not tell you in the file itself. Check that flag.To cover a larger set, split it into narrower filters (by country, employee band, or industry) and run one export per slice. Use counts first to size a filter before exporting it — it is exact, uncapped, and costs no credits.