"They just opened 6 SDR roles" is one of the strongest timing signals in outbound. If a company is hiring for the team that would use your product, you have a reason to reach out now. Most teams get that signal from LinkedIn scrapes or expensive data vendors. But most tech companies already publish their entire job board as clean JSON through their applicant tracking system (ATS). This tutorial shows how to turn that into a company-level hiring-signal feed you can refresh every week.

The data source: public ATS job-board APIs

Career pages are usually powered by one of a handful of ATSs, and each exposes a public, unauthenticated endpoint meant for embedding job lists:

ATS Public endpoint pattern
Greenhouse https://boards-api.greenhouse.io/v1/boards/{slug}/jobs?content=true
Lever https://api.lever.co/v0/postings/{slug}?mode=json (EU: api.eu.lever.co)
Ashby https://api.ashbyhq.com/posting-api/job-board/{slug}?includeCompensation=true
SmartRecruiters https://api.smartrecruiters.com/v1/companies/{id}/postings

These are the employer's system of record. Roles show up here first and disappear when they're filled, so the data is fresher and cleaner than most aggregators.

A 20-line DIY version (Greenhouse only)

import httpx, collections def greenhouse_signal(slug): jobs = httpx.get(f"https://boards-api.greenhouse.io/v1/boards/{slug}/jobs", params={"content": "true"}, timeout=30).json()["jobs"] by_dept = collections.Counter( (j.get("departments") or [{"name": "Other"}])[0]["name"] for j in jobs) sales = [j for j in jobs if any(k in j["title"].lower() for k in ("sdr", "bdr", "account executive", "sales"))] return {"company": slug, "open_roles": len(jobs), "sales_roles": len(sales), "top_departments": by_dept.most_common(5)} print(greenhouse_signal("stripe")) 

That works, but a real feed also needs:

  • detection of which ATS a company uses,
  • Lever's EU cluster and SmartRecruiters' paging,
  • seniority classification,
  • salary parsing,
  • memory between runs, so you can say "+11 roles since last week".

That last part is what makes it a signal rather than just a list.

The hosted version: Hiring Signal Feed

Hiring Signal Feed is an Apify Actor that does all of the above. It returns one row per company, with:

  • open_roles, roles_by_department, roles_by_seniority, remote_roles
  • new_roles_7d and new_roles_30d
  • open_roles_change, roles_added_since_last_run and roles_closed_since_last_run, from a snapshot it keeps for you
  • salary_bands and the ats the company uses

Optionally, it also returns normalized job rows. Pricing is $0.01 per company row (or $0.0015 per job row), plus $0.02 per run.

pip install apify-client pandas 
import os, pandas as pd from apify_client import ApifyClient client = ApifyClient(os.environ["APIFY_TOKEN"]) accounts = ["greenhouse:stripe", "greenhouse:discord", "ashby:ramp", "https://jobs.lever.co/spotify", "notion"] # bare names are auto-detected  run = client.actor("eligible_fame/hiring-signal-feed").call(run_input={ "companies": accounts, "outputMode": "companies", "titleKeywords": ["sales", "sdr", "bdr", "account executive"], "snapshotStoreName": "my-sales-hiring-watch", }) df = pd.DataFrame(client.dataset(run["defaultDatasetId"]).iterate_items()) df = df[df.row_type == "company"] print(df[["company", "ats", "open_roles", "new_roles_30d", "roles_added_since_last_run"]] .sort_values("new_roles_30d", ascending=False)) 

Because titleKeywords is set, every count is computed only on sales roles. So new_roles_30d = 9 means nine new sales jobs this month.

Score and route accounts

df["score"] = (df.new_roles_30d.fillna(0) * 2 + df.roles_added_since_last_run.fillna(0) * 3 + df.open_roles.fillna(0) * 0.2) hot = df[df.score >= 10].sort_values("score", ascending=False) hot.to_csv("hot_accounts.csv", index=False) 

Get the actual postings for personalization

Run again with outputMode: "jobs" for just the hot accounts. Each job row includes title, department, location, posted_at, salary and the posting url. That's ideal for a first line like "Saw you're hiring a Founding AE in Austin…".

Make it weekly

  1. Save the input as an Apify Task and attach a Schedule (for example, Mondays at 07:00).
  2. Keep snapshotStoreName the same, so each run diffs against the last one.
  3. Pipe results to your CRM:
    • In Clay, use an HTTP API column that calls run-sync-get-dataset-items.
    • In n8n, Make or Zapier, use the Apify app (Run Actor, then Get dataset items, then upsert into HubSpot or Sheets).

cURL for the HTTP column:

curl -X POST "https://api.apify.com/v2/acts/eligible_fame~hiring-signal-feed/run-sync-get-dataset-items?token=$APIFY_TOKEN" \ -H "Content-Type: application/json" \ -d '{"companies":["{{domain_or_slug}}"],"outputMode":"companies"}' 

Signals beyond sales hiring

The same feed answers other questions. A few common ones:

  • Selling data tools: filter for data engineer and analytics.
  • Selling security: watch for security and GRC roles.
  • Investors: track open_roles_change_pct across a portfolio for early momentum or slowdowns.
  • Recruiters: look for companies whose new_roles_7d jumps in your niche.

A sudden spike in manager and lead/principal seniority often means a new team is being built. That's usually a better trigger than one-off backfills.

Limitations to know

  • It only covers companies that use these four ATSs. That's most venture-backed tech, plus many enterprises on SmartRecruiters, but not every employer. If a company isn't found, you get a free error row.
  • posted_at semantics differ between ATSs. Some companies repost roles, so the snapshot diff (roles_added_since_last_run) is the most reliable "new" signal.
  • Salary only appears where the employer publishes it.

Wrap-up

Hiring data is the cheapest high-intent signal there is, because companies publish it themselves. Whether you write the 20-line script or use Hiring Signal Feed, the key is tracking change over time at the company level, not just collecting rows.