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

How-to guide

How to Track Job Openings for Your Outbound (and Why It Works)

Poll the public job boards of your target accounts, turn each new role into a buyer, and look that buyer up in one call.

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

TL;DR

  • A job opening is a public statement of intent. A company hiring a head of RevOps is about to spend on RevOps tooling. The role tells you who to write to and what to say.
  • LeadOcean does not scrape job boards. You poll the boards (free), and LeadOcean answers the next question: who owns that problem at that company, with a checked work email.
  • Setup takes about 40 minutes. Polling and sizing cost nothing. Each opening you act on spends about 3 to 5 records, so the 1,000 free records cover 200 or more openings.

Prerequisites

  • A LeadOcean key in $LEADOCEAN_API_KEY. The free plan works: 1,000 records, no card, 5 requests a second.
  • Python 3.9 or newer. The script uses only the standard library.
  • A list of target accounts with the job board token of each. For Greenhouse it is the last part of boards.greenhouse.io/{token}.
  • A place to send results: a file, a Slack webhook or your sequencer's import.
bash
export LEADOCEAN_API_KEY="paste-your-key-here"

Why a job opening is worth a message

An opening tells you three things a firmographic list cannot: the problem is funded, it is urgent, and you know which team owns it. A posted role means a headcount budget was approved. That is a stronger buying signal than a company size band.

Use it in three ways.

  • Sell to the hiring team. A company hiring three SDRs needs prospecting data and a sequencer. Write to the sales leader.
  • Sell the replacement for the hire. A role that a tool could cover is a pitch: show what the tool does before the seat is filled.
  • Sell to recruiters. Many openings at one account means the talent team is under load.

The signal decays. A role open for a week is a better reason to write than one open for three months. Poll daily and act on new entries only.

Step 1: List accounts and their board tokens

Greenhouse publishes each company's open roles as JSON with no authentication. The Job Board API lists them at GET https://boards-api.greenhouse.io/v1/boards/{board_token}/jobs. Other applicant tracking systems have their own public feeds. Check each vendor's docs before you rely on one.

Save this as watchlist.json. The values are placeholders.

json
[
  {"domain": "acme.com", "board": "acme"},
  {"domain": "example.io", "board": "exampleio"}
]

Step 2: Pull open roles and keep the new ones

The script fetches each board, remembers job ids it has seen and keeps only new roles whose title matches the teams you sell to. State lives in a local file.

python
import json, os, re, urllib.request

MATCH = re.compile(r"(revops|revenue operations|sales development|sdr|demand gen|growth)", re.I)
seen = set(json.load(open("seen.json"))) if os.path.exists("seen.json") else set()
hits = []

for acct in json.load(open("watchlist.json")):
    url = f"https://boards-api.greenhouse.io/v1/boards/{acct['board']}/jobs"
    jobs = json.load(urllib.request.urlopen(url, timeout=20))["jobs"]
    for job in jobs:
        key = f"{acct['board']}:{job['id']}"
        if key not in seen and MATCH.search(job["title"]):
            hits.append({"domain": acct["domain"], "title": job["title"],
                         "url": job["absolute_url"], "updated_at": job["updated_at"]})
        seen.add(key)

json.dump(sorted(seen), open("seen.json", "w"))
json.dump(hits, open("hits.json", "w"), indent=2)
print(f"{len(hits)} new matching roles")

The first run reports every matching open role. Run it once to seed seen.json and discard that output, or the first batch will be old news.

Step 3: Map the role to a buyer

Decide who feels the problem before you search. The hiring team is rarely the person who signs.

Role postedSearch for
SDR or sales developmentjobFunction Sales & Business Development, jobLevel VP or Director
RevOps or revenue operationstitle containing "revenue operations", seniority head or director
Demand generationjobFunction Advertising & Marketing, jobLevel VP or Director

Keep the table in your script as a dictionary keyed on the matched word. One role, one buyer profile.

Step 4: Size the buyers for free

Before you spend a record, check that anyone fits. A count is free. Send count=true and limit=1 as query parameters, not in the body.

bash
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 '{
    "domain": ["acme.com"],
    "jobFunction": ["Sales & Business Development"],
    "jobLevel": ["VP", "Director"]
  }'

Read meta.total. If it is 0, skip the account. Enum values are exact strings, so check them with GET /v1/enums (free).

Step 5: Pull the people and enrich one

Run the same body without count. Filters are arrays, the body is flat JSON, and limit runs from 1 to 100.

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"],
    "jobFunction": ["Sales & Business Development"],
    "jobLevel": ["VP", "Director"],
    "emailStatus": ["verified", "catch_all_valid"],
    "limit": 3
  }'

This spends up to 3 records, one per person returned. To get the address, enrich by person_id, which each row carries at meta.source_ids.person_id. 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": "PERSON_ID_FROM_SEARCH", "reveal_email": true}'

Then write the opener from the opening, not from the person:

text
Subject: Your SDR hiring at Acme

Hi Jane,

Saw Acme is hiring a Sales Development Rep (link). A new rep
needs a list on day one. Want me to send a sample of 25 for
your ICP before the seat is filled?

Step 6: Run it daily

Chain steps 2 to 5 in one script and schedule it. Cron, GitHub Actions and n8n all work.

bash
0 7 * * 1-5 cd /opt/jobwatch && python3 watch.py && python3 lookup.py

An agent can do steps 4 and 5 through the hosted MCP server at https://api.leadocean.io/mcp. It exposes 13 tools. The sizing tool, leadocean_count_leads, is free. For the same pattern on product launches, see tracking product updates.

Which openings are worth a message

Rank hits before they reach a person.

  • Leadership roles. A new head of sales or RevOps will own the tooling budget. Highest value.
  • Clusters. Three roles on one team in one week means a funded push. Write once, to the team lead.
  • Single junior roles. Weak. Skip unless the role names a tool you replace.
  • Roles open over 60 days. The company may be struggling to hire. Good for recruiting and automation pitches, poor for anything else.

Send at most one opening-based email per account per 30 days. Log the date next to the job id in seen.json.

What you get

The watcher prints one line and writes hits.json. A run looks like this (placeholder data).

json
[
  {
    "domain": "acme.com",
    "title": "Sales Development Representative",
    "url": "https://boards.greenhouse.io/acme/jobs/1000001",
    "updated_at": "2026-10-20T09:30:00-04:00"
  }
]

The free count from step 4, run on 2026-10-21 through leadocean_count_leads. Filters: country US, employeeRange 51-200, jobLevel VP, jobFunction Sales & Business Development.

Result:

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

That is 18,567 sales VPs at US companies of 51 to 200 people. The count is mailable by default (verified, catch-all valid or catch-all email). Without an email filter the same query matches 60,456 people. The call spent no records.

The people search returns the documented shape, abbreviated below (response shape from the API reference, placeholder values).

json
{
  "data": [
    {
      "profile_data": {"profile_full_name": "Jane Doe", "profile_headline": "VP Sales"},
      "contact_data": {
        "has_email": true,
        "email_status": "verified",
        "contact_current_experiences": [{"job_title": "VP Sales", "company_name": "Acme", "company_domain": "acme.com"}]
      },
      "meta": {"source_ids": {"person_id": "1234567890"}}
    }
  ],
  "meta": {"count": 1, "limit": 3, "credits": 1, "nextCursor": null}
}

Use fields to keep only what you need, for example "fields": ["profile_data.profile_full_name"].

Troubleshooting

ErrorCauseFix
401Missing or wrong x-api-keyExport $LEADOCEAN_API_KEY in the shell that runs the script.
429Requests above the plan's rate limit (5 a second on Free, 200 on Agency)Honour Retry-After and sleep between accounts.
402Free records are spentMove to Solo ($97 a month) or Agency ($497 a month). See pricing.
Empty dataAn enum value that does not match exactly, or nobody at that domain fitsRun the free count first and check values with GET /v1/enums.
Board returns 404Wrong board token, or the company uses another ATSOpen the careers page and read the token from the URL.
Export stopsOne export request holds up to 50,000 rows and a day holds 500,000Split the list across requests or days.

FAQ

Does LeadOcean detect job openings itself?

No. LeadOcean holds people and company data, not postings. The opening comes from the board you poll. LeadOcean tells you who to contact once one appears.

Is it fine to use public job boards this way?

The Greenhouse Job Board API is documented as publicly available with no authentication on read endpoints. Other boards have their own terms, so read them before you automate. Also respect the rate limits they state.

How many records does this spend?

About 3 to 5 per opening you act on: up to 3 for the search rows and one for the work email. At 40 openings a month that is under 200 records. Paid plans use a fair-use pace, not credits.

What about a vendor that sends job alerts for me?

Some data vendors sell monitoring as a paid service. Crustdata's write-up covers the idea from a vendor's side. The script above is self-run and free. An alert service saves the scripting.

How is this different from job-change signals?

A job opening shows a company's intent. A job change shows a person's move. You can use both: see job change signals and the recipe for writing to a mover's former boss. Every guide is in the blog.

Find the buyer behind every opening

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

Get your free API key →