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

How-to guide

How to Build an Internal Sales Tool (With a Signal API)

Define a signal as filters, size it free, pull the buyer at each matching account and alert your team. Four calls and one script.

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

An internal sales tool on signal data is a saved query, a nightly diff and an alert. This guide builds it against LeadOcean's REST API with real queries. It takes about 60 minutes.

A signal here means a filter you can query: a funding round, a detected technology, a headcount band, a buyer's job level. The dataset is refreshed monthly and each record carries a fetched_at date. "Real time" in your tool means you poll when you choose, not that the data changes by the minute.

TL;DR

  • Time: about 60 minutes with curl and a 40-line Python script.
  • Cost: about 53 LeadOcean records on the example (25 company rows, 25 buyer rows, 3 enrichments). Counts are free.
  • Result: a script that finds accounts matching a signal, names the buyer at each one and posts only new matches to Slack.

Prerequisites

  • A LeadOcean account on the free plan: 1,000 records for life, no card, 5 requests a second. Start free.
  • An API key from the app, exported as $LEADOCEAN_API_KEY.
  • curl and Python 3 with requests installed.
  • A Slack incoming webhook URL, exported as $SLACK_WEBHOOK_URL. Any endpoint that accepts a JSON POST works instead.
  • An ICP in one sentence. The example: US companies with 51-200 staff that run HubSpot and raised a Series A in 2025 or later.

Step 1: Define the signal and size it for free

Write the signal as filters, then count before you spend. Send count=true as a query parameter with limit 1. You get a total and no rows.

bash
curl -s -X POST "https://api.leadocean.io/v1/people/search?count=true" \
  -H "x-api-key: $LEADOCEAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "jobLevel": ["VP"],
    "jobFunction": ["Sales & Business Development"],
    "country": ["US"],
    "employeeRange": ["51-200"],
    "technologies": ["HubSpot"],
    "limit": 1
  }'

Filters are arrays of strings. Enum values are exact: VP not vp, and Sales & Business Development with the ampersand. Fetch the list with GET /v1/enums/job_function or the MCP tool leadocean_list_enum_values.

To size the accounts instead of the people, call GET /v1/companies/count with the company filters. It is free and returns breakdowns by industry, headcount band, HQ country and HQ state.

Do not put funding filters in the free count. Count without them, then expect the funded list to be smaller at search time.

Step 2: Find the accounts that match the signal

Company search takes the funding filters. Each company returned costs one record.

bash
curl -s -X POST "https://api.leadocean.io/v1/companies/search" \
  -H "x-api-key: $LEADOCEAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "employeeRange": ["51-200"],
    "hqCountry": ["US"],
    "technologies": ["HubSpot"],
    "fundingType": ["Series A"],
    "minLastFundingYear": 2025,
    "hasMailableContacts": true,
    "limit": 25
  }'

hasMailableContacts: true drops accounts where nobody has a usable email. That keeps you from paying for accounts you cannot reach. Page with meta.nextCursor passed back as cursor. One search pages to 10,000 rows, then meta.depthCapped is true. Narrow the filters instead of walking the index.

Keep each row's company domain. Step 3 uses it.

Step 3: Pull the buyer at each account

People search takes a domain array, so one call covers a batch of accounts. Ask for the job level you sell to.

bash
curl -s -X POST "https://api.leadocean.io/v1/people/search" \
  -H "x-api-key: $LEADOCEAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "domain": ["acme.com", "example.io"],
    "jobLevel": ["VP", "C-Team"],
    "jobFunction": ["Sales & Business Development"],
    "emailStatus": ["verified", "catch_all_valid"],
    "limit": 25
  }'

Rows are thin and carry no contact points. Each has meta.source_ids.person_id. Keep it, because enrich takes person_id and there is no name plus domain lookup. To list everyone at one account, use GET /v1/companies/acme.com/people. It stops at 10,000 rows.

Step 4: Diff nightly and alert Slack

The tool is a script that remembers what it has seen. This one reruns steps 2 and 3, skips known accounts and posts the new ones.

python
import json, os, pathlib, requests

BASE = "https://api.leadocean.io"
H = {"x-api-key": os.environ["LEADOCEAN_API_KEY"]}
SEEN = pathlib.Path("seen.json")
seen = set(json.loads(SEEN.read_text())) if SEEN.exists() else set()

signal = {
    "employeeRange": ["51-200"], "hqCountry": ["US"],
    "technologies": ["HubSpot"], "fundingType": ["Series A"],
    "minLastFundingYear": 2025, "hasMailableContacts": True,
    "limit": 25,
}
companies = requests.post(f"{BASE}/v1/companies/search", headers=H, json=signal).json()["data"]
domains = [c["company_data"]["company_domain"] for c in companies]
new = [d for d in domains if d not in seen]
if not new:
    raise SystemExit("nothing new")

buyers = requests.post(f"{BASE}/v1/people/search", headers=H, json={
    "domain": new, "jobLevel": ["VP", "C-Team"],
    "jobFunction": ["Sales & Business Development"], "limit": 25,
}).json()["data"]

