TL;DR
- Read boxes from the Streak API, enrich each work email with one LeadOcean call, and write job title and company back to a box field. No middleware.
- A first run takes about 40 minutes and spends at most 20 of your 1,000 free records. A miss is a 404 and costs nothing.
- You do not need eight tools. Streak has an API that can edit box fields. You need one data source behind it.
I did not run eight Streak add-ons for this page, so there is no scoreboard. A technical buyer needs something smaller: which call fills the empty fields, and where do the results land? Streak runs inside Gmail and keeps contacts on each box. It does not fill in titles or phones, and this recipe closes that gap.
LeadOcean has no Streak app and no Gmail extension. This is a script you run against both APIs. See the broader enrichment tool roundup and best CRM enrichment tools if you want to compare vendors first.
Prerequisites
- A LeadOcean API key from app.leadocean.io. The free plan has no card and 1,000 records, one-off.
- A Streak API key. Streak says to open Gmail, click the Streak icon, choose Integrations, and select Create New Key (authentication docs, September 2026). The key carries all your privileges, so keep it secret.
- A pipeline key, and two text fields on the pipeline for title and company. Note their field keys.
- Python 3 with
requests.
export LEADOCEAN_API_KEY="paste-your-key-here"
export STREAK_API_KEY="paste-your-streak-key-here"
export PIPELINE_KEY="paste-your-pipeline-key-here"
pip install requestsStep 1: Pull boxes from Streak
Streak signs requests with HTTP Basic Auth: the API key is the username and the password is empty. Everything runs over HTTPS (authentication docs, September 2026).
curl -s "https://api.streak.com/api/v1/pipelines/$PIPELINE_KEY/boxes?limit=50&page=0" \
-u "$STREAK_API_KEY:"The list call takes limit, a 0-indexed page and sortBy (creationTimestamp or lastUpdatedTimestamp). The response flags hasNextPage when more remain (list boxes docs, September 2026).
import os, requests
STREAK = ("https://api.streak.com/api/v1", (os.environ["STREAK_API_KEY"], ""))
def boxes(page=0):
base, auth = STREAK
r = requests.get(
f"{base}/pipelines/{os.environ['PIPELINE_KEY']}/boxes",
params={"limit": 50, "page": page}, auth=auth, timeout=30)
r.raise_for_status()
return r.json()Each entity returns two identical key fields, for example key and boxKey (object model docs, September 2026). Use boxKey.
Step 2: Get a work email for each box
Streak boxes carry the deal, not a clean contact record. The simplest reliable source is a custom text field where your team stores the contact's work email. On GET, a box's fields is an object keyed by field key, such as {"1002": "Developer"}. A field with no value is left out (get a box and field values docs, October 2026).
EMAIL_FIELD = "1001" # your pipeline's email field key
TITLE_FIELD = "1002"
COMPANY_FIELD = "1003"
def email_of(box):
return (box.get("fields") or {}).get(EMAIL_FIELD)
todo = [b for b in boxes() if email_of(b) and not (b.get("fields") or {}).get(TITLE_FIELD)]Skip boxes that already have a title. That keeps record spend near zero on re-runs.
Step 3: Enrich each email
Enrich takes exactly one of linkedin_url, email, phone or person_id. Send the work email.
curl -s -X POST "https://api.leadocean.io/v1/people/enrich" \
-H "x-api-key: $LEADOCEAN_API_KEY" \
-H "content-type: application/json" \
-d '{"email": "jane@acme.com"}'A 404 means we hold nothing for that person. It is not billed. Treat it as a skip.
LO = {"x-api-key": os.environ["LEADOCEAN_API_KEY"], "content-type": "application/json"}
def enrich(email):
r = requests.post("https://api.leadocean.io/v1/people/enrich",
headers=LO, json={"email": email}, timeout=30)
if r.status_code == 404:
return None
if r.status_code in (401, 402, 429):
raise SystemExit(f"stop: {r.status_code} {r.text[:120]}")
r.raise_for_status()
return r.json()["data"]["contact_data"]Step 4: Write back to the box
Streak's update call is POST /api/v1/boxes/{boxKey} with a JSON body. The fields property takes field keys and values, such as { "1007": "String" }, and Streak ignores properties it does not list (update box docs, September 2026).
import json
def push(box, cd):
exp = (cd.get("contact_current_experiences") or [{}])[0]
fields = {}
if exp.get("job_title"):
fields[TITLE_FIELD] = exp["job_title"]
if exp.get("company_name"):
fields[COMPANY_FIELD] = exp["company_name"]
if not fields:
return False
base, auth = STREAK
r = requests.post(f"{base}/boxes/{box['boxKey']}",
auth=auth, json={"fields": fields}, timeout=30)
r.raise_for_status()
return TrueStreak notes that contacts and organizations on a box are replaced by whatever you send. So do not send them here. Send fields only.
Step 5: Run it on 20 boxes first
Print what would change and write nothing. Then switch writes on.
import time
DRY = True
spent = written = 0
for box in todo[:20]:
cd = enrich(email_of(box))
time.sleep(0.4)
if cd is None:
continue
spent += 1
exp = (cd.get("contact_current_experiences") or [{}])[0]
print(box["boxKey"], "->", exp.get("job_title"), exp.get("company_name"))
if not DRY:
written += push(box, cd)
print(f"records spent: {spent} written: {written}")The 0.4 second pause keeps you under the 5 requests a second on Free.
Step 6: Size the list of boxes you do not have yet
Enrichment fixes the rows you hold. To find new ones, count first. The count is free.
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 '{"jobLevel": ["VP"], "jobFunction": ["Sales & Business Development"], "country": ["US"]}'Then search the same body without count, with limit up to 100 and the cursor from meta. Create each result as a box with POST /api/v2/pipelines/{pipelineKey}/boxes (it needs a name). Paid plans can use POST /v1/exports for up to 50,000 rows a request and 500,000 a day. The same count is a free MCP tool: leadocean_count_leads. For the use case in general, see CRM enrichment.
Step 7: Keep it fresh without re-spending records
Run the script on a schedule, not once. The filter in Step 2 already skips boxes that have a title, so a nightly run only touches new boxes. That is the cheapest way to keep a pipeline clean.
Three habits keep the spend near zero.
- Filter on the empty field before you call. Never enrich a box that is already filled.
- Log every 404. Do not retry the same email tomorrow. The dataset is refreshed monthly, so retry once a month at most.
- Stop the loop on 401, 402 or 429. The
enrichfunction above does that by raising.
Streak also supports webhooks that fire on box changes, and a Zapier integration for teams without a server (overview, September 2026). If you go that route, point the webhook at a small function that runs Steps 3 and 4 for one box. Do not enrich on every edit. Enrich when the email field first appears.
Which route fits which team
Use the script when one person owns the pipeline and wants titles and companies filled this week. It is about 60 lines and one cron entry.
Use a no-code tool such as Zapier or n8n when the team will not maintain Python. Both can call the same two endpoints with an HTTP step. The request bodies in this page transfer as they are.
Use a paid plan when you pass a few hundred boxes a month. Solo is $97 a month with phones and bulk CSV export included. Agency is $497 a month for teams, with 200 requests a second. Free covers the first 1,000 records, once.
What you get
The free sizing call through the MCP tool leadocean_count_leads, run 2026-10-21 with jobLevel VP, jobFunction Sales & Business Development, country US and the default mailable filter (verified, catch_all_valid or catch_all). It spent 0 records.
{
"total": 129494,
"totalIsExact": true,
"totalAll": 350517,
"distinctCompanies": 58121,
"credits": 0
}So 129,494 people match and are mailable by that default. 350,517 match with no email condition.
A single enrichment returns this response shape from the API reference, with placeholder values (Acme, Jane Doe, a 555 number). Nothing here is a real record.
{
"data": {
"contact_data": {
"contact_current_experiences": [
{ "job_title": "VP Sales", "company_name": "Acme" }
],
"contact_phones": [ { "phone": "+1 415 555 0133" } ]
}
}
}Since October 2026, person enrich also returns career history: past roles, education, certifications and awards.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| 401 from LeadOcean | Bad or missing x-api-key | Re-copy the key from app.leadocean.io and check $LEADOCEAN_API_KEY is exported |
| 401 from Streak | Key invalid or no access to the pipeline | Streak says either case returns 401. Create a new key under Integrations |
| 400 from Streak | Request made over http | Use https://api.streak.com (error codes, September 2026) |
| 429 | Above the plan's rate limit: 5 a second on Free, 200 on Agency | Sleep between calls and retry after the Retry-After header |
| 402 | Out of records on the Free plan | Upgrade or stop. Misses and counts never spend records |
| Empty search results | Enum value is not an exact string, such as vp instead of VP | Check allowed values with leadocean_list_enum_values or GET /v1/enums |
FAQ
Does Streak have built-in enrichment?
This page makes no claim either way. Streak's API docs list boxes, pipelines, fields and contacts, and nothing about a data provider (September 2026). So the recipe above brings its own.
Can the API send or track email?
No. Streak says its Gmail connection means email tracking and mail merge are unavailable through the API (overview, September 2026). Enrich the data, then send from Gmail.
How many records does a run cost?
One record per person found. A 404 miss costs nothing, and counts are free. A run of 20 boxes spends at most 20 of the 1,000 free records. See pricing for paid plans.
Does it work without a custom email field?
You need an email from somewhere. You can also enrich from a LinkedIn URL with the linkedin_url input.
Fill your Streak boxes with titles and phones
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →