# FreshHire — Verified ATS Job Changes API (`cesarjaks/freshhire-verified-ats-job-changes-api`) Actor

Monitor Greenhouse, Lever and Ashby company career pages for new openings, material updates, reopened jobs and confirmed closures. Includes a normalized ATS job scraper snapshot mode.

- **URL**: https://apify.com/cesarjaks/freshhire-verified-ats-job-changes-api.md
- **Developed by:** [Cesar Joaquin Lares Illescas](https://apify.com/cesarjaks) (community)
- **Stats:** 2 total users, 1 monthly users, 50.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 ats board monitoreds

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

## FreshHire — ATS Job Scraper & Job Change Monitoring

Track hiring activity directly from company job boards. FreshHire collects **Greenhouse Jobs, Lever Jobs and Ashby Jobs** in a consistent format, then monitors those boards for new openings, meaningful updates, confirmed closures and reopened positions.

Use **Snapshot mode** to get current vacancies or **Changes mode** to receive only the changes detected since earlier scans. Persistent monitoring saves you from comparing repeated job lists yourself. Each change includes stable identifiers and observation times; updates identify the changed fields and their previous values.

FreshHire gives recruiting platforms, job aggregators, labor market researchers and automation developers a shared interface to the **Greenhouse Jobs API**, Lever and Ashby public job boards. Use it to:

- Feed new job alerts and recruiting workflows with newly observed openings.
- Keep job directories current with updates, closures and reopened listings.
- Follow hiring activity at selected companies or departments.
- Supply structured job data to APIs, dashboards and AI agents.

FreshHire retrieves public ATS data directly. No ATS login, browser setup, proxy subscription or paid enrichment API is required.

### Quick start

1. Open the Actor's **Input** tab and paste one of the JSON examples below.
2. Select **256 or 512 MB** of memory and set a maximum cost per run in Apify's run options.
3. Run the Actor. View or export records from its default dataset.
4. Check **`RUN_SUMMARY`** in the run's default Key-Value Store, especially when the dataset is empty or the run is partial.

**Changes mode charges for successful monitoring scans even when no changes are found.** The first baseline also incurs scan fees. See [pricing](#pricing) before scheduling recurring runs.

#### Snapshot: get current Greenhouse jobs

```json
{
  "mode": "snapshot",
  "boards": [
    {"source": "greenhouse", "boardId": "greenhouse", "company": "Greenhouse"}
  ],
  "includeDescriptions": false,
  "maxResults": 100
}
```

This returns up to 100 currently listed jobs with `changeType: "snapshot"`. Snapshot mode does not maintain monitoring history: its `firstSeenAt` and `lastSeenAt` describe observations in that run.

#### Changes: monitor Ashby jobs across runs

```json
{
  "mode": "changes",
  "monitorId": "my-job-monitor-v1",
  "stateStoreName": "freshhire-my-job-monitor-v1",
  "boards": [
    {"source": "ashby", "boardId": "Ashby", "company": "Ashby"}
  ],
  "includeDescriptions": false,
  "includeDetails": false,
  "maxResults": 100
}
```

Run this input once to establish a baseline, then again with the **same `monitorId`, `stateStoreName` and board settings**. The first complete scan records current jobs and emits no change rows. Later runs compare against that stored history. A complete scan with no changes also returns an empty dataset; both runs still incur startup and successful board-scan fees.

Choose an unused state-store name for your first monitor so FreshHire can create it under its storage permissions. Keep that store between runs. Different monitor IDs keep independent histories, including when they share a store. A new monitor ID starts a new baseline.

The example board identifiers are public boards used in FreshHire's integration checks. Their jobs and availability can change. Substitute the boards you want to monitor.

### Pricing

FreshHire uses **Pay Per Event** with these prices in USD:

| Charge | Price | When it applies |
| --- | ---: | --- |
| Actor start (`apify-actor-start`) | **$0.00005 per run** | Automatic startup charge at the documented 256–512 MB allocation, including runs that return no records or fail. |
| Dataset result (`apify-default-dataset-item`) | **$0.001 per delivered result** | Each record written to the default dataset: a snapshot job or a change event. |
| Board scanned (`board-scanned`) | **$0.002 per successfully monitored ATS board** | Changes mode only, after a complete successful scan is saved to monitoring state. |

**Monitoring is charged for checking boards, even when no changes are detected.** Baseline initialization, unchanged boards and valid empty boards qualify for the board-scan charge. Every new scheduled run can incur these charges again.

Failed, skipped or incomplete boards do not incur a board-scan fee. Snapshot mode has no board-scan fee. Diagnostic errors and `RUN_SUMMARY` are not paid dataset records. Startup charges still apply when a run produces no output.

For a normal run at the documented memory allocation:

- **Snapshot:** `$0.00005 + ($0.001 × delivered results)`.
- **Changes:** `$0.00005 + ($0.002 × successful complete board scans) + ($0.001 × delivered changes)`.

| Example | Event fees |
| --- | ---: |
| Snapshot delivering 100 jobs | $0.10005 |
| First baseline for one board, no change rows | $0.00205 |
| One unchanged board, no change rows | $0.00205 |
| One board with three delivered changes | $0.00505 |
| Three unchanged boards, no change rows | $0.00605 |

These examples assume sufficient budget, one startup event and no crash-related replay. Apify charges startup and dataset events automatically; FreshHire charges only the custom board-scan event. If a run is allocated more than 1 GB, Apify can charge multiple startup units, so use 256–512 MB for the per-run startup price above. See [Apify's event-pricing rules](https://docs.apify.com/actors/publishing/monetize/pay-per-event) and the Actor's Pricing tab for the applicable rates.

#### Spending limits and pending results

Set the **maximum cost per run** in Apify Console or the run API options. It is a per-run limit, not a monthly monitoring budget. `maxResults` separately caps delivered rows; it does not cap board scans or their fees.

FreshHire checks affordability before starting a new paid scan or delivering another result. A low budget can leave boards uninitialized, skip scans or leave eligible changes waiting for delivery. Check `spendingLimitReached`, `partial`, `boardsSkipped` and `pendingChanges` in `RUN_SUMMARY`.

In Changes mode, complete scan state and eligible events are saved before the custom board charge. Undelivered committed changes remain pending for later runs. Reuse the same monitor and boards with sufficient budget and result capacity to deliver them. Pending changes are delivered before a fresh scan of their board; if some remain, that scan is skipped. Snapshot results have no persistent delivery queue: rerun a snapshot to retrieve results omitted by its limits.

**Output filters do not reduce monitoring fees.** All boards in `boards` remain configured for scanning, including when `sources`, `companies` or keywords exclude their results. Remove a board from `boards` to stop monitoring it, after consuming its pending results.

#### Billing and recovery limitations

Checkpoints suppress repeated custom charges during retries of the same Apify run. If a charge's outcome is uncertain, FreshHire stops further paid work rather than risking a repeated charge. This can leave a saved scan unbilled; it does not erase monitoring history or pending changes.

Dataset delivery is **at least once**. A crash after a dataset write but before acknowledgement can cause the same event to be delivered and automatically charged again on recovery. Deduplicate downstream by `eventId`; deduplication does not reverse a dataset fee. Exactly-once delivery or charging after a crash is not guaranteed.

SDK counts and platform counters can become visible at different times. Use the run's platform event counts when reconciling charges; `RUN_SUMMARY.billing` is operational accounting, not a settled customer invoice. Abrupt platform termination can prevent a final summary from being written.

### Supported ATS boards

Each `boards` entry needs a `source` and either a `boardId` or a supported hosted-board `url`. `company` is an optional label; otherwise it defaults to the board identifier.

| Provider | Example board entry |
| --- | --- |
| Greenhouse Jobs API | `{"source":"greenhouse","boardId":"greenhouse","company":"Greenhouse"}` |
| Lever Jobs, global | `{"source":"lever","boardId":"leverdemo","region":"global"}` |
| Lever Jobs, Europe | `{"source":"lever","boardId":"YOUR_BOARD","region":"eu"}` — replace `YOUR_BOARD` with your EU board identifier. |
| Ashby Jobs | `{"source":"ashby","boardId":"Ashby","company":"Ashby"}` |

For example, use `{"source":"ashby","url":"https://jobs.ashbyhq.com/Ashby"}` instead of its `boardId` entry. Supported hosted domains include `boards.greenhouse.io`, `job-boards.greenhouse.io`, `jobs.lever.co`, `jobs.eu.lever.co` and `jobs.ashbyhq.com`.

Supply the ATS board root, not an individual job URL or custom company careers page. Arbitrary fetch URLs, credentials, query strings, fragments and custom ports are rejected. Identical boards are checked once; conflicting company labels for a duplicate board are rejected. Lever's global and EU regions are separate identities.

### How Job Change Monitoring works

Each board creates its baseline independently on its first complete successful scan. `RUN_SUMMARY.boardsInitialized` identifies those boards. Existing vacancies are recorded as the baseline, not emitted as new jobs.

| Change type | Meaning |
| --- | --- |
| `new` | A provider job ID newly observed in this monitor's retained history. It does not prove that the vacancy was just published. |
| `updated` | A material field changed, such as title, department, location, workplace type, employment type, compensation, URLs or an enabled description. |
| `closed` | A previously open job was absent from **two consecutive complete successful scans**. It disappeared from the public board; this does not prove that the role was filled. |
| `reopened` | The same provider ID returned after a confirmed closure, while its closed history was still retained. |

Observation timestamps, source-date-only changes, reordered fields/lists, whitespace and irrelevant description HTML formatting do not generate updates. `changedFields` and `previousValues` explain material updates. Description changes are tracked only with `includeDescriptions: true`.

Failed or incomplete scans cannot confirm closures. A failed or skipped scan resets the absence streak, so another two complete successful scans without the job are needed. A valid empty board is a successful scan and follows the same closure rule.

Keep company labels, `includeDescriptions` and `includeDetails` stable within a monitor. Incompatible setting changes are rejected; create a new monitor ID when deliberately starting a different baseline. Full board snapshots are stored independently of output filters. Changing filters cannot close jobs, but it does not replay historical changes that were previously filtered out. Already-pending events keep their original eligibility.

### Filters and useful settings

| Input | Default | Behavior |
| --- | --- | --- |
| `mode` | `snapshot` | `snapshot` for current jobs; `changes` for persistent monitoring. |
| `monitorId` | Required for Changes | Stable name for an independent monitoring history. |
| `stateStoreName` | `freshhire-state` | Named Key-Value Store retained across monitoring runs. |
| `titleKeywords` | `[]` | Case-insensitive substring matches in the title. |
| `locationKeywords` | `[]` | Case-insensitive substring matches in the primary `location` field. |
| `workplaceTypes` | `[]` | Exact values: `remote`, `hybrid`, `onsite`, `unknown`. |
| `sources` / `companies` | `[]` | Output filters for ATS providers or company labels. Company matching is case-insensitive and exact. |
| `maxResults` | `1000` | Dataset row cap, from 1 to 10,000. Does not make a board scan incomplete. |
| `includeDescriptions` | `false` | Include plain text descriptions and monitor their changes. |
| `includeDetails` | `false` | Fetch Greenhouse details, including available publication dates and structured pay ranges. Adds a request per posting. |

Keywords use **OR within a list, AND between populated filter groups**. For example, `titleKeywords: ["engineer", "developer"]` together with `locationKeywords: ["London"]` matches either title keyword plus London in the primary location. Empty lists accept all values. Unknown workplace types are not guessed from location text.

### Output and run summary

Records include provider and company identifiers, titles, available locations, workplace and employment types, source dates, observation dates, available compensation and job/apply URLs. Unknown or unavailable values remain null or `unknown`. Additional provider fields and provenance can be present.

This is an **illustrative change record**, not a live vacancy:

```json
{
  "source": "ashby",
  "company": "Example Company",
  "boardId": "example",
  "jobId": "54b0f045-3763-4823-9fdc-c2d14b797920",
  "stableId": "ashby:example:54b0f045-3763-4823-9fdc-c2d14b797920",
  "eventId": "c9f7a9a16c09ae732056d0b02ba954780149a32085c5160427faf10397d8af03",
  "title": "Backend Engineer",
  "department": "Engineering",
  "location": "Remote - United States",
  "workplaceType": "remote",
  "employmentType": "full_time",
  "sourcePublishedAt": "2026-10-20T09:00:00Z",
  "sourceUpdatedAt": null,
  "firstSeenAt": "2026-10-21T12:00:00Z",
  "lastSeenAt": "2026-10-22T12:00:00Z",
  "salaryMin": 140000,
  "salaryMax": 175000,
  "salaryCurrency": "USD",
  "salaryInterval": "year",
  "jobUrl": "https://jobs.ashbyhq.com/example/54b0f045-3763-4823-9fdc-c2d14b797920",
  "applyUrl": "https://jobs.ashbyhq.com/example/54b0f045-3763-4823-9fdc-c2d14b797920/application",
  "changeType": "updated",
  "detectedAt": "2026-10-22T12:00:00Z",
  "changedFields": ["salaryMax"],
  "previousValues": {"salaryMax": 170000},
  "provenance": {
    "sourcePublishedAt": {
      "field": "publishedAt",
      "endpoint": "list",
      "confidence": "source",
      "meaning": "last_publication",
      "status": "valid"
    }
  }
}
```

`stableId` identifies a job across scans using its source, board and provider ID; Lever also includes its region. `eventId` identifies a particular change for deduplication and is absent from snapshot records. Similar titles at different companies remain distinct.

Read `RUN_SUMMARY` alongside the dataset. It reports requested/successful boards, `failedBoards`, `boardsSkipped`, `boardsInitialized`, fetched/delivered job counts, pending changes, duration, warnings and partial/incomplete flags. `newJobs`, `updatedJobs`, `closedJobs` and `reopenedJobs` count delivered rows; `detectedChanges` counts newly detected events before filtering. Its `billing` section and `boardResults` describe custom scan-charge outcomes and spending-limit handling.

#### Source dates and compensation

| Value | Meaning |
| --- | --- |
| `firstSeenAt` | First successful observation in this monitor's retained history; in Snapshot mode, the current run's observation. Never substituted for source publication. |
| `lastSeenAt` | Last successful scan containing the job. A closure retains its previous observation time. |
| `detectedAt` | When FreshHire detected the snapshot/event. Delayed delivery preserves the event's original detection time. |
| Greenhouse `first_published` | Preserved as `sourcePublishedAt` when available. Documented on the detail endpoint; enable `includeDetails` for those requests. |
| Greenhouse `updated_at` | Source update time from list/detail responses, normalized as `sourceUpdatedAt`. |
| Lever dates | Publication/update timestamps are not guaranteed and remain null. Optional numeric `createdAt` is normalized to `sourceCreatedAt`, not treated as a publication date. |
| Ashby `publishedAt` | Last publication time, which may differ from first publication; preserved as `sourcePublishedAt` with provenance. |

Dates with timezone information are normalized to UTC; missing, invalid or timezone-less dates remain null. Salary values retain their source currency and interval, with no FX conversion, annualization or salary inference from prose. Greenhouse documented cents may be converted to major currency units. Multiple or ambiguous pay tiers remain structured ranges instead of being merged across currencies or intervals.

### Scheduling and API access

Save a Changes input as an Apify task. Run it once for the baseline, then add that task to a schedule with the same monitor and store. For example, `0 */6 * * *` runs every six hours. Choose a timezone explicitly and a run timeout shorter than the schedule interval. See [Apify's scheduling instructions](https://docs.apify.com/actors/running/schedules).

Use one active run per monitor. Avoid overlapping schedules or manual runs against the same monitor. Different monitor IDs can operate independently. Each scheduled execution has its own spending limit and can incur startup and monitoring fees.

The Actor's **API** tab provides authenticated examples for starting runs and retrieving datasets. You can also invoke a saved task, then read its run's default dataset and `RUN_SUMMARY` using Apify's API clients. Keep your Apify token in an environment variable or secret store. No separate FreshHire server or external database is needed. Send delivered events to your own alerting or automation workflow.

### Troubleshooting and limitations

| Symptom | What to check |
| --- | --- |
| Empty Changes dataset | `boardsInitialized`, `detectedChanges`, filters, `pendingChanges` and `partial`. A baseline or unchanged scan can return no rows and still incur monitoring fees. |
| 404 / board not found | Confirm the public board identifier and Lever region. A failed request is not an empty board. |
| 403 or authentication challenge | Verify source access and permissions. FreshHire does not bypass access controls. |
| 429 / timeouts | Increase request pacing or reduce concurrency. Retries and excessive delays are bounded. |
| Pending changes or spending limit | Retain the same monitor/store/boards and provide sufficient budget and `maxResults` for delivery. |
| State conflict / lock busy | Wait for the existing run to finish. An uncertain cloud write can leave a lease until its one-hour expiry; do not forcibly clear a live lease. |
| Description/detail settings changed | Restore the prior settings or choose a new monitor ID for a new baseline. |
| State-size limit | Review board size and description settings. A new monitor without descriptions can reduce state size; incomplete scans cannot confirm closures. |

Polling cannot detect jobs that appear and disappear between scans. A closure confirms absence from the public board, not that a role was filled. Providers may silently omit postings; validation and two-scan confirmation reduce transient false closures but cannot establish completeness beyond the provider's response. A replacement provider ID appears as a new job and eventual closure of the old ID.

Reopening recognition is limited to retained history: closed jobs default to 180 days and 10,000 records per board. Old closed records can be pruned with a warning; a later reappearance then becomes `new`. Source API changes require ongoing adapter maintenance.

Independent Cloud runs have verified snapshots, persistent baselines, unchanged monitoring, lock contention, event counts and graceful budget handling on the documented boards. Controlled cloud crash recovery and forced cloud lifecycle transitions have not been verified; local tests cover those transitions. These checks do not guarantee exactly-once delivery, customer invoice settlement or capacity for every board size.

### Responsible use

Use public job information only where permitted by source terms and applicable law. Choose reasonable polling intervals and respect access restrictions. Public availability does not grant unrestricted redistribution rights. FreshHire does not collect candidate records, submit applications, bypass authentication or CAPTCHAs, or enrich listings with unrelated personal data.

### Technical details

FreshHire uses Python 3.12+, the official Apify Python SDK and asynchronous HTTP requests. Each source has an independent adapter. Requests use timeouts, bounded retries, rate-limit handling and configurable pacing. No browser automation or LLM processing is used.

| Provider | Public API used |
| --- | --- |
| Greenhouse | `https://boards-api.greenhouse.io/v1/boards/{boardId}/jobs`; optional job details. See [Greenhouse Job Board API](https://docs.greenhouse.io/job-board). |
| Lever | `https://api.lever.co/v0/postings/{boardId}?mode=json`, or `api.eu.lever.co` for EU boards. See [Lever Postings API](https://github.com/lever/postings-api). |
| Ashby | `https://api.ashbyhq.com/posting-api/job-board/{boardId}?includeCompensation=true`. Postings marked `isListed=false` are excluded. See [Ashby Public Job Posting API](https://developers.ashbyhq.com/docs/public-job-posting-api). |

Request concurrency defaults to 3 (range 1–5), with at least 0.5 seconds between requests to a provider host by default. HTTP timeouts default to 30 seconds and transient retries to 3. Source responses have a 20 MiB size limit. `maxJobsPerBoard` defaults to 20,000, with a configurable maximum of 50,000; exceeding it makes the scan incomplete.

Monitoring uses versioned board records in a named Key-Value Store, protected by a request-queue lease in Cloud and a file lock locally. These stores do not provide transactional, fenced compare-and-swap writes. A board state has an 8 MiB safety bound; pending events are bounded to twice `maxJobsPerBoard`. Excessive state fails explicitly. Retired monitors and removed boards are not automatically deleted; retain pending data before deciding to archive or delete their storage. See [Apify storage concepts](https://docs.apify.com/sdk/python/docs/concepts/storages) and [Actor permissions](https://docs.apify.com/actors/development/permissions).

#### Local development

Store users can run FreshHire directly in Apify; these commands are for developers with the source files. Requires Python 3.12+ and Node.js for the CLI:

```powershell
python -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install -r requirements.txt -r requirements-dev.txt
npx --yes apify-cli@1.10.0 validate-schema
npx --yes apify-cli@1.10.0 run --entrypoint src --input-file examples/ashby.json
python -m pytest -q
```

On Linux/macOS, activate with `source .venv/bin/activate`. The Python entry point is `python -m src`; the Dockerfile uses `apify/actor-python:3.12`. Local named storage must survive between monitoring runs. Keep local and Cloud stores separate and run one local writer per monitor. Internal validation reports and maintenance procedures are in the source checkout's `docs` directory; customer-facing behavior and recovery limits are described above.

##### Local Docker execution

From a source checkout in PowerShell:

```powershell
docker build -t freshhire:local .
$freshHireRoot = (Get-Location).Path
$freshHireStorage = Join-Path $freshHireRoot "storage/docker-manual"
New-Item -ItemType Directory -Force -Path $freshHireStorage | Out-Null
docker run --rm --memory 256m --memory-swap 256m `
  --env APIFY_IS_AT_HOME=false `
  --env APIFY_LOCAL_STORAGE_DIR=/usr/src/app/storage `
  --env CRAWLEE_STORAGE_DIR=/usr/src/app/storage `
  --mount "type=bind,source=$freshHireRoot/examples/lever.json,target=/usr/src/app/INPUT.json,readonly" `
  --mount "type=bind,source=$freshHireStorage,target=/usr/src/app/storage" `
  freshhire:local
```

This keeps local output in `storage/docker-manual`. For monitoring, replace `examples/lever.json` with `examples/local-monitor-smoke.json` and repeat with the same storage mount, monitor ID and store name. Named monitoring state survives container removal only when that mount is retained. Inspect the local dataset and `RUN_SUMMARY`; do not run simultaneous containers against the same monitoring storage.

# Actor input Schema

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

Snapshot returns current jobs. Changes creates a first-run baseline, then returns new, updated, reopened and confirmed closed jobs.

## `boards` (type: `array`):

Supply source plus boardId or an allowlisted hosted ATS board URL in url. company is an optional display label. Lever region is global (default) or eu. Do not supply arbitrary company career URLs. Identical boards are checked once; conflicting duplicates are rejected.

## `monitorId` (type: `string`):

Required in changes mode. Stable identifier for this independent monitor, for example recruiting-us-v1. Keep it unchanged across runs. Use a new identifier to intentionally start a new baseline.

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

Named Apify Key-Value Store shared by subsequent runs. Independent monitor IDs use separate records. Keep the store private and do not delete it while monitoring.

## `includeDescriptions` (type: `boolean`):

Include normalized plain text descriptions and detect description changes. Increases traffic and state size. Keep this setting stable within a monitor.

## `includeDetails` (type: `boolean`):

Request each Greenhouse job detail for first_published and available structured pay ranges. Adds one request per posting. Keep this setting stable within a monitor.

## `titleKeywords` (type: `array`):

Case-insensitive substring matching. Any keyword may match within this list; different filter groups are combined with AND. An empty list accepts all titles.

## `locationKeywords` (type: `array`):

Case-insensitive location substring matching; any keyword may match. Empty accepts all locations.

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

Optional exact workplace types. Unknown values remain unknown; location text is not used to guess remote work.

## `sources` (type: `array`):

Return results from these ATS platforms. The configured boards are still fully monitored.

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

Optional exact company labels, matched case-insensitively. Empty accepts all companies.

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

Limits dataset rows, not board completeness. Changes above the limit remain pending for later runs. A limit can produce a partial result; inspect RUN_SUMMARY.

## `concurrency` (type: `integer`):

Maximum simultaneous outbound HTTP requests. Start with the conservative default.

## `requestIntervalSeconds` (type: `number`):

Minimum pacing interval between requests to a provider host. Retry-After and exponential backoff can increase the delay.

## `requestTimeoutSeconds` (type: `number`):

Bound for HTTP request timeouts. Timed-out boards do not generate closures.

## `maxRetries` (type: `integer`):

Retries after the initial request for transient network failures, rate limits and eligible server errors. Permanent access failures are not bypassed.

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

Safety bound on one board. Exceeding it marks that scan incomplete, preserves its job snapshot and resets closure confirmation; this is not a result filter.

## `closedRetentionDays` (type: `integer`):

Retain closed postings to recognize reopening. Reappearance after retention expires is treated as newly observed.

## `maxClosedJobsPerBoard` (type: `integer`):

Oldest closed records are pruned if the bound is reached. Reopening recognition is limited to retained records.

## Actor input object example

```json
{
  "mode": "snapshot",
  "boards": [
    {
      "source": "ashby",
      "boardId": "Ashby",
      "company": "Ashby"
    }
  ],
  "stateStoreName": "freshhire-state",
  "includeDescriptions": false,
  "includeDetails": false,
  "titleKeywords": [],
  "locationKeywords": [],
  "workplaceTypes": [],
  "sources": [],
  "companies": [],
  "maxResults": 1000,
  "concurrency": 3,
  "requestIntervalSeconds": 0.5,
  "requestTimeoutSeconds": 30,
  "maxRetries": 3,
  "maxJobsPerBoard": 20000,
  "closedRetentionDays": 180,
  "maxClosedJobsPerBoard": 10000
}
```

# Actor output Schema

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

JSON results from snapshot or changes mode. A first monitoring baseline emits no change events.

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

Run status, board counts, fetched and delivered jobs, event counts, duration, partial status, diagnostics and PPE charge counts when configured.

# 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 = {
    "boards": [
        {
            "source": "ashby",
            "boardId": "Ashby",
            "company": "Ashby"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("cesarjaks/freshhire-verified-ats-job-changes-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 = { "boards": [{
            "source": "ashby",
            "boardId": "Ashby",
            "company": "Ashby",
        }] }

# Run the Actor and wait for it to finish
run = client.actor("cesarjaks/freshhire-verified-ats-job-changes-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 '{
  "boards": [
    {
      "source": "ashby",
      "boardId": "Ashby",
      "company": "Ashby"
    }
  ]
}' |
apify call cesarjaks/freshhire-verified-ats-job-changes-api --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,cesarjaks/freshhire-verified-ats-job-changes-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/r1IgCd6cW4FkoyG5Z/builds/h9fCiIWeBmeWVSOMJ/openapi.json