lines = [f"{len(new)} new accounts, {len(buyers)} buyers found"]
requests.post(os.environ["SLACK_WEBHOOK_URL"], json={"text": "\n".join(lines)})
SEEN.write_text(json.dumps(sorted(seen | set(new))))

Run it from cron once a day. The data moves monthly, so most nights find no new domains. The company search still returns its 25 rows each run, so a nightly run spends up to 25 records. Run it weekly if that matters. The domain sits at company_data.company_domain in the company record.

Step 5: Enrich only the buyers a rep picks

Search wide, enrich narrow. Enrich takes exactly one of linkedin_url, email, phone or person_id.

bash
curl -s -X POST "https://api.leadocean.io/v1/people/enrich" \
  -H "x-api-key: $LEADOCEAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"person_id": "1234567890", "reveal_email": true}'

reveal_email defaults to false. Set it to true to get the address, at no extra record. The response also carries past roles in resume_data, which is useful for a first line. A 404 means nobody matched and costs nothing. Check email_status before anyone sends, because bounced emails are not refunded.

For hundreds of accounts, stop looping. POST /v1/exports takes up to 50,000 rows a request and 500,000 a day. Bulk CSV export is not on Free. It is included on Solo ($97 a month) and Agency ($497 a month).

Step 6: Put the free count in front of every pull

Four choices decide whether the tool stays cheap and trusted. The first is a free count before any pull, so reps see the list size before a record is spent.

python
count = requests.post(f"{BASE}/v1/companies/count", headers=H, json={
    "employeeRange": ["51-200"], "hqCountry": ["US"], "technologies": ["HubSpot"],
}).json()["data"]["total"]
print(f"{count} accounts match before the funding filter")
  • Count first, always. Put the free count behind a button. Reps see the list size before any record is spent.
  • Store the person_id and the domain. They let you re-enrich later without carrying a LinkedIn URL or email between systems.
  • Keep one signal per saved query. A funding query and a technology query should alert separately, so a rep knows why an account showed up.
  • Check fetched_at before outreach. A role that changed since the last monthly refresh will not show yet. Verify the title on LinkedIn for top accounts.

On a paid plan the same script runs inside a fair-use pace, not a credit balance. Reaching the pace slows the key until the window resets. Nothing stops and nothing needs a top-up.

What you get

Step 1 ran on 2026-10-21 through the free leadocean_count_leads tool. Query: jobLevel VP, jobFunction Sales & Business Development, country US, employeeRange 51-200.

json
{ "total": 18567, "totalIsExact": true, "totalAll": 60456, "distinctCompanies": 12192, "credits": 0 }

total counts people with a mailable email (verified, catch_all_valid or catch_all). totalAll is the same filters with no email condition. Adding technologies: ["HubSpot"] on the same date gave 4,781 mailable VPs (13,902 with no email condition) at 3,069 employers.

leadocean_count_companies on 2026-10-21 with employeeRange 51-200, hqCountry US and technologies HubSpot counted 22,147 companies. California held 3,653 of them.

A people search row (response shape from the API reference, placeholder values):

json
{
  "data": [
    {
      "profile_data": { "profile_full_name": "Jane Doe" },
      "meta": { "source_ids": { "person_id": "1234567890" } }
    }
  ],
  "meta": { "nextCursor": "opaque-string", "depthCapped": false }
}

Troubleshooting

ErrorCauseFix
401Missing or wrong x-api-keyRe-copy the key from the app. Confirm $LEADOCEAN_API_KEY is set in that shell.
429Above the plan's rate limit: 5 requests a second on Free, 200 on AgencyHonour Retry-After and sleep between calls.
402Out of recordsThe free plan holds 1,000 records for life. Move to a paid plan, see pricing.
Empty resultsA bad enum value, or filters too narrowFetch exact strings from GET /v1/enums/{name}. Drop fundingType or widen minLastFundingYear.
meta.depthCapped trueOne search reached 10,000 rowsNarrow the filters, for example by hqState or employeeRange.
400 export_limit_rowsAsked for more than 50,000 rows in one exportSplit the filters into several exports.

FAQ

Is the data real time?

No. The dataset is refreshed monthly and each record has a fetched_at date. Poll on your own schedule and diff against what you have seen.

What counts as a signal?

Any filter you can query: last funding type and year, detected technologies, headcount, job level, job function. Hiring and social signals are covered in hiring signals and how to track them and the social posts API guide.

How many records will a pilot cost?

The example spends about 53. The free plan holds 1,000 once, with no card.

Can an AI agent run the watchlist?

Yes. The hosted MCP server at https://api.leadocean.io/mcp exposes 13 tools, including free counts. Sign-in is OAuth, so no key is pasted.

Does it include a sequencer or CRM?

No. LeadOcean is the data layer. Send the output to your own CRM or sequencer, or to Slack as above.

Related reading: internal recruiting tools on people data uses the same pattern.

Size your first signal list for free

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

Get your free API key →