# ATS Job Monitor — New Postings Only (Greenhouse, Lever, Ashby) (`drkiwi/hiring-signal-alerts`) Actor

Yesterday's job list is noise. Get only the roles that weren't there last run — Greenhouse, Lever and Ashby, with signed webhooks and per-signal pricing.

- **URL**: https://apify.com/drkiwi/hiring-signal-alerts.md
- **Developed by:** [Kevin Baldassari](https://apify.com/drkiwi) (community)
- **Categories:** Jobs, Automation, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $5.00 / 1,000 new job detecteds

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.

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

## Greenhouse, Lever & Ashby Job Scraper — Only New Postings

**Yesterday's job list is noise.** This Actor watches your target companies' job boards and reports only the roles that weren't there on the last run.

"Only new jobs" is easy to promise and hard to get right. This one is built around the parts that go wrong — and it ships a health check that tells you when something upstream breaks, instead of leaving you to notice your alerts went quiet.

What "reliably" means here, concretely:

- **A board glitch doesn't become fifty fake alerts.** If a company's board briefly returns a partial or empty list — a deploy, an ATS migration, a rate limiter that still answers `200` — a naive differ forgets those jobs and re-reports every one of them as new on the next run. This Actor requires a posting to be absent across several consecutive runs before forgetting it.
- **State actually survives between scheduled runs.** Tracking data lives in a persistent named store, not in per-run storage that is recreated every time. Get this wrong and the Actor silently re-baselines on every run and reports everything as new, forever.
- **A failed fetch never corrupts the diff.** If one company can't be reached, its state is left untouched rather than advanced on data that never arrived.
- **You are never charged for a signal you didn't receive.** If a spend limit is reached mid-run, the undelivered postings are withheld from the state too, so the next run reports them again instead of losing them.
- **You can predict the bill before you buy.** Real measured posting volumes are published below, not left for you to discover on your first invoice.
- **You can tell when it stops working.** Job board APIs change without warning. A built-in health check verifies all three providers still return the fields this Actor depends on, and fails loudly if not — schedule it and you find out before your alerts silently go empty.
- **Webhooks can be verified.** Every POST can be signed, so your endpoint can prove it came from this Actor and hasn't been replayed.

### What it does

- **Tracks any company** across Greenhouse, Lever and Ashby job boards — tell it the ATS or let it detect it automatically.
- **Detects only what's new.** It remembers every job it has already seen, so a run over unchanged data returns zero new postings instead of the same list again.
- **Shows you signal from run one.** A 7-day lookback means your first run already surfaces the roles posted this week, rather than an empty table.
- **Delivers the signal** to a dataset row and, optionally, a webhook POST fired once per new matching job.

### Who it's for

- **Sales / BD teams** — get pinged when a target account posts a role that signals a buying trigger (a "DevOps Engineer" opening at a prospect means infrastructure budget).
- **Recruiters / talent teams** — track when competitors open roles in specific departments, and benchmark their hiring velocity.
- **Investors / market researchers** — monitor portfolio or watchlist companies' hiring as a growth or slowdown proxy.

***

### ⚠️ Read this first: how the first run works

The Actor reports what's new *since it started tracking a company*. On the very first run there is nothing to compare against, so it records everything currently open as the **baseline**. What it reports on that first run is your choice:

| `firstRunLookbackDays` | First run reports | Cost of that run |
|---|---|---|
| **`7`** (prefilled) | roles the ATS says were posted in the last 7 days | small — a few dozen roles |
| `0` | nothing; pure baseline | **free** |
| — with `alertOnFirstRun` | every open role, however old | expensive — 500+ roles at a big company |

**The default input uses a 7-day lookback**, so your very first run already shows real hiring signals instead of an empty table. Set it to `0` if you want a strictly free baseline and are happy to wait for the next run.

Either way, **from the second run onward you only ever get genuinely new postings.** The lookback applies to a company's first run only.

Things worth knowing:

1. **You must put the Actor on a schedule.** A single run gives you a snapshot. The product is the ongoing feed.
2. **Every company gets its own first run.** Add a company to your list six months from now and the same logic applies to it alone — your existing companies are unaffected.
3. **Avoid `alertOnFirstRun` unless you want a full snapshot.** It charges for every open role, including ones posted years ago. The lookback exists to give you the useful part at a fraction of the cost.

***

### How it works

1. **Add your companies.** Each needs its ATS slug — the company identifier in its job board URL. Set `ats`, or leave it as `auto` to detect it.
2. **Put it on a schedule.** Save your input as a Task, then **Task → Schedules → Create schedule**. Daily is a good default; hourly if you're in a competitive sales cycle. The Actor does not schedule itself.
3. **Get notified only on new matches.** New roles appear in the dataset with `isNewThisRun: true`, and each fires a webhook if you set a `webhookUrl`.

#### Finding a company's slug

It's the identifier in the job board URL:

| Board URL | Slug | ATS |
|---|---|---|
| `boards.greenhouse.io/stripe` | `stripe` | greenhouse |
| `jobs.lever.co/palantir` | `palantir` | lever |
| `jobs.ashbyhq.com/ramp` | `ramp` | ashby |

Many companies host the board on their own domain (`stripe.com/jobs`). The slug is usually still the company name in lowercase — set `ats: "auto"` and let the Actor find it. If a slug is wrong, that company is skipped with a log message and the rest of your list still runs.

***

### Input

```json
{
  "companies": [
    { "name": "Stripe",   "slug": "stripe",   "ats": "greenhouse" },
    { "name": "Palantir", "slug": "palantir", "ats": "lever" },
    { "name": "Ramp",     "slug": "ramp",     "ats": "ashby" },
    { "name": "Vanta",    "slug": "vanta",    "ats": "auto" }
  ],
  "keywords": ["engineer", "devops", "sales"],
  "locations": ["Berlin", "Remote"],
  "firstRunLookbackDays": 7,
  "webhookUrl": "https://hooks.example.com/new-job"
}
```

Only `companies` is required.

| Field | Type | Default | What it does |
|---|---|---|---|
| `companies` | array | — | Companies to track. Each needs `slug`; `name` and `ats` are optional. |
| `keywords` | string\[] | none | Alert only on job titles containing one of these. |
| `departments` | string\[] | none | Alert only on these departments. |
| `locations` | string\[] | none | Alert only on these locations. |
| `webhookUrl` | string | none | POST once per new matching job. |
| `webhookSecret` | string (secret) | none | Signs every webhook so your endpoint can verify it. See below. |
| `healthCheck` | boolean | `false` | Ignore the company list and check the three ATS APIs for breakage instead. Free. |
| `firstRunLookbackDays` | integer | `0` (form prefills `7`) | On a company's **first run only**, also report roles the ATS says were posted within this many days. Max 90. |
| `alertOnFirstRun` | boolean | `false` | Report **every** open role on a company's first run, however old. Expensive — see above. Overrides the lookback. |
| `onlyPushNewJobs` | boolean | `false` | Push only new jobs to the dataset instead of all open roles. |
| `resetState` | boolean | `false` | Clear tracking state and re-baseline. |
| `stateStoreName` | string | `hiring-signal-state` | Named key-value store holding the "already seen" state. |
| `maxConcurrency` | integer | `5` | Companies fetched in parallel. |
| `requestTimeoutSecs` | integer | `60` | Timeout for one ATS request. |
| `maxRequestRetries` | integer | `3` | Retries before giving up on a company. |

#### How filters behave

All three filters are **case-insensitive substring** matches. A job must satisfy **every filter you set**, and **any one term** within a filter. So `keywords: ["engineer","designer"]` + `locations: ["berlin"]` means *(engineer OR designer) AND berlin*.

Filters control **alerts and billing, not the dataset**. By default every open role still lands in the dataset so you keep the full picture; jobs that are new but filtered out are flagged `isNewThisRun: true` and cost you nothing. Set `onlyPushNewJobs: true` if you want a leaner dataset.

***

### Output

One dataset row per job:

```json
{
  "company": "Ramp",
  "ats": "ashby",
  "jobId": "34413f8d-26bf-4bbc-8ade-eb309a0e2245",
  "title": "Security Engineer, Cloud",
  "department": "Engineering",
  "location": "New York, NY (HQ)",
  "remote": true,
  "employmentType": "Full-time",
  "salary": "$211.4K – $290.6K • Offers Equity",
  "url": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245",
  "publishedAt": "2026-08-04T14:22:10.000Z",
  "firstSeenAt": "2026-08-05T09:00:00.000Z",
  "isNewThisRun": true,
  "matchedKeywords": ["engineer"]
}
```

Two dates, and the difference matters:

- **`publishedAt`** — when the *company* published the role, straight from the ATS. Use this to judge how fresh a posting is.
- **`firstSeenAt`** — when *this Actor* first saw it. On an ongoing schedule the two are close; on a first run `publishedAt` can be much older.

`matchedKeywords` is `null` when no keyword filter is set.

The dataset has two ready-made views in the Console: **New jobs this run** and **All tracked jobs**.

#### What each ATS actually publishes

Optional fields are `null` when the source board doesn't provide them — this varies by platform, and it's worth knowing before you build a filter around one:

| Field | Greenhouse | Lever | Ashby |
|---|---|---|---|
| `department` | ✅ | ✅ | ✅ |
| `location` | ✅ | ✅ | ✅ |
| `publishedAt` | ✅ | ✅ | ✅ |
| `remote` | inferred from location text | ✅ from workplace type | ✅ real flag |
| `employmentType` | sometimes | ✅ | ✅ |
| `salary` | rarely | rarely | ✅ when disclosed |

`remote` on Greenhouse is a best-effort read of the location string, so a remote-friendly role advertised as "New York" reads as `false`.

#### Webhook payload

One POST per new matching job — never batched:

```json
{
  "event": "new_job_posting",
  "company": "Ramp",
  "title": "Security Engineer, Cloud",
  "url": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245",
  "detectedAt": "2026-08-05T09:00:00.000Z"
}
```

A webhook endpoint that is down or returns an error is retried, then logged and skipped — it never fails the run or costs you the rest of your signals.

#### Verifying that a webhook really came from us

Set `webhookSecret` and every POST carries two extra headers:

```
x-hiring-signal-timestamp: 1786041600
x-hiring-signal-signature: sha256=9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
```

The signature is `HMAC-SHA256` of `<timestamp>.<raw request body>`, hex-encoded. The timestamp is signed *together with* the body, so a captured request can't be replayed later.

**Node.js**

```js
import { createHmac, timingSafeEqual } from 'node:crypto';

function isValid(rawBody, headers, secret) {
  const timestamp = headers['x-hiring-signal-timestamp'];
  const received = headers['x-hiring-signal-signature'];

  // Reject anything older than 5 minutes.
  if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;

  const expected = 'sha256=' +
    createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex');

  const a = Buffer.from(expected);
  const b = Buffer.from(received ?? '');
  return a.length === b.length && timingSafeEqual(a, b);
}
```

**Python**

```python
import hmac, hashlib, time

def is_valid(raw_body: bytes, headers, secret: str) -> bool:
    timestamp = headers.get("x-hiring-signal-timestamp", "")
    received = headers.get("x-hiring-signal-signature", "")

    if abs(time.time() - float(timestamp)) > 300:
        return False

    expected = "sha256=" + hmac.new(
        secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(expected, received)
```

Verify against the **raw** body bytes, before any JSON parsing — re-serialising changes the bytes and the signature won't match.

***

### Knowing when it breaks

Public job board APIs change without notice. When that happens the usual symptom isn't an error, it's silence: alerts quietly stop arriving and you assume nobody is hiring.

Run the Actor with `healthCheck: true` and it ignores your company list and checks the providers themselves:

- can a known-good board still be reached on each of Greenhouse, Lever and Ashby?
- does every job still carry an id, title and URL?
- are ids still unique?
- are `publishedAt`, `location` and `department` still being populated?
- does a non-existent company still return a clean 404?

It writes one row per provider and **fails the run** if anything critical broke, so Apify's run notifications reach you. It touches no tracking state and is never charged.

Schedule it daily alongside your real runs if you depend on this Actor for anything that matters.

***

### Pricing

**$0.02 per new job detected and delivered.** Not per run, not per API call, not per dataset row.

- **Only new jobs that pass your filters are charged.** Filters *lower* your bill.
- **Rows that aren't new are free.** A run where nothing changed costs you nothing.
- **A pure baseline run is free** — with `firstRunLookbackDays: 0`, a company's first run reports nothing and charges nothing.
- **With the prefilled 7-day lookback, a first run costs a little** because it reports real recent postings. Measured on the exact prefilled input (Dropbox, Palantir, Linear + the `engineer`/`sales` keyword filter): 364 open roles scanned, 13 posted in the last 7 days, **4 matched the filter — so 4 events, $0.08**. Remove the keyword filter and the same run charges 13 events, $0.26.
- You can cap total spend per run in the Actor's settings.

#### What that actually costs you

Most pay-per-event pricing leaves you guessing at the bill, so here is real data. Measured across 19 live boards in August 2026, a **median company publishes 43 new roles per 30 days** (quartiles: 29 / 43 / 111). Small companies post 2–5 a month; the largest post 180–260.

| Your setup | Alerts / month | Cost / month |
|---|---|---|
| 10 companies, no filters | ~430 | ~$9 |
| 20 companies, no filters | ~860 | ~$17 |
| 20 companies, with filters | ~250 | ~$5 |
| 50 companies, no filters | ~2,150 | ~$43 |

If you track large, fast-hiring companies, use filters. That's what they're for, and it's the difference between $43 and $5.

**One-off first-run cost.** With the 7-day lookback, adding companies costs roughly a quarter of the monthly figures above, once — and your filters apply to it just as they do to everything else. Ten companies unfiltered ≈ 110 recent roles ≈ $2.20, and you see a week of hiring activity immediately. Compare that with `alertOnFirstRun`, which for the same ten companies would report ~1,700 mostly stale roles at ~$34.

***

### Things worth knowing

**Put each independent watchlist in its own state store.** Two schedules tracking the same company with the same `stateStoreName` share one baseline — whichever runs first consumes the signal, and the other sees nothing new. If you want a daily sales alert *and* a weekly research report over overlapping companies, give one of them a different `stateStoreName`.

**A job that briefly disappears and comes back is not re-alerted.** Boards glitch, and companies migrate mid-quarter. A posting must be missing from several consecutive runs before it's forgotten — otherwise a blip would arrive as a burst of false "new job" alerts, on your bill.

**A genuinely re-opened role does count as new.** If a company closes a role and re-posts it weeks later, that's a fresh hiring signal and you'll be told.

**A broken slug doesn't break the batch.** It's logged and skipped, and every other company still runs. Check the run log or the `RUN_SUMMARY` record for the reason.

**A company with no open roles is fine.** It's tracked normally and reports zero jobs.

**Some companies run boards on two ATS platforms.** ClickHouse, for example, serves the same roles on both Greenhouse and Ashby. With `auto`, the platform that actually has open roles wins. Set `ats` explicitly to force one.

**`resetState: true` and changing `stateStoreName` both start a fresh baseline** — free, but you lose the tracking history and that run reports nothing.

***

### Limits

- Greenhouse, Lever and Ashby only. No Workday, SmartRecruiters or LinkedIn.
- No historical backfill: it reports what's new since *you* started tracking, not since the company was founded.
- Webhook delivery only — no email.
- Public job boards only. Nothing private, nothing behind a login.

***

### FAQ

**Why did my first run report nothing?**
You ran it with `firstRunLookbackDays: 0`. That's the strict setting: the first run for a company only records a baseline, so you aren't alerted (or charged) for roles that have been open for months. Set the lookback to 7 to see the last week's postings immediately, or just run it again tomorrow.

**Is the lookback the same as historical backfill?**
No. It reaches back a bounded number of days on a company's first run only, so you're not starting from an empty screen. It will not reconstruct a company's entire posting history, and it never applies to later runs.

**Why do `publishedAt` and `firstSeenAt` differ?**
`publishedAt` is the company's own date for the role; `firstSeenAt` is when this Actor first saw it. On a first run with a lookback they can be days apart — that's expected. On an ongoing daily schedule they'll usually be within a day of each other.

**Why not just call the free ATS APIs myself?**
You can — for one company, once. What this removes is the rest: normalizing three different JSON schemas into one row shape, keeping per-company state across scheduled runs so you know what changed, and the edge cases you only find in production. Boards that answer `200` with an empty array. Companies that migrate ATS mid-quarter and leave a stale empty board behind. Companies that run two ATS at once. Titles with stray whitespace that break naive deduplication. Transient upstream blips that a naive differ reports as fifty brand-new jobs.

**How is this different from other ATS scrapers on the Store?**
Several now advertise a diff or delta mode, so "only new jobs" alone is no longer the distinction. The difference is in the failure cases listed at the top of this page — absence tolerance, state that genuinely persists across scheduled runs, never charging for an undelivered signal — plus a built-in health check, signed webhooks, and published volume figures so you can predict your bill. If you're comparing options, the useful test is to run each for a few days and see which one reports zero on an unchanged board and doesn't spam you when a board hiccups.

**Is the health check charged?**
No. It reports no jobs, so there is nothing to charge for. Schedule it as often as you like.

**Do I have to use the webhook secret?**
No, it's optional. Set it if your endpoint is publicly reachable and you want to reject anything that didn't come from this Actor — which, if you're triggering internal automation off these alerts, you should.

**How do I keep the cost down?**
Use filters. You're charged only for new jobs that match them, so a keyword or department filter is a direct discount. Tracking a few very large companies unfiltered is the expensive case.

**How often should I schedule it?**
Daily suits most use cases. Hourly makes sense for competitive sales triggers where being first matters. The frequency doesn't change your bill — you're charged per new posting, not per run, so an hourly schedule over a quiet week still costs nothing.

**Can I get alerted only about a specific department or location?**
Yes — `departments` and `locations`, alongside `keywords`. They combine with AND.

**What happens if my webhook endpoint is down?**
It's retried, then logged and skipped. The run still succeeds and the jobs are still in the dataset, so you can recover anything you missed.

**Do I need to check the dataset if I use webhooks?**
No, but it's useful. The webhook carries the essentials (company, title, URL, timestamp); the dataset row adds department, location, remote, employment type and salary.

***

*Keywords: ATS scraper, Greenhouse API, Lever jobs, Ashby jobs, hiring signals, new job alerts, sales intelligence, recruiting intelligence, competitor hiring tracker, job posting webhook.*

# Actor input Schema

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

Required, unless you are running a health check. List of companies to monitor. Each entry needs a 'slug' (the company identifier in the ATS job board URL). 'ats' can be 'greenhouse', 'lever', 'ashby', or 'auto' (default) to detect it automatically. 'name' is an optional display label.

## `keywords` (type: `array`):

Only alert on jobs whose title matches one of these (case-insensitive substring). Leave empty for no filter.

## `departments` (type: `array`):

Only alert on jobs whose department matches one of these (case-insensitive substring). Leave empty for no filter.

## `locations` (type: `array`):

Only alert on jobs whose location matches one of these (case-insensitive substring). Leave empty for no filter.

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

If set, a POST request fires for each new matching job.

## `webhookSecret` (type: `string`):

If set, every webhook is signed so your endpoint can verify it really came from this Actor and has not been replayed. The POST carries 'x-hiring-signal-timestamp' and 'x-hiring-signal-signature: sha256=...', where the signature is HMAC-SHA256 of '<timestamp>.<raw body>' using this secret. Verification example in the README.

## `firstRunLookbackDays` (type: `integer`):

The first run for a company normally records a baseline and reports nothing, because there is no previous run to compare against. Set this to e.g. 7 and that first run also reports roles the ATS says were published in the last 7 days — so you see real signals immediately instead of an empty result. Applies to a company's first run only; every run after that reports genuinely new postings regardless. Set to 0 to disable.

## `alertOnFirstRun` (type: `boolean`):

Treats EVERY currently open role as new on a company's first run, however old it is. This can be expensive on large boards (a single company may have 500+ open roles, all charged) — prefer the lookback setting above. Overrides the lookback when enabled.

## `onlyPushNewJobs` (type: `boolean`):

By default the dataset contains every job currently open at the tracked companies, with 'isNewThisRun' flagging the new ones. Enable this to push only the newly detected jobs.

## `healthCheck` (type: `boolean`):

Ignores the company list and instead verifies that the Greenhouse, Lever and Ashby APIs still return the fields this Actor depends on. Reports one row per provider and fails the run if any check fails. Free — it never charges. Schedule it daily if you rely on this Actor for anything important: it catches upstream changes before they silently empty your alerts.

## `resetState` (type: `boolean`):

Clears the stored 'seen jobs' state for the companies in this input before running. The run then behaves as a first run for them. Useful for testing and for re-baselining.

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

Named key-value store holding the 'jobs already seen' state between runs. Leave as-is unless you want separate, independent baselines (for example a test schedule and a production schedule). Must be alphanumerics and dashes only. This cannot be the run's default store, because Apify creates a fresh default store for every run.

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

How many company job boards to fetch in parallel.

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

Timeout for a single HTTP request to an ATS API.

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

How many times to retry a failing ATS request (network errors, timeouts, HTTP 429 and 5xx) before giving up on that company.

## Actor input object example

```json
{
  "companies": [
    {
      "name": "Dropbox",
      "slug": "dropbox",
      "ats": "greenhouse"
    },
    {
      "name": "Palantir",
      "slug": "palantir",
      "ats": "lever"
    },
    {
      "name": "Linear",
      "slug": "linear",
      "ats": "ashby"
    }
  ],
  "keywords": [
    "engineer",
    "sales"
  ],
  "firstRunLookbackDays": 7,
  "alertOnFirstRun": false,
  "onlyPushNewJobs": false,
  "healthCheck": false,
  "resetState": false,
  "stateStoreName": "hiring-signal-state",
  "maxConcurrency": 5,
  "requestTimeoutSecs": 60,
  "maxRequestRetries": 3
}
```

# Actor output Schema

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

One row per job currently open at the tracked companies. The ones that are new since the last run carry isNewThisRun: true — those are the signals.

## `runSummary` (type: `string`):

Per-company breakdown of this run: jobs seen, new, alerted, and the reason any company was skipped.

## `healthReport` (type: `string`):

Only present on runs started with healthCheck: true. Per-provider status, field coverage and any detected upstream drift.

# API

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

## JavaScript example

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

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

// Prepare Actor input
const input = {
    "companies": [
        {
            "name": "Dropbox",
            "slug": "dropbox",
            "ats": "greenhouse"
        },
        {
            "name": "Palantir",
            "slug": "palantir",
            "ats": "lever"
        },
        {
            "name": "Linear",
            "slug": "linear",
            "ats": "ashby"
        }
    ],
    "keywords": [
        "engineer",
        "sales"
    ],
    "firstRunLookbackDays": 7
};

// Run the Actor and wait for it to finish
const run = await client.actor("drkiwi/hiring-signal-alerts").call(input);

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

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

```

## Python example

```python
from apify_client import ApifyClient

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

# Prepare the Actor input
run_input = {
    "companies": [
        {
            "name": "Dropbox",
            "slug": "dropbox",
            "ats": "greenhouse",
        },
        {
            "name": "Palantir",
            "slug": "palantir",
            "ats": "lever",
        },
        {
            "name": "Linear",
            "slug": "linear",
            "ats": "ashby",
        },
    ],
    "keywords": [
        "engineer",
        "sales",
    ],
    "firstRunLookbackDays": 7,
}

# Run the Actor and wait for it to finish
run = client.actor("drkiwi/hiring-signal-alerts").call(run_input=run_input)

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

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

```

## CLI example

```bash
echo '{
  "companies": [
    {
      "name": "Dropbox",
      "slug": "dropbox",
      "ats": "greenhouse"
    },
    {
      "name": "Palantir",
      "slug": "palantir",
      "ats": "lever"
    },
    {
      "name": "Linear",
      "slug": "linear",
      "ats": "ashby"
    }
  ],
  "keywords": [
    "engineer",
    "sales"
  ],
  "firstRunLookbackDays": 7
}' |
apify call drkiwi/hiring-signal-alerts --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,drkiwi/hiring-signal-alerts"
        }
    }
}

```

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/K4k9aFbgRF0biQ5or/builds/QVPmyrx4iWtXlo5yQ/openapi.json
