Pre-Black Friday: 20% off, for as long as you stayEARLYBFon Solo and AgencyEnds Nov 28Start free

How-to guide

How to Page Through a People Search (Node.js)

A 40-line script that sends one search, follows meta.nextCursor, backs off on 429 and writes every row to a file.

Get your free API key →Free to start. No credit card. 1,000 records to spend whenever you like.

You page a LeadOcean people search by sending meta.nextCursor back as cursor until none comes back. The script below does that with built-in fetch and writes each person to a JSONL file.

TL;DR

  • One script sends POST /v1/people/search, follows meta.nextCursor and appends each row to a JSONL file.
  • It takes about 10 minutes to set up and spends 200 LeadOcean records at the sample cap. Each person returned is one record.
  • A 429 is retried after the seconds in Retry-After. Nothing else is retried.

Prerequisites

  • Node.js 18 or newer (built-in fetch, no packages).
  • A LeadOcean API key in $LEADOCEAN_API_KEY. The free plan gives you 1,000 records for life and no card.
  • Your rate limit: 5 requests a second on Free, 50 on Solo, 200 on Agency. See /pricing.

Step 1: Size the search for free

Count before you page. A search with count=true as a query parameter and limit=1 costs nothing and returns meta.total (capped at 100,000) with empty data.

bash
curl -s -X POST "https://api.leadocean.io/v1/people/search?count=true" \
  -H "x-api-key: $LEADOCEAN_API_KEY" \
  -H "content-type: application/json" \
  -d '{"title":["Head of Sales"],"country":["US"],"limit":1}'

Read meta.total and meta.totalIsExact. If totalIsExact is false, the total is a floor.

Step 2: Write the script

The body is flat JSON. Filters are arrays, limit runs 1 to 100, and cursor carries your place. Save this as page.mjs.

js
import { appendFileSync, writeFileSync } from "node:fs";

const KEY = process.env.LEADOCEAN_API_KEY;
const MAX_ROWS = 200; // your record budget
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

async function search(body, tries = 5) {
  for (let i = 0; i < tries; i++) {
    const res = await fetch("https://api.leadocean.io/v1/people/search", {
      method: "POST",
      headers: { "x-api-key": KEY, "content-type": "application/json" },
      body: JSON.stringify(body),
    });
    if (res.status === 429) {
      const wait = Number(res.headers.get("retry-after")) || 1;
      await sleep(wait * 1000);
      continue;
    }
    if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
    return res.json();
  }
  throw new Error("still rate limited after retries");
}

const query = { title: ["Head of Sales"], country: ["US"], limit: 100 };
writeFileSync("people.jsonl", "");
let rows = 0;
let cursor;

while (rows < MAX_ROWS) {
  const page = await search(cursor ? { ...query, cursor } : query);
  for (const person of page.data) {
    if (rows >= MAX_ROWS) break;
    appendFileSync("people.jsonl", JSON.stringify(person) + "\n");
    rows++;
  }
  cursor = page.meta.nextCursor;
  if (!cursor) break; // last page, or meta.depthCapped is true
}
console.log(`wrote ${rows} rows`);

Run it with node page.mjs.

Step 3: Understand how paging stops

The loop ends in one of three ways. Your own MAX_ROWS is hit, nextCursor comes back null because the results ran out, or the page has meta.depthCapped: true.

One search pages through at most 10,000 rows by default. Past that no cursor is issued. Narrow the filters and run several smaller searches instead of paging harder.

Pass nextCursor back exactly as received. Cursors are signed, and an edited one returns 400 "Invalid cursor". Do not store a cursor between runs. Restart the query instead.

Note that MAX_ROWS = 200 with limit: 100 spends exactly 200 records. Set limit to the rows you still need on the last page if your budget is not a multiple of 100.

Step 4: Check the retry path

The retry waits for Retry-After, which is in seconds. A 429 costs no records. Above your plan's rate (5 a second on Free) it is a short pause. On a paid plan that reached its fair-use window, the API paces you to 1 request a minute, and Retry-After says so.

What you get

Response shape from the API reference (placeholder values). data is a list of people with no contact points revealed. meta carries the page size and the cursor.

json
{
  "success": true,
  "data": [
    {
      "profile_data": { "...": "name, title, company, location" },
      "contact_data": {
        "has_email": true,
        "has_phone": false,
        "email_status": "verified"
      }
    }
  ],
  "meta": {
    "count": 100,
    "limit": 100,
    "nextCursor": "<signed cursor string>"
  }
}

Each line in people.jsonl is one data item. has_email and has_phone tell you whether a contact point exists. To reveal one, enrich the person with POST /v1/people/enrich.

Troubleshooting

ErrorCauseFix
401Missing or wrong x-api-keyCheck $LEADOCEAN_API_KEY is exported in the shell that runs Node.
429Above your plan's rate: 5 a second on Free, 200 on AgencyWait the Retry-After seconds, as the script does. Lower your request pace.
402Records are spentOn Free the 1,000 are a one-off allowance and do not reset. A paid account is paced to 1 request a minute instead.
400 "Invalid cursor"Cursor edited, truncated, or from a different searchPass meta.nextCursor back unchanged. Restart the query if you changed a filter.
Empty data or a 400Filter value is not an exact enum stringCheck values with GET /v1/enums. Enum values are case-exact.
Walk stops near 10,000 rowsmeta.depthCapped is trueNarrow the filters and split the job into several searches.

FAQ

How many rows can one search page through?

One search pages through 10,000 rows by default. The page where it stops has meta.depthCapped: true and no nextCursor. Support can raise the figure for a genuine large export.

Does paging cost records?

Yes. Each person returned counts one record, so a page of 100 is 100 records. A count, an empty page and a 429 cost nothing.

Should I use bulk export instead?

For large pulls, yes. POST /v1/exports takes up to 50,000 rows a request and 500,000 a day. Bulk CSV export is not on Free.

Can I run this from TypeScript, Go or C#?

Yes. The same loop works in TypeScript, Go and C#. For the concept behind it, read cursor pagination explained.

Run your first paged search on the free plan

Free to start. No credit card. 1,000 records to spend whenever you like.

Get your free API key →