You can enrich a list of LinkedIn URLs in Java with one HTTP call per URL to POST /v1/people/enrich. The code below reads a text file, retries on 429, skips misses and writes one JSON line per person.
TL;DR
- Java 17 and the built-in
java.net.http.HttpClient. No Maven dependencies. - About 15 minutes to set up, plus at least 0.2 seconds per URL at a steady pace.
- It spends one LeadOcean record per URL we hold. A URL we hold nothing for returns 404 and costs nothing. The Free plan has 1,000 records for life.
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. - A file named
urls.txtwith one LinkedIn profile URL per line.
export LEADOCEAN_API_KEY="your-key-here"
printf '%s\n' \
"https://www.linkedin.com/in/jane-doe-example" \
"https://www.linkedin.com/in/john-roe-example" > urls.txtStep 1: Understand the call
Enrich takes exactly one identifier: linkedin_url, email, phone or person_id. Send it in a JSON body to https://api.leadocean.io/v1/people/enrich with the header x-api-key.
Set reveal_email to true to get the email and phone we hold at no extra record. Leave it out and you get the profile and career history only.
curl -s -X POST https://api.leadocean.io/v1/people/enrich \
-H "x-api-key: $LEADOCEAN_API_KEY" \
-H "content-type: application/json" \
-d '{"linkedin_url":"https://www.linkedin.com/in/jane-doe-example","reveal_email":true}'Run this once before any Java. A 200 means your key and URL format are right.
Step 2: Write Enrich.java
Save this as Enrich.java. It reads the key from the environment, never from source.
import java.net.URI;
import java.net.http.*;
import java.nio.file.*;
import java.util.*;
public class Enrich {
static final HttpClient HTTP = HttpClient.newHttpClient();
static final String KEY = System.getenv("LEADOCEAN_API_KEY");
static String enrich(String linkedinUrl) throws Exception {
String body = "{\"linkedin_url\":\"" + linkedinUrl.replace("\"", "") + "\",\"reveal_email\":true}";
HttpRequest req = HttpRequest.newBuilder(URI.create("https://api.leadocean.io/v1/people/enrich"))
.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());
int status = res.statusCode();
if (status == 200) return res.body().replaceAll("\\s*\\n\\s*", " ").strip();
if (status == 404) return null; // nobody held, no record spent
if (status == 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(status + " " + res.body());
}
throw new IllegalStateException("still rate limited: " + linkedinUrl);
}
public static void main(String[] args) throws Exception {
List<String> urls = Files.readAllLines(Path.of("urls.txt")).stream()
.map(String::strip).filter(u -> !u.isEmpty()).distinct().toList();
List<String> out = new ArrayList<>();
int misses = 0;
for (String u : urls) {
String json = enrich(u);
Thread.sleep(200); // 5 a second, the Free limit
if (json == null) { misses++; continue; }
out.add(json);
}
Files.write(Path.of("enriched.jsonl"), out);
System.out.printf("%d enriched, %d not found%n", out.size(), misses);
}
}Run it with the single-file launcher:
java Enrich.javaStep 3: Handle rate limits and paging
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 so you rarely see a 429. On Agency, drop it to 5 milliseconds or run several threads. Each thread shares the same HttpClient, which is safe.
Paging here means chunking your own list. Enrich has no cursor, because each call returns one person. For a long file, split urls.txt into files of a few thousand lines, run one at a time and keep each enriched.jsonl. A crash then costs you one chunk, not the whole run.
The distinct() call removes duplicate URLs so you never pay twice for the same person.
Step 4: Write results where you can use them
The output is JSON Lines: one full response per line. Read it with any JSON library, or jq:
jq -r '.data.contact_data | [.email_status, .has_phone] | @csv' enriched.jsonlAdd Jackson or Gson later if you want typed objects. For a CSV, pick the fields you need from data.contact_data.contact_emails[] and data.profile_data. Check email_status before you send anything. We do not refund or credit back bounced emails.
Since 17 October 2026 the response also carries resume_data: past roles, education, certifications and awards, each when we hold them. Past employers have no company_id yet.
What you get
This is the response shape from the API reference, with placeholder values. Fields appear only when we hold them.
{
"success": true,
"data": {
"profile_data": { "profile_full_name": "Jane Doe", "profile_headline": "VP Sales at Acme" },
"contact_data": {
"contact_emails": [{ "email": "jane.doe@acme.com", "type": "work", "status": "verified" }],
"contact_phones": [{ "phone": "+1 415 555 0133", "type": "mobile" }],
"contact_current_experiences": [{ "job_title": "VP Sales", "company_name": "Acme" }],
"has_email": true,
"has_phone": true,
"email_status": "verified"
},
"resume_data": { "experiences": [], "educations": [] }
},
"meta": { "key_hash": "...", "fetched_at": "2026-10-01" }
}Source: response shape from the API reference, api.leadocean.io/openapi.json, September 2026. Email and phone arrays appear only with reveal_email set to true.
The program ends by printing one line in the form 2 enriched, 0 not found, with the counts for your list.
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 or lower the thread count. The code already honors Retry-After. |
| 402 | You spent your records. Free is 1,000 for life. | Move to Solo at $97 a month or Agency at $497 a month. See pricing. |
| 404 | We hold nothing for that person. | Skip it. The code counts it as a miss, and it costs nothing. |
| 400 or a malformed-body error | More than one identifier, or a broken URL string. | Send exactly one of linkedin_url, email, phone or person_id. |
still rate limited after 5 tries | Many threads sharing one key. | Reduce parallelism or ask for a higher plan. |
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 list of 1,000 URLs spend?
At most 1,000, one per URL we hold. URLs we hold nothing for return 404 and cost nothing. Duplicates are removed in the code before the first call.
Can I send a name and company instead of a URL?
Not directly. Enrich takes one of linkedin_url, email, phone or person_id. Search by domain with /v1/people/search first, then enrich the person_id.
Is there a batch endpoint?
Enrich is one person per call. For very large lists, run threads within your plan's rate limit. See the same recipe in Go, C# and TypeScript, and more recipes in the blog.
Enrich your first LinkedIn URLs with a free API key
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →