You get a three-part pipeline: a LeadOcean export, split by email_status, created as prospects in Outreach and enrolled in two sequences. Verified addresses go to your main sequence. Catch-all addresses go to a second one, so a bad domain cannot drag down the first. LeadOcean is a plain REST API with a key, and Outreach has a REST API with OAuth, so a short script does the job.
Split the exports because status decides risk. A verified address has been checked against the mailbox. A catch-all domain accepts everything, so a catch-all address may still bounce. LeadOcean marks it in email_status, and LeadOcean gives no refund for bounces.
Related: the LeadOcean integrations hub, the Smartlead recipe, the Salesloft recipe, the Reply.io recipe and pricing.
How it works
Six steps. Outreach authenticates every API call with OAuth 2.0 and a bearer token that lives 2 hours (source: Outreach getting started, September 2026).
- Get both credentials. Sign up at app.leadocean.io and copy a key from the API keys page. In Outreach, create an app, add the
prospects.allscope on its API access tab, and run the OAuth flow to get an access token. - Size the list. Run a free count first. A search with
count=trueandlimit=1costs nothing, andmeta.totalis capped at 100,000. - Export verified. POST your filters to
/v1/exportswithemailStatusset toverified. - Export catch-all. Repeat with
catch_all_validandcatch_all. Same filters, a second file. - Create two sequences in Outreach. One for verified, one for catch-all.
- Create the prospects. Post each row to
POST /api/v2/prospectsand tag it with its status and file. Then add each tag group to the matching sequence.
Run a 100-row test first and open one prospect before you send the rest. Free has no bulk CSV export, so on Free search with POST /v1/people/search, then call POST /v1/people/enrich with reveal_email set to true for each person to get the address. Each person returned costs one record.
Prefer to click? The LeadOcean app has an Exports page with a filter builder, a column picker, the record price before you start and a Download button. Outreach also imports CSV files, in its app and through an imports API that needs the imports.write scope (source: Outreach bulk API and imports, October 2026).
Configuration
Steps 3 and 4, the exports. Several values in one filter join with OR.
curl -X POST https://api.leadocean.io/v1/exports \
-H "x-api-key: $LEADOCEAN_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"outreach verified","filters":{"jobLevel":["VP"],"jobFunction":["Sales & Business Development"],"country":["CA"],"emailStatus":["verified"],"emailType":["work"]},"limit":10000,"columns":["profile_first_name","profile_last_name","current_job_title","current_company_name","current_company_domain","email_N_address","email_N_status"]}'
curl -H "x-api-key: $LEADOCEAN_API_KEY" https://api.leadocean.io/v1/exports/EXPORT_ID
curl -H "x-api-key: $LEADOCEAN_API_KEY" -o verified.csv https://api.leadocean.io/v1/exports/EXPORT_ID/downloadFor the second file, change emailStatus to ["catch_all_valid","catch_all"] and download to catchall.csv. Contact columns need a key with the enrich scope.
Step 6, the push. Outreach follows JSON:API, so the body wraps attributes in data. Outreach limits the API to 10,000 requests an hour per user (source: Outreach getting started, September 2026), so the script pauses between calls.
import csv, os, sys, time, requests
URL = "https://api.outreach.io/api/v2/prospects"
HEAD = {"Authorization": f"Bearer {os.environ['OUTREACH_ACCESS_TOKEN']}",
"Content-Type": "application/vnd.api+json"}
path, tag = sys.argv[1], sys.argv[2]
for r in csv.DictReader(open(path)):
if not r["email_1_address"]:
continue
body = {"data": {"type": "prospect", "attributes": {
"emails": [r["email_1_address"]],
"firstName": r["profile_first_name"],
"lastName": r["profile_last_name"],
"title": r["current_job_title"],
"company": r["current_company_name"],
"tags": [tag, "lo-" + r["email_1_status"]],
"source": "LeadOcean",
}}}
res = requests.post(URL, headers=HEAD, json=body)
if res.status_code == 429:
time.sleep(60) # Outreach's reset header is a time, not seconds
res = requests.post(URL, headers=HEAD, json=body)
if not res.ok:
print(r["email_1_address"], res.status_code, res.text[:200])
time.sleep(0.4)Run it twice: python push.py verified.csv lo-main and python push.py catchall.csv lo-catchall. At 0.4 seconds a call, 10,000 rows take about 70 minutes, which also keeps you under the hourly cap. Access tokens last 2 hours, so refresh the token for longer runs. Every Outreach response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset, and a 429 means the hour is spent.
This sends one prospect per request. Outreach's bulk actions change records that already exist, so they do not replace this loop. For very large files, its CSV imports API is the batch route.
Field mapping
Map the status as well as the address. Outreach has no field for it, so the script writes it into a tag, which you can filter on when you enroll prospects. Tags are a plain list of strings on the prospect (source: Outreach Prospect reference, September 2026).
| LeadOcean field | Outreach field | Note |
|---|---|---|
email_1_address | emails[] | Array of strings. |
email_1_status | tags[] | Written as lo-verified or lo-catch_all. |
profile_first_name | firstName | Empty if the profile has no split name. |
profile_last_name | lastName | |
current_job_title | title | |
current_company_name | company | Outreach says this is the account name if the prospect has an account. |
current_company_domain | custom1 | Custom fields must be defined by an admin; GET /api/v2/types lists them. |
phone_1_number | workPhones[] | Add the column to the export. Needs a plan with phones. Use mobilePhones if you know it is a mobile. |
Create the account first if you want prospects grouped by company. The Outreach docs say you cannot write to an account's prospects list, so set the account on each prospect instead (source: Outreach common patterns, September 2026).
Cost at 10,000 rows
A 10,000-row run needs LeadOcean Solo at $97 a month, because bulk CSV export is not on Free. Outreach has no public price. Its pricing page says to request pricing, and lists packages by AI credits and API calls (source: Outreach pricing, October 2026).
| Item | Cost | Note |
|---|---|---|
| LeadOcean Free | $0 | 1,000 records for life. No bulk CSV export. Test with search. |
| LeadOcean Solo | $97 a month | Bulk CSV export included. Fair-use pace, no credits. |
| LeadOcean Agency | $497 a month | For teams. Exports up to 50k rows a file, 200 requests a second. |
| Outreach seats | No public price | Request pricing from Outreach. |
| Outreach API calls | 500,000 on Amplify Plus, 1,000,000 on Amplify Pro | Listed on the pricing page. 10,000 rows is 10,000 calls. |
The pricing page lists API calls for the two top packages and not for the smaller ones. Check that your package includes API access before you build. That is our reading of the page.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| 401 from Outreach | Access token expired after 2 hours. | Refresh it. Store the token and reuse it. |
| 403 from Outreach | The app lacks prospects.all, or governance rules limit what the user can create. | Add the scope on the API access tab, or use a user with rights. |
| 429 from Outreach | Over the hourly request limit, or tokens fetched more than once a minute. | Keep the pause between calls. Reuse the token. |
| Duplicate prospect errors | The ruleset blocks duplicate emails. | Check the ruleset permitDuplicateProspects setting or dedupe the CSV. |
| 402 on LeadOcean export | Fewer records left than limit reserves. | Lower limit, or move to a paid plan. |
| 403 on LeadOcean export | Contact columns asked without the enrich scope. | Use a key with that scope, or drop the email columns. |
| 410 on LeadOcean download | The file expired. | Create the export again. |
FAQ
Why split catch-all from verified?
A catch-all domain accepts any address, so you cannot confirm the mailbox exists. Verified addresses carry less bounce risk. Separate sequences let you cap catch-all volume and keep the main one clean.
Which statuses should I send?
Send verified to the main sequence and catch_all_valid plus catch_all to the second. Leave risky, unknown, untested, invalid, role, disposable, spam_trap, abuse, derived and none out.
Can I import without code?
Yes. Download the CSV from the Exports page at app.leadocean.io and import it in Outreach. Map email_1_status to a tag or custom field so you can still split by status.
Can an AI agent run this?
Yes. LeadOcean's MCP server is at https://api.leadocean.io/mcp with 13 tools, including export_leads at one record per row. Outreach runs its own MCP server with OAuth 2.1 sign-in (source: Outreach MCP server, October 2026). We have not tested the two together, so the script stays the path for 10,000 rows.
Export your first Outreach list with a free LeadOcean key
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →