# ATS Jobs Scraper API - Greenhouse, Workday, Lever & Ashby (`kamerozkan/ats-job-change-feed`) Actor

ATS job scraper & jobs API for Greenhouse, Workday, Lever, Ashby, Workable, Personio, Recruitee & Teamtailor career pages. Scrape normalized job postings with apply URLs, or a change feed of new, updated & closed jobs. No false closures when a source fails.

- **URL**: https://apify.com/kamerozkan/ats-job-change-feed.md
- **Developed by:** [Kamer Ozkan](https://apify.com/kamerozkan) (community)
- **Categories:** Jobs
- **Stats:** 3 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.60 / 1,000 job or 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.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#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.

In JavaScript/TypeScript projects, use official [JavaScript/TypeScript client](https://docs.apify.com/api/client/js/docs.md):

```bash
npm install apify-client
```

In Python projects, use official [Python client library](https://docs.apify.com/api/client/python/docs.md):

```bash
pip install apify-client
```

In shell scripts, use [Apify CLI](https://docs.apify.com/cli/docs.md):

````bash
# MacOS / Linux
curl -fsSL https://apify.com/install-cli.sh | bash
# Windows
irm https://apify.com/install-cli.ps1 | iex
```bash

In AI frameworks, you might use the [Apify MCP server](https://docs.apify.com/integrations/mcp.md).

If your project is in a different language, use 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

**Monitor company hiring changes across eight ATS platforms - directly from career pages,
with reliable closed-job events and no false closures when a source fails.**

ATS Job Change Feed is a source-direct **ATS job scraper API**, **career page monitor**, and
**hiring-signal feed** for Greenhouse, Workday, Lever, Ashby, Workable, Personio, Recruitee,
and Teamtailor. Give it company websites or ATS board URLs and receive one normalized schema
with stable job IDs plus deterministic `CREATED`, `UPDATED`, and `CLOSED` events.

Use it to track competitor hiring, trigger B2B sales workflows, build job alerts, or maintain
a reliable recruiting-analytics feed. In scheduled `changes` mode, unchanged jobs produce no
result records, so frequent monitoring does not repeatedly bill the same snapshot.

> **Unofficial integration.** This Actor is independently developed and is not affiliated
> with, sponsored by, or endorsed by Greenhouse, Lever, Ashby, Workable, Recruitee,
> Personio, Teamtailor, or Workday. Product names are used only to identify compatible
> public career-board formats. It reads public company job postings only - never candidates,
> applications, resumes, or private ATS data.

### Why this ATS job scraper is different

Most job scrapers return a fresh snapshot and leave change tracking, deduplication, and
failure handling to you. This Actor is designed as an operational job change feed.

| Typical snapshot scraper | ATS Job Change Feed |
| --- | --- |
| Returns every open job again | Emits only real changes in `changes` mode |
| Downstream system must infer closures | Emits explicit `CLOSED` events |
| Empty response can look like mass closure | Failed or malformed sources retain prior state |
| One ATS-specific schema | One schema across eight ATS formats |
| Requires known ATS URLs | Can discover boards from company websites |
| Generic locations and text | DACH locations, KldB, languages, skills, and salary signals |
| Duplicate suppression is opaque | Cross-ATS duplicates remain visible and auditable |

### What can this career page monitor do?

Job postings often appear on the employer's own ATS career board before they reach job
aggregators. The Actor connects to those source endpoints and:

- reads public career-board JSON, XML, and RSS without a browser, login, or candidate
  account;
- discovers supported ATS boards from public company and careers pages;
- normalizes eight ATS formats into one stable schema;
- assigns deterministic IDs based on the ATS board and source job ID;
- assigns stable company identities and marks likely cross-ATS duplicates;
- records `firstSeenAt`, `lastSeenAt`, published, and updated timestamps;
- detects new, edited, and closed jobs across scheduled runs;
- merges multi-location variants instead of returning duplicates;
- recognizes DACH job types such as `WORKING_STUDENT`, `APPRENTICESHIP`, and `MINIJOB`;
- enriches DACH locations, KldB groups, language requirements, skills, and salary ranges;
- keeps a failed board's previous snapshot, preventing false mass closures;
- can bootstrap a versioned 10,000-board candidate catalog and process it in deterministic
  lifecycle-managed chunks.

### Start monitoring jobs in 30 seconds

Paste one or more company career pages. The Actor discovers supported ATS boards and creates
the first baseline:

```json
{
  "boards": [],
  "companyUrls": [
    "https://linear.app/careers"
  ],
  "outputMode": "changes",
  "stateKey": "competitor-hiring-watchlist",
  "emitInitialSnapshotChanges": true
}
````

Save the input as a task and schedule it every ten minutes. Later runs return only new,
updated, and closed jobs. If a board fails, its last successful snapshot is preserved and no
synthetic closures are emitted.

### Supported career boards

| ATS | Accepted board URL |
| --- | --- |
| Greenhouse | `https://job-boards.greenhouse.io/{company}` |
| Greenhouse legacy | `https://boards.greenhouse.io/{company}` |
| Lever | `https://jobs.lever.co/{company}` |
| Lever EU | `https://jobs.eu.lever.co/{company}` |
| Ashby | `https://jobs.ashbyhq.com/{company}` |
| Workable | `https://apply.workable.com/{company}` |
| Recruitee | `https://{company}.recruitee.com` |
| Personio | `https://{company}.jobs.personio.de` |
| Teamtailor | `https://{company}.teamtailor.com` |
| Workday | `https://{tenant}.wd{cluster}.myworkdayjobs.com/{career-site}` |

You can also provide an explicit `ats` and `slug`. A single Workable job URL does not contain
the company's account slug and cannot be used as a board URL. Workday requires a board URL
because both its tenant and career-site name are needed.

Teamtailor is read from its public jobs RSS feed and requires no API token. Workday search
results are fetched with consistency-checked pagination. When `includeDescriptions` is
enabled, the Actor also requests every Workday job-detail record; disabling descriptions
reduces request count substantially on large Workday boards.

### Company-domain discovery

When the ATS URL is unknown, pass a company home page or careers page in `companyUrls`.
The Actor scans a small, bounded set of public pages and follows career-related links. Every
discovered board and job keeps its origin URL and discovery-page provenance.

```json
{
  "boards": [],
  "companyUrls": ["https://linear.app/careers"],
  "outputMode": "changes",
  "stateKey": "linear-watchlist"
}
```

The discovery result is written to the default key-value store as `DISCOVERY_REPORT`. A URL
that cannot be resolved does not stop other companies by default. Set
`failOnDiscoveryError` to `true` when complete discovery is required.

### Monitor explicit ATS board URLs

Use `changes` mode for monitoring. The first run emits every current job as `CREATED`.
Later runs return only jobs that appeared, changed, or closed.

```json
{
  "boards": [
    {
      "url": "https://job-boards.greenhouse.io/greenhouse",
      "company": "Greenhouse"
    },
    {
      "url": "https://jobs.lever.co/ae-2",
      "company": "AE"
    }
  ],
  "outputMode": "changes",
  "stateKey": "competitor-watchlist",
  "includeDescriptions": true,
  "includeCompensation": true,
  "emitInitialSnapshotChanges": true
}
```

For continuous monitoring, save this input as an Apify task and run it on a schedule. Keep
the same `stateKey` for that watchlist and do not overlap runs using the same key.

### Input reference

| Field | Type | Default | Description |
| --- | --- | --- | --- |
| `boards` | array | `[]` | Board URL strings or `{ url, ats, slug, company }` objects |
| `companyUrls` | string\[] | `[]` | Company home or careers URLs to scan for supported ATS boards |
| `catalog` | object | disabled | Bootstrap/process the bundled 10K candidate catalog and configure chunk/lifecycle rules |
| `outputMode` | string | `changes` | `changes`, `jobs`, or `both` |
| `stateKey` | string | `default` | Snapshot namespace; use a unique value per independent watchlist |
| `includeDescriptions` | boolean | `true` | Return descriptions and detect description edits |
| `includeCompensation` | boolean | `true` | Normalize salary data when exposed by the ATS |
| `emitInitialSnapshotChanges` | boolean | `true` | Emit `CREATED` for every job on the first run |
| `maxConcurrency` | integer | `10` | Boards fetched in parallel, from 1 to 50 |
| `requestTimeoutSecs` | integer | `30` | Timeout for each request attempt |
| `maxRequestRetries` | integer | `2` | Retries for rate limits, server errors, and network failures |
| `failOnBoardError` | boolean | `false` | Fail the run instead of returning a partial result |
| `discoveryMaxPages` | integer | `5` | Maximum public pages scanned for each company URL |
| `failOnDiscoveryError` | boolean | `false` | Fail if any company URL cannot be resolved |
| `freshnessSlaMinutes` | integer | `10` | Source-to-detection SLA target used in freshness reports |

At least one item is required across `boards` and `companyUrls`, unless `catalog.enabled`
is `true`.

#### Output modes

- `changes` - recommended for scheduled monitoring. Full jobs on the first run, then only
  changes.
- `jobs` - every currently open job on every run.
- `both` - open jobs followed by change records. This can produce two billable records for
  the same job on the first run.

### Output example

The records below follow one real job on a real Recruitee career board through its
lifecycle. The normalized job record and the `CREATED` event are unedited output from
live scheduled runs on 2026-07-28. The `UPDATED` and `CLOSED` events were produced by
the production change engine replaying the same board with one salary edit and one
closure, because neither had yet occurred naturally on this board when this page was
written; every field is real engine output. Long descriptions are shortened here.

#### 1. Normalized job record (`jobs` mode)

One remote role posted across four locations, merged into a single record instead of
four duplicates. The Cologne location activates DACH enrichment: structured
`locationDetails`, `dachRegion`, a conservative KldB occupational group, and extracted
language requirements. Fields the source does not publish stay `null`.

```json
{
  "recordType": "job",
  "status": "OPEN",
  "id": "fb650323dd54bfe995e5ed74be41edf3",
  "jobKey": "recruitee:jobs:2659009",
  "ats": "recruitee",
  "boardSlug": "jobs",
  "sourceJobId": "2659009",
  "company": "Tellent",
  "title": "Senior Backend Engineer (Node.js)",
  "locations": [
    "Remote job",
    "Amsterdam, Noord-Holland, Netherlands",
    "Cologne, Nordrhein-Westfalen, Germany",
    "Poznań, Wielkopolskie, Poland"
  ],
  "locationDetails": [
    {
      "raw": "Remote job",
      "city": null,
      "region": null,
      "country": null,
      "countryCode": null,
      "isDach": false,
      "isRemote": true
    },
    {
      "raw": "Amsterdam, Noord-Holland, Netherlands",
      "city": null,
      "region": null,
      "country": null,
      "countryCode": null,
      "isDach": false,
      "isRemote": false
    },
    {
      "raw": "Cologne, Nordrhein-Westfalen, Germany",
      "city": "Cologne",
      "region": "North Rhine-Westphalia",
      "country": "Germany",
      "countryCode": "DE",
      "isDach": true,
      "isRemote": false
    },
    {
      "raw": "Poznań, Wielkopolskie, Poland",
      "city": null,
      "region": null,
      "country": null,
      "countryCode": null,
      "isDach": false,
      "isRemote": false
    }
  ],
  "dachRegion": "DE",
  "primaryLocation": "Remote job",
  "department": null,
  "team": null,
  "employmentType": "FULL_TIME",
  "employmentTypeRaw": "fulltime_permanent",
  "workplaceType": "REMOTE",
  "workplaceTypeRaw": "remote",
  "salary": null,
  "descriptionText": "Note: The listed salary range is based on the salary range for the Netherlands. Salary ranges may vary by loca ...",
  "descriptionHtml": "(HTML description, shortened here)",
  "publishedAt": "2026-07-23T11:22:12.000Z",
  "updatedAt": "2026-07-23T11:22:12.000Z",
  "sourceUrl": "https://careers.tellent.com/o/senior-backend-engineer-nodejs",
  "applyUrl": "https://careers.tellent.com/o/senior-backend-engineer-nodejs/c/new",
  "boardUrl": "https://jobs.recruitee.com",
  "sourceApiUrl": "https://jobs.recruitee.com/api/offers",
  "boardOrigin": "explicit",
  "discoveredFrom": null,
  "companyId": "e4a478fc861a91b37c616f24",
  "companyDomain": null,
  "canonicalJobId": "4810f6bb90b4cd1e72be89863716be64",
  "duplicateGroupId": null,
  "isDuplicate": false,
  "duplicateOf": null,
  "kldb": {
    "code": "43",
    "title": "Informatik-, Informations- und Kommunikationstechnologieberufe",
    "level": 2,
    "version": "KldB 2010 – Fassung 2020",
    "confidence": 0.9,
    "method": "title-keyword-v1"
  },
  "languageRequirements": [
    {
      "language": "English",
      "level": "C1",
      "evidence": "rimary area of expertise. Strong communication skills and fluency in English. What we offer Hybrid or remote working setup across Amsterdam, the Cologne area, and Po"
    }
  ],
  "skills": [
    "Node.js",
    "React"
  ],
  "firstSeenAt": "2026-07-28T12:27:33.329Z",
  "lastSeenAt": "2026-07-28T12:27:33.329Z",
  "scrapedAt": "2026-07-28T12:27:33.329Z"
}
```

#### 2. `CREATED` and `UPDATED` change events (`changes` mode)

Every change event carries convenient top-level fields for tables plus the full
normalized job in `job`. The `CREATED` event below is the real first observation of the
job above.

```json
{
  "recordType": "change",
  "eventType": "CREATED",
  "eventId": "fa7c2b45bb559a4feaa57995a7914972",
  "detectedAt": "2026-07-28T12:27:33.329Z",
  "jobKey": "recruitee:jobs:2659009",
  "ats": "recruitee",
  "boardSlug": "jobs",
  "company": "Tellent",
  "title": "Senior Backend Engineer (Node.js)",
  "primaryLocation": "Remote job",
  "sourceUrl": "https://careers.tellent.com/o/senior-backend-engineer-nodejs",
  "boardOrigin": "explicit",
  "discoveredFrom": null,
  "companyId": "e4a478fc861a91b37c616f24",
  "canonicalJobId": "4810f6bb90b4cd1e72be89863716be64",
  "duplicateGroupId": null,
  "isDuplicate": false,
  "duplicateOf": null,
  "changedFields": [],
  "job": "(full normalized job record embedded, identical to the record above)"
}
```

When the employer later publishes a salary range, the fingerprint diff emits exactly one
`UPDATED` event, and `changedFields` names precisely what changed:

```json
{
  "recordType": "change",
  "eventType": "UPDATED",
  "eventId": "18f9d2662c7a695f4995e59fd2004de1",
  "detectedAt": "2026-07-28T13:27:31.212Z",
  "jobKey": "recruitee:jobs:2659009",
  "ats": "recruitee",
  "boardSlug": "jobs",
  "company": "Tellent",
  "title": "Senior Backend Engineer (Node.js)",
  "primaryLocation": "Remote job",
  "sourceUrl": "https://careers.tellent.com/o/senior-backend-engineer-nodejs",
  "boardOrigin": "explicit",
  "discoveredFrom": null,
  "companyId": "e4a478fc861a91b37c616f24",
  "canonicalJobId": "4810f6bb90b4cd1e72be89863716be64",
  "duplicateGroupId": null,
  "isDuplicate": false,
  "duplicateOf": null,
  "changedFields": [
    "salary",
    "updatedAt"
  ],
  "job": {
    "status": "OPEN",
    "title": "Senior Backend Engineer (Node.js)",
    "salary": {
      "min": 65000,
      "max": 80000,
      "currency": "EUR",
      "interval": "year",
      "summary": "EUR 65,000 to 80,000 per year",
      "source": "structured",
      "confidence": 0.95
    },
    "firstSeenAt": "2026-07-28T12:27:33.329Z",
    "lastSeenAt": "2026-07-28T13:27:31.212Z",
    "note": "remaining job fields unchanged, shortened here"
  }
}
```

#### 3. `CLOSED`: what the feed guarantees when a job disappears

- **Explicit closure.** The job's final known state is delivered with `status: "CLOSED"`
  and its complete `firstSeenAt` / `lastSeenAt` history, so downstream systems never
  have to infer closures from absence.
- **No stale content.** Stored snapshots deliberately drop descriptions, so a `CLOSED`
  record reports them as `null` instead of replaying outdated text.
- **No false mass closures.** A failed, throttled, or malformed board keeps its previous
  snapshot and emits no `CLOSED` events at all.
- **No noise.** The unchanged third job on this board produced no record in the same
  run, and an immediate repeat run on an unchanged board writes zero rows and bills
  zero events (measured: a 22-role board billed 22 events on first observation, 0 on
  the repeat).

```json
{
  "recordType": "change",
  "eventType": "CLOSED",
  "eventId": "68f701d5a7f1d6fdfe428090d3c77b9e",
  "detectedAt": "2026-07-28T13:27:31.212Z",
  "jobKey": "recruitee:jobs:2650251",
  "ats": "recruitee",
  "boardSlug": "jobs",
  "company": "Tellent",
  "title": "Team Lead Customer Support - DACH",
  "primaryLocation": "Remote job",
  "sourceUrl": "https://careers.tellent.com/o/team-lead-customer-support-dach",
  "boardOrigin": "explicit",
  "discoveredFrom": null,
  "companyId": "e4a478fc861a91b37c616f24",
  "canonicalJobId": "a2862a89b543c4ad7f2bb7576fc9b217",
  "duplicateGroupId": null,
  "isDuplicate": false,
  "duplicateOf": null,
  "changedFields": [
    "status"
  ],
  "job": {
    "recordType": "job",
    "status": "CLOSED",
    "id": "cfcce6be6280bf1e233895acbbcb511c",
    "jobKey": "recruitee:jobs:2650251",
    "ats": "recruitee",
    "boardSlug": "jobs",
    "sourceJobId": "2650251",
    "company": "Tellent",
    "title": "Team Lead Customer Support - DACH",
    "locations": [
      "Remote job",
      "Poznań, Wielkopolskie, Poland"
    ],
    "locationDetails": [
      {
        "raw": "Remote job",
        "city": null,
        "region": null,
        "country": null,
        "countryCode": null,
        "isDach": false,
        "isRemote": true
      },
      {
        "raw": "Poznań, Wielkopolskie, Poland",
        "city": null,
        "region": null,
        "country": null,
        "countryCode": null,
        "isDach": false,
        "isRemote": false
      }
    ],
    "dachRegion": null,
    "primaryLocation": "Remote job",
    "department": null,
    "team": null,
    "employmentType": "FULL_TIME",
    "employmentTypeRaw": "fulltime_fixed_term",
    "workplaceType": "REMOTE",
    "workplaceTypeRaw": "remote",
    "salary": null,
    "descriptionText": null,
    "descriptionHtml": null,
    "publishedAt": "2026-07-14T07:24:04.000Z",
    "updatedAt": "2026-07-14T12:48:50.000Z",
    "sourceUrl": "https://careers.tellent.com/o/team-lead-customer-support-dach",
    "applyUrl": "https://careers.tellent.com/o/team-lead-customer-support-dach/c/new",
    "boardUrl": "https://jobs.recruitee.com",
    "sourceApiUrl": "https://jobs.recruitee.com/api/offers",
    "boardOrigin": "explicit",
    "discoveredFrom": null,
    "companyId": "e4a478fc861a91b37c616f24",
    "companyDomain": null,
    "canonicalJobId": "a2862a89b543c4ad7f2bb7576fc9b217",
    "duplicateGroupId": null,
    "isDuplicate": false,
    "duplicateOf": null,
    "kldb": null,
    "languageRequirements": [],
    "skills": [],
    "firstSeenAt": "2026-07-28T12:27:33.329Z",
    "lastSeenAt": "2026-07-28T12:27:33.329Z",
    "scrapedAt": "2026-07-28T12:27:33.329Z"
  }
}
```

### Company identity and cross-ATS duplicates

Jobs receive a deterministic `companyId`. Discovered boards use the input company domain;
explicit boards fall back to a normalized company name. Across successful boards in the same
run, likely duplicate jobs are clustered using normalized title, compatible location and
employment type, description similarity, and publication timing.

The Actor does not silently delete duplicate records. It marks non-canonical members with
`isDuplicate=true`, sets `duplicateOf` to the richer canonical source record, and gives every
cluster member the same `canonicalJobId` and `duplicateGroupId`. Filter `isDuplicate=false`
when a single-record feed is desired. Keeping the source records makes migration periods and
partial board failures auditable.

### DACH enrichment

Every job includes structured `locationDetails` and a `dachRegion` (`DE`, `AT`, `CH`, or
`MULTI`) when a supported country or major city is recognized. Titles are mapped
conservatively to the official two-digit occupational main groups from the
[KldB 2010 - revised 2020](https://statistik.arbeitsagentur.de/DE/Navigation/Grundlagen/Klassifikationen/Klassifikation-der-Berufe/KldB2010-Fassung2020/KldB2010-Fassung2020-Nav.html).
The result includes its level, version, confidence, and the `title-keyword-v1` method so it
cannot be confused with an official individual-case coding decision.

`languageRequirements` extracts German, English, French, Italian, Dutch, and Spanish
requirements with explicit CEFR levels or a normalized qualitative level. `skills` contains
canonical matches for common engineering, data, cloud, enterprise, and go-to-market tools.
When the ATS has no structured compensation, plausible EUR or CHF ranges can be recovered
from salary-context sentences in the description. Every salary reports `source` and
`confidence`.

### Change detection and reliability

Each successfully fetched board receives its own snapshot in the persistent
`ats-job-change-feed-state` key-value store. Business fields are fingerprinted individually,
so `UPDATED` records identify exactly which normalized fields changed.

Safety behavior:

- HTTP rate limits, server errors, and network failures are retried.
- A malformed HTTP 200 response is rejected instead of interpreted as an empty board.
- Failed boards keep their previous snapshot and emit no `CLOSED` events.
- Location ordering is canonicalized for comparison to avoid false updates.
- Snapshot state advances only after that board's complete output is stored.
- If the run's maximum charge cannot cover a whole board, that board is deferred atomically.
  Increase the maximum charge and rerun; its prior snapshot remains intact.

The `RUN_SUMMARY` record reports discovery, successful, failed, and budget-deferred board
counts. `DISCOVERY_REPORT` lists the pages scanned, resolved boards, and per-company errors.

### Ten-minute monitoring and SLA

For a ten-minute feed, save the Actor input as an Apify task and create an exclusive schedule
with cron expression `*/10 * * * *`. Exclusive scheduling prevents a new run from starting
while the prior one is still active. Keep one stable `stateKey` and use the same timezone for
operational dashboards; `UTC` is the simplest default.

Every run writes `FRESHNESS_REPORT` to the default key-value store. It includes per-board
fetch duration, source timestamp age, failures, and p50/p95/max detection latency. Baseline
`CREATED` events are excluded. Later `CREATED` events use `publishedAt`, `UPDATED` events use
`updatedAt`, and events without a compatible source timestamp are not presented as measured
SLA samples. `complianceRate=null` therefore means “no measurable events,” not 0% compliance.
Use `publishing/examples/ten-minute-freshness-monitor.json` as the task input.

### Push changes into Make, n8n, or your own webhook

Add a webhook for the `ACTOR.RUN.SUCCEEDED` event (Actor page > Integrations). In
scheduled `changes` mode a run's dataset contains only the new `CREATED`, `UPDATED`,
and `CLOSED` records, so your endpoint receives the run notice and fetches exactly the
delta in one call:

```
GET https://api.apify.com/v2/datasets/{resource.defaultDatasetId}/items?format=json&clean=1
```

In Make and n8n you can skip the webhook entirely: both have a native Apify integration
with a run-finished trigger and a get-dataset-items action, so a scheduled
monitor-then-notify flow takes two modules.

### Pilot quality benchmark

Every run also writes `QUALITY_REPORT`. It measures board and discovery success rates, unique
companies toward the 500-company pilot target, field completeness, ATS distribution,
duplicate groups, DACH coverage, enrichment coverage, and errors grouped by ATS. This keeps
the pilot exit criteria tied to observed data instead of a successful process exit alone.

The bundled pilot was generated from versioned public board candidates and validated on
26 July 2026. It selected 500 live boards across all eight adapters and normalized 15,923
open jobs. A rate-limited immediate repeat emitted zero observed changes or synthetic
closures on successful sources. The same fixed 500-board list was then rechecked with
per-provider concurrency limits: 500/500 boards succeeded and returned the same aggregate
job count. The machine-readable reports and fixed input are included with the release
artifacts.

Historical catalog candidates are never presented as verified companies. A candidate enters
the live pilot only after its adapter returns a structurally valid response.

### 10,000-board catalog

Set `catalog.enabled=true` and `catalog.bootstrapBundled=true` to initialize the bundled
10,000-board candidate catalog. Its URLs were derived from versioned Common Crawl URL Index
snapshots; every new record starts as `PENDING`, not `ACTIVE`.

```json
{
  "boards": [],
  "companyUrls": [],
  "catalog": {
    "enabled": true,
    "key": "production-10k",
    "bootstrapBundled": true,
    "process": true,
    "chunkSize": 100
  },
  "outputMode": "changes",
  "stateKey": "production-10k",
  "includeDescriptions": false,
  "includeCompensation": false,
  "emitInitialSnapshotChanges": false,
  "maxConcurrency": 20
}
```

The catalog uses `PENDING`, `ACTIVE`, `RETRY`, `STALE`, and `EXPIRED` states. Successful
fetches activate an entry. Failures get a retry deadline without advancing the job snapshot.
Repeatedly failing old entries become tombstoned `EXPIRED` records; a later company-domain
rediscovery can reactivate them as `PENDING`.

Every catalog run writes `CATALOG_REPORT` and `CATALOG_TASK_PLAN`. The task plan contains a
deterministic input for each chunk. A single rotating task does **not** provide ten-minute
freshness across 10,000 boards; create one exclusive scheduled task per chunk, then verify
account concurrency and CU cost in the cloud before enabling the full schedule.

### StepStone scope decision

StepStone is intentionally not included as a ninth adapter. Its current general terms
explicitly prohibit scraping content for another purpose, while its documented JobFeed API
is a contracted customer/HR-tech interface for sending listings to StepStone rather than a
public bulk-read API. The separate StepStone feasibility phase therefore triggered its
predefined compliance stop-loss. No proxy, CAPTCHA bypass, or StepStone data collection code
is shipped. Reconsider it only with written bulk-read/reuse permission or an appropriately
licensed official read API.

### Pricing behavior

The billable `job-result` event is one record written to the default dataset. In `changes`
mode, unchanged jobs produce no result events at all - if nothing moved on a board, that run
costs you nothing beyond the Actor start fee.

This is verified behaviour, not a claim: a first observation of a board with 22 open roles
billed 22 events; the very next run against the same board found no changes and billed zero.

#### What daily monitoring actually costs

Free-plan example: one board with 500 open roles, checked every day for 30 days,
with a typical ten changes a day:

| | Job or change events | Actor start | Total |
|---|---:|---:|---:|
| Day 1 baseline | $1.00 | $0.005 | $1.005 |
| Each later day | $0.02 | $0.005 | $0.025 |
| **30-day total** | **$1.58** | **$0.15** | **$1.73** |

The first run bills the initial 500 records. The following 29 runs bill only ten
changed records each, plus the start event on every run. Plan discounts can
reduce these prices.

Apify's maximum total charge setting is respected, and the exact current prices are displayed
in the Apify Store pricing section before every run.

Apify's maximum total charge setting is respected. This Actor does not silently advance a
board's snapshot after withholding some of its results for budget reasons.

### Use cases

- Monitor target companies for new or closed roles
- Build a job-board feed from primary company sources
- Generate sales signals from hiring activity
- Track competitor headcount priorities
- Analyze role, location, salary, and remote-work changes
- Create DACH-focused job alerts for Ausbildung, Werkstudent, or Minijob roles

This Actor is also exposed to AI agents through Apify's MCP server
(mcp.apify.com): an agent can discover it by search and run it with the same
pay-per-event billing, with no separate integration.

### Data and privacy

This Actor reads public company job postings only. It does not:

- log in to an ATS;
- access internal jobs, candidates, applications, resumes, or contact details;
- submit job applications;
- send source data to an external enrichment service.

For company-domain discovery, it requests at most `discoveryMaxPages` public HTML pages per
input URL. Private, local, credential-bearing, nonstandard-port, and private-DNS targets are
rejected to prevent server-side request forgery.

Inputs, outputs, and snapshots remain in the Apify storages associated with the user's
account. Users are responsible for ensuring their use of public job data complies with
applicable laws and source-site terms.

### Current limitations

- Company-domain discovery reads public static HTML. JavaScript-only sites, custom career
  stacks, or pages without a supported ATS link can remain unresolved; provide an explicit
  board URL in those cases.
- Company identity uses the discovered domain or normalized company name; legal-entity
  hierarchies and renamed/acquired companies can still require downstream overrides.
- Duplicate detection is conservative metadata, not destructive suppression. Same-titled
  requisitions can require downstream review.
- KldB, language, skill, location, and description-salary enrichment is deterministic
  heuristic metadata. It should be reviewed before regulated, statistical, or compensation
  decisions; the built-in city and skill dictionaries are intentionally bounded.
- Source providers can change their public formats; partial-run safety prevents a format
  failure from creating false closures.
- The bundled 10K catalog is a candidate inventory, not a claim that all 10,000 historical
  URLs are currently live. Lifecycle state and live fetch reports provide that distinction.
- Full 10K/10-minute operation requires multiple scheduled tasks and enough cloud
  concurrency. Local benchmarks cannot substitute for the final account-specific CU and
  pricing check.
- Snapshot writes are atomic per board, but dataset output and snapshot persistence cannot be
  committed as one cross-storage transaction. A crash between them can repeat an event.

### Support

Open an issue on the Actor page with the board URL, run ID, expected behavior, and the
relevant output field. Do not post API tokens, candidate information, or other secrets.

# Actor input Schema

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

Public Greenhouse, Lever, Ashby, Workable, Recruitee, Personio, Teamtailor, or Workday board URLs. Each is normalized into one schema with stable job IDs. Billing is one job-result per row written; in changes mode, unchanged jobs write no rows and bill nothing.

## `companyUrls` (type: `array`):

Company home or careers URLs. The Actor discovers supported ATS boards from a bounded set of public pages and preserves the discovery provenance.

## `catalog` (type: `object`):

Optionally bootstrap and process the bundled 10,000-board candidate catalog in deterministic chunks. Historical candidates become ACTIVE only after a live fetch succeeds.

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

Output open jobs, change events, or both.

## `stateKey` (type: `string`):

Snapshot name used to compare scheduled runs. Keep it stable for one watchlist: the first run creates the baseline, later runs emit only real CREATED, UPDATED, and CLOSED events, and a failed board keeps its previous snapshot instead of producing false closures.

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

Include HTML and plain-text descriptions and detect description edits. Workday uses one additional detail request per open job.

## `includeCompensation` (type: `boolean`):

Normalize structured compensation and parse plausible EUR/CHF salary ranges from descriptions.

## `emitInitialSnapshotChanges` (type: `boolean`):

Emit a CREATED event for every current job when no prior snapshot exists.

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

Maximum number of career boards fetched in parallel.

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

Timeout for each HTTP attempt.

## `maxRequestRetries` (type: `integer`):

Retry count for rate limits, server errors, and network failures.

## `failOnBoardError` (type: `boolean`):

Fail the whole run if any board cannot be fetched. Failed boards never generate false CLOSED events.

## `discoveryMaxPages` (type: `integer`):

Maximum public pages scanned on each company website while looking for supported ATS links.

## `failOnDiscoveryError` (type: `boolean`):

Fail the whole run when any company URL fails or has no supported ATS board.

## `freshnessSlaMinutes` (type: `integer`):

Target source-to-detection latency in minutes, measured after the baseline run when source timestamps are available.

## Actor input object example

```json
{
  "boards": [
    {
      "url": "https://job-boards.greenhouse.io/greenhouse",
      "company": "Greenhouse"
    }
  ],
  "companyUrls": [],
  "catalog": {
    "enabled": false,
    "key": "default",
    "bootstrapBundled": false,
    "process": true,
    "chunkSize": 100,
    "staleAfterDays": 7,
    "expireAfterDays": 30,
    "maxConsecutiveFailures": 5,
    "retryAfterMinutes": 60
  },
  "outputMode": "changes",
  "stateKey": "default",
  "includeDescriptions": true,
  "includeCompensation": true,
  "emitInitialSnapshotChanges": true,
  "maxConcurrency": 10,
  "requestTimeoutSecs": 30,
  "maxRequestRetries": 2,
  "failOnBoardError": false,
  "discoveryMaxPages": 5,
  "failOnDiscoveryError": false,
  "freshnessSlaMinutes": 10
}
```

# Actor output Schema

## `dataset` (type: `string`):

No description

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

No description

## `discoveryReport` (type: `string`):

No description

## `freshnessReport` (type: `string`):

No description

## `qualityReport` (type: `string`):

No description

## `catalogReport` (type: `string`):

No description

## `catalogTaskPlan` (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("kamerozkan/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("kamerozkan/ats-job-change-feed").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).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 kamerozkan/ats-job-change-feed --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=kamerozkan/ats-job-change-feed",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

```json
{
    "openapi": "3.0.1",
    "info": {
        "title": "ATS Jobs Scraper API - Greenhouse, Workday, Lever & Ashby",
        "description": "ATS job scraper & jobs API for Greenhouse, Workday, Lever, Ashby, Workable, Personio, Recruitee & Teamtailor career pages. Scrape normalized job postings with apply URLs, or a change feed of new, updated & closed jobs. No false closures when a source fails.",
        "version": "1.0",
        "x-build-id": "YqgbCbWI1LGnNvdcL"
    },
    "servers": [
        {
            "url": "https://api.apify.com/v2"
        }
    ],
    "paths": {
        "/acts/kamerozkan~ats-job-change-feed/run-sync-get-dataset-items": {
            "post": {
                "operationId": "run-sync-get-dataset-items-kamerozkan-ats-job-change-feed",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for its completion, and returns Actor's dataset items in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        },
        "/acts/kamerozkan~ats-job-change-feed/runs": {
            "post": {
                "operationId": "runs-sync-kamerozkan-ats-job-change-feed",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor and returns information about the initiated run in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/runsResponseSchema"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/acts/kamerozkan~ats-job-change-feed/run-sync": {
            "post": {
                "operationId": "run-sync-kamerozkan-ats-job-change-feed",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for completion, and returns the OUTPUT from Key-value store in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        }
    },
    "components": {
        "schemas": {
            "inputSchema": {
                "type": "object",
                "properties": {
                    "boards": {
                        "title": "ATS career board URLs",
                        "type": "array",
                        "description": "Public Greenhouse, Lever, Ashby, Workable, Recruitee, Personio, Teamtailor, or Workday board URLs. Each is normalized into one schema with stable job IDs. Billing is one job-result per row written; in changes mode, unchanged jobs write no rows and bill nothing.",
                        "default": [
                            {
                                "url": "https://job-boards.greenhouse.io/greenhouse",
                                "company": "Greenhouse"
                            }
                        ]
                    },
                    "companyUrls": {
                        "title": "Company career pages to discover",
                        "type": "array",
                        "description": "Company home or careers URLs. The Actor discovers supported ATS boards from a bounded set of public pages and preserves the discovery provenance.",
                        "items": {
                            "type": "string"
                        },
                        "default": []
                    },
                    "catalog": {
                        "title": "10K board catalog",
                        "type": "object",
                        "description": "Optionally bootstrap and process the bundled 10,000-board candidate catalog in deterministic chunks. Historical candidates become ACTIVE only after a live fetch succeeds.",
                        "properties": {
                            "enabled": {
                                "title": "Enable catalog",
                                "description": "Use persistent catalog lifecycle and chunk selection.",
                                "type": "boolean",
                                "default": false
                            },
                            "key": {
                                "title": "Catalog key",
                                "description": "Persistent namespace for this independent board catalog.",
                                "type": "string",
                                "default": "default",
                                "minLength": 1,
                                "maxLength": 120,
                                "pattern": "^[a-zA-Z0-9._-]+$"
                            },
                            "bootstrapBundled": {
                                "title": "Bootstrap bundled 10K",
                                "type": "boolean",
                                "description": "Merge the bundled, Common Crawl-derived 10,000 candidate boards into the named catalog.",
                                "default": false
                            },
                            "process": {
                                "title": "Process selected chunk",
                                "type": "boolean",
                                "description": "Fetch the selected chunk. Disable to initialize or inspect the catalog only.",
                                "default": true
                            },
                            "chunkSize": {
                                "title": "Boards per chunk",
                                "description": "Fixed number of catalog slots assigned to each task.",
                                "type": "integer",
                                "default": 100,
                                "minimum": 1,
                                "maximum": 500
                            },
                            "chunkIndex": {
                                "title": "Fixed chunk index",
                                "type": "integer",
                                "description": "Optional deterministic chunk override. When omitted, the exclusive-run cursor advances automatically.",
                                "minimum": 0,
                                "maximum": 100000
                            },
                            "staleAfterDays": {
                                "title": "Stale after days",
                                "description": "Mark an active board stale after this many days without success.",
                                "type": "integer",
                                "default": 7,
                                "minimum": 1,
                                "maximum": 365
                            },
                            "expireAfterDays": {
                                "title": "Expire after days",
                                "description": "Tombstone an old repeatedly failing board after this many days.",
                                "type": "integer",
                                "default": 30,
                                "minimum": 2,
                                "maximum": 730
                            },
                            "maxConsecutiveFailures": {
                                "title": "Maximum consecutive failures",
                                "description": "Failure count required before an old board can expire.",
                                "type": "integer",
                                "default": 5,
                                "minimum": 1,
                                "maximum": 100
                            },
                            "retryAfterMinutes": {
                                "title": "Retry delay minutes",
                                "description": "Cooldown applied after a failed board fetch.",
                                "type": "integer",
                                "default": 60,
                                "minimum": 1,
                                "maximum": 43200
                            },
                            "includeStatuses": {
                                "title": "Eligible lifecycle states",
                                "type": "array",
                                "description": "Lifecycle states eligible for chunk selection.",
                                "default": [
                                    "PENDING",
                                    "ACTIVE",
                                    "RETRY",
                                    "STALE"
                                ],
                                "items": {
                                    "type": "string"
                                }
                            }
                        },
                        "default": {
                            "enabled": false,
                            "key": "default",
                            "bootstrapBundled": false,
                            "process": true,
                            "chunkSize": 100,
                            "staleAfterDays": 7,
                            "expireAfterDays": 30,
                            "maxConsecutiveFailures": 5,
                            "retryAfterMinutes": 60
                        }
                    },
                    "outputMode": {
                        "title": "Output mode",
                        "enum": [
                            "jobs",
                            "changes",
                            "both"
                        ],
                        "type": "string",
                        "description": "Output open jobs, change events, or both.",
                        "default": "changes"
                    },
                    "stateKey": {
                        "title": "State key",
                        "type": "string",
                        "description": "Snapshot name used to compare scheduled runs. Keep it stable for one watchlist: the first run creates the baseline, later runs emit only real CREATED, UPDATED, and CLOSED events, and a failed board keeps its previous snapshot instead of producing false closures.",
                        "default": "default"
                    },
                    "includeDescriptions": {
                        "title": "Include descriptions",
                        "type": "boolean",
                        "description": "Include HTML and plain-text descriptions and detect description edits. Workday uses one additional detail request per open job.",
                        "default": true
                    },
                    "includeCompensation": {
                        "title": "Include compensation",
                        "type": "boolean",
                        "description": "Normalize structured compensation and parse plausible EUR/CHF salary ranges from descriptions.",
                        "default": true
                    },
                    "emitInitialSnapshotChanges": {
                        "title": "Emit CREATED on first run",
                        "type": "boolean",
                        "description": "Emit a CREATED event for every current job when no prior snapshot exists.",
                        "default": true
                    },
                    "maxConcurrency": {
                        "title": "Maximum concurrency",
                        "minimum": 1,
                        "maximum": 50,
                        "type": "integer",
                        "description": "Maximum number of career boards fetched in parallel.",
                        "default": 10
                    },
                    "requestTimeoutSecs": {
                        "title": "Request timeout",
                        "minimum": 5,
                        "maximum": 120,
                        "type": "integer",
                        "description": "Timeout for each HTTP attempt.",
                        "default": 30
                    },
                    "maxRequestRetries": {
                        "title": "Request retries",
                        "minimum": 0,
                        "maximum": 5,
                        "type": "integer",
                        "description": "Retry count for rate limits, server errors, and network failures.",
                        "default": 2
                    },
                    "failOnBoardError": {
                        "title": "Fail on board error",
                        "type": "boolean",
                        "description": "Fail the whole run if any board cannot be fetched. Failed boards never generate false CLOSED events.",
                        "default": false
                    },
                    "discoveryMaxPages": {
                        "title": "Discovery pages per company",
                        "minimum": 1,
                        "maximum": 20,
                        "type": "integer",
                        "description": "Maximum public pages scanned on each company website while looking for supported ATS links.",
                        "default": 5
                    },
                    "failOnDiscoveryError": {
                        "title": "Fail on unresolved company",
                        "type": "boolean",
                        "description": "Fail the whole run when any company URL fails or has no supported ATS board.",
                        "default": false
                    },
                    "freshnessSlaMinutes": {
                        "title": "Freshness SLA target",
                        "minimum": 1,
                        "maximum": 1440,
                        "type": "integer",
                        "description": "Target source-to-detection latency in minutes, measured after the baseline run when source timestamps are available.",
                        "default": 10
                    }
                }
            },
            "runsResponseSchema": {
                "type": "object",
                "properties": {
                    "data": {
                        "type": "object",
                        "properties": {
                            "id": {
                                "type": "string"
                            },
                            "actId": {
                                "type": "string"
                            },
                            "userId": {
                                "type": "string"
                            },
                            "startedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "finishedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "status": {
                                "type": "string",
                                "example": "READY"
                            },
                            "meta": {
                                "type": "object",
                                "properties": {
                                    "origin": {
                                        "type": "string",
                                        "example": "API"
                                    },
                                    "userAgent": {
                                        "type": "string"
                                    }
                                }
                            },
                            "stats": {
                                "type": "object",
                                "properties": {
                                    "inputBodyLen": {
                                        "type": "integer",
                                        "example": 2000
                                    },
                                    "rebootCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "restartCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "resurrectCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "computeUnits": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "options": {
                                "type": "object",
                                "properties": {
                                    "build": {
                                        "type": "string",
                                        "example": "latest"
                                    },
                                    "timeoutSecs": {
                                        "type": "integer",
                                        "example": 300
                                    },
                                    "memoryMbytes": {
                                        "type": "integer",
                                        "example": 1024
                                    },
                                    "diskMbytes": {
                                        "type": "integer",
                                        "example": 2048
                                    }
                                }
                            },
                            "buildId": {
                                "type": "string"
                            },
                            "defaultKeyValueStoreId": {
                                "type": "string"
                            },
                            "defaultDatasetId": {
                                "type": "string"
                            },
                            "defaultRequestQueueId": {
                                "type": "string"
                            },
                            "buildNumber": {
                                "type": "string",
                                "example": "1.0.0"
                            },
                            "containerUrl": {
                                "type": "string"
                            },
                            "usage": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "integer",
                                        "example": 1
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "usageTotalUsd": {
                                "type": "number",
                                "example": 0.00005
                            },
                            "usageUsd": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "number",
                                        "example": 0.00005
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
```
