Hiring Signals API — Open Roles & Hiring Surge by Company
Pricing
from $3.00 / 1,000 company hiring signal returneds
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
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
a day ago
Last modified
Categories
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 want | Run | Price |
|---|---|---|
| Every open role, with descriptions, as rows | Career Site Jobs API (ats-jobs-api) | $1.00 / 1,000 jobs |
Only what changed, role by role, with previousValues | New 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 accelerating | this Actor | $5.00 / 1,000 companies per run |
| Remote roles from the public remote job boards | Remote 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:
| Rung | Source | Requests | resolvedFrom | resolutionConfidence |
|---|---|---|---|---|
| 0 | You sent a board (greenhouse:stripe, a board URL) or a preset | 0 | input-board / preset | 1.0 |
| 1 | The bundled registry: 75 verified boards, each with the company's own domain | 0 | registry | 0.95 |
| 2 | The detection cache: a domain an earlier run resolved | 0 | detection-cache | 0.9, or the original rung's if lower |
| 3 | A slug guess — greenhouse:, lever:, ashby: + the domain's label — verified, by the list itself or by the board's public page | 1–6 | slug-guess | 0.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) |
| 4 | The company's own careers pages: /careers, /jobs, / — and www./careers when the apex never answers | 1–4, plus robots.txt | careers-page | 0.6 |
| 5 | Nothing found | 0 | a 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
| Field | Type | Default | What it does |
|---|---|---|---|
companies (required) | array of strings | — | Domains, ats:slug boards, career-page URLs or preset:<name>, one per line |
mode | monitor | snapshot | monitor | monitor diffs against the previous run and keeps 90 days of history; snapshot is one run, no state, no diff |
firstRunBehavior | baseline-only | emit-all | baseline-only | baseline-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 |
emitRoles | boolean | false | Also return one role row per posting. Charged at $0.001 each |
roleScope | new | changed | all | new | Which postings emitRoles returns |
webhookUrl | string (secret) | none | One POST when the run finishes — see Alerts |
titleFilter | array of strings | none | Count only roles whose title contains one of these |
titleExclude | array of strings | none | Leave out roles whose title contains one of these |
seniorityLevels | multi-select | all | intern, junior, mid, senior, lead, principal, manager, director, executive |
employmentTypes | multi-select | all | FullTime, PartTime, Contract, Temporary, Internship, Volunteer, Other; roles with no published type are left out when set |
workplaceTypes | multi-select | all | remote, hybrid, onsite; roles with no signal are left out when set |
countryCodes | array of strings | none | ISO 3166-1 alpha-2; country names and US states / Canadian provinces are matched too |
locationFilter | array of strings | none | Substring match on the location text |
hasSalary | boolean | false | Count only roles with a structured pay range |
salaryFloor | integer | 0 | Count only roles whose published pay reaches this, in the role's own currency, no conversion |
postedWithinDays | integer 0–365 | 0 | Count only roles first published within this many days; undated roles are kept |
atsPlatforms | multi-select | all | Which platforms a domain may resolve to; boards you name and preset members are always read |
changedFields | multi-select | title, location, salary | What counts as a changed role; department and workplaceType also available |
maxRolesPerBoard | integer 1–20000 | 2000 | A board that fills this is flagged truncated, its diff suspended, nothing charged |
maxCompanies | integer 1–2000 | 500 | Distinct companies per run; the rest are free max-companies rows |
stateStoreName | string | hiring-signals-api-state | The named store for snapshots, history and the domain cache |
requestTimeoutSecs | integer 5–120 | 20 | Per-request timeout for board reads |
maxRunSecs | integer 60–3600 | 240 | Wall-clock budget; resolving domains may use three quarters of it |
maxConcurrency | integer 1–12 | 6 | Companies in flight |
proxyConfiguration | proxy | Apify datacenter | Used 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:
rowType | When | Charged |
|---|---|---|
company-signals | One per resolved company per run | Yes, unless free (see below) |
role | One per posting, only with emitRoles | Yes, $0.001 |
diagnostic | Anything that could not be resolved or read | Never |
run-summary | Once per run, at the end | Never |
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):
errorType | What it means |
|---|---|
ats-not-detected | No rung could tie the domain to a board; triedCandidates lists everything tried |
slug-guess-rejected | A guessed board answered HTTP 200 and could not be tied to the domain — the ladder went on |
robots-disallowed | The careers pages rung 4 would read, or a JazzHR board page, are disallowed by robots.txt, so they were not fetched |
platform-excluded | The domain's board is on a platform atsPlatforms left out |
count-unavailable | The platform was detected but publishes no list this Actor can count for this board |
board-not-found · timeout · network-error · http-error · parse-error · unavailable | The board could not be read, and how |
outage-guard | The board came back all but empty against its previous snapshot — see Reliability |
snapshot-not-saved | The company's row was delivered but its state could not be stored |
freemail · invalid-input · preset-unknown · max-companies | The entry is a consumer mail domain, not a company, names no preset, or came after maxCompanies |
deadline · budget-reached | maxRunSecs or your maximum total charge stopped the run before this company |
locked | Another run holds the same stateStoreName |
webhook-failed | The end-of-run POST did not land; every row is still in the dataset |
no-boards · no-results · unexpected-error | Nothing 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.
| Field | Filled |
|---|---|
department | 35 / 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 title | 27 / 40 — the rest count as mid |
employmentType | 24 / 40; Greenhouse, Workday, JazzHR and Rippling publish none |
workplaceType | 19 / 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
monitorrun of each company (with the defaultfirstRunBehavior: 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 —
maxRunSecsor your maximum total charge. - Every diagnostic row, the
run-summaryrow 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-startincluded — 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.
| Event | Free | Starter (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
outageGuardThresholdroles (10) and its board comes back with 10 % or fewer, the comparison is abandoned: no company row, the previous snapshot kept, a freeoutage-guardrow. On a company-level product this matters more than on a role feed — one bad fetch would flipopenRolesDelta,closedRolesandhiringSurgefor 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
maxRolesPerBoardhas 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
stateStoreNamenever run at once: the second stops with a freelockedrow. - 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-errorrows 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
maxRolesPerBoardis 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 backats-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.boardSourceandaggregatorBoardsare 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-clientclient = 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
- YouTube Transcript API — captions as timed segments, text, SRT or VTT, with language fallback and translation.
- YouTube Comments API — comments and replies with likes, pinned and hearted flags, newest or top sort.
- YouTube Channel API — a channel's videos, Shorts and live streams, plus YouTube search.
- Podcast Search, Episodes & Charts API — Apple Podcasts search, charts and full episode feeds.
- Bluesky Scraper — profiles, posts, followers and follows from the public AT Protocol API.
- Telegram Channel Scraper — posts, views and channel stats from public Telegram channels.
- Substack Scraper — posts with full free text, comments and publication profiles.
- Hacker News API — stories, comments, users, front page and a structured "Who is hiring?" parser from the official HN APIs.
News, documents & the web
- Google News Search, Topics & Real Article URLs — news search and topic feeds with the publisher's real URL decoded.
- Website to Markdown — Content Extractor for LLMs & RAG — any site as clean Markdown, text and heading-aware chunks.
- Internet Archive API — archive.org search, item metadata, files and reviews.
- Wayback Machine Toolkit — archived URL inventories, snapshots and text diffs between dates.
- Website Technology Detector — the tech stack behind any site, with the evidence for each detection.
- Domain Intelligence API — DNS, RDAP registration, TLS certificate and HTTP facts in one row per domain.
- SEO Page Audit — sitemap crawl with on-page checks, structured data and broken-link reports.
- Keyword Suggestions API — Google, YouTube, Bing, Amazon and eBay autocomplete with alphabet and question expansions.
- Website Contact Extractor — emails, phone numbers and social profiles from any list of websites.
- Web Search Results API — Bing and DuckDuckGo organic results with snippets, no key, no browser.
- Company Enrichment API — a domain in, a company profile out: firmographics, contacts, tech stack, DNS and hiring signal.
- Company Dossier API — one company in, twelve sections out: profile, tech, contacts, DNS, open roles, news, SEC filings, federal awards, recalls, YC batch and apps.
Business, finance & jobs
- Congress & Insider Trades API — STOCK Act periodic transaction reports and SEC Form 4 insider trades in one schema.
- Federal Contracts, Grants & Lobbying API — SAM.gov opportunities, USAspending awards, Grants.gov notices and Senate lobbying filings in one schema.
- SEC EDGAR API — filings, XBRL financials and full-text search by ticker or CIK.
- Clinical Trials & FDA API — ClinicalTrials.gov studies plus openFDA recalls, labels, approvals, 510(k)s and adverse-event reports.
- Product & Vehicle Recalls API — CPSC, NHTSA, FDA and USDA recalls, vehicle complaints and ratings, plus a VIN decoder.
- Y Combinator Companies, Batches & Founders — the YC directory with founders and social links, filterable by batch, industry and hiring status.
- Career Site Jobs API — jobs straight from Greenhouse, Lever, Ashby, Workable and 10+ other ATS career sites.
- New Job Postings Monitor — new, closed and changed postings on the career sites you watch.
- Remote Jobs API — RemoteOK, Remotive, We Work Remotely, Himalayas, Jobicy and more in one schema, deduplicated.
- Shopify Products API — any Shopify store's catalogue, variants, prices and stock signals.
- Shopify Store Monitor — price drops, sales, restocks, sell-outs and new products on any Shopify store, one row per change.
Apps & games
- App Store & Google Play Reviews API — reviews from both stores with ratings, versions and developer replies.
- App Store Top Charts & App Search API — Apple top charts by country and genre, plus app search and details.
- App Store Keyword Rank Tracker — where any app ranks for any keyword on the App Store and Google Play, with rank changes and ASO suggestions.
- Steam Reviews API — Steam reviews with playtime, helpfulness and game details.
- Steam Game Data API — prices, tags, review scores, live player counts and top charts.