You page through a LeadOcean people search by sending meta.nextCursor back as cursor until it comes back null. The Java below does that with the built-in HttpClient, retries on 429 and writes one JSON line per page.
TL;DR
- Java 17 and
java.net.http.HttpClient. No Maven dependencies. - About 15 minutes to set up. A run is limited by the plan's rate limit, not by the code.
- It spends one LeadOcean record per person returned. The Free plan has 1,000 records for life, so cap the test at 3 pages.
Prerequisites
- JDK 17 or later (
java -version). - A LeadOcean API key from app.leadocean.io. Free needs no card.
- The key in your shell as
$LEADOCEAN_API_KEY.
export LEADOCEAN_API_KEY="your-key-here"Step 1: Size the search for free
Count first. A search with count=true as a query parameter and limit 1 costs no records and returns meta.total, capped at 100,000.
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":["VP Sales"],"country":["US"],"limit":1}'Enum filters such as jobLevel take exact strings. List them with GET /v1/enums before you guess. One search pages to 10,000 rows, then stops, so narrow the filters if the total is bigger.
Step 2: Write Pager.java
The request body is flat JSON. Filters are arrays, limit runs from 1 to 100, and cursor carries the position. Save this as Pager.java. The key comes from the environment, never from source.
import java.net.URI;
import java.net.http.*;
import java.nio.file.*;
import java.util.*;
import java.util.regex.*;
public class Pager {
static final HttpClient HTTP = HttpClient.newHttpClient();
static final String KEY = System.getenv("LEADOCEAN_API_KEY");
static final Pattern NEXT = Pattern.compile("\"nextCursor\"\\s*:\\s*\"([^\"]+)\"");
static String page(String cursor) throws Exception {
String body = "{\"title\":[\"VP Sales\"],\"country\":[\"US\"],\"limit\":25"
+ (cursor == null ? "" : ",\"cursor\":\"" + cursor + "\"") + "}";
HttpRequest req = HttpRequest.newBuilder(URI.create("https://api.leadocean.io/v1/people/search"))
.header("x-api-key", KEY).header("content-type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body)).build();
for (int attempt = 0; attempt < 5; attempt++) {
HttpResponse<String> res = HTTP.send(req, HttpResponse.BodyHandlers.ofString());
if (res.statusCode() == 200) return res.body().replaceAll("\\s*\\n\\s*", " ").strip();
if (res.statusCode() == 429) { // above the plan's rate limit
long wait = Long.parseLong(res.headers().firstValue("Retry-After").orElse("1"));
Thread.sleep(wait * 1000);
continue;
}
throw new IllegalStateException(res.statusCode() + " " + res.body());
}
throw new IllegalStateException("still rate limited");
}
public static void main(String[] args) throws Exception {
int maxPages = args.length > 0 ? Integer.parseInt(args[0]) : 3; // test small first
List<String> out = new ArrayList<>();
String cursor = null;
for (int n = 0; n < maxPages; n++) {
String json = page(cursor);
out.add(json);
Matcher m = NEXT.matcher(json);
if (!m.find()) break; // null cursor: the walk is over
cursor = m.group(1);
Thread.sleep(200); // 5 a second, the Free limit
}
Files.write(Path.of("people.jsonl"), out);
System.out.printf("%d pages written%n", out.size());
}
}Run it with the single-file launcher. The argument is the page cap:
java Pager.java 3Step 3: Handle rate limits and cursors
The Free plan allows 5 requests a second per account. Solo allows 50 and Agency allows 200. Above that the API answers 429 with a Retry-After header. The code sleeps for that many seconds and tries again, up to 5 times.
The fixed Thread.sleep(200) keeps you under the Free limit. On Agency, drop it to 5 milliseconds. Do not run pages in parallel: each cursor comes from the previous response.
Send meta.nextCursor back exactly as received. Cursors are signed, and an edited one is refused with 400. Do not store a cursor for later. Restart the query instead.
The walk ends when nextCursor is null. On a search that hits the 10,000 row depth cap, the last page carries meta.depthCapped: true and no cursor. The answer is narrower filters and several smaller searches.
Step 4: Write results where you can use them
The output is JSON Lines: one full page per line. Read it with any JSON library, or with jq:
jq -c '.data[]' people.jsonl > people-flat.jsonl
jq -r '.meta | [.count, .nextCursor != null] | @csv' people.jsonlSearch returns a thin projection with no email or phone. To get contact data, enrich the person_id values you want with POST /v1/people/enrich. For a whole list as a file, use POST /v1/exports instead of paging. It takes up to 50,000 rows a request and 500,000 a day. Bulk CSV export is not on Free.
What you get
This is the response shape from the API reference, with placeholder values.
{
"success": true,
"data": [
{ "profile_data": { "...": "name, headline, location, current role" } }
],
"meta": {
"count": 25,
"limit": 25,
"nextCursor": "opaque-signed-string"
}
}Source: response shape from the API reference, api.leadocean.io/openapi.json, September 2026. The last page has "nextCursor": null. The console line from the program reads 3 pages written, or fewer if the search ends sooner.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| 401 | Missing or wrong x-api-key. System.getenv returned null. | Run echo $LEADOCEAN_API_KEY in the same shell. Copy the key again from the app. |
| 429 | You went above the plan's rate limit: 5 a second on Free, 200 on Agency. | Keep the sleep. The code already honors Retry-After. |
| 402 | Free plan only: the 1,000 records are spent and do not reset. Paid plans slow down instead. | Move to Solo at $97 a month or Agency at $497 a month. See pricing. |
| 400 "Invalid cursor" | The cursor was edited, truncated or reused with a different search. | Pass meta.nextCursor back unchanged, with the same filters. Otherwise start over. |
Empty data or a 400 on page 1 | A filter value is not an exact enum string. | Check the allowed values with GET /v1/enums. |
| No cursor before you expected | The search reached the 10,000 row depth cap (meta.depthCapped). | Narrow the filters and run smaller searches. |
FAQ
Does Java need an SDK for this?
No. java.net.http.HttpClient ships with the JDK since version 11. The API is plain JSON over HTTPS with one header.
How many records does a full walk spend?
One per person returned, so a 10,000 row walk spends 10,000. Counts, empty pages and 404s cost nothing. Size the search first and test with 3 pages.
Can I jump to page 50?
No. A cursor is a position in one walk, so you move forward one page at a time. To split the work, narrow the filters into several searches.
Should I use search or an export?
Search suits sampling, small lists and feeding enrichment. For a large list as a file, use POST /v1/exports. See the same recipe in Go, C# and TypeScript.
Page through your first people search with a free API key
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →