Hiring Signals API — Open Roles & Hiring Surge by Company avatar

Hiring Signals API — Open Roles & Hiring Surge by Company

Pricing

from $3.00 / 1,000 company hiring signal returneds

Go to Apify Store
Hiring Signals API — Open Roles & Hiring Surge by Company

Hiring Signals API — Open Roles & Hiring Surge by Company

Turn a list of companies, ATS boards or a preset into one row per company per run: open roles now, roles opened and closed since your last run, breakdowns by department, location and seniority, remote and salary-disclosure share, top titles and a hiring-surge flag. No API key.

Pricing

from $3.00 / 1,000 company hiring signal returneds

Rating

0.0

(0)

Developer

Insight Solutions

Insight Solutions

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

a day ago

Last modified

Share

Give it a list of companies and get hiring signals back, one row per company per run. Send company domains, ATS boards as ats:slug, career-page URLs or a named preset, and each row tells you how many roles the company has open right now, how many opened and closed since your last run, what it is hiring for and where, at what level, how much of it is remote, how much of it publishes a salary — and whether this is a hiring surge measured against that company's own 30-day average.

Every number is read live from the company's own applicant-tracking board — Greenhouse, Lever, Ashby, Workable, Recruitee, SmartRecruiters, Teamtailor, Personio, Breezy HR, Workday, JazzHR, BambooHR and Rippling — never from a job board, a cached index or a commercial database. One row per company per run, and one price for it. The surge is measured against that company's own 30-day average, and we print the average. The first run of a monitored company is free. And a company with zero open roles is still an answer, and still charged — "twelve roles open yesterday, none today, here is the board that proves it" is the hiring-freeze signal.

At a glance

Input — this is the Store prefill; paste it and run:

{ "companies": ["greenhouse:stripe", "lever:leverdemo", "ashby:ramp",
"workable:lyst", "recruitee:vandebron", "cloudflare.com"],
"mode": "monitor", "firstRunBehavior": "baseline-only", "emitRoles": false,
"maxRolesPerBoard": 2000, "maxRunSecs": 240, "maxConcurrency": 6,
"proxyConfiguration": { "useApifyProxy": true } }

Output — one company-signals row per company per run. The fields you will use most are openRoles, newRoles, closedRoles, hiringSurge, newRatio, byDepartment, bySeniority, remoteShare, salaryDisclosedShare and topTitles (full list under Output reference). hiringSurge is measured against that company's own trailing 30-day average, which is published beside it as newPerDay30Days and historyDays — so you can check it. A board we could not resolve or could not read comes back as a free diagnostic row (ok: false, errorType, error) instead of a charge.

Price — $5.00 per 1,000 companies per run on the FREE tier (+ $0.001 per run). The first run of any monitored company is free, and so are unresolved boards, boards held back by the outage guard, every diagnostic, the run summary, the webhook and every run that returns nothing. Job rows are optional and cost $0.001 each. Pay-per-event, no API key, no browser, limited permissions — works over the Apify MCP server (mcp.apify.com) and with agentic (x402) payments.

From code — client.actor("insight.solutions/hiring-signals-api").call(run_input={…}) with apify-client, or POST https://api.apify.com/v2/acts/insight.solutions~hiring-signals-api/run-sync-get-dataset-items.


What you get

One row per company. Abridged, from the test suite: stripe.com and greenhouse:stripe sent in one list, on the second day of a monitor whose history held two new roles in the week before — and six roles opened today.

{
"ok": true,
"rowType": "company-signals",
"input": ["stripe.com", "greenhouse:stripe"],
"companyKey": "greenhouse:stripe",
"companyName": "Stripe",
"companyDomain": "stripe.com",
"atsPlatform": "greenhouse",
"atsBoardUrl": "https://boards.greenhouse.io/stripe",
"boardSource": "ats",
"resolvedFrom": "registry",
"resolutionConfidence": 0.95,
"presets": ["fintech", "yc-top"],
"openRoles": 16,
"previousOpenRoles": 10,
"openRolesDelta": 6,
"newRoles": 6, "closedRoles": 0, "changedRoles": 0, "repostedRoles": 0,
"byDepartment": [{ "value": "Engineering", "count": 8, "share": 0.5 }],
"byCountry": [{ "value": "US", "count": 8, "share": 0.5 }, { "value": "IE", "count": 2, "share": 0.125 }],
"bySeniority": { "intern": 1, "junior": 1, "mid": 9, "senior": 1, "lead": 1,
"principal": 1, "manager": 1, "director": 1, "executive": 0 },
"remoteShare": 0.5,
"salaryDisclosedCount": 2, "salaryDisclosedShare": 0.125,
"topTitles": [{ "title": "Account Executive, Bridge", "count": 2 }],
"newTitles": ["Solutions Engineer 0", "Solutions Engineer 1", "Solutions Engineer 2"],
"hiringSurge": true,
"newRatio": 24,
"newPerDay30Days": 0.25,
"historyDays": 8,
"mode": "monitor", "firstRun": false,
"previousRunAt": "2026-09-16T06:00:00.000Z", "lastRunAt": "2026-09-17T06:00:00.000Z",
"truncated": false, "filtersApplied": [], "rolesFilteredOut": 0,
"requestCount": 1, "billable": true,
"source": "boards-api.greenhouse.io",
"sourceUrl": "https://boards.greenhouse.io/stripe"
}

Read the surge off the row: six new roles against a baseline of 0.25 a day over 8 days of history is a ratio of 24, and six clears the bar of max(5, 3 × 0.25) = 5. Nothing about that conclusion is hidden. The two spellings in input are one company, read once and billed once.

Which of our four jobs Actors to run

Four of our listings read job data. They are priced against each other on purpose:

You wantRunPrice
Every open role, with descriptions, as rowsCareer Site Jobs API (ats-jobs-api)$1.00 / 1,000 jobs
Only what changed, role by role, with previousValuesNew Job Postings Monitor (job-postings-monitor)$1.00 / 1,000 companies + $1.20 / 1,000 changes
One number per company: how many, what kind, and is it acceleratingthis Actor$5.00 / 1,000 companies per run
Remote roles from the public remote job boardsRemote Jobs API (remote-jobs-api)$0.50 / 1,000 jobs

job-postings-monitor costs $0.001 per company plus $0.0012 per change, so below four changes in a run it is cheaper and it gives you the roles. At four or more, this is cheaper — and it gives you the breakdown either way.

How a company is resolved

A board is the product here, so the Actor works hard to find the right one — and refuses to guess. Five rungs, cheapest first, stopping at the first verified board:

RungSourceRequestsresolvedFromresolutionConfidence
0You sent a board (greenhouse:stripe, a board URL) or a preset0input-board / preset1.0
1The bundled registry: 75 verified boards, each with the company's own domain0registry0.95
2The detection cache: a domain an earlier run resolved0detection-cache0.9, or the original rung's if lower
3A slug guess — greenhouse:, lever:, ashby: + the domain's label — verified, by the list itself or by the board's public page1–6slug-guess0.8 on a URL match (a posting on the company's domain, or the board page linking it), 0.7 on a company-name match (on the list, or in the board page's title)
4The company's own careers pages: /careers, /jobs, / — and www./careers when the apex never answers1–4, plus robots.txtcareers-page0.6
5Nothing found0a free ats-not-detected row—

Why an empty 200 is not an answer. Rung 3 asks boards-api.greenhouse.io for a board named after the domain. A board that answers HTTP 200 is accepted only if it carries at least one posting and it is demonstrably this company's: a posting URL on the company's own registrable domain, a company name on the list that matches the domain, or — one more request, because Ashby's list names no organisation and a Greenhouse company_name can say Fin on the intercom board — the board's public page (jobs.ashbyhq.com/plaid) linking the company's domain or carrying its name in the title. greenhouse:apex exists for some company called Apex; without that check every apex.com in the world would inherit it. A refused guess is not silent — it is a free row you can read:

{
"ok": false,
"rowType": "diagnostic",
"input": ["apex.com"],
"errorType": "slug-guess-rejected",
"error": "Greenhouse board \"apex\" answered HTTP 200 with zero postings, so nothing ties it to apex.com. An empty 200 is not an answer; the board was discarded and the ladder continued.",
"companyDomain": "apex.com",
"atsPlatform": "greenhouse",
"atsBoardSlug": "apex",
"sourceUrl": "https://boards-api.greenhouse.io/v1/boards/apex/jobs?content=true",
"billable": false
}

When every rung comes up empty you get one free ats-not-detected row carrying triedCandidates — up to twelve {rung, url, status, reason} entries, everything that was tried — and the sentence "Send the board as ats:slug and the signal runs."

Only Greenhouse, Lever and Ashby are guessed at. Workable, Recruitee, Personio, Breezy, Teamtailor, BambooHR, JazzHR and Rippling key their boards on a subdomain, where a wildcard host can answer for any name, so a guess there proves nothing; those platforms are found on the company's careers page (rung 4) or sent as ats:slug.

Deduplication is on the resolved board. stripe.com, www.stripe.com, greenhouse:stripe and https://boards.greenhouse.io/stripe in one list are one company, read once, billed once, with every spelling in input. A preset that includes a board you also named is folded in the same way. Free-mail domains (gmail.com), IP addresses and entries that are not a company are free rows before a single request.

A successful rung 3 or 4 is remembered. In monitor mode the answer is cached in your state store and renewed every time its board is read successfully, so a scheduled watchlist pays the ladder once per domain. A cached board that has since died is forgotten, so the next run resolves the domain afresh. snapshot mode stores nothing between runs, so a domain is resolved on every snapshot run.

Quick start

One company, as a one-off snapshot:

{ "companies": ["cloudflare.com"], "mode": "snapshot" }

A preset, monitored daily — 21 developer-tools companies from one line:

{ "companies": ["preset:devtools"], "mode": "monitor", "stateStoreName": "devtools-watch" }

Forty domains out of a CRM export, senior hiring only:

{ "companies": ["stripe.com", "brex.com", "ramp.com", "gusto.com", "…36 more"],
"mode": "snapshot", "seniorityLevels": ["manager", "director", "executive"] }

Presets are preset:devtools (21 boards), preset:fintech (15), preset:ai-labs (15) and preset:yc-top (16), drawn from the same verified registry the Career Site Jobs API ships. The run-summary row of any run lists their sizes.

To monitor, run once to lay down the baseline — free — then schedule it daily or weekly with Actions → Schedule Actor. Give each watchlist its own stateStoreName.

Input reference

FieldTypeDefaultWhat it does
companies (required)array of strings—Domains, ats:slug boards, career-page URLs or preset:<name>, one per line
modemonitor | snapshotmonitormonitor diffs against the previous run and keeps 90 days of history; snapshot is one run, no state, no diff
firstRunBehaviorbaseline-only | emit-allbaseline-onlybaseline-only: a company's first monitored run records the baseline, returns its row with every diff field null and charges nothing. emit-all: the first run is charged, exactly like a snapshot
emitRolesbooleanfalseAlso return one role row per posting. Charged at $0.001 each
roleScopenew | changed | allnewWhich postings emitRoles returns
webhookUrlstring (secret)noneOne POST when the run finishes — see Alerts
titleFilterarray of stringsnoneCount only roles whose title contains one of these
titleExcludearray of stringsnoneLeave out roles whose title contains one of these
seniorityLevelsmulti-selectallintern, junior, mid, senior, lead, principal, manager, director, executive
employmentTypesmulti-selectallFullTime, PartTime, Contract, Temporary, Internship, Volunteer, Other; roles with no published type are left out when set
workplaceTypesmulti-selectallremote, hybrid, onsite; roles with no signal are left out when set
countryCodesarray of stringsnoneISO 3166-1 alpha-2; country names and US states / Canadian provinces are matched too
locationFilterarray of stringsnoneSubstring match on the location text
hasSalarybooleanfalseCount only roles with a structured pay range
salaryFloorinteger0Count only roles whose published pay reaches this, in the role's own currency, no conversion
postedWithinDaysinteger 0–3650Count only roles first published within this many days; undated roles are kept
atsPlatformsmulti-selectallWhich platforms a domain may resolve to; boards you name and preset members are always read
changedFieldsmulti-selecttitle, location, salaryWhat counts as a changed role; department and workplaceType also available
maxRolesPerBoardinteger 1–200002000A board that fills this is flagged truncated, its diff suspended, nothing charged
maxCompaniesinteger 1–2000500Distinct companies per run; the rest are free max-companies rows
stateStoreNamestringhiring-signals-api-stateThe named store for snapshots, history and the domain cache
requestTimeoutSecsinteger 5–12020Per-request timeout for board reads
maxRunSecsinteger 60–3600240Wall-clock budget; resolving domains may use three quarters of it
maxConcurrencyinteger 1–126Companies in flight
proxyConfigurationproxyApify datacenterUsed only for careers pages during resolution; ATS feeds are read directly

Three more are accepted over the API and are not on the form: company (one more entry, added to companies), outageGuardThreshold (integer, default 10: a board is guarded only when its previous snapshot held more than this many roles; 0 turns the guard off) and unresolvedFallback (only "none" in this version).

Filters change what the aggregate counts, never the price. seniorityLevels: ["senior", "lead"] makes openRoles mean senior open roles. Every row carries filtersApplied — the active filters as name=value strings — and rolesFilteredOut, so a number is never read out of context. In monitor mode the filters also decide what is tracked, so changing them between runs looks like roles opening and closing; set them once per watchlist.

Output reference

Every row carries every column, null where it does not apply, so a mixed dataset exports to one stable table. Four row types, told apart by rowType:

rowTypeWhenCharged
company-signalsOne per resolved company per runYes, unless free (see below)
roleOne per posting, only with emitRolesYes, $0.001
diagnosticAnything that could not be resolved or readNever
run-summaryOnce per run, at the endNever

Identity: companyKey (the dedupe and join key, ats:slug case-folded) · companyName · companyDomain · atsPlatform · atsPlatformLabel · atsBoardSlug · atsBoardUrl · boardSource (ats) · aggregatorBoards (reserved, always null) · resolvedFrom (input-board, preset, registry, detection-cache, slug-guess, careers-page) · resolutionConfidence · resolutionEvidence · presets.

Counts: openRoles · previousOpenRoles · openRolesDelta · newRoles · closedRoles · changedRoles · repostedRoles · unchangedRoles. The diff fields are null on a first run, in snapshot mode and on a truncated board. A role closed and re-posted under a new ID counts once, as new (repostedRoles), not as a closure plus an opening; a bare updatedAt bump is never a change. So openRoles = newRoles + changedRoles + unchangedRoles, and openRoles = previousOpenRoles + newRoles − closedRoles − repostedRoles.

Breakdowns: byDepartment, byLocation, byCountry — top ten {value, count, share}, roles with no value counted under (unspecified), ties broken by count then value · bySeniority — the nine seniorityLevels bands, summing to openRoles; mid is a title with no seniority word, exactly as the filter defines it · byEmploymentType — the seven types plus unknown · byWorkplaceType — remote, hybrid, onsite, unknown · remoteShare · salaryDisclosedCount · salaryDisclosedShare · salaryCurrencies · topTitles — top ten {title, count}; a leading Senior/Sr./Staff/Principal/Lead, a trailing parenthetical and a trailing location are ignored for grouping, and the most frequent original spelling is shown · newTitles — this run's new titles, newest first, up to 25. A role listed in several locations counts in each, so byLocation shares can sum above 1.

Signal and history: hiringSurge · newRatio · newPerDay30Days · historyDays — the baseline the surge was tested against, before this run · newLast7Days · closedLast7Days · newLast30Days · closedLast30Days — trailing windows, this run included.

Provenance: mode · firstRun · firstSeenAt · previousRunAt · lastRunAt · truncated · filtersApplied · rolesFilteredOut · requestCount · billable.

role rows carry the Career Site Jobs API's row — jobId, boardSlug, title, department, team, employmentType, seniority, locationRaw, locations, isRemote, workplaceType, salaryMin, salaryMax, salaryCurrency, salaryPeriod, applyUrl, jobUrl, postedAt, updatedAt — with descriptionText and descriptionHtml always null, plus changeType (new, changed or open), changedFields, previousValues, reposted, previousJobId, firstSeenAt, and companyKey / companyDomain to join back to the company row.

The run-summary row carries companiesRequested, companiesResolved, companiesBilled, companiesFree, surgingCompanies, totalOpenRoles, totalNewRoles, totalClosedRoles, requestCount, registryVersion and presetSizes. It makes preset discovery unnecessary and keeps the dataset non-empty on a quiet day.

Diagnostic rows carry error (the sentence to read) and errorType (the token to branch on):

errorTypeWhat it means
ats-not-detectedNo rung could tie the domain to a board; triedCandidates lists everything tried
slug-guess-rejectedA guessed board answered HTTP 200 and could not be tied to the domain — the ladder went on
robots-disallowedThe careers pages rung 4 would read, or a JazzHR board page, are disallowed by robots.txt, so they were not fetched
platform-excludedThe domain's board is on a platform atsPlatforms left out
count-unavailableThe platform was detected but publishes no list this Actor can count for this board
board-not-found · timeout · network-error · http-error · parse-error · unavailableThe board could not be read, and how
outage-guardThe board came back all but empty against its previous snapshot — see Reliability
snapshot-not-savedThe company's row was delivered but its state could not be stored
freemail · invalid-input · preset-unknown · max-companiesThe entry is a consumer mail domain, not a company, names no preset, or came after maxCompanies
deadline · budget-reachedmaxRunSecs or your maximum total charge stopped the run before this company
lockedAnother run holds the same stateStoreName
webhook-failedThe end-of-run POST did not land; every row is still in the dataset
no-boards · no-results · unexpected-errorNothing was sent, nothing could be read, or a fault the Actor did not expect

How the surge is computed

hiringSurge = newRoles ≥ max(5, 3 × newPerDay30Days)
newRatio = newRoles ÷ newPerDay30Days

newPerDay30Days is the company's own average of new roles per day over the trailing 30 days of history before this run — historyDays says how many days that is. Two rules make the flag worth trusting:

  • It is measured against the history the run started with. Folding today in first would let a big day dilute the very baseline it is measured against — the bigger the surge, the more it would hide itself.
  • It is never true on a first run. There is no baseline yet, and a rule of "five or more new roles" would flag every company of any size the first time it is seen. The first run records the baseline; the second can surge.

The floor of five keeps a two-person company going from one opening to two from "surging"; the multiple of three keeps a company that opens thirty roles on an ordinary day from being flagged for it. newRatio is null when the trailing average is zero — there is no rate to divide by — and hiringSurge still fires off the floor, so a company waking up from silence with five openings is caught. Read historyDays: an average over three days is a three-day average.

The rule, its thresholds and the code are the same ones the New Job Postings Monitor ships.

What fills, and what usually does not

Measured on the thirteen boards captured in this Actor's test fixtures — one per platform, 40 roles in all — not on a census; the first live run's numbers across real companies will be published here.

FieldFilled
department35 / 40 roles; Teamtailor and Workday boards published none
postedAt (role rows)33 / 40; the Workday, BambooHR, Rippling and JazzHR lists publish no absolute date
a seniority word in the title27 / 40 — the rest count as mid
employmentType24 / 40; Greenhouse, Workday, JazzHR and Rippling publish none
workplaceType19 / 40
a structured salary (salaryDisclosedShare)6 / 40 roles, on 5 of 13 boards

salaryDisclosedShare is structured ATS compensation only. BambooHR and Rippling publish pay as a sentence, which is never parsed, so their share is always 0.

What you are never charged for

  • The first monitor run of each company (with the default firstRunBehavior: baseline-only) — the row is delivered, free.
  • A domain no rung could resolve, and every refused slug guess.
  • Free-mail domains, invalid entries, unknown presets, entries past maxCompanies.
  • A board that 404s, times out or cannot be read.
  • A board held back by the outage guard — no company row is written for it at all.
  • A board that filled maxRolesPerBoard (truncated: true, openRoles: null).
  • Every company the run never reached — maxRunSecs or your maximum total charge.
  • Every diagnostic row, the run-summary row and the webhook POST.
  • A run that produced no paid row: it finishes FAILED with the reason in its status message and bills nothing, actor-start included — unless every company it read was a free baseline or was held back by the outage guard, which is the Actor working as designed and finishes SUCCEEDED, still free.

A company with zero open roles is still an answer, and still charged. A board that answered and lists nothing — or that your filters emptied — is openRoles: 0 with the board URL that proves it. That is the hiring-freeze answer, and it is exactly what an investor watching a portfolio pays for.

Pricing

Pay per event. $5.00 per 1,000 companies per run at the Free and Starter tiers, less above that.

EventFreeStarter (Bronze)Scale (Silver)Business (Gold)
actor-start — once per run, after the first paid row$0.001$0.001$0.001$0.001
company-signals — once per company per run$0.005$0.005$0.004$0.003
role — once per role row, only with emitRoles$0.001$0.001$0.0008$0.0006

Turning filters on does not make it cheaper: a company is one answer however you slice it, and the row records the slice.

Role rows are paid, at exactly the Career Site Jobs API's job-posting price. A role row is that Actor's row without the description, so it costs the same there and here. They are off by default, so the aggregate buyer never pays for them, and newTitles stays free on the company row, so a buyer who only wants to know what is being hired never needs them. If you want every posting with its description, run the Career Site Jobs API at $1.00 per 1,000 jobs.

Worked example — the Store prefill, scheduled daily. Day one: six free baselines, $0.000. Every day after: $0.001 + 6 × $0.005 = $0.031. 200 target accounts every weekday for a month (22 runs): the first run is free, then 21 × ($0.001 + 200 × $0.005) = $21.021.

Alerts: the run webhook

Set webhookUrl and the run ends by POSTing one JSON body to it:

{
"actorRunId": "kX9…",
"companies": 25, // company-signals rows this run delivered
"newRoles": 31, "closedRoles": 7, "changedRoles": 4,
"hiringSurge": true, // true when any company below is surging
"topCompanies": [ // surging first, then by new roles; at most 25
{ "companyKey": "greenhouse:stripe", "companyName": "Stripe", "companyDomain": "stripe.com",
"atsPlatform": "greenhouse", "openRoles": 16, "openRolesDelta": 6, "newRoles": 6,
"closedRoles": 0, "hiringSurge": true, "newRatio": 24,
"newTitles": ["Solutions Engineer 0", "Solutions Engineer 1", "Solutions Engineer 2"] }
],
"datasetUrl": "https://api.apify.com/v2/datasets/…/items?clean=true&format=json"
}

It is free, and deliberately boring: one destination, https:// or http:// only, and a URL with a username and password in it is refused; a redirect to another host is never followed — one same-host hop is, never a downgrade to http; a 10-second timeout, one retry, a 64 KB cap met by dropping whole company entries from the tail; and the URL is treated as a secret, because most alerting endpoints authenticate by path — only its host ever reaches the log or a row. A POST that fails is a free webhook-failed row; the rows were pushed and the snapshots written before it was attempted. It fires on failed runs too — except a run sent no companies at all, or one stopped by the state-store lock, which touched nothing.

Reliability

  • An ATS outage never becomes a hiring freeze. If a company's previous snapshot held more than outageGuardThreshold roles (10) and its board comes back with 10 % or fewer, the comparison is abandoned: no company row, the previous snapshot kept, a free outage-guard row. On a company-level product this matters more than on a role feed — one bad fetch would flip openRolesDelta, closedRoles and hiringSurge for the whole account at once. The next run seeing the same collapse is treated as confirmation, and the closures are reported then.
  • Unread is not closed. A board that fills maxRolesPerBoard has its diff suspended; the fingerprints it could not read are carried forward, and dropped only after 45 days unseen.
  • Nothing is billed before delivery. Rows reach the dataset before they are charged, snapshots are written after their rows are pushed, and your maximum total charge is checked at the real per-event prices before each paid row — role rows are trimmed first, so a company row is the last thing a budget gives up. A budget stop writes every snapshot it delivered and leaves the others untouched.
  • A misbehaving state store cannot destroy your history. An unreadable snapshot makes that company a free baseline — never a wave of false closures — and is left as it is. Two runs sharing one stateStoreName never run at once: the second stops with a free locked row.
  • One company's failure is its own. Every other company in the run is read, delivered and billed normally.

FAQ

Can I send domains instead of board slugs? Yes — that is what the ladder above is for. A domain in the registry costs nothing to resolve; an unknown one costs one to eleven cheap requests, the board read included, and in monitor mode the answer is then cached in your state store so later runs skip the ladder. The row tells you how it was resolved and how sure we are.

What is a preset? A named bundle from a registry of 75 boards read from their live endpoints: preset:devtools, preset:fintech, preset:ai-labs, preset:yc-top. It is a starting list, not an index of the market.

What happens on the first run? In monitor mode each company's first run records its baseline and returns its row with openRoles and every breakdown filled, the diff fields null, firstRun: true — free. From the second run on you get the diff and the surge, and each row is charged. firstRunBehavior: "emit-all" charges the first run instead. snapshot mode has no first run: every row is charged.

Why is my openRoles null? The board filled maxRolesPerBoard (truncated: true) — raise it. The row is free.

Why no headcount? Open roles are not headcount, and a headcount derived from an open-role count is a guess squared. There is no headcount or employee-count column, no hiring-intent score and no AI narrative: every column is a count, a share or a title read off the board, and hiringSurge is the only conclusion drawn — with its rule and its baseline printed beside it.

Why only company career boards? Because the answer should be the company's own. See Limitations.

Can I get the roles too? Yes: emitRoles: true, with roleScope new, changed or all, at $0.001 a row. For descriptions, use the Career Site Jobs API.

Does it work over MCP? Yes. It runs with limited permissions, pay-per-event pricing and no Standby, so it works over the Apify MCP server and with x402 agentic payments.

Limitations

  • The upstream formats may change. Thirteen vendor feeds; a changed one produces free parse-error rows for its companies and never affects the rest.
  • Workday and JazzHR are beta adapters, and BambooHR and Rippling are new. Workday's list endpoint publishes no absolute posting date, and JazzHR's board is an HTML page.
  • A board that fills maxRolesPerBoard is not counted and its diff is suspended; SmartRecruiters and Workday page at 100 roles per request up to that ceiling.
  • The slug guess covers Greenhouse, Lever and Ashby only. Other platforms are found from the careers page or sent as ats:slug. A company whose careers page loads its board with JavaScript and is not in the registry comes back ats-not-detected.
  • The remote-board fallback (unresolvedFallback: "aggregator-boards") is not in this version. A company with no public ATS board is a free diagnostic row. boardSource and aggregatorBoards are on the row for when it ships; the answer it gives will be a floor, not a count.
  • No headcount, no hiring-intent score, no AI anything — by design, as above.
  • No LinkedIn, Indeed or Glassdoor, by choice, for two reasons: they sit behind anti-bot walls this Actor would need a browser and evasion to cross, and their terms of service forbid it.
  • No descriptions, in any row. The Greenhouse, Lever and Ashby list endpoints return posting bodies in the same response as the list; they are discarded unread, never stored and never returned. Run the Career Site Jobs API for them.
  • Role rows cut by your budget are not re-sent on the next run: the company row that counted them was delivered.
  • Changing filters between monitor runs looks like roles opening and closing.

Privacy

The shared ATS adapters read no personal field at all — no hiring manager, no recruiter, no posting creator — so there is nothing to drop; a test asserts that no name present in any captured fixture reaches any row. Descriptions, where recruiter details live, are never read. robots.txt is evaluated per RFC 9309 on every HTML page this Actor fetches — the careers pages in rung 4 and the JazzHR board page — and a refusal is reported, not worked around. No cookies, no login, no API key of anyone's. The webhook URL is treated as a secret. If you combine this data with personal data, GDPR/CCPA obligations are yours. Not affiliated with Greenhouse, Lever, Ashby, Workable, Recruitee, SmartRecruiters, Teamtailor, Personio, Breezy HR, Workday, JazzHR, BambooHR, Rippling or any employer whose board is read.

Use it from an AI agent, or from code

One JSON object in, one flat array out. The Actor runs with limited permissions, uses pay-per-event pricing and never enters Standby, so it works over the Apify MCP server (mcp.apify.com) and with x402 agentic payments.

curl -X POST "https://api.apify.com/v2/acts/insight.solutions~hiring-signals-api/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"companies":["cloudflare.com","greenhouse:stripe"],"mode":"snapshot"}'
from apify_client import ApifyClient # pip install apify-client
client = ApifyClient("<APIFY_TOKEN>")
run = client.actor("insight.solutions/hiring-signals-api").call(run_input={
"companies": ["preset:devtools"],
"mode": "monitor",
"stateStoreName": "devtools-watch",
})
for row in client.dataset(run["defaultDatasetId"]).iterate_items():
if row["rowType"] == "company-signals":
print(row["companyName"], row["openRoles"], row["newRoles"], row["hiringSurge"], sep=" | ")

Our other Actors

Every Insight Solutions Actor is pay-per-result with no browser, no login and no API key, and every one of them returns free diagnostic rows instead of billing for failures. Prices are per 1,000 results.

Video, audio & social

News, documents & the web

Business, finance & jobs

Apps & games