An email marketing database is a list of people with a work email and a deliverability status on every row. You build one by filtering a people dataset, sizing it for free, then exporting the columns your sender needs.
TL;DR
- Four API calls take you from a filter to a CSV: size, check columns, export, download.
- It takes about 15 minutes. The recipe below spends 500 LeadOcean records. Sizing and polling are free. The export needs Solo ($97 a month) or above.
- Keep the
email_1_statuscolumn. It is the difference between a database and a pile of addresses.
Prerequisites
- A LeadOcean account at app.leadocean.io. The free plan has 1,000 records, one-off, no card, and covers Steps 1 to 3. Bulk CSV export is not on Free: Steps 4 and 5 need Solo ($97 a month) or above.
- An API key from the app, exported as
$LEADOCEAN_API_KEY. Free keys get both scopes, search and enrich. curland Python 3 for the last step.- Your sender's import format. Most want a CSV with an email column and a name column.
export LEADOCEAN_API_KEY="paste-your-key-here"Step 1: Write the audience as filters
Describe who you want as one flat JSON object. Filters are arrays of strings, and enum values are exact strings.
This example is VP and Director level marketing people in the US with a verified work email.
{
"jobFunction": ["Advertising & Marketing"],
"jobLevel": ["Director", "VP"],
"country": ["US"],
"emailType": ["work"],
"emailStatus": ["verified"]
}Start narrow. A database of 500 right-fit people beats 50,000 loose matches. Pick one job function, one or two levels, and one region per list. Add employeeRange or industry when the message differs by company type.
Save it as audience.json. The same keys work in the search body, in the export filters object and in the MCP tools.
Do not guess enum values. Case and wording matter: Director works, director does not. Check the exact strings with GET /v1/enums or the free leadocean_list_enum_values MCP tool.
Step 2: Size the list before you spend anything
Send the filters to people search with count=true as a query parameter and limit set to 1. It returns the total and no rows, so it costs no records.
curl -s -X POST "https://api.leadocean.io/v1/people/search?count=true" \
-H "x-api-key: $LEADOCEAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"jobFunction": ["Advertising & Marketing"],
"jobLevel": ["Director", "VP"],
"country": ["US"],
"emailType": ["work"],
"emailStatus": ["verified"],
"limit": 1
}'Read meta.total. The number is capped at 100,000. If meta.totalIsExact is false, your list is bigger than the cap.
Run the count once per segment. Compare the totals and decide which segments are worth a first send. Each call is free, so test ten variants before you export one.
Two rules for a sane list. If the total is under a few hundred, loosen one filter. If it is over what you will send in a quarter, add one.
Step 3: Pick the columns
List the export columns. The catalogue endpoint needs no key.
curl -s https://api.leadocean.io/v1/exports/columns | python3 -m json.tool | head -40For a sending list you need five groups: identity, role, company, email, and email status. These column ids cover them:
profile_full_namecurrent_job_titlecurrent_company_namecurrent_company_domainemail_N_addressandemail_N_status
You request numbered columns as email_N_address. They come back as email_1_address, email_2_address and so on. Set caps to {"emails": 1} so you get one address per person, not three.
Every row costs one record whatever the columns. Adding columns does not change the price.
Skip columns you will not use. A smaller file is easier to audit, and your sender will not ask you to map fields you never needed.
Step 4: Start the export
POST /v1/exports takes filters, limit, columns and caps. One export holds up to 50,000 rows, and the account ceiling is 500,000 rows a day. The call returns at once with an id and a status of queued.
curl -s -X POST https://api.leadocean.io/v1/exports \
-H "x-api-key: $LEADOCEAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "us-marketing-leaders",
"limit": 500,
"filters": {
"jobFunction": ["Advertising & Marketing"],
"jobLevel": ["Director", "VP"],
"country": ["US"],
"emailType": ["work"],
"emailStatus": ["verified"]
},
"columns": [
"profile_full_name",
"current_job_title",
"current_company_name",
"current_company_domain",
"email_N_address",
"email_N_status"
],
"caps": { "emails": 1 }
}'The export reserves up to limit records up front and bills each row written. Unused records are released when it ends. That is why a 500-row export fails with a 402 if your balance is 400.
Prefer a UI? The Exports page in the app does the same job: filter builder, column picker, the record price before you start, and a Download button.
Step 5: Poll, download, then check the status column
Poll GET /v1/exports/{id} until status is done. Wait the number of seconds in pollAfter between calls. Then fetch the file.
EXPORT_ID="paste-id-from-step-4"
curl -s -H "x-api-key: $LEADOCEAN_API_KEY" \
"https://api.leadocean.io/v1/exports/$EXPORT_ID"
curl -s -H "x-api-key: $LEADOCEAN_API_KEY" \
"https://api.leadocean.io/v1/exports/$EXPORT_ID/download" \
-o us-marketing-leaders.csvBefore you import the file, keep only the rows you would send to and drop duplicate addresses. Two minutes here saves a bounce rate you cannot undo. Run it on every export, not only the first.
import csv
SEND_OK = {"verified", "catch_all_valid"}
seen, rows = set(), []
with open("us-marketing-leaders.csv", newline="") as f:
for r in csv.DictReader(f):
addr = (r["email_1_address"] or "").strip().lower()
if addr and r["email_1_status"] in SEND_OK and addr not in seen:
seen.add(addr)
rows.append(r)
with open("send-list.csv", "w", newline="") as f:
w = csv.DictWriter(f, fieldnames=rows[0].keys())
w.writeheader()
w.writerows(rows)
print(len(rows), "rows ready")The search index carries no per-address verification proof, so a search count of verified is an upper bound. Enrichment applies the stricter per-address rule, so spot-check a few rows with POST /v1/people/enrich by person_id before a large send. The enriched status is the one to trust. See email hygiene for what to do with the rest, and B2B data cleansing for keeping the database clean after you import it.
A database is not a one-off file. The dataset is refreshed monthly, and each record carries its own fetched_at date. Treat each export as a snapshot. Name it with the segment and the date, keep the CSV, and re-run the same filters when you need a fresh slice. The filter JSON is the real asset, because it is the definition of your audience.
Import send-list.csv into your sender and map four fields: email, first name, company and title. Keep the segment name as a tag so you can compare replies by audience later.
What you get
Sizing is free, so here are two real counts from the LeadOcean MCP server on 2026-10-21. Both are US Director and VP marketing people.
| Filters | Count | Date |
|---|---|---|
emailType work, emailStatus verified (a count by status, not a mailable total) | 64,881 | 2026-10-21 |
| Tool default, no email filter passed (counts verified, catch_all_valid or catch_all), any email type | 295,160 | 2026-10-21 |
The two rows use different email filters, so do not subtract one from the other. The second is the tool's default mailable audience, not all matches. The same filters with no email condition match 808,986.
The count call returns this shape. This is the response shape from the API reference, with placeholder values.
{
"success": true,
"data": [],
"meta": {
"total": 64881,
"totalIsExact": true,
"countOnly": true,
"credits": 0
}
}The export call returns this. Values are placeholders.
{
"success": true,
"data": {
"id": "exp_123",
"name": "us-marketing-leaders",
"status": "queued",
"requested": 500,
"estimatedTotal": 64881,
"pollAfter": 5
}
}The downloaded CSV has the columns you asked for. One placeholder row:
profile_full_name,current_job_title,current_company_name,current_company_domain,email_1_address,email_1_status
Jane Doe,VP Marketing,Acme,acme.com,jane.doe@acme.com,verifiedTroubleshooting
| Error | Cause | Fix |
|---|---|---|
| 401 | Missing or wrong x-api-key header | Copy the key from the app again. Check that $LEADOCEAN_API_KEY is set in this shell. |
| 429 | Above your plan's rate limit per account (5 a second on Free, 200 on Agency) | Slow the loop. Poll an export at the pollAfter interval, not in a tight loop. |
| 402 | Fewer records left than the export needs | Lower limit. Records are reserved for the whole export before it starts. |
| Empty results | A filter value that is not an exact enum string | Pull the strings from GET /v1/enums or leadocean_list_enum_values. Director, not director. |
400 export_limit_rows | limit is above 50,000 rows for one export | Split the audience by country or job level and run several exports. |
429 export_limit_daily | You passed the 500,000 rows a day ceiling | Wait for resetsAt in the response, then run the next export. |
FAQ
What is an email marketing database?
It is a stored list of contacts with an email address, a name, a role and a deliverability status per address. The status is what makes it safe to send. A list without it is a spreadsheet.
What is a safe email status to send to?
verified and catch_all_valid are the safe ones. catch_all means the domain accepts anything, so the address cannot be confirmed either way. Never send to invalid, spam_trap, abuse or disposable. Use care with risky, role and derived.
What happens when an address bounces?
Nothing is refunded. There is no credit-back for bounced emails, so read email_status before you send. Your sender also needs a tool built for outreach to track bounces and replies. LeadOcean is the data, not the sender.
Should I build a database or buy a list?
Build it from filters. A bought file is a frozen snapshot of someone else's idea of your audience. A filter set is yours, you can resize it for free, and you can re-export it when the data refreshes. More guides on this are in the blog hub.
How much does a list like this cost?
The free plan gives you 1,000 records once, no card, for sizing and sampling. The export in this recipe needs a paid plan: Solo is $97 a month and Agency $497 a month, flat, with a fair-use pace and no per-record price. See pricing. Sizing is free on every plan.
Build your first email marketing database. Size it free, export when it looks right.
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →