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

Integrations

LeadOcean + Google Sheets: enrich rows automatically

A short Apps Script that reads a column of emails or LinkedIn URLs, calls LeadOcean, and writes the result back to the same row.

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

You can enrich a Google Sheet with LeadOcean using Apps Script and no add-on. The script reads an email or LinkedIn URL from each row, calls POST /v1/people/enrich, and writes job title, company and phone back to the same row. Google Sheets has no native HTTP block, so Apps Script's UrlFetchApp is the integration.

This is a recipe, not a marketplace app. LeadOcean has no Sheets add-on, and you do not need one.

How it works

  1. Create an API key. Sign up at app.leadocean.io, open API keys, and copy a key. The Free plan gives both scopes (search and enrich) inside 1,000 records.
  2. Lay out the sheet. Put emails in column A or LinkedIn URLs in column B. Leave columns C to G empty for the output, and add a status column in H.
  3. Store the key. In Extensions, Apps Script, open Project Settings and add a script property named LEADOCEAN_API_KEY. Never paste the key into a cell or the code.
  4. Paste the script from the Configuration section and save.
  5. Run enrichRows once by hand. Google asks you to authorize external requests. Accept it for your own script.
  6. Watch the rows fill. Rows with a result get done in column H. A person LeadOcean holds nothing for gets no match.
  7. Schedule it. In Triggers, add a time-driven trigger for enrichRows, every hour or every day.

Dedupe is built in. The script skips any row where column H is already filled, and it skips an email it has already seen in the same run. A re-run never spends a second record on the same row.

Configuration

This script uses the canonical endpoint and the x-api-key header. It processes 200 rows per run so it stays inside Apps Script's 6-minute execution limit. RATE sends that many calls at once, then waits a second, to stay under your plan's requests a second.

javascript
const BASE = "https://api.leadocean.io";
const BATCH = 200;
const RATE = 5;   // requests a second: 5 on Free, 50 on Solo, 200 on Agency

function enrichRows() {
  const key = PropertiesService.getScriptProperties().getProperty("LEADOCEAN_API_KEY");
  const sheet = SpreadsheetApp.getActiveSheet();
  const last = sheet.getLastRow();
  if (last < 2) return;
  const rows = sheet.getRange(2, 1, last - 1, 8).getValues(); // A..H
  const seen = {};
  const todo = [];

  rows.forEach((r, i) => {
    const email = String(r[0]).trim().toLowerCase();
    const linkedin = String(r[1]).trim();
    const id = email || linkedin;
    if (!id || r[7] || seen[id]) return;       // skip done and duplicate rows
    seen[id] = true;
    if (todo.length < BATCH) todo.push({ row: i + 2, email, linkedin });
  });

  const requests = todo.map(t => ({
    url: BASE + "/v1/people/enrich",
    method: "post",
    contentType: "application/json",
    headers: { "x-api-key": key },
    payload: JSON.stringify(t.email
      ? { email: t.email, reveal_email: true }
      : { linkedin_url: t.linkedin, reveal_email: true }),
    muteHttpExceptions: true
  }));

  for (let s = 0; s < requests.length; s += RATE) {
    const responses = UrlFetchApp.fetchAll(requests.slice(s, s + RATE));
    responses.forEach((res, k) => {
      const row = todo[s + k].row;
      const code = res.getResponseCode();
      if (code === 404) { sheet.getRange(row, 8).setValue("no match"); return; }
      if (code !== 200) { sheet.getRange(row, 8).setValue("error " + code); return; }
      const d = JSON.parse(res.getContentText()).data;
      const prof = d.profile_data || {};
      const c = d.contact_data || {};
      const job = (c.contact_current_experiences || [])[0] || {};
      const phone = (c.contact_phones || [])[0] || {};
      sheet.getRange(row, 3, 1, 5).setValues([[
        prof.profile_full_name || "", job.job_title || "", job.company_name || "",
        phone.phone || "", prof.profile_url || ""
      ]]);
      sheet.getRange(row, 8).setValue("done");
    });
    Utilities.sleep(1000);
  }
}

