# HiringCafe Jobs Scraper (ATS-sourced jobs, filters, alerts) (`datahamster/hiringcafe-jobs`) Actor

Hiring cafe scraper for jobs from company career sites: scans the recently-posted feed and returns the postings matching your keywords and countries, each with the employer's own apply link, salary, seniority and remote flag. Monitor mode alerts on new jobs. You pay only for rows returned.

- **URL**: https://apify.com/datahamster/hiringcafe-jobs.md
- **Developed by:** [Viktor Dubnytskiy](https://apify.com/datahamster) (community)
- **Categories:** Jobs
- **Stats:** 3 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.64 / 1,000 result items

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## HiringCafe Jobs Scraper (ATS-sourced jobs, filters, alerts)

HiringCafe collects openings straight from company career pages and applicant tracking systems (Workday, Greenhouse, Lever, Ashby, BambooHR, PeopleAdmin, Zoho and hundreds more) instead of re-listing job-board ads. This actor reads **HiringCafe's recently-posted feed**, keeps the postings that match your keywords and countries, and returns each one as a flat row with **the employer's own apply link**.

### What this actor does and does not do

- **Discovery is the recently-posted feed, filtered by keyword and location** — the jobs HiringCafe added in roughly the last day (a few hundred postings, three pages of 200). It is *not* a search over HiringCafe's full index of tens of thousands of openings.
- Why: `hiringcafe.com/robots.txt` disallows the site's own search URLs (`Disallow: /*?searchState=*`, `Disallow: /*?page=*`) and allows `/recently-posted-jobs` and `/job/`. This actor reads only the allowed routes.
- What that makes it good at: **a daily feed of brand-new openings for your keywords**, which is exactly what monitor mode is for. What it is not good at: back-filling every job that ever matched a term.

### What you get

One row per posting: `id`/`jobId`, `url`, `title`, `company`, `companyDomain`, `location`, `isRemote`, `workplaceType`, `employmentType`, `seniority`, `jobCategory`, `salaryMin`, `salaryMax`, `salaryCurrency`, `postedAt`, `applyUrl`, `ats`, `dedupId`, `description`, `query`, `page`, `rank`, `source`, `scrapedAt`.

`dedupId` is HiringCafe's own cluster id for an opening (`<ats>___<board token>___<posting id>`): the same job re-posted on another board carries the same `dedupId`, so it is what you group or de-duplicate on across runs, while `id` stays unique per posting.

The feed itself carries only `title`, `company`, `location`, `jobCategory` and `postedAt`. Everything else — **apply link, salary, employment type, seniority, remote flag, description** — comes from the job page, which is why *Fetch job pages* is on by default. It costs one extra request per kept job; turn it off for a fast, cheap title-and-company list.

### Example output

One full row (*Fetch job pages* on):

```json
{
  "id": "mwhcioxukzkehm06",
  "url": "https://hiringcafe.com/job/business-specialist-iii-university-of-idaho-moscow-idaho-mwhcioxukzkehm06",
  "title": "Business Specialist III",
  "company": "University of Idaho",
  "companyDomain": "uidaho.edu",
  "location": "Moscow, Idaho, United States",
  "isRemote": false,
  "workplaceType": "Onsite",
  "employmentType": "Full Time",
  "seniority": "Mid Level",
  "jobCategory": "Finance and Accounting",
  "salaryMin": 57200.0,
  "salaryMax": 57200.0,
  "salaryCurrency": "USD",
  "postedAt": "2026-09-13T18:17:37.000Z",
  "applyUrl": "https://uidaho.peopleadmin.com/postings/52742",
  "ats": "peopleadmin",
  "dedupId": "peopleadmin___uidaho_com___52742",
  "query": "business specialist",
  "rank": 37
}
```

More rows from the example dataset:

| `title` | `company` | `location` | `salaryMin`–`salaryMax` | `ats` |
|---|---|---|---|---|
| Revenue Audit Supervisor | Rain Rock Casino | Yreka, California, United States | 49920–62400 USD | saashr |
| Business Specialist III | University of Idaho | Moscow, Idaho, United States | 57200 USD | peopleadmin |
| Health Technician (Phlebotomy) | Department of Veterans Affairs | Appleton, Wisconsin, United States | — | — |

### Use cases

- **New-job alerts.** Run it daily in monitor mode on your keywords and get a webhook or Telegram message for every new opening, with the apply link already in the row.
- **Fresh ATS leads.** `ats` and `applyUrl` tell you which system a company just posted through — a live signal for anyone selling to recruiters.
- **Salary tracking on new postings.** `salaryMin`/`salaryMax` are filled only when the employer published a range, so the numbers are real, not estimates.

### Input

| Field | Meaning | Default |
|---|---|---|
| `searches` | Keywords to keep; every word of a line must appear in title, company or category | `["manager"]` |
| `locations` | Country names or ISO-2 codes (country level only) | empty = all |
| `remoteOnly` | Keep only remote jobs (switches job pages on) | `false` |
| `postedWithinDays` | Drop postings older than this many days | empty |
| `maxPages` | Feed pages to scan, 200 postings each (1-25) | `5` |
| `fetchDetails` | Open each kept job's page | `true` |
| `maxItems` | Stop after this many rows | `100` |
| `mode` | `scrape` or `monitor` (only new/changed since last run) | `scrape` |
| `monitorStateId`, `webhookUrl`, `telegramBotToken`, `telegramChatId` | Monitor-mode state key and alert targets | empty |

### Pricing

| Event | Price |
|---|---|
| result | $0.0008 per job ($0.80 per 1,000) |
| monitor-check | $0.005 per monitor run |
| change | $0.0008 per new/changed job |

You are charged only for rows actually pushed. Postings dropped by your filters — including the remote-only filter, which is applied after the job page is read — are never pushed and never charged. No proxy is needed, so there is no proxy surcharge.

### Why this actor

- **Robots-respecting.** Only the routes `hiringcafe.com/robots.txt` allows are read; the site's own search URLs are not touched.
- **The employer's own apply link**, not a redirect through an aggregator.
- **Honest salaries** — a range appears only when the employer published one; HiringCafe's internal estimate is never passed off as a real range.
- **Monitor mode** with webhook and Telegram alerts for new jobs on a keyword.
- **An empty run is explained, not guessed.** The `RUN_SUMMARY` record separates "no posting matched your keywords" from "the source refused the request", and refusals push nothing and charge nothing.

### Limits

- **The feed is the scope.** Only recently posted jobs are visible — currently around 600 postings across three pages, refreshed daily. A keyword with no fresh postings returns nothing; that is an answer, not a failure.
- Keyword matching is a **whole-word** match over title, company and category — it is not HiringCafe's semantic search. `manager` matches "Kitchen Manager" but not "Management"; short words behave sanely (`it` does not match "Substitute", `rn` does not match "Intern").
- The country filter reads the **last segment** of the posting's location text, so a posting that names no country there ("New York City +3 more", "Huntingtown or Prince Frederick") is dropped even though it may be in the country you asked for.
- `postedAt` comes from the feed when the age filter runs, and is then refined by the job page, which uses HiringCafe's estimated publish date. A row can therefore end up with a `postedAt` slightly outside the window you asked for.
- Without *Fetch job pages* there is no apply link, salary, seniority, employment type, remote flag or description.
- No applicant, recruiter or viewer data is collected — the source exposes per-user activity on each posting and this actor never reads it.

### FAQ

**Can I search HiringCafe's whole index?** Not with this actor. `robots.txt` disallows the search URLs, so discovery is the recently-posted feed. For ongoing coverage of a keyword, run it daily in monitor mode — over a week you accumulate every new posting that matched.

**Do I need a proxy?** No. The source answers the platform directly; the actor retries the occasional anti-bot interstitial by itself.

**Why did I get fewer rows than `maxItems`?** The feed had fewer matching postings, or `maxPages` capped the scan, or a filter removed them. The `RUN_SUMMARY` record in the run's key-value store carries `emptyReason`.

**Is the apply link a redirect?** No. `applyUrl` is the URL on the employer's own career site or ATS.

**Does it return personal data?** No. Rows are job-level and company-level only.

### Changelog

- 0.1: initial release — recently-posted feed with keyword, country, remote and age filters, job-page enrichment, monitor mode; an anti-bot interstitial is retried and then reported as a block instead of an empty dataset.

***

If this actor saved you time, a short review on its Store page genuinely helps other people find it. Found a bug or need a field that is missing? Open a ticket on the **Issues** tab.

# Actor input Schema

## `maxItems` (type: `integer`):

Stop after this many results (you are charged only for pushed items)

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

scrape = full results; monitor = only new/changed items since the previous run of this task

## `monitorStateId` (type: `string`):

Optional state id when not running as a saved task (monitor mode)

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

POST a change summary here in monitor mode

## `telegramBotToken` (type: `string`):

Optional: bot token for monitor-mode change summaries

## `telegramChatId` (type: `string`):

Optional: chat id that receives monitor-mode summaries

## `searches` (type: `array`):

Keywords to keep, one per line, e.g. "manager", "registered nurse", "data analyst". A posting is kept when every word of a line appears in its title, company or category. Leave empty to return the whole recent feed.

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

Country names or ISO-3166 alpha-2 codes, e.g. "United States", "DE", "Canada". Matched against the posting's location text, so it works at country level only. Leave empty for every country.

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

true = keep only jobs the employer marked remote. The remote flag lives on the job page, so this switches Fetch job pages on. Example: false.

## `postedWithinDays` (type: `integer`):

Keep only postings published within this many days, e.g. 1. The feed is already recent, so this narrows it further. Leave empty for no age limit.

## `maxPages` (type: `integer`):

How many pages of the recently-posted feed to scan; one page is 200 postings. The feed is usually 3 pages, and the scan stops at its last page. Range 1-25.

## `fetchDetails` (type: `boolean`):

true = open each kept job's page for the apply link, salary, employment type, seniority, remote flag and description (one request per kept job). false = feed fields only: title, company, location, category, posted date. Example: true.

## Actor input object example

```json
{
  "maxItems": 100,
  "mode": "scrape",
  "searches": [
    "manager"
  ],
  "remoteOnly": false,
  "maxPages": 5,
  "fetchDetails": true
}
```

# Actor output Schema

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

All pushed rows (dataset, JSON)

## `resultsTable` (type: `string`):

Dataset in the Console viewer

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

RUN\_SUMMARY record

# 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 = {
    "searches": [
        "manager"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("datahamster/hiringcafe-jobs").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 = { "searches": ["manager"] }

# Run the Actor and wait for it to finish
run = client.actor("datahamster/hiringcafe-jobs").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 '{
  "searches": [
    "manager"
  ]
}' |
apify call datahamster/hiringcafe-jobs --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,datahamster/hiringcafe-jobs"
        }
    }
}
```

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/pd016d4dLhx1zXjNV/builds/L5gnlkqqNASgEtCRL/openapi.json
