Pre-Black Friday: 20% off, for as long as you stayEARLYBFon Solo and AgencyEnds Nov 28Start free

Salesforce integration

LeadOcean + Salesforce: enrich Contacts on a schedule

A script that pulls Contacts missing data, enriches each one through the LeadOcean API and writes the result back with Bulk API 2.0.

Get your free API key →Free to start. No credit card. 1,000 records to spend whenever you like.

You get a job that finds Salesforce Contacts with a blank email, phone or title, enriches each one through LeadOcean, and writes the answers back. It runs outside Salesforce, so it needs no package and no Apex. It talks to Salesforce through Bulk API 2.0 and to LeadOcean through https://api.leadocean.io. LeadOcean has no Salesforce app. This is a plain HTTP recipe.

How it works

Bulk API 2.0 suits this job. Salesforce says any operation over 2,000 records is a good candidate for it, and in Bulk API 2.0 only ingest jobs consume batches, not query jobs (Salesforce, September 2026).

  1. Add two Contact fields. Create a date field LeadOcean_Enriched_On__c and a text field LeadOcean_Email_Status__c. The date is your dedupe flag.
  2. Get a LeadOcean key. Sign up at app.leadocean.io/register, no card. Export it as LEADOCEAN_API_KEY.
  3. Query the gap. POST /services/data/vXX.X/jobs/query with a SOQL string. Results come back as CSV (Salesforce, September 2026).
  4. Enrich each row. For every distinct email in the CSV, call POST /v1/people/enrich. Send linkedin_url instead when the Contact has one and no email.
  5. Write back. Build a CSV keyed on the Contact Id and create a Bulk API 2.0 ingest job with operation update. Upload it, then set the state to UploadComplete (Salesforce, September 2026).
  6. Schedule it. Run the script from cron or any CI scheduler, daily or weekly. The date filter in step 3 means each run only touches new gaps.

Keep the first run small. Add LIMIT 100 to the SOQL, run the whole loop, and open five Contacts in Salesforce to check the values. Then raise the limit. A sandbox is the safest place for that first pass.

Use update, not upsert. Every row already exists in Salesforce, so write by Id. Upsert needs an external ID field and suits loads where records may be new (Salesforce, September 2026). Writing by Id also means the job never creates a duplicate Contact.

Configuration

The SOQL in step 3. It picks Contacts that have a LinkedIn URL or email, are missing data, and were never enriched:

json
{
  "operation": "query",
  "query": "SELECT Id, Email, Phone, Title, LinkedIn_URL__c FROM Contact WHERE LeadOcean_Enriched_On__c = NULL AND (Email = NULL OR Phone = NULL OR Title = NULL) AND (Email != NULL OR LinkedIn_URL__c != NULL) LIMIT 10000"
}

LinkedIn_URL__c is a custom field name. Use whatever yours is called. The enrich call in step 4, with a placeholder email:

bash
curl -X POST https://api.leadocean.io/v1/people/enrich \
  -H "x-api-key: $LEADOCEAN_API_KEY" \
  -H "content-type: application/json" \
  -d '{"email":"jane.doe@acme.com","reveal_email":true}'

Send exactly one of linkedin_url, email, phone or person_id. reveal_email costs no extra record, and phones only come back when it is true. A person LeadOcean holds nothing for is a 404 and costs nothing. Stamp those Contacts with today's date anyway, so they are not retried every run.

The ingest job body for step 5:

json
{ "object": "Contact", "operation": "update", "contentType": "CSV", "lineEnding": "LF" }

Field mapping

The enrich response is one leadocean.person.v1 object under data, with profile_data, contact_data and resume_data (LeadOcean OpenAPI, October 2026). Paths below start inside data.

LeadOcean fieldSalesforce fieldNote
contact_data.contact_emails[].emailEmailWrite only if Email is blank. Pick the entry whose type is work.
contact_data.contact_emails[].statusLeadOcean_Email_Status__cverified, catch_all_valid or catch_all are mailable. Read it before sending.
contact_data.contact_phones[].phonePhoneWrite only if blank. Skip the field when none comes back.
contact_data.contact_current_experiences[].job_titleTitleCurrent role. Overwrite only a blank or stale title.
contact_data.contact_current_experiences[].company_domainAccount.WebsiteUse for account matching, not Contact writes.
profile_data.profile_headlineDescriptionOptional. Handy for routing.
today's dateLeadOcean_Enriched_On__cYour dedupe flag. Set it on 404s too.

Dedupe in three places. The date field stops reruns. Calling LeadOcean once per distinct email stops paying twice for duplicate Contacts. Writing by Id creates no new records.

Past roles come back too. Since October 2026 the response carries resume_data.experiences[] with title, company name and dates for each past role LeadOcean holds. Past employers have no company id yet. Write them to a long text field if your reps use job history.

Cost at 10,000 rows

Enriching 10,000 Contacts costs 10,000 records on LeadOcean, which fits Solo at $97 a month. Solo allows 10k enrichments every 5 hours and 50k a week, at 50 requests a second. Free holds 1,000 records for life, enough to test one slice. Agency is $497 a month if a team shares the pace.

LeadOceanSalesforce
Test runFree: 1,000 records, one-off, no cardDeveloper Edition: 15,000 API calls a day
10,000 rowsSolo, $97 a month. No per-record priceBulk API 2.0: a few calls, 1 batch for up to 10,000 records
Row-by-row REST insteadSame record count10,000 API calls against the daily total

Salesforce sets the daily API total per org. Enterprise Edition gets 100,000 plus per-license calls plus purchased add-ons, and API calls from Bulk API 2.0 count toward it (Salesforce, September 2026). Bulk jobs are limited to 15,000 batches per rolling 24 hours, and a batch holds up to 10,000 records. One 10,000-row update job is about one batch. Query jobs consume none.

At 50 requests a second, 10,000 enrich calls take about 200 seconds. See pricing.

Troubleshooting

SymptomCauseFix
401 from LeadOceanKey missing or wrongSend it in the x-api-key header, no spaces.
400 from LeadOceanTwo identifiers sent, or noneSend one of email or linkedin_url.
402 from LeadOceanFree 1,000 records spentMove to Solo. Free records do not renew.
429 from LeadOceanOver the plan's rate limitWait the Retry-After seconds. Free is 5 a second, Solo 50.
REQUEST_LIMIT_EXCEEDED from SalesforceToo many long-running requests at onceProduction orgs allow 25 concurrent requests of 20 seconds or more. Run fewer in parallel.
Ingest job fails on a rowBad value for a field typeFetch failedResults for the job and fix those rows.
Same Contacts come back every runDedupe date not writtenStamp LeadOcean_Enriched_On__c on every row, 404s included.

FAQ

Does LeadOcean have a Salesforce app?

No. It is a REST API and an MCP server. This recipe calls the REST API from a script.

How do I get phone numbers?

Set reveal_email to true on the enrich call and read contact_data.contact_phones[].phone. For a short list you can also call GET /v2/people/phone. That endpoint costs one record per call, including a call that finds nothing.

Can I enrich Accounts too?

Yes. POST /v1/companies/enrich takes a domain. Query Accounts with a blank industry or headcount, then write back the same way.

How fresh is the data?

The dataset refreshes monthly, and each record carries its own fetched_at date. Rerun the job monthly with the dedupe date cleared for stale rows. More recipes sit in the integrations hub, such as Retool, Notion and Folk.

Enrich your first Salesforce Contacts on the free plan

Free to start. No credit card. 1,000 records to spend whenever you like.

Get your free API key →