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 Ruby)

Auth, cursor paging, 429 retries and a CSV file in one Ruby script under 60 lines.

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

You can walk a LeadOcean people search in Ruby with one script: send a flat JSON body, read meta.nextCursor, send it back, and stop when it is null. This guide adds the key header, a retry on 429 and a CSV writer.

TL;DR

  • The script is 34 lines of Ruby. It uses net/http, json and csv.
  • It takes about 10 minutes, including getting a key.
  • The sample run spends 100 LeadOcean records (4 pages of 25 people). The free plan gives you 1,000 records for life.

Prerequisites

  • A LeadOcean account. Start free at app.leadocean.io, no card needed.
  • An API key from the app, exported as $LEADOCEAN_API_KEY.
  • Ruby 3.0 or newer. On Ruby 3.4 and later, run gem install csv if require "csv" fails.
bash
export LEADOCEAN_API_KEY="paste-your-key-here"

Step 1: Size the search for free

Count first. A count spends nothing, and it tells you how many pages you are about to buy.

Send count=true as a query parameter, with limit 1. The JSON body rejects unknown fields, so the flag cannot go there. meta.total is capped at 100,000.

bash
curl -s -X POST "https://api.leadocean.io/v1/people/search?count=true&limit=1" \
  -H "x-api-key: $LEADOCEAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"title":["Head of Sales"],"country":["US"]}'

Step 2: Write the Ruby script

The endpoint is POST /v1/people/search on https://api.leadocean.io. The body is flat JSON. Filters such as title and country are arrays of strings. limit runs from 1 to 100.

Save this as search.rb.

ruby
require "net/http"
require "json"
require "csv"

URI_SEARCH = URI("https://api.leadocean.io/v1/people/search")

def post(body)
  5.times do
    req = Net::HTTP::Post.new(URI_SEARCH)
    req["x-api-key"] = ENV.fetch("LEADOCEAN_API_KEY")
    req["Content-Type"] = "application/json"
    req.body = JSON.generate(body)
    res = Net::HTTP.start(URI_SEARCH.host, URI_SEARCH.port, use_ssl: true) { |h| h.request(req) }
    if res.code == "429"
      sleep [res["Retry-After"].to_i, 1].max
      next
    end
    raise "status #{res.code}: #{res.body}" unless res.code == "200"
    return JSON.parse(res.body)
  end
  raise "still rate limited"
end

body = { title: ["Head of Sales"], country: ["US"], limit: 25 }

CSV.open("people.csv", "w") do |csv|
  4.times do
    page = post(body)
    page["data"].each { |row| csv << [JSON.generate(row)] }
    cursor = page.dig("meta", "nextCursor")
    break if cursor.nil?
    body[:cursor] = cursor
  end
end

Three details matter, and each one is a common way to break a paging loop. Send the cursor back exactly as returned. Cursors are signed, so an edited one gets a 400, and so does one from a different search. And the loop caps at 4 pages so a test run cannot spend more than 100 records.

Step 3: Retry on 429

A 429 means you went over your plan's rate: 5 requests a second on Free, 50 on Solo, 200 on Agency. The reply carries a Retry-After header in seconds (RFC 9110, September 2026).

The post method above reads that header, sleeps, and tries again up to 5 times. A loop that sends one request at a time rarely hits the limit on Solo or Agency. It can on Free if you add threads.

Step 4: Run it and check the file

bash
ruby search.rb
wc -l people.csv

Each CSV row holds one person as a JSON string. That keeps the sample short, and you lose nothing: every field the search returns is in the row. To get columns, read profile_data and contact_data from each hash and write the fields you want.

What you get

The Step 1 count below was run on 2026-10-21 with the LeadOcean MCP tool leadocean_count_leads, which is free. It filtered title "Head of Sales" in the US.

json
{
  "total": 6564,
  "totalIsExact": true,
  "totalAll": 16363,
  "distinctCompanies": 4932,
  "credits": 0
}

total counts people with a verified, catch-all-valid or catch-all address. totalAll is the same filter with no email condition. At 25 a page, 6,564 rows is 263 pages and 16,363 is 655. One search stops at 10,000 rows (meta.depthCapped). Narrow the filters before you walk a list that large.

Each page returns this shape (response shape from the API reference, placeholder values):

json
{
  "success": true,
  "data": [
    { "profile_data": { "...": "..." }, "contact_data": { "has_email": true, "email_status": "verified" } }
  ],
  "meta": { "count": 25, "nextCursor": "opaque-signed-string" }
}

Search returns a thin projection with no contact points. To reveal an email or phone, enrich the person with POST /v1/people/enrich using the person_id, or call GET /v2/people/phone. Each reveal spends records, so filter your CSV first. See pricing for what each plan allows.

Troubleshooting

ErrorCauseFix
401Missing or wrong x-api-key headerCheck $LEADOCEAN_API_KEY is set in the shell that runs ruby. ENV.fetch raises KeyError if it is not.
429Over the plan's rate (5 a second on Free, 200 on Agency)Honor Retry-After. Slow the loop or upgrade the plan.
402Free plan only: the 1,000 records are spent and do not resetMove to a paid plan. Paid plans are never refused: past their pace they slow to 1 request a minute (a 429).
400 Invalid cursorCursor edited, stored or reused on a different searchPass meta.nextCursor unchanged. Restart the search to get a fresh one.
Empty data or a 400Filter value does not match an enum exactlyCheck values with GET /v1/enums or leadocean_list_enum_values.
No nextCursor, depthCapped trueThe walk reached 10,000 rowsNarrow the filters and run smaller searches.

FAQ

Is GET or POST better for search?

Use POST. Filters are arrays, and a JSON body is easier to read than a long query string. The endpoint accepts both.

How many records does paging cost?

One record per person returned. A count, an empty page and a 429 cost nothing. Four pages of 25 is 100 records.

Can I run pages in parallel?

No. A cursor is a position in one walk, so each page depends on the last. Run separate searches in parallel, within your rate limit.

Is this the right tool for 100,000 rows?

No. For large lists use POST /v1/exports, which takes up to 50,000 rows a request. Bulk CSV export is not on Free.

Where do I go next?

The same walk is covered in Go, C# and TypeScript. All of them sit under the API tutorials hub, and plans are on pricing.

Run your first Ruby people search on the free plan

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

Get your free API key →