# ATS Jobs & Change Feed (`sapph1re/public-ats-job-change-feed`) Actor

Normalize public Greenhouse, Lever, Lever EU, and Ashby job boards, then emit stable job-change events.

- **URL**: https://apify.com/sapph1re/public-ats-job-change-feed.md
- **Developed by:** [Roman V](https://apify.com/sapph1re) (community)
- **Categories:**
- **Stats:** 3 total users, 2 monthly users, 85.7% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 results

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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## ATS Jobs & Change Feed

Normalize public Greenhouse, Lever, Lever EU, and Ashby job boards into one
stable schema. Use snapshot mode for a current export or changes mode to emit
only `NEW`, `UPDATED`, and `REMOVED` records compared with a prior snapshot.

### Why this Actor

- Uses the ATS providers' public job-board APIs; no applicant data, cookies, or
  customer credentials are requested.
- Emits stable `canonical_job_id` and `content_hash` values.
- Deduplicates before writing dataset items, so duplicate records are not
  emitted as billable results.
- Keeps source errors in the `OUTPUT` summary instead of disguising an error as
  a successful job record.
- Preserves source URLs and direct application links.

### Input

Add each public board to `sources` with its provider and board token. For
example, `stripe` for Greenhouse, `spotify` for Lever, or `ashby` for Ashby.
The optional `companyName` becomes the normalized display name when the public
API does not include one.

For a recurring change feed, set `outputMode` to `changes` and select the prior
run's `current_snapshot_dataset_id` in `previousDatasetId`. The dataset picker
requests read-only access so the chain also works when the Actor runs with
limited permissions. Every changes run stores a fresh full snapshot separately
and reports its ID in the `OUTPUT` summary, so that ID becomes the baseline for
the next run. You can alternatively pass normalized items inline as
`previousJobs`, but do not provide both inputs.

If the selected dataset is inaccessible or is not a compatible snapshot, the
run fails before fetching any boards. `OUTPUT` reports a stable `error_code` and
an actionable `error_message`; no partial change feed is emitted against a
missing or malformed baseline.

### Output

Each dataset item includes stable identity, normalized title/company/location,
description, direct application and job URLs, provider provenance, timestamps,
and a `change_type`. The default key-value-store record `OUTPUT` contains source
counts, deduplication totals, event counts, the next snapshot dataset ID, and
explicit partial-failure details.

### Scope

This Actor reads published employer job postings only. It does not submit job
applications, collect candidate data, bypass access controls, or scrape private
ATS administration APIs.

# Actor input Schema

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

One object per public job board. boardName is the token used in the provider's public job-board URL.

## `outputMode` (type: `string`):

snapshot returns all current jobs; changes returns NEW and UPDATED jobs plus optional REMOVED and UNCHANGED events.

## `previousJobs` (type: `array`):

Optional prior normalized items used to calculate change events. Use this or previousDatasetId, not both.

## `previousDatasetId` (type: `string`):

Recommended for recurring change feeds. Select the dataset reported as current\_snapshot\_dataset\_id by the preceding run.

## `emitUnchanged` (type: `boolean`):

Include UNCHANGED records when outputMode is changes.

## `emitRemoved` (type: `boolean`):

Include jobs present in previousJobs but absent from the current boards.

## `query` (type: `string`):

Case-insensitive substring matched against title, company, and description.

## `location` (type: `string`):

Case-insensitive substring matched against normalized location text.

## `remoteOnly` (type: `boolean`):

Return only records explicitly marked remote by the ATS.

## `publishedAfter` (type: `string`):

Optional ISO 8601 timestamp. Jobs without a published timestamp are excluded when this is set.

## `maxJobsPerSource` (type: `integer`):

Deterministic per-board output ceiling after normalization and filtering.

## `maxTotalJobs` (type: `integer`):

Hard ceiling across all boards, applied before change events are emitted.

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

Number of public boards fetched at once.

## `includeRaw` (type: `boolean`):

Attach the original provider record for debugging. Off by default to keep datasets compact.

## Actor input object example

```json
{
  "sources": [
    {
      "provider": "greenhouse",
      "boardName": "stripe",
      "companyName": "Stripe"
    },
    {
      "provider": "lever",
      "boardName": "spotify",
      "companyName": "Spotify"
    },
    {
      "provider": "ashby",
      "boardName": "ashby",
      "companyName": "Ashby"
    }
  ],
  "outputMode": "snapshot",
  "previousJobs": [],
  "emitUnchanged": false,
  "emitRemoved": true,
  "query": "",
  "location": "",
  "remoteOnly": false,
  "publishedAfter": "",
  "maxJobsPerSource": 1000,
  "maxTotalJobs": 5000,
  "maxConcurrency": 5,
  "includeRaw": false
}
```

# Actor output Schema

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

No description

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

No description

# 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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("sapph1re/public-ats-job-change-feed").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 = {}

# Run the Actor and wait for it to finish
run = client.actor("sapph1re/public-ats-job-change-feed").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 '{}' |
apify call sapph1re/public-ats-job-change-feed --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,sapph1re/public-ats-job-change-feed"
        }
    }
}

```

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/db5l7QlsQpgp4JDZG/builds/EkC2O7wnA8amnfdbm/openapi.json