The same request from a terminal, to test a key before you touch the sheet:

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. There is no name plus domain lookup. To start from a company domain, search first with /v1/people/search, then enrich by person_id.

The Free plan allows 5 requests a second, Solo 50 and Agency 200. fetchAll sends its list in parallel, so set RATE to your plan's figure or you will see 429 responses with a Retry-After header.

Field mapping

Paths are under data in the response, from the enrich docs, October 2026.

LeadOcean fieldSheet columnNote
input email or linkedin_urlA or BExactly one per call
profile_data.profile_full_nameCFull name as one string
contact_data.contact_current_experiences[0].job_titleDThe current role
contact_data.contact_current_experiences[0].company_nameESame object as the title
contact_data.contact_phones[0].phoneFOnly with reveal_email: true
profile_data.profile_urlGUseful as a stable key for later dedupe
script resultHdone, no match or error <code>

The script sends reveal_email: true, which also returns contact_data.contact_emails[] with a status per address. It costs no extra record.

Cost at 10,000 rows

Ten thousand rows cost 10,000 records at most, and fewer if some do not match. A person LeadOcean holds nothing for returns 404 and costs nothing.

ItemWhat it costs
LeadOcean Free$0, but the 1,000 records are for life, so Free covers 1,000 rows, not 10,000
LeadOcean Solo$97 a month. Allows 10k enrichments every 5 hours, so one pass fits
LeadOcean Agency$497 a month. 1M enrichments a day
Apps Script$0. Limit: 20,000 URL Fetch calls a day on a Gmail account, 100,000 on Google Workspace (Google quotas page, October 2026)

The Apps Script limit is the one to watch. At one call per row, a 10,000-row sheet uses half of a Gmail account's daily URL Fetch quota. Each execution is also capped at 6 minutes, which is why the script batches 200 rows and runs on a trigger.

Paid plans have a fair-use pace, not credits. Reaching it slows the key until the window resets. Nothing to top up. See pricing.

Troubleshooting

SymptomCauseFix
error 401 in column HKey missing or wrong property nameCheck the script property is LEADOCEAN_API_KEY and the header is x-api-key
error 402Free plan's 1,000 records are spentMove to Solo, or enrich a smaller list
error 429Calls sent faster than the plan's requests a secondSet RATE to your plan's figure, or move to a plan with a higher rate
Every row says no matchEmails are personal or mistypedTry the LinkedIn URL column, and trim whitespace
Script stops partway6-minute execution limitLower BATCH and let the hourly trigger finish the rest
"Authorization required"First run not approvedRun once by hand and accept the prompt

FAQ

Is there a LeadOcean add-on for Google Sheets?

No. This recipe uses Apps Script, which ships with every Sheet. You own the code and can change the mapping.

Can I enrich from a company domain?

Not directly. Enrich needs an email, LinkedIn URL, phone or person_id. Use GET /v1/companies/{domain}/people or /v1/people/search to list people at a domain, paste the person_id values, and enrich those.

Will a re-run charge me twice?

Not if column H is filled. The script skips finished rows. Clear the cell to re-enrich a row on purpose.

When should I skip the Sheet?

Use it for lists up to a few tens of thousands of rows that already live in a Sheet. Beyond that, the Apps Script quotas get in the way.

For a big list, skip the Sheet. Size it first with the free GET /v1/companies/count call or the leadocean_count_leads MCP tool, then pull it with POST /v1/exports. The app at app.leadocean.io also has an Exports page with a filter builder and a price preview, and you can import the CSV into a Sheet afterwards. Bulk CSV export is not on Free.

Keep one habit whichever route you take: read email_status before you send anything. Bounced emails are not refunded.

A Sheet is also a poor system of record. If several people edit the same rows while the trigger runs, the script can overwrite a hand edit in columns C to G. Give the script its own tab, and let people copy results out.

The same pattern works from Retool, Notion and Folk. Back to all integrations.

Fill your next Google Sheet from one API key

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

Get your free API key →