# Hiring Signals & Salary Changes — ATS Job Tracker (`agency-shift/hiring-signals-scraper`) Actor

Track company hiring changes across four ATS platforms. Get full job descriptions, published salaries, remote status and source links. Compare runs to find new, changed, reopened and no-longer-listed jobs with persistent history.

- **URL**: https://apify.com/agency-shift/hiring-signals-scraper.md
- **Developed by:** [Valdeir Lima](https://apify.com/agency-shift) (community)
- **Categories:** Jobs, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.98 / 1,000 job or hiring-change records

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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 & Salary Changes

Turn company career boards into structured job data and repeatable hiring signals. Collect jobs from **Greenhouse, Lever, Ashby and publicly accessible Recruitee boards**, then compare repeat scans to find new listings, edited descriptions, published pay changes, reopened roles and jobs no longer listed.

Use it to maintain a job board, research recruiting demand, follow a company watchlist or refresh a recruiting dataset. Supply the career-board URLs you want to track; the Actor does not discover companies automatically.

### Start with a snapshot

```json
{
  "boardUrls": ["https://jobs.ashbyhq.com/Ashby"],
  "mode": "snapshot",
  "maxResults": 1000
}
```

Select **Start**, then open **Jobs & hiring changes** to preview or export the default dataset as JSON, CSV or Excel. Open **Scan summary** for board outcomes and warnings. Snapshot mode returns the current listings and does not read or update monitor history.

### Track changes across runs

```json
{
  "boardUrls": ["https://jobs.ashbyhq.com/Ashby"],
  "mode": "changes",
  "monitorName": "my-company-watchlist",
  "maxResults": 1000,
  "emitInitialSnapshot": true,
  "missingConfirmations": 2
}
```

Run the same input again to compare it with the saved observation. For recurring checks, save an Apify task and add an Apify schedule. Choose an interval long enough for each run to finish.

The first successful scan of each board establishes a **baseline**. It does not claim every existing job is newly posted. Set `emitInitialSnapshot` to `false` to establish this baseline without delivering baseline records. Later scans return only relevant events:

| Event | Meaning |
| --- | --- |
| `baseline` | A job present when this monitor first scanned the board. |
| `new` | A job first observed after a baseline already exists. |
| `changed` | One or more meaningful job fields changed. `changedFields` and `previousValues` explain the difference. |
| `closed` | A previously seen job was missing from the required successful scans. `closureReason` is `no-longer-listed`. This does not establish that the position was filled. |
| `reopened` | The same posting ID appeared again after a confirmed closure. |
| `snapshot` | A current listing returned by Snapshot mode. |

Compared fields include title, job/application URL, locations, department, employment type, workplace/remote status, description and published compensation. Whitespace and equivalent ordering in array fields do not by themselves create a change. Source timestamp changes alone are not treated as meaningful job edits. No event means no qualifying observed change; it does not prove nothing changed between scans.

### Supported career boards

| ATS | Accepted board URL format | Notes |
| --- | --- | --- |
| Greenhouse | `https://job-boards.greenhouse.io/COMPANY` or `https://boards.greenhouse.io/COMPANY` | Optional per-job detail requests enrich published pay ranges. |
| Lever | `https://jobs.lever.co/COMPANY` or `https://jobs.eu.lever.co/COMPANY` | Includes global and EU boards with pagination. |
| Ashby | `https://jobs.ashbyhq.com/COMPANY` | Includes published compensation when available; excludes postings explicitly marked unlisted. |
| Recruitee | `https://COMPANY.recruitee.com` | Requires that the careers API is publicly accessible without authentication. |

Supported individual job URLs are normalized to their entire board. Custom company career domains, other ATS platforms, logins and private ATS data are not supported. Duplicate URLs resolving to the same board are scanned once. Enter 1–20 URLs per run.

**Recruitee availability:** Recruitee documents a Careers Site API token requirement with a **10 February 2027** deadline for unauthenticated requests. This Actor currently has no Recruitee token input. Boards that already require authentication fail explicitly, and support for unauthenticated boards may end as that requirement is enforced. See [Recruitee authentication documentation](https://docs.recruitee.com/reference/authentication-1).

### Fields and data quality

Each record includes posting identity, provider, company, title, source URL, observation time and available job details. `company` is the career-board slug unless the provider supplies a company name. Unavailable values are `null` or empty collections.

- **Descriptions:** plain text from the source job description and applicable requirements or other text sections.
- **Salary:** `compensation` contains only employer-published pay data. There are no salary estimates, inferred currencies, annualization or inferred pay intervals. Text-only pay information may remain in `descriptionText` without structured numeric values. Multiple salary ranges, tiers and compensation components stay separate in `compensation.components`; the top-level min/max can be null when there is no single unambiguous range. Greenhouse pay intervals remain null when its API does not provide them.
- **Remote work:** source indicators only. An unknown remote status is null, not false. Provider definitions of remote or hybrid work may differ.
- **Dates:** `observedAt` is this Actor's scan time. `firstSeenAt` is the first time this monitor observed the job, not its posting date; it is null in Snapshot mode. `lastSeenAt` is the last scan in which it was present. `publishedAt` uses Greenhouse's `first_published`, Ashby's `publishedAt` or Recruitee's `published_at` when supplied. Greenhouse update dates and Lever creation dates are not repurposed as publication dates. `sourceUpdatedAt` is separate.
- **Identity:** combine `boardKey` and `jobId` to identify a posting. A repost under a different ID is a separate posting. `eventId` identifies an event delivery and supports deduplication.

### Example output

This shortened, illustrative record shows a published salary range changing. The company, job, dates and pay values are fictional; this is not a live listing. Full records also include description, application URL and other available details.

```json
{
  "schemaVersion": 1,
  "eventId": "a31e62d891740a903c8d781fa3d18719e2712bef9a4c21368a68c14e20f71b09",
  "eventType": "changed",
  "provider": "lever",
  "boardKey": "lever:global:example-company",
  "jobId": "example-job",
  "company": "example-company",
  "title": "Software Engineer",
  "location": "United States",
  "workplaceType": "remote",
  "remote": true,
  "compensation": {
    "currency": "USD", "min": 130000, "max": 170000,
    "interval": "year", "text": null
  },
  "changedFields": ["compensation"],
  "previousValues": {
    "compensation": {
      "currency": "USD", "min": 120000, "max": 160000,
      "interval": "year", "text": null
    }
  },
  "publishedAt": null,
  "firstSeenAt": "2026-09-20T09:00:00.000Z",
  "lastSeenAt": "2026-09-24T09:00:00.000Z",
  "observedAt": "2026-09-24T09:00:00.000Z",
  "sourceUrl": "https://jobs.lever.co/example-company/example-job"
}
```

### Closure safeguards

By default, an absent job needs **two successful complete scans** before a `closed` event is produced. Failed requests, malformed responses and detected incomplete scans do not advance that board's history or missing counters. A job that reappears before closure resets its missing counter.

When a board previously had at least **10 active jobs** and a scan returns **fewer than half** that count, missing counters and closures for absent jobs are held. Jobs actually observed can still update. `SUMMARY` reports `closureGuardTriggered`. If the drop is expected, set `allowLargeClosures` to `true`; the missing-confirmation threshold still applies.

Removing a board from `boardUrls` does not close its jobs or erase its state. No scan is made for omitted boards. An outage or a budget limit is never interpreted as an empty board.

### Inputs

| Field | Default | Purpose |
| --- | --- | --- |
| `boardUrls` | Required | 1–20 supported HTTPS URLs. |
| `mode` | `snapshot` | `snapshot` or `changes`. |
| `monitorName` | Empty | Required for Changes mode; 1–40 lowercase letters, numbers or hyphens, beginning with a letter or number. |
| `maxResults` | `1000` | Maximum delivered output records per run; range 1–10,000. |
| `maxJobsPerBoard` | `2000` | Complete-scan safety limit; range 1–5,000. An oversized board fails rather than becoming a truncated baseline. |
| `missingConfirmations` | `2` | Successful missing scans required before closure; range 2–5. |
| `emitInitialSnapshot` | `true` | Deliver baseline records in Changes mode. |
| `includeGreenhousePayRanges` | `true` | Fetch structured Greenhouse pay ranges using one additional detail request per job. |
| `allowLargeClosures` | `false` | Permit missing counters and closures after a large board-size drop. |

Keep `includeGreenhousePayRanges` unchanged for an existing Changes monitor. The Actor rejects a change for a saved Greenhouse board to avoid treating a data-collection setting change as employer pay changes. Use a new `monitorName` to start over with a different setting.

### Limits, history and delivery

`maxResults` and the Apify spending cap limit **delivered records**, not the total jobs fetched. The Actor must obtain a complete board to compare it safely. `maxJobsPerBoard` separately rejects oversized scans. Boards are processed in input order; a run that hits an output limit may leave later boards unprocessed. Increase the limit or split the watchlist into independently named monitors when needed.

In Changes mode, a successful observation and its pending events are saved before delivery. If output or spending limits are reached, the next run with the same account, Actor and `monitorName` resumes pending events before scanning that board again. Snapshot mode has no pending-output history and can return a truncated snapshot at the output limit.

Monitor history is stored in an Apify named key-value store whose name is hashed from the account, Actor and monitor name. The same monitor name intentionally shares history across tasks in that account. Different monitor names keep independent histories. Named storage persists across runs until it is deleted. A monitor also uses a request-queue lease to prevent overlapping runs from editing the same history. An overlapping run exits with an error; after a crash the lease expires automatically.

Delivery follows **at-least-once semantics**. A crash after a dataset write but before its delivery checkpoint can cause the same event to appear again on a later run. Use the stable `eventId` to deduplicate downstream. Such redelivery can be charged again. Pending records retain their original observation timestamps. History checkpoints are limited to 16 MB per board; if exceeded, the board fails while its prior checkpoint remains.

### Pricing

The launch price is **$2.98 per 1,000 delivered job or event records** (`$0.00298` each), plus Apify's standard **$0.00005 Actor-start event** at up to 1 GB of allocated memory. The minimum run spending cap is **$0.01**; this is a budget setting, not a minimum charge. The Store's Pricing tab is authoritative for the active price and any memory-dependent start charges.

Each delivered snapshot, baseline, new, changed, closed or reopened record is billable. A Changes scan with no delivered events has no job-record charge. A silent baseline (`emitInitialSnapshot:false`) has no baseline-record charge. Pending events are charged when delivered, including a later resumed run. The price includes platform usage rather than adding a separate usage fee.

### Run summary and troubleshooting

The default dataset contains job/event records. The default key-value store contains `SUMMARY`: per-board status, warnings, prepared or pending counts when applicable, failed boards, unprocessed boards, output counts and limit status. `ERROR` exists only for a fatal run error. Input-validation failures may occur before a summary is created.

A mixed result can have a successful Apify run with a `partial` summary. Inspect `SUMMARY` before treating a watchlist as fully scanned. If every board fails, the Actor fails the run. If no events appear, check the mode, baseline option, unchanged-job counts, limits and board errors. For an unsupported URL, use the ATS-hosted career-board address. For a busy monitor, wait for its current run to finish. To reset history without deleting any storage, choose a new monitor name.

This Actor observes publicly listed jobs. It does not submit applications, access candidate records or determine whether a company hired someone. Availability and fields depend on the source board. For support, include the run ID, board URL and relevant `SUMMARY` error; do not send API tokens or other credentials.

# Actor input Schema

## `boardUrls` (type: `array`):

Enter 1–20 supported HTTPS board URLs, one per line. Supported hosts: job-boards.greenhouse.io, boards.greenhouse.io, jobs.lever.co, jobs.eu.lever.co, jobs.ashbyhq.com and company.recruitee.com. A supported job URL selects its entire board. Custom company career domains are not supported. Recruitee works only where its careers API remains publicly accessible.

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

Snapshot returns current jobs without reading or changing monitor history. Changes records a first baseline, then returns new, changed, reopened and confirmed no-longer-listed jobs on later runs.

## `monitorName` (type: `string`):

Required in Changes mode. Reuse this name with the same Apify account and Actor to continue history across runs and tasks. Use 1–40 lowercase letters, numbers or hyphens, starting with a letter or number. A new name starts a new baseline. Overlapping runs of the same monitor are rejected.

## `maxResults` (type: `integer`):

Maximum job or event records delivered in this run (1–10,000), also limited by your Apify spending cap. This limits output, not jobs fetched. In Changes mode, pending records are saved and delivered first on a later run of this monitor. Snapshot output can be truncated. Boards are processed in input order.

## `emitInitialSnapshot` (type: `boolean`):

In Changes mode, return baseline records when a board is first observed by this monitor. Turn off to save the first baseline silently and receive only later changes. Baseline records are charged when delivered. Does not affect Snapshot mode.

## `includeGreenhousePayRanges` (type: `boolean`):

Request each Greenhouse job's detail endpoint to include published structured pay ranges. Adds one request per job and can increase runtime. Keep this setting unchanged for an existing Changes monitor, or choose a new monitor name. Turning it off still retains salary text in job descriptions.

## `missingConfirmations` (type: `integer`):

In Changes mode, a previously seen job must be absent from this many successful complete scans before a closed event is emitted. Closed means no longer listed, not filled. Failed or incomplete scans and the large-drop safeguard do not advance the missing counter.

## `maxJobsPerBoard` (type: `integer`):

Safety limit for complete board scans (1–5,000). If a board exceeds this count, that board fails instead of saving a truncated snapshot; its previous history and closure counters are kept. This is separate from the output limit.

## `allowLargeClosures` (type: `boolean`):

Default false: if a board previously had at least 10 active jobs and now lists fewer than half that number, hold missing counters and closures for absent jobs. Seen jobs can still update. Enable only when a large drop is expected; the missing-confirmation threshold still applies.

## Actor input object example

```json
{
  "boardUrls": [
    "https://jobs.ashbyhq.com/Ashby"
  ],
  "mode": "snapshot",
  "monitorName": "",
  "maxResults": 1000,
  "emitInitialSnapshot": true,
  "includeGreenhousePayRanges": true,
  "missingConfirmations": 2,
  "maxJobsPerBoard": 2000,
  "allowLargeClosures": false
}
```

# Actor output Schema

## `jobs` (type: `string`):

Current jobs in Snapshot mode, or baseline/new/changed/closed/reopened records in Changes mode. Use eventId to deduplicate resumed deliveries.

## `summary` (type: `string`):

Per-board outcomes, warnings, failed and unprocessed boards, output counts and limit status. May be absent if input validation fails before scanning starts.

## `error` (type: `string`):

Error message and timestamp. This record exists only after a fatal run error; a missing record on a successful run is expected.

# 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 = {
    "boardUrls": [
        "https://jobs.ashbyhq.com/Ashby"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("agency-shift/hiring-signals-scraper").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 = { "boardUrls": ["https://jobs.ashbyhq.com/Ashby"] }

# Run the Actor and wait for it to finish
run = client.actor("agency-shift/hiring-signals-scraper").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 '{
  "boardUrls": [
    "https://jobs.ashbyhq.com/Ashby"
  ]
}' |
apify call agency-shift/hiring-signals-scraper --silent --output-dataset

```

## MCP server setup

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

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/eLaZAvm968mRxb6Bg/builds/tyTcPoEClIbVtgM8v/openapi.json
