Documentation

Bulk CSV export

Any people search filter set, up to 50,000 rows per export, delivered as a plain .csv — never zipped, never compressed — with an email when it is ready. Always asynchronous: a request is admitted or refused on the spot, and a worker does the walk.

Start an export

POST /v1/exports · needs the search scope, plus enrich when the columns reveal contact points

The body is { filters, limit, preset | columns, caps?, reveal?, name? }. filters is any people-search filter set without the paging fields (cursor, limit, count, facets). limit is how many rows you want; the export stops there or at the end of the result, whichever comes first.

curl -X POST https://api.leadocean.io/v1/exports \
  -H "x-api-key: $LEADOCEAN_API_KEY" -H "content-type: application/json" \
  -d '{
    "name": "CTOs in Germany",
    "filters": { "title": ["CTO"], "country": ["DE"], "emailStatus": ["verified"] },
    "limit": 5000,
    "preset": "full"
  }'
200 OK · application/json
HTTP 202
{
  "success": true,
  "data": {
    "id": "68d5f1c2a9b3e40012ab34cd",
    "name": "CTOs in Germany",
    "status": "queued",
    "requested": 5000,
    "estimatedTotal": 18342,
    "reveal": "email_phone",
    "columns": ["person_id", "person_group_id", "…61 ids…"],
    "perRow": 1,
    "remaining": 89990,
    "pollAfter": 10,
    "file": null
  }
}

The answer is 202 with the job. perRow is what each row costs in records — always 1 — remaining is what the account has left after this reservation, estimatedTotal is how many people matched when the job was admitted.

Columns and presets

GET /v1/exports/columns (free, public) lists every column id, its group, and whether it is on the search row. Two presets name a set for you:

  • full — the default. 45 columns: identity, profile, address, contact flags, up to three emails and two phones, the current role, education and skills. Reveals emails and phones, so it needs the enrich scope.
  • search — the 27 columns a search result already carries: identity, headline, location, current employer and title, and the contact flags (has an email, email status) but never an address. No enrich scope needed.

Or send columns, an ordered list of ids, for exactly the set you want. Repeated blocks — emails, phones, past roles, schools, certifications, awards — become numbered columns (email_1_address, email_2_address, …); caps sets how many of each (defaults: emails 3, phones 2, past 3, educations 2, certifications 2, awards 1).

A row is one record, whatever it carries. 10,000 rows with emails and phones cost 10,000 records — half what the same people cost through search and then enrich one by one. Bulk is the cheaper way to enrich, by design. The records are reserved when the export is admitted and released for rows never written.

Two honesties about the profile text: about half of the summaries were stored by the crawl as a preview and end in an ellipsis, and an education entry is the institution and degree as one line of text, not separate fields. Certifications are not in the default: the index holds them as one unsplittable string, so the columns exist in the catalogue but stay empty.

Poll, download, cancel

GET /v1/exports/{id} · free

GET /v1/exports/{id}/download · free · text/csv

DELETE /v1/exports/{id} · cancel

GET /v1/exports?status=&limit=&cursor= · list, newest first

statusMeaning
queuedAdmitted. The rows are reserved against your ceilings and your records from this moment.
runningThe worker is walking the search and writing the file. rows counts up; a cancel stops it at the next checkpoint and keeps what is written.
doneThe file is on storage. file.bytes, file.sha256 and file.expiresAt are set; download it with your key or the emailed link.
failedSomething broke mid-way. error.code says what; the rows that were written are still downloadable and are the only ones billed.
cancelledYou stopped it. Rows written stay billed and downloadable; rows never written are released.

Poll every pollAfter seconds (10). When status is done, GET …/download streams the file with your key. The ready email carries a link that works without a key for 24 hours; the file itself is kept for 7 days, then deleted — file.expiresAt says when.

Rows are billed as they are written, at checkpoints: a job that fails or is cancelled after 12,000 rows bills 12,000, and the rest of the reservation is released (released on the job).

Ceilings and refusals

Three ceilings sit on top of your records: 50,000 rows per export, 500,000 rows a day and 10,000,000 rows a billing period, per account. They count rows already reserved by exports still going plus rows written by finished ones, so twenty parallel starts cannot overshoot. They can be raised for an account — write to support@leadocean.io. GET /v1/usage reports where you stand under exports.

StatuscodeWhy
400export_limit_rowslimit is above the per-export ceiling (50,000 by default).
400export_filter_unsupportedA filter in the set cannot be applied by the index. details.filters names it. An export is never run half-filtered.
429export_limit_daily / export_limit_monthlyThe account has reached its rows for the day (500,000) or the period (10,000,000). details.remaining and details.resetsAt say how much and when.
402quotalimit is more than the records you have left this period. The message quotes both figures.
403scopeThe column set reveals emails or phones and the key does not hold the enrich scope.
503export_storage_unavailableObject storage is unreachable. Transient; nothing is reserved.

One export runs per account at a time; the rest queue in order, already reserved and already counted.

From the app and from MCP

The same jobs are on the Exports page — the filter builder, a column picker with the presets, the price before you start, and a Download button — and on MCP as leadocean_export_leads, leadocean_get_export and leadocean_list_exports. An agent can start an export from a conversation and hand back the download link when it is ready.