# Hiring Signals API — Open Roles & Hiring Surge by Company (`insight.solutions/hiring-signals-api`) Actor

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.

- **URL**: https://apify.com/insight.solutions/hiring-signals-api.md
- **Developed by:** [Insight Solutions](https://apify.com/insight.solutions) (community)
- **Categories:** Jobs, Lead generation, Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 company hiring signal returneds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

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

**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:

```json
{ "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.

```json
{
  "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](https://apify.com/insight.solutions/ats-jobs-api) (`ats-jobs-api`) | $1.00 / 1,000 jobs |
| Only what changed, role by role, with `previousValues` | [New Job Postings Monitor](https://apify.com/insight.solutions/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](https://apify.com/insight.solutions/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:

```json
{
  "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:**

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

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

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

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

```json
{ "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 `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.

| 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](https://apify.com/insight.solutions/ats-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:

```jsonc
{
  "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.

```bash
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"}'
```

```python
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**

- [YouTube Transcript API](https://apify.com/insight.solutions/youtube-transcript-api) — captions as timed segments, text, SRT or VTT, with language fallback and translation.
- [YouTube Comments API](https://apify.com/insight.solutions/youtube-comments-api) — comments and replies with likes, pinned and hearted flags, newest or top sort.
- [YouTube Channel API](https://apify.com/insight.solutions/youtube-channel-api) — a channel's videos, Shorts and live streams, plus YouTube search.
- [Podcast Search, Episodes & Charts API](https://apify.com/insight.solutions/podcast-api) — Apple Podcasts search, charts and full episode feeds.
- [Bluesky Scraper](https://apify.com/insight.solutions/bluesky-scraper) — profiles, posts, followers and follows from the public AT Protocol API.
- [Telegram Channel Scraper](https://apify.com/insight.solutions/telegram-channel-scraper) — posts, views and channel stats from public Telegram channels.
- [Substack Scraper](https://apify.com/insight.solutions/substack-scraper) — posts with full free text, comments and publication profiles.
- [Hacker News API](https://apify.com/insight.solutions/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](https://apify.com/insight.solutions/google-news-api) — news search and topic feeds with the publisher's real URL decoded.
- [Website to Markdown — Content Extractor for LLMs & RAG](https://apify.com/insight.solutions/website-content-extractor) — any site as clean Markdown, text and heading-aware chunks.
- [Internet Archive API](https://apify.com/insight.solutions/internet-archive-api) — archive.org search, item metadata, files and reviews.
- [Wayback Machine Toolkit](https://apify.com/insight.solutions/wayback-toolkit) — archived URL inventories, snapshots and text diffs between dates.
- [Website Technology Detector](https://apify.com/insight.solutions/website-tech-detector) — the tech stack behind any site, with the evidence for each detection.
- [Domain Intelligence API](https://apify.com/insight.solutions/domain-intelligence-api) — DNS, RDAP registration, TLS certificate and HTTP facts in one row per domain.
- [SEO Page Audit](https://apify.com/insight.solutions/seo-page-audit) — sitemap crawl with on-page checks, structured data and broken-link reports.
- [Keyword Suggestions API](https://apify.com/insight.solutions/keyword-suggestions-api) — Google, YouTube, Bing, Amazon and eBay autocomplete with alphabet and question expansions.
- [Website Contact Extractor](https://apify.com/insight.solutions/website-contact-extractor) — emails, phone numbers and social profiles from any list of websites.
- [Web Search Results API](https://apify.com/insight.solutions/web-search-api) — Bing and DuckDuckGo organic results with snippets, no key, no browser.
- [Company Enrichment API](https://apify.com/insight.solutions/company-enrichment-api) — a domain in, a company profile out: firmographics, contacts, tech stack, DNS and hiring signal.
- [Company Dossier API](https://apify.com/insight.solutions/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](https://apify.com/insight.solutions/congress-insider-trades-api) — STOCK Act periodic transaction reports and SEC Form 4 insider trades in one schema.
- [Federal Contracts, Grants & Lobbying API](https://apify.com/insight.solutions/federal-contracts-grants-api) — SAM.gov opportunities, USAspending awards, Grants.gov notices and Senate lobbying filings in one schema.
- [SEC EDGAR API](https://apify.com/insight.solutions/sec-edgar-api) — filings, XBRL financials and full-text search by ticker or CIK.
- [Clinical Trials & FDA API](https://apify.com/insight.solutions/clinical-trials-fda-api) — ClinicalTrials.gov studies plus openFDA recalls, labels, approvals, 510(k)s and adverse-event reports.
- [Product & Vehicle Recalls API](https://apify.com/insight.solutions/product-recalls-api) — CPSC, NHTSA, FDA and USDA recalls, vehicle complaints and ratings, plus a VIN decoder.
- [Y Combinator Companies, Batches & Founders](https://apify.com/insight.solutions/yc-companies-directory) — the YC directory with founders and social links, filterable by batch, industry and hiring status.
- [Career Site Jobs API](https://apify.com/insight.solutions/ats-jobs-api) — jobs straight from Greenhouse, Lever, Ashby, Workable and 10+ other ATS career sites.
- [New Job Postings Monitor](https://apify.com/insight.solutions/job-postings-monitor) — new, closed and changed postings on the career sites you watch.
- [Remote Jobs API](https://apify.com/insight.solutions/remote-jobs-api) — RemoteOK, Remotive, We Work Remotely, Himalayas, Jobicy and more in one schema, deduplicated.
- [Shopify Products API](https://apify.com/insight.solutions/shopify-products-api) — any Shopify store's catalogue, variants, prices and stock signals.
- [Shopify Store Monitor](https://apify.com/insight.solutions/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](https://apify.com/insight.solutions/app-reviews-api) — reviews from both stores with ratings, versions and developer replies.
- [App Store Top Charts & App Search API](https://apify.com/insight.solutions/app-charts-api) — Apple top charts by country and genre, plus app search and details.
- [App Store Keyword Rank Tracker](https://apify.com/insight.solutions/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](https://apify.com/insight.solutions/steam-reviews-api) — Steam reviews with playtime, helpfulness and game details.
- [Steam Game Data API](https://apify.com/insight.solutions/steam-store-stats-api) — prices, tags, review scores, live player counts and top charts.

# Actor input Schema

## `companies` (type: `array`):

One per line: a company domain (`stripe.com`), a board as `ats:slug` (`greenhouse:stripe`), a career-page URL, or `preset:devtools` / `preset:fintech` / `preset:ai-labs` / `preset:yc-top`. A domain is resolved to its board without a search engine; when it cannot be, you get a free diagnostic listing everything that was tried. The same company named twice is read and billed once.

## `mode` (type: `string`):

`monitor` diffs every company against the previous run and keeps 90 days of history, so new, closed and changed roles and the hiring surge are filled in. `snapshot` is a one-off: no state, no diff, every company charged. Use snapshot for a diligence pass, monitor on a schedule.

## `firstRunBehavior` (type: `string`):

`baseline-only` records the baseline on a company's first monitored run and returns its row with every diff field null — and charges nothing for it. `emit-all` returns the full row on the first run and charges it, exactly as snapshot mode would. Only matters in monitor mode.

## `emitRoles` (type: `boolean`):

Adds a `role` row per posting on top of each company row — title, department, location, salary, apply URL, never a description. Charged at $0.001 per role row, the same price as the Career Site Jobs API, which is what these rows are. Off by default; `newTitles` on the company row is free.

## `roleScope` (type: `string`):

Only used when role rows are on. `new` returns roles that appeared since the previous run, `changed` adds edited ones with `changedFields` and `previousValues`, `all` returns every open role. In snapshot mode and on a first run there is no previous run, so only `all` returns rows.

## `webhookUrl` (type: `string`):

One HTTP(S) POST when the run finishes, carrying a digest of the companies that moved: top 25 by new roles, surging first. Free, 10-second timeout, one retry, 64 KB cap, redirects off the host never followed. Treated as a secret — only its host is ever logged. Leave empty for no webhook.

## `titleFilter` (type: `array`):

Count only roles whose title contains one of these, case-insensitively. Filters change what the aggregate counts — `openRoles` becomes "open roles matching the filter" — and every row records `filtersApplied` so a number is never read out of context. Filtering never changes the price.

## `titleExclude` (type: `array`):

Leave out roles whose title contains any of these, case-insensitively. Applied after the include list, so a role matching both is left out. Useful for `["intern", "contract"]` when you only want permanent hiring counted.

## `seniorityLevels` (type: `array`):

Count only these bands, read from the title (no ATS publishes a level). `mid` means a title with no seniority word at all, `lead` also covers Staff, `executive` also covers VP. The same vocabulary as the `bySeniority` breakdown, so the two always agree. Leave empty for every level.

## `employmentTypes` (type: `array`):

Count only these employment types. Roles whose ATS publishes no type are left out when this is set, because there is nothing to match them against — Greenhouse, for one, never publishes it. Leave empty to count every role.

## `workplaceTypes` (type: `array`):

Count only remote, hybrid or on-site roles. A role counts as remote when the ATS says so or its location text does. Roles the ATS says nothing about are left out when this is set — and a `remoteShare` of 1.0 under `remote` is a tautology, which is why the row says the filter was on.

## `countryCodes` (type: `array`):

Count only roles in these countries, as ISO 3166-1 alpha-2 codes such as `US`, `GB`, `DE`. Country names the ATS wrote out are matched too, and a US state or Canadian province implies its country. Leave empty for every country.

## `locationFilter` (type: `array`):

Count only roles whose location text contains one of these strings, case-insensitively — for cities and regions a country code cannot express, such as `Berlin`, `Bay Area` or `EMEA`. Leave empty for every location.

## `hasSalary` (type: `boolean`):

Count only roles carrying a structured pay range from the ATS — never one parsed out of prose. Most roles publish none, so expect `openRoles` to fall sharply. For the share of a board that publishes pay, leave this off and read `salaryDisclosedShare`.

## `salaryFloor` (type: `integer`):

Count only roles whose published pay reaches this number, compared against the top of the range in the role's own currency and period, with no conversion. Roles with no published pay are left out. Leave at 0 for no floor.

## `postedWithinDays` (type: `integer`):

Count only roles first published within this many days. Roles whose ATS publishes no date are kept, so an undated board is never emptied by this filter. Leave at 0 for no age limit.

## `atsPlatforms` (type: `array`):

Only affects company domains: when set, a domain may only resolve to a board on these platforms, and a registry or cached answer on another platform becomes a free platform-excluded row. Boards you send as ats:slug and preset members are always read. Leave empty to allow all thirteen.

## `changedFields` (type: `array`):

Which fields have to move before a role counts in `changedRoles` (monitor mode). Title, location and salary by default; department and workplace type are also available. Descriptions are never tracked, and a bare updatedAt bump is never a change.

## `maxRolesPerBoard` (type: `integer`):

A board that fills this ceiling has its diff suspended and its row flagged `truncated` with `openRoles` null and nothing charged — roles we did not read are never reported as closed. Keep it above your largest board; 2000 covers almost every company.

## `maxCompanies` (type: `integer`):

A ceiling on distinct companies read in one run, after presets are expanded and duplicates folded. Entries past it get a free `max-companies` row and are not read or charged. It bounds the bill of a run you did not size yourself.

## `stateStoreName` (type: `string`):

The named key-value store holding each company's previous snapshot, its 90-day history and the domain-to-board cache, in monitor mode. One store per watchlist: name your own to keep separate watchlists apart. Two runs sharing one store never run at once — the second stops with a free row.

## `requestTimeoutSecs` (type: `integer`):

Per-request timeout for board reads. A URL is abandoned once its attempts have used twice this, or 40 seconds, whichever is longer. Raise it only for very large SmartRecruiters or Workday boards.

## `maxRunSecs` (type: `integer`):

Wall-clock budget for the whole run. Resolving domains may use up to three quarters of it; the rest is kept for reading boards. Companies not reached get a free deadline row, keep their snapshots and are never charged.

## `maxConcurrency` (type: `integer`):

How many companies are resolved and read at the same time. Each worker keeps its own proxy session for careers pages, and no more than one request at a time ever goes to a single host. Six suits almost every watchlist.

## `proxyConfiguration` (type: `object`):

Used only for the company careers pages read while resolving a domain. The ATS feeds themselves are public syndication endpoints and are always read directly. The Apify datacenter proxy is the default; switch to residential only if careers pages refuse it.

## Actor input object example

```json
{
  "companies": [
    "greenhouse:stripe",
    "lever:leverdemo",
    "ashby:ramp",
    "workable:lyst",
    "recruitee:vandebron",
    "cloudflare.com"
  ],
  "mode": "monitor",
  "firstRunBehavior": "baseline-only",
  "emitRoles": false,
  "roleScope": "new",
  "titleFilter": [],
  "titleExclude": [],
  "seniorityLevels": [],
  "employmentTypes": [],
  "workplaceTypes": [],
  "countryCodes": [],
  "locationFilter": [],
  "hasSalary": false,
  "salaryFloor": 0,
  "postedWithinDays": 0,
  "atsPlatforms": [],
  "changedFields": [
    "title",
    "location",
    "salary"
  ],
  "maxRolesPerBoard": 2000,
  "maxCompanies": 500,
  "stateStoreName": "hiring-signals-api-state",
  "requestTimeoutSecs": 20,
  "maxRunSecs": 240,
  "maxConcurrency": 6,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `results` (type: `string`):

Every row of the run in the default dataset: company-signals rows, role rows when emitRoles is on, free diagnostic rows and the run-summary row. Delivered as JSON items.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "companies": [
        "greenhouse:stripe",
        "lever:leverdemo",
        "ashby:ramp",
        "workable:lyst",
        "recruitee:vandebron",
        "cloudflare.com"
    ],
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("insight.solutions/hiring-signals-api").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "companies": [
        "greenhouse:stripe",
        "lever:leverdemo",
        "ashby:ramp",
        "workable:lyst",
        "recruitee:vandebron",
        "cloudflare.com",
    ],
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("insight.solutions/hiring-signals-api").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "companies": [
    "greenhouse:stripe",
    "lever:leverdemo",
    "ashby:ramp",
    "workable:lyst",
    "recruitee:vandebron",
    "cloudflare.com"
  ],
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call insight.solutions/hiring-signals-api --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,insight.solutions/hiring-signals-api"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/k9lxeAZE9UNFTIywr/builds/LhqOpACufgh9swSjW/openapi.json
