You page through a LeadOcean people search by sending the same request again with the cursor from meta.nextCursor until it stops coming back. This guide gives you one C# file that does that, retries on 429 and writes each person to a JSONL file.
TL;DR
- Time: about 10 minutes, mostly spent picking your filters.
- Cost: 300 records with the code as written (3 pages of 100). Each person returned counts one record. The Free plan has 1,000 records for life.
- The loop reads
meta.nextCursor, waits out a 429 usingRetry-After, and stops at your page cap.
Prerequisites
- .NET 8 or later. The file uses top-level statements and no NuGet packages.
- A LeadOcean API key in the environment variable
LEADOCEAN_API_KEY. Free keys can search and enrich. Get one free, no card. - A terminal. Create the project with
dotnet new console -n PeoplePager.
Step 1: Set the key and the client
Read the key from the environment, never from source. Every request goes to https://api.leadocean.io with the header x-api-key.
using System.Net;
using System.Text;
using System.Text.Json.Nodes;
var key = Environment.GetEnvironmentVariable("LEADOCEAN_API_KEY")
?? throw new InvalidOperationException("Set LEADOCEAN_API_KEY");
using var http = new HttpClient { BaseAddress = new Uri("https://api.leadocean.io") };
http.DefaultRequestHeaders.Add("x-api-key", key);One HttpClient for the whole run is the right shape. Do not create one per page.
Step 2: Send a search with a retry on 429
The search is POST /v1/people/search with a flat JSON body. Filters are arrays of strings. A 429 means you are above your plan's rate limit: 5 requests a second on Free, 50 on Solo, 200 on Agency. The response carries Retry-After, so wait that long and send again.
async Task<JsonNode> Search(JsonObject body)
{
for (var attempt = 1; ; attempt++)
{
using var content = new StringContent(body.ToJsonString(), Encoding.UTF8, "application/json");
using var res = await http.PostAsync("/v1/people/search", content);
if (res.StatusCode == (HttpStatusCode)429 && attempt <= 5)
{
var wait = res.Headers.RetryAfter?.Delta ?? TimeSpan.FromSeconds(attempt);
await Task.Delay(wait);
continue;
}
res.EnsureSuccessStatusCode();
return JsonNode.Parse(await res.Content.ReadAsStringAsync())!;
}
}The StringContent is built inside the loop because a request body cannot be reused after it has been sent. A 429 costs no records.
Step 3: Walk the cursor and write the file
Each page returns up to limit people (1 to 100, default 25). Pass meta.nextCursor back as cursor. When it is missing, there is no next page.
string? cursor = null;
var written = 0;
await using var file = new StreamWriter("people.jsonl");
for (var page = 1; page <= 3; page++)
{
var body = new JsonObject
{
["title"] = new JsonArray("VP Sales"),
["country"] = new JsonArray("US"),
["limit"] = 100,
};
if (cursor != null) body["cursor"] = cursor;
var result = await Search(body);
foreach (var person in result["data"]!.AsArray())
{
await file.WriteLineAsync(person!.ToJsonString());
written++;
}
cursor = result["meta"]?["nextCursor"]?.GetValue<string>();
if (cursor == null) break;
}
Console.WriteLine($"Wrote {written} people");Steps 1 to 3 together are one Program.cs, 47 lines of code. Run it with dotnet run. Keep the same filters on every page: the cursor belongs to the query that issued it.
Raise the 3 to walk further. One search pages through 10,000 rows by default. At that point meta.depthCapped is true and no cursor comes back. Narrow the filters and run a second search instead of paging harder.
What you get
With three full pages of matches, the program prints one line when it finishes:
Wrote 300 peoplepeople.jsonl holds one person per line. This is the response shape from the API reference, with placeholder values. The search returns a thin projection with no contact points.
{
"success": true,
"data": [
{ "profile_data": { "...": "Jane Doe, VP Sales, Acme" } }
],
"meta": {
"count": 100,
"limit": 100,
"nextCursor": "<opaque cursor string>"
}
}The last page has no nextCursor. A capped walk has meta.depthCapped: true instead. To size a search before you spend records, send count=true as a query parameter with limit 1. That call is free and meta.total is capped at 100,000.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| 401 | Missing or wrong x-api-key | Check LEADOCEAN_API_KEY is set in the shell that runs dotnet run. Create a key in the app if needed. |
| 429 | Above the plan's rate limit: 5 a second on Free, 200 on Agency | The code waits for Retry-After and retries 5 times. Add a short delay between pages to stay under the limit. |
| 402 | Out of records. The Free plan's 1,000 are spent | Move to Solo ($97 a month) or Agency ($497 a month). See pricing. |
Empty data on page 1 | A filter value is not an exact match, such as the wrong case on an enum | Read the allowed values with leadocean_list_enum_values or GET /v1/enums. |
No nextCursor before you expected | The search ended, or meta.depthCapped is true at 10,000 rows | Narrow the filters and run several smaller searches. |
| 400 on the request body | An unknown field in the JSON body | The body rejects unknown fields. Send only documented filters, and send count=true in the query string. |
FAQ
Should I use GET or POST for the search?
Use POST with a JSON body. Both work on /v1/people/search, but filters in a body are easier to build as arrays in C# than as a long query string.
Does paging cost more than one big request?
No. The cost is per person returned, so 3 pages of 100 cost the same 300 records as one request of 300 would. The limit ceiling is 100, so you page.
How do I stop early without wasting records?
Lower the page cap in the for loop, or lower limit. A page of 5 costs 5 records. Test with a small limit first, then raise it.
Can I get emails and phones in the same call?
No. Search returns a thin projection. Take a person_id from the result and call POST /v1/people/enrich, or use the work email and phone endpoints, which spend records of their own.
Is there a version for other languages?
Yes. The same loop is on the Go, TypeScript and Rust pages. The hub for this series is the blog.
Page through your first 1,000 people free
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →