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 (in C#)

One file, 47 lines, .NET 8 or later. Auth, cursor paging, retries on 429 and a JSONL file at the end.

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

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 using Retry-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.

csharp
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.

csharp
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.

csharp
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:

text
Wrote 300 people

people.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.

json
{
  "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

ErrorCauseFix
401Missing or wrong x-api-keyCheck LEADOCEAN_API_KEY is set in the shell that runs dotnet run. Create a key in the app if needed.
429Above the plan's rate limit: 5 a second on Free, 200 on AgencyThe code waits for Retry-After and retries 5 times. Add a short delay between pages to stay under the limit.
402Out of records. The Free plan's 1,000 are spentMove to Solo ($97 a month) or Agency ($497 a month). See pricing.
Empty data on page 1A filter value is not an exact match, such as the wrong case on an enumRead the allowed values with leadocean_list_enum_values or GET /v1/enums.
No nextCursor before you expectedThe search ended, or meta.depthCapped is true at 10,000 rowsNarrow the filters and run several smaller searches.
400 on the request bodyAn unknown field in the JSON bodyThe 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 →