TL;DR
- Search the Copper API for people, enrich each work email with one LeadOcean call, and write the title and phone back with a Copper PUT. No middleware.
- A first run over 100 contacts takes about 45 minutes and spends at most 100 of your 1,000 free records. A miss is a 404 and costs nothing.
- You do not need six tools. Copper already syncs Gmail, Calendar and Contacts. You need one data source behind its API.
Search results for "Copper CRM enrichment tools" are mostly six-logo roundups. A technical buyer needs less: which call fills the empty fields, and where do the results land? Copper's own page describes the CRM as built around Gmail, Calendar and Drive (September 2026). That sync fills in names and email threads. It does not fill in job titles or phones, and that gap is what this recipe closes.
LeadOcean has no Copper app. This is a script you run against both APIs.
Prerequisites
- A LeadOcean API key from app.leadocean.io. The free plan has no card and 1,000 records, one-off. Free keys get the enrich scope.
- A Copper API key, and the email of the user who owns it. Copper calls these the access token and user email in its request docs (September 2026).
- Python 3 with
requests.
export LEADOCEAN_API_KEY="paste-your-key-here"
export COPPER_TOKEN="paste-your-copper-token-here"
export COPPER_EMAIL="you@acme.com"
pip install requestsStep 1: Pull people from Copper
Copper's people search is a POST with a JSON body. Criteria combine with AND, and a page is 20 records by default, so set page_size (people search docs, September 2026). Copper caps any search at its first 100,000 records.
curl -s -X POST "https://api.copper.com/developer_api/v1/people/search" \
-H "X-PW-AccessToken: $COPPER_TOKEN" \
-H "X-PW-Application: developer_api" \
-H "X-PW-UserEmail: $COPPER_EMAIL" \
-H "Content-Type: application/json" \
-d '{"page_size": 100, "page_number": 1, "sort_by": "date_created", "sort_direction": "desc"}'Copper has no documented filter for an empty job title. So pull the page and filter in code: keep people with a work email and no title.
import os, requests
COPPER = {
"X-PW-AccessToken": os.environ["COPPER_TOKEN"],
"X-PW-Application": "developer_api",
"X-PW-UserEmail": os.environ["COPPER_EMAIL"],
"Content-Type": "application/json",
}
def copper_people(page=1):
r = requests.post(
"https://api.copper.com/developer_api/v1/people/search",
headers=COPPER,
json={"page_size": 100, "page_number": page},
timeout=30,
)
r.raise_for_status()
return [p for p in r.json() if p.get("emails") and not p.get("title")]Step 2: Enrich each email
Enrich takes exactly one of linkedin_url, email, phone or person_id. Send the Copper work email. On an email lookup reveal_email defaults to true, so phones come back at no extra record.
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):
raise SystemExit(f"stop: {r.status_code} {r.text[:120]}")
r.raise_for_status()
return r.json()["data"]["contact_data"]Step 3: Write back with a Copper PUT
Send only the fields you change. Copper warns that reading a record and sending it whole can wipe connect fields, so never PUT the full object back (update docs, September 2026).
def push(person, cd):
exp = (cd.get("contact_current_experiences") or [{}])[0]
body = {}
if exp.get("job_title"):
body["title"] = exp["job_title"]
phones = [p["phone"] for p in cd.get("contact_phones") or []]
if phones and not person.get("phone_numbers"):
body["phone_numbers"] = [{"number": phones[0], "category": "work"}]
if not body:
return False
r = requests.put(
f"https://api.copper.com/developer_api/v1/people/{person['id']}",
headers=COPPER, json=body, timeout=30)
r.raise_for_status()
return TrueCopper lists title as a standard person field and accepts phone_numbers as a list of number and category pairs in its create docs (September 2026). Check that your account's field layout shows both before a live run.
Step 4: Run it on 20 people first
Do a dry run. Print what would change and write nothing. Then switch writes on.
import time
DRY = True
spent = changed = 0
for person in copper_people()[:20]:
cd = enrich(person["emails"][0]["email"])
time.sleep(0.4) # stays far under Copper's 180 requests a minute
if cd is None:
continue
spent += 1
exp = (cd.get("contact_current_experiences") or [{}])[0]
print(person["id"], "->", exp.get("job_title"), exp.get("company_name"))
if not DRY:
changed += push(person, cd)
print(f"records spent: {spent} written: {changed}")Copper limits all calls to 180 a minute, and bulk endpoints to 3 a second (September 2026). The loop above makes two calls per person with a pause, so it stays under both.
Step 5: Find the contacts Copper does not have yet
Enrichment fixes the rows you hold. To add rows, size the list first. The count is free and spends no records.
curl -s "https://api.leadocean.io/v1/people/search?count=true&limit=1" \
-H "x-api-key: $LEADOCEAN_API_KEY" -H "content-type: application/json" \
-X POST -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 for the next page. Create each result in Copper with POST /developer_api/v1/people. That endpoint takes name, emails and phone_numbers (September 2026). Paid plans can also use POST /v1/exports for up to 50,000 rows a request and 500,000 a day, then import the CSV into Copper.
What you get
The dry run prints one line per contact, and the final line is your audit trail. This is the format with placeholder values.
4210001 -> Head of Sales Acme
4210002 -> VP Marketing Example Co
records spent: 2 written: 0The enrich call returns this shape. It is the response shape from the API reference, with placeholder values.
{
"success": true,
"data": {
"contact_data": {
"email_status": "verified",
"contact_phones": [{ "phone": "+1 415 555 0133" }],
"contact_current_experiences": [
{ "company_name": "Acme", "company_domain": "acme.com", "job_title": "Head of Sales" }
]
}
}
}Sizing run on 2026-10-21 with leadocean_count_leads: jobLevel VP, jobFunction Sales & Business Development, country US. It returned 129,494 people. The tool's default counts only verified, catch_all_valid and catch_all addresses, so that is a mailable count. The same filters with no email condition match 350,517. The REST meta.total is capped at 100,000, so the curl in Step 5 shows 100,000 for this segment.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
| 401 from LeadOcean | Missing or wrong x-api-key. | Re-export $LEADOCEAN_API_KEY from app.leadocean.io. |
| 402 | Out of records. On Free that is after your 1,000. | Read GET /v1/account, then move to Solo at $97 a month. |
| 429 from LeadOcean | Above the plan's rate limit: 5 a second on Free, 200 on Agency. | Sleep and retry. A sequential loop will not hit it. |
| 429 from Copper | Above 180 requests a minute, or 3 a second on bulk endpoints. | Slow the loop. The pause in Step 4 is enough. |
| 404 on enrich | We hold nothing for that email. | Not billed. Skip the row. |
| Empty search results | A bad enum value, such as vp for jobLevel. | Use exact strings: VP, C-Team. Check leadocean_list_enum_values. |
FAQ
Does LeadOcean have a native Copper integration?
No. There is no Copper app, and no Chrome extension. You call both APIs from a script, a scheduled job or a workflow tool such as n8n.
Will this overwrite titles my team typed in?
Not as written. Step 1 keeps only people with no title, and Step 3 sends a phone only when Copper holds none. Widen the filter on purpose, never by accident.
How many records does a run spend?
One per contact we hold. A 100-contact run spends at most 100 records. The dataset refreshes monthly, so re-running the same contact inside 30 days returns the same record and spends another.
What does it cost after the free 1,000?
Solo is $97 a month for one person. Agency is $497 a month for teams, with 200 requests a second. Paid plans have a fair-use pace, not credits. See pricing.
For other CRMs, see the best CRM enrichment tools, Zoho CRM enrichment tools and the CRM enrichment use case.
Fill your Copper contacts with titles and phones
Free to start. No credit card. 1,000 records to spend whenever you like.
Get your free API key →