# Remote Jobs Scraper — Pay Ranges & Region Eligibility (`mochiboo/remote-jobs-ats-scraper`) Actor

Remote roles read from employers' own Greenhouse boards, not from aggregators — so you see openings that never reach RemoteOK or Himalayas. Every row carries the employer's posted pay range (67% of remote roles) and a normalised answer to who may apply: country, region or state.

- **URL**: https://apify.com/mochiboo/remote-jobs-ats-scraper.md
- **Developed by:** [mochi](https://apify.com/mochiboo) (community)
- **Categories:** Jobs, Automation, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

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

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

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

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

# README

## Remote Jobs Scraper — Pay Ranges & Region Eligibility

**Remote job listings taken straight from employers' own job boards**, not from a jobs
aggregator. Every row tells you three things a remote-jobs feed usually can't: what the employer
actually pays, **which countries a candidate may be based in**, and whether the role is remote-only
or also has an office.

Built for anyone assembling a remote-work product: job boards, newsletters, Slack/Discord job
feeds, recruiting desks, remote-work researchers, and agents that need structured job data.

***

### Why this is different from every other remote-jobs scraper

Almost every remote-jobs actor on the store reads a third-party aggregator — RemoteOK, Himalayas,
Jobicy, Arbeitnow, Remotive, We Work Remotely. Those sites are useful, but they only carry roles a
company **chose to syndicate** to them, and their salary numbers are frequently estimates.

This actor reads the **employer's own Greenhouse job board** instead. That has three consequences:

| | Aggregator-based scrapers | This actor |
|---|---|---|
| Coverage | Only jobs posted to that one site | Every open remote role on the company's own board |
| Freshness | Whatever the aggregator last ingested | The employer's live board |
| Pay | Often estimated, often missing | The range the **employer published** — 67% of remote roles carry one |
| Eligibility | Raw location string, e.g. `Remote - USA` | Parsed: `remoteScope`, `remoteCountries`, `remoteSubdivisions` |

Measured on **1,995 live remote postings across 25 company boards (2026-08-05)**: 67% disclosed a
pay range, and the boards used **460 distinct spellings of the location field**. Turning that mess
into an answerable question is the work this actor does for you.

***

### What it returns

One row per remote role. Selected fields:

**Role**
`company` · `title` · `jobUrl` · `department` / `departments` · `offices` · `jobId` ·
`requisitionId` · `firstPublished` · `updatedAt` · `daysSinceFirstPublished` · `language`

**Remote eligibility (the differentiator)**

| Field | Meaning |
|---|---|
| `isRemote` | The role is remote in some form |
| `remoteScope` | `worldwide` · `region` · `country` · `subdivision` · `unspecified` |
| `remoteCountries` | ISO-3166 alpha-2 codes, e.g. `["US","CA"]` |
| `remoteRegions` | Macro-regions the posting named: `EMEA`, `APAC`, `LATAM`, `NORAM`, `EUROPE`, `MENA`, `ANZ`, `AFRICA`, `AMER` |
| `remoteSubdivisions` | States/provinces when the role is narrower than a country, e.g. `["ON","BC","AB","NS"]` |
| `hasOnsiteOption` | The posting also lists a physical office (i.e. it is hybrid-capable) |
| `remoteUnparsed` | Any part of the location string we would not guess at — **we report it, we do not invent a country** |
| `location` | The employer's raw string, always preserved |

**Pay** (parsed into numbers, not strings)
`hasPayRange` · `payMin` · `payMax` · `payCurrency` · `payCurrencySymbol` · `payPeriod`
(`year`/`month`/`hour`) · `payPeriodInferred` · `payPeriodConflict` · `payIsOte` · `paySource`
(`structured` or `prose`) · `payRangeRaw` · `payRangeCount` · `payRangeSuspect`

`payRangeCount > 1` means the posting quotes several bands (US pay zones are common) and
`payMin`/`payMax` are the first of them.

***

### Example row

```json
{
  "company": "Coinbase",
  "title": "Accounting Manager, GL Operations & Integrations",
  "location": "Remote - Canada",
  "isRemote": true,
  "remoteScope": "country",
  "remoteCountries": ["CA"],
  "remoteRegions": [],
  "remoteSubdivisions": [],
  "hasOnsiteOption": false,
  "remoteUnparsed": [],
  "department": "Accounting",
  "hasPayRange": true,
  "payMin": 170000,
  "payMax": 170000,
  "payCurrency": "CAD",
  "payPeriod": "year",
  "payPeriodInferred": false,
  "payRangeSuspect": false,
  "daysSinceFirstPublished": 12,
  "jobUrl": "https://job-boards.greenhouse.io/coinbase/jobs/7139612"
}
```

***

### Input

Everything is optional — **run it with no input at all** and it scans a built-in registry of
**128 company job boards**, verified live and ordered by how many remote roles each was carrying.

| Input | Default | What it does |
|---|---|---|
| `companies` | `[]` (use the registry) | Company names, board tokens, or board URLs |
| `eligibleInCountry` | `""` | ISO code, e.g. `US`, `GB`, `DE`, `IN`, `CA` — keep only roles someone in that country can take |
| `strictCountryMatch` | `false` | Require the country to be named outright (drops worldwide, macro-regions and bare "Remote") |
| `maxResults` | `20` | Rows to write, shared round-robin across boards |
| `maxBoards` | `40` | Upper bound on boards touched per run |
| `onlyWithPayRange` | `false` | Keep only roles that disclose pay |
| `titleKeywords` | `[]` | e.g. `["engineer","designer"]` |
| `departments` | `[]` | e.g. `["engineering"]` |
| `postedWithinDays` | `0` | Only roles first published in the last N days |
| `includeDescription` | `false` | Adds `descriptionText` |
| `allowUnverifiedMatches` | `false` | See "It refuses rather than guesses" below |

#### Recipes

- **A fresh remote-jobs feed** — schedule daily with `postedWithinDays: 1`.
- **Remote roles a German candidate can take** — `eligibleInCountry: "DE"`.
- **Remote pay benchmarking** — `onlyWithPayRange: true`, `titleKeywords: ["engineer"]`.
- **Watch specific employers** — `companies: ["GitLab","Coinbase","Samsara"]`.

***

### Country matching, precisely

`eligibleInCountry: "GB"` keeps a role when:

1. the posting names the UK (`Remote - UK`, `United Kingdom (Remote)`, `Remote, United Kingdom`), **or**
2. it names a macro-region containing the UK (`Remote-EMEA`, `Remote-Europe`), **or**
3. it is worldwide, or names no place at all (`Remote`, `Distributed`).

Set `strictCountryMatch: true` to keep only case 1. A posting whose location we could *not* read
is never counted as a match under either mode — it is reported in `remoteUnparsed` instead.

***

### It refuses rather than guesses

- **Ambiguous places stay ambiguous.** `Remote - CA` could be California or Canada. We leave it
  unresolved rather than pick. `Remote - Georgia` resolves to the US state only when the posting
  carries another US signal.
- **A board we are not sure about is skipped, not returned.** Resolving the company "Remote" hits
  the board token `remote`, which belongs to "General Assembly Remote Jobs". Unverified matches are
  reported by name, not silently returned as the company you asked for.
- **Bad-looking pay is flagged, not laundered.** `payRangeSuspect` marks bands whose top is more
  than 20× the bottom — nearly always a typo in the employer's posting.
- **Every run prints its own accounting**: postings inspected, remote found, written, over the cap,
  boards failed, companies unresolved. Also stored under the `RUN_SUMMARY` key.

***

### Legal & privacy

- Source is **Greenhouse's documented public Job Board API** (`boards-api.greenhouse.io/v1/boards/…`)
  — no key, no login, no paywall. Its `robots.txt` restricts only `/embed/`, and names no AI
  crawler; `job-boards.greenhouse.io/robots.txt` has every directive commented out.
- **Lever and Ashby are deliberately not supported.** `jobs.lever.co` names `ClaudeBot` with
  `Disallow: /`; `api.ashbyhq.com/robots.txt` returns 401, so permission cannot be established.
  Narrow and clean beats broad and arguable.
- **Organisation-level data only.** Company, role, location, department, pay band. No applicant,
  employee or recruiter identity is read or emitted — the endpoint exposes none. GDPR/PDPA-safe.

### Limits, stated plainly

- **Greenhouse only.** Companies on Workday, Lever, Ashby, SmartRecruiters or a bespoke careers
  page are out of scope for this actor.
- **Coverage is the built-in registry** (128 boards, ~3,200 remote roles at build time) plus any
  company you name yourself. It is not "every remote job on the internet", and does not claim to be.
- Pay disclosure varies enormously by employer: 100% at some (Affirm, Twilio, Grafana Labs), 2% at
  others (Stripe). The 67% figure is the population average across the sample, not a promise per company.
- Greenhouse publishes no documented rate limit; very large `maxBoards` values are untested.

***

### Output stability — for scheduled and automated use

If you are wiring this into a pipeline rather than clicking Run, this is the contract:

- **Fields are added, not removed or renamed.** New columns may appear in a future version; the ones
  documented above keep their names and types.
- **Pay fields are numbers or `null`** — never a string, never a formatted range.
- **`RUN_SUMMARY`** in the key-value store is the completeness check. It carries `boardsFetched`,
  `boardsFailed`, `unresolved`, `jobsSeen`, `remoteSeen`, `written`, `withPayRange`, `filtered`,
  a `payDisclosurePct` for the run, and an `unresolved` array. Read it instead of trusting the row
  count — a short run because boards failed looks identical to a short run because nobody is hiring
  remotely, and only `RUN_SUMMARY` separates them.
- **Rows are written as they are scraped, not batched at the end.** An aborted or timed-out run leaves
  the postings it already collected in the dataset, so a large `maxBoards` value that hits a time limit
  still returns usable data rather than nothing.
- **Found a field that changed on you?** Open an issue on the **Issues** tab. Anything that would
  break a running pipeline gets a deprecation alias rather than a rename.

### Use with AI agents (MCP)

This actor is callable through [Apify's MCP server](https://mcp.apify.com), so an agent can pull
remote-eligible roles with pay ranges without you writing any integration code.

**Want only this actor as a tool, with no store search in the way?** Point the agent at
`https://mcp.apify.com/?actors=mochiboo/remote-jobs-ats-scraper` and it loads as a single named tool —
the agent calls it directly instead of searching for it and picking something else.

Every input is optional: with no `companies` it reads a bundled registry of 128 boards verified live
at build time, ordered by remote-role density, so an agent calling it with `{}` gets a useful sample
rather than a validation error.

### Related actors

- **[Greenhouse Jobs Scraper](https://apify.com/mochiboo/greenhouse-jobs-salary-scraper)** — every open role on a named board, remote or not.
- **[Workday Jobs Scraper](https://apify.com/mochiboo/workday-jobs-salary-scraper)** — the same pay-range extraction for companies on Workday.

### Support & feedback

Hit a board that will not resolve, or a pay format that parses wrong? Open an issue on the **Issues**
tab with the board token. If it saved you time, a store review genuinely helps; this actor is new and
has none yet.

# Actor input Schema

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

Company names, Greenhouse board tokens, or board URLs. Leave EMPTY to scan the built-in registry of 128 verified Greenhouse boards, ordered by how many remote roles each carries.

## `eligibleInCountry` (type: `string`):

Two-letter country code, e.g. US, GB, DE, IN, CA. Matches roles that name the country, plus worldwide roles and macro-regions that contain it (EMEA, APAC, LATAM…). Leave empty for all remote roles.

## `strictCountryMatch` (type: `boolean`):

Require the country to be named outright. Excludes worldwide postings, macro-regions and postings that say only "Remote".

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

Rows to write. Shared round-robin across boards so one large employer cannot use up the whole cap.

## `maxBoards` (type: `integer`):

Upper bound on how many company boards a run touches.

## `onlyWithPayRange` (type: `boolean`):

67% of remote roles on these boards disclose a range (measured on 1,995 live postings). Turn on for compensation work.

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

Case-insensitive substrings, ANY of which may match, e.g. engineer, designer, sales.

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

Case-insensitive substrings matched against the posting's departments, e.g. engineering, marketing.

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

Keep only roles first published in the last N days. 0 = no limit. Use with a schedule to get a fresh-postings feed.

## `includeDescription` (type: `boolean`):

Adds descriptionText (plain text). Makes rows much larger.

## `allowUnverifiedMatches` (type: `boolean`):

Off by default: if a guessed board token belongs to a differently-named company, the run reports it instead of returning another company's roles.

## Actor input object example

```json
{
  "companies": [],
  "eligibleInCountry": "",
  "strictCountryMatch": false,
  "maxResults": 20,
  "maxBoards": 40,
  "onlyWithPayRange": false,
  "titleKeywords": [],
  "departments": [],
  "postedWithinDays": 0,
  "includeDescription": false,
  "allowUnverifiedMatches": false
}
```

# 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": [],
    "eligibleInCountry": "",
    "maxResults": 20,
    "maxBoards": 40,
    "titleKeywords": [],
    "departments": [],
    "postedWithinDays": 0
};

// Run the Actor and wait for it to finish
const run = await client.actor("mochiboo/remote-jobs-ats-scraper").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": [],
    "eligibleInCountry": "",
    "maxResults": 20,
    "maxBoards": 40,
    "titleKeywords": [],
    "departments": [],
    "postedWithinDays": 0,
}

# Run the Actor and wait for it to finish
run = client.actor("mochiboo/remote-jobs-ats-scraper").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": [],
  "eligibleInCountry": "",
  "maxResults": 20,
  "maxBoards": 40,
  "titleKeywords": [],
  "departments": [],
  "postedWithinDays": 0
}' |
apify call mochiboo/remote-jobs-ats-scraper --silent --output-dataset

```

## MCP server setup

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

```

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/Kn9qKpaGhRPUaqfwc/builds/Do2ruIdG9P9BgiRuu/openapi.json
