You enrich a list of LinkedIn URLs in C# by sending each one to POST /v1/people/enrich and writing the response to a file. The whole program fits in under 60 lines of .NET with no packages.
TL;DR
- Time: about 15 minutes to set up. Run time is roughly one request per URL, held to your plan's rate limit (5 a second on Free, 200 on Agency).
- Cost: one LeadOcean record per URL that matches a person. A URL we hold nothing for returns 404 and spends nothing.
- Result: a
enriched.jsonlfile with one person per line, plus a count of hits and misses.
Prerequisites
- .NET 8 SDK or later (
dotnet --version). - A LeadOcean API key from the free plan, exported as
$LEADOCEAN_API_KEY. The free plan gives 1,000 records for life and no card. - A
urls.txtfile with one LinkedIn profile URL per line.
export LEADOCEAN_API_KEY="your-key-here"
dotnet new console -n Enrich && cd Enrich
printf "https://www.linkedin.com/in/jane-doe\n" > urls.txtStep 1: Set up the client and read the list
Create one HttpClient, set the x-api-key header once and read your URLs. The base address is https://api.leadocean.io.
using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
var key = Environment.GetEnvironmentVariable("LEADOCEAN_API_KEY")
?? throw new Exception("Set LEADOCEAN_API_KEY first.");
using var http = new HttpClient { BaseAddress = new Uri("https://api.leadocean.io") };
http.DefaultRequestHeaders.Add("x-api-key", key);
var urls = File.ReadLines("urls.txt").Where(l => l.Trim().Length > 0).ToList();
using var output = new StreamWriter("enriched.jsonl");
int hits = 0, misses = 0;Step 2: Write the enrich call with retries on 429
Enrich takes exactly one of linkedin_url, email, phone or person_id. Send reveal_email as true to get the contact points in the same call, at no extra record.
A 429 means you went above your plan's rate limit. Read the Retry-After header and wait that long. If it is missing, back off 1, 2, 4 seconds.
async Task<JsonElement?> Enrich(string url)
{
for (var attempt = 0; attempt < 5; attempt++)
{
var res = await http.PostAsJsonAsync("/v1/people/enrich",
new { linkedin_url = url, reveal_email = true });
if (res.StatusCode == HttpStatusCode.NotFound) return null;
if ((int)res.StatusCode == 429)
{
var wait = res.Headers.RetryAfter?.Delta ?? TimeSpan.FromSeconds(Math.Pow(2, attempt));
await Task.Delay(wait);
continue;
}
res.EnsureSuccessStatusCode();
using var doc = await JsonDocument.ParseAsync(await res.Content.ReadAsStreamAsync());
return doc.RootElement.GetProperty("data").Clone();
}
throw new Exception($"Gave up after 5 tries: {url}");
}A 404 is a normal miss, not an error. 401 and 402 are not retried: EnsureSuccessStatusCode stops the run so you see them.
Step 3: Page through the list and write results
Process the list in pages of 100. After each page, flush the file so a crash loses at most one page. Add the loop below the local function, or above it: top-level statements allow both.
foreach (var page in urls.Chunk(100))
{
foreach (var url in page)
{
var person = await Enrich(url);
if (person is null) { misses++; continue; }
hits++;
await output.WriteLineAsync(person.Value.GetRawText());
}
await output.FlushAsync();
Console.WriteLine($"{hits} found, {misses} not found");
}Run it with dotnet run. The program is sequential. If answers come back faster than 5 a second on Free, the 429 handler waits and retries. To go faster on Agency (200 a second), wrap the inner loop in Parallel.ForEachAsync with MaxDegreeOfParallelism set below your limit, and keep the 429 handler.
Step 4: Check the run before you scale it
Run 20 URLs first. Compare hits and misses against what you expected, then open two lines of enriched.jsonl and confirm email_status and contact_current_experiences are populated.
If the hit rate is low, the cause is usually the input. Strip tracking parameters such as ?utm_source= from each URL, and drop trailing slashes. Trim whitespace too, because a stray space on a line from a spreadsheet export is a common reason for a miss.
Check your balance for free with GET /v1/account or GET /v1/usage. Neither spends a record.
curl -s https://api.leadocean.io/v1/account -H "x-api-key: $LEADOCEAN_API_KEY"Once the sample looks right, run the full file. Everything the program wrote stays in enriched.jsonl, so you can load it into a database or a CSV step afterwards. See the full API reference for every field on the person object.
What you get
Each line of enriched.jsonl is one data object. This is the response shape from the API reference (openapi.json, checked September 2026), with placeholder values:
{
"profile_data": { "profile_full_name": "Jane Doe", "profile_url": "https://www.linkedin.com/in/jane-doe" },
"contact_data": {
"contact_emails": [{ "email": "jane@acme.com", "type": "work", "status": "verified" }],
"contact_phones": [{ "phone": "+1 415 555 0133", "type": "mobile" }],
"has_email": true,
"has_phone": true,
"email_status": "verified",
"contact_current_experiences": [{ "job_title": "VP Sales", "company_name": "Acme", "company_domain": "acme.com" }]
},
"resume_data": { "experiences": [], "educations": [] }
}Read email_status before you send anything. LeadOcean does not refund bounces. resume_data carries past roles, education and certifications when we hold them; past employers have no company_id yet.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| 401 | Missing or wrong x-api-key | Check $LEADOCEAN_API_KEY is exported in the shell that runs dotnet run. |
| 429 | Requests above the plan's rate limit (5 a second on Free, 200 on Agency) | Keep the Retry-After wait in Step 2. Lower parallelism. |
| 402 | Out of records on the Free plan | Upgrade, or enrich fewer URLs. The 1,000 Free records are one-off. |
| 404 | We hold nothing for that URL | Not an error and it costs nothing. The program counts it as a miss. |
| Every call is a miss | URL is a company page, a search link or has a typo | Use a profile URL of the form linkedin.com/in/.... |
| Run stops mid-list | A 5xx, a network error, or five 429s in a row on one URL | Rerun on the URLs not yet in the file, so you do not pay twice. Hits already written stay. |
FAQ
Do I need a NuGet package?
No. HttpClient, System.Text.Json and Chunk ship with .NET 8. Add Polly only if you want circuit breakers on top of the loop.
How many records will 500 URLs use?
At most 500, one per matched person. Misses are free, so the real number is your match rate times 500. The free plan covers 1,000 in total.
Can I enrich by email instead of LinkedIn URL?
Yes. Replace linkedin_url with email in the request body. Send exactly one identifier per call.
What if I have thousands of URLs?
Run it on Solo ($97 a month, 50 requests a second, 10k enrichments every 5 hours) or Agency ($497 a month, 200 a second, 1M enrichments a day). Paid plans have a fair-use pace, not credits. Reaching it slows the key until the window resets; nothing stops. See pricing.
Are there versions in other languages?
Yes (all in the blog): Go, TypeScript and Rust.
Enrich your first LinkedIn URLs from C# today
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →