Pre-Black Friday: 20% off, for as long as you stayEARLYBFon Solo and AgencyEnds Nov 28Start free

How-to guide

How to Enrich a List of LinkedIn URLs (in Rust)

One Rust file that reads URLs, calls the enrich endpoint, backs off on 429 and writes each answer to a JSON lines file.

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

You can enrich a list of LinkedIn URLs in Rust with one file and two crates: read the URLs, call POST /v1/people/enrich for each, wait out any 429, and write the answers to a JSON lines file. This guide gives you that file and explains each part.

TL;DR

  • One Rust file, 43 lines, two crates (reqwest and serde_json). It reads urls.txt and writes people.jsonl.
  • Time: about 10 minutes to set up. At 4 requests a second, 1,000 URLs take roughly 4 minutes.
  • Cost: 1 LeadOcean record per URL we hold a match for. A miss is a 404 and is free, so the Free plan's 1,000 records cover up to 1,000 matches.

Prerequisites

  • Rust 1.75 or newer with cargo.
  • A LeadOcean API key, exported as $LEADOCEAN_API_KEY. The Free plan gives you one with 1,000 records for life, no card.
  • A urls.txt file with one LinkedIn profile URL per line.

Step 1: Get a key and prepare the list

Sign up at app.leadocean.io, create a key and put it in your shell. Never write the key into the source file.

bash
export LEADOCEAN_API_KEY="your-key-here"
cargo new enrich && cd enrich
cargo add reqwest --features blocking,json
cargo add serde_json
printf '%s\n' \
  "https://www.linkedin.com/in/jane-doe-acme" \
  "https://www.linkedin.com/in/john-roe-acme" > urls.txt

The enrich endpoint takes exactly one identifier per call: linkedin_url, email, phone or person_id. Enrich does not page. You make one call per URL, so "paging" here means walking your file line by line.

Step 2: Write the Rust program

Replace src/main.rs with this. Requests go to https://api.leadocean.io with the x-api-key header.

rust
use reqwest::blocking::Client;
use serde_json::{json, Value};
use std::{env, fs, thread, time::Duration};

fn enrich(c: &Client, key: &str, url: &str) -> (u16, Value) {
    for _ in 0..5 {
        let sent = c
            .post("https://api.leadocean.io/v1/people/enrich")
            .header("x-api-key", key)
            .json(&json!({ "linkedin_url": url, "reveal_email": true }))
            .send();
        let Ok(res) = sent else {
            thread::sleep(Duration::from_secs(1));
            continue;
        };
        let status = res.status().as_u16();
        if status == 429 {
            let wait = res.headers().get("retry-after")
                .and_then(|v| v.to_str().ok())
                .and_then(|s| s.parse::<u64>().ok())
                .unwrap_or(1);
            thread::sleep(Duration::from_secs(wait.max(1)));
            continue;
        }
        let text = res.text().unwrap_or_default();
        return (status, serde_json::from_str(&text).unwrap_or(json!(text)));
    }
    (0, json!("gave up after 5 tries"))
}

fn main() {
    let key = env::var("LEADOCEAN_API_KEY").expect("set LEADOCEAN_API_KEY");
    let client = Client::new();
    let mut out = String::new();
    for url in fs::read_to_string("urls.txt").expect("urls.txt").lines() {
        let url = url.trim();
        if url.is_empty() { continue; }
        let (status, response) = enrich(&client, &key, url);
        out += &format!("{}\n", json!({ "linkedin_url": url, "status": status, "response": response }));
        thread::sleep(Duration::from_millis(250));
    }
    fs::write("people.jsonl", out).expect("write people.jsonl");
}

Three choices in that file matter:

  • reveal_email: true returns the contact points on the record. It adds no extra record.
  • The 250 ms pause keeps you at 4 requests a second, under the Free plan's limit of 5.
  • On a 429 the code reads Retry-After, sleeps that many seconds, and tries again, up to 5 times. See API rate limits and 429 errors.

Step 3: Run it

bash
cargo run --release
head -c 600 people.jsonl

Each line is one JSON object with the URL, the HTTP status and the full response. A 200 is a match and a 404 is nobody found. Filter on status before you load rows anywhere else.

Step 4: Parse the fields you need

The file stores the raw JSON so you lose nothing. When you know which fields you want, deserialize response into a struct with serde. The current role is in contact_current_experiences[] and phones are in contact_phones[].phone. Since October 2026 the same call also returns career history in resume_data: past roles, education, certifications and awards.

Past employers carry no company_id yet. If you need a company record, call POST /v1/companies/enrich with the domain.

What you get

The response shape from the API reference, with placeholder values. This is not a real person.

json
{
  "success": true,
  "data": {
    "profile_data": { "...": "name, headline, location" },
    "contact_data": { "...": "present because reveal_email was true" },
    "resume_data": { "...": "experiences[], educations[], certifications[]" }
  },
  "meta": { "source": "leadocean" }
}

Source: response shape from the API reference (openapi.json, October 2026). Each record also carries a fetched_at date, and the dataset refreshes monthly.

Troubleshooting

ErrorCauseFix
401Missing or wrong x-api-key, or $LEADOCEAN_API_KEY is empty in this shellRe-export the key and check with echo ${#LEADOCEAN_API_KEY}
429You sent faster than the plan allows: 5 a second on Free, 200 on AgencyRaise the sleep, or rely on the Retry-After retry in the code
402The records are spent. Free is 1,000 for lifeStop the run and upgrade. Paid plans slow at their pace instead of returning 402
404No person matched that URL. It is freeKeep the row, mark it unmatched, move on

FAQ

Can I run the calls in parallel?

Yes. Use tokio with the async reqwest client and a semaphore, or std::thread workers. Keep the total under your plan's per-account limit: 5 requests a second on Free, 50 on Solo, 200 on Agency. Sequential is fine below about 10,000 URLs.

Does a URL with no match use a record?

No. A person we hold nothing for is a 404 and costs nothing. Only a match returns a record.

Should I use GET or POST?

Both work on /v1/people/enrich. POST with a JSON body avoids URL-encoding the LinkedIn address. Use the canonical path shown above, not the deprecated alias.

What if I have more than 10,000 URLs?

Run the same loop in batches and write each batch to its own file. Paid plans have a fair-use pace instead of credits, and reaching it slows the key rather than stopping it.

Is there a bulk option?

Not for a list of URLs. POST /v1/exports builds a CSV from search filters, not from identifiers you supply. It takes up to 50,000 rows a request and 500,000 a day, and it is not on Free. It starts on Solo at $97 a month.

See the same recipe in Go, C# and TypeScript.

For the no-code version of this lookup, see LinkedIn to email.

Enrich your first 1,000 LinkedIn URLs free

Free to start. No credit card. 1,000 records to spend whenever you like.

Get your free API key →