# Remote Jobs Scraper - RemoteOK, WWR, Jobicy & Working Nomads (`rjmacarthy/remote-jobs-scraper`) Actor

Remote jobs from RemoteOK, We Work Remotely, Jobicy and Working Nomads in one normalized, de-duplicated dataset: parsed salary min/max, normalized categories, keyword/location/salary filters and an only-new-jobs mode for alerts.

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

## Pricing

from $2.00 / 1,000 remote job scrapeds

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?

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

## Remote Jobs Scraper - RemoteOK, We Work Remotely, Jobicy & Working Nomads

Get remote job listings from **four remote job boards in one run**: [Remote OK](https://remoteok.com), [We Work Remotely](https://weworkremotely.com), [Jobicy](https://jobicy.com) and [Working Nomads](https://www.workingnomads.com). Every job comes back in the same clean schema. **Duplicates across boards are merged** into one record that lists every board it appeared on. Salaries are parsed into min/max/currency/period, and categories are normalized, so a "Back-End Programming" job and a "Software Engineering" job both end up as `software-dev`. Filter by keyword, category, candidate location, minimum salary and posting age, or turn on **only new jobs** for a scheduled job alert. Most remote-job Actors cover a single board or return the same job three times. This one reads public JSON APIs and RSS feeds only: no browser, no login, fast and cheap.

### What data can you get?

| Field | Example |
|---|---|
| `title`, `company` | Senior React Full stack Developer, Lemon.io |
| `category` (normalized) | `software-dev`, `design`, `marketing`, `sales`, `customer-support`, `product`, `data`, `devops`, `finance`, `hr`, `writing`, `other` |
| `sourceCategory`, `tags` | Back-End Programming; `["python", "django"]` |
| `jobType` | full-time, part-time, contract, freelance, internship |
| `locationRestriction` | USA Only, Anywhere in the World, Europe, LATAM |
| `salaryMin`, `salaryMax`, `salaryCurrency`, `salaryPeriod`, `salaryText` | 80000, 200000, USD, year, "$80,000 - $200,000 / year" |
| `postedAt`, `expiresAt` | ISO 8601 dates |
| `descriptionText`, `descriptionHtml` | Full job description |
| `applyUrl` | The original posting on the source board |
| `source`, `sources`, `sourceUrls` | Remote OK; every board that listed the job |
| `companyUrl`, `companyLogo` | Company website (We Work Remotely) |
| `id`, `url`, `scrapedAt` | `remoteok:1137475`, job page, scrape time |

### Use cases

- **Remote job alerts:** schedule a daily run with `onlyNew` and send new matches to Slack or e-mail.
- **Job boards and newsletters:** feed a curated "remote Python jobs" page or weekly digest, with attribution and links back to each source (see Sources and attribution).
- **Salary research:** compare advertised remote pay by category and region with the `salaries` view.
- **Recruiting and market research:** see which companies are hiring remotely, for what roles, and where candidates may live.

### How to use

1. Pick the **sources** (all four by default).
2. Add **keywords** (any match on title, tags or company), and optionally **categories**, **candidate location**, **minimum salary** and **posted within (days)**.
3. Set **Max jobs** and run. Download the results as JSON, CSV or Excel, or read them through the API.

Remote Python developer jobs from all four boards:

```json
{
    "keywords": ["python", "django"],
    "categories": ["software-dev"],
    "maxItems": 50
}
```

Customer support jobs open to US residents:

```json
{
    "categories": ["customer-support"],
    "locationRestriction": ["USA"],
    "maxItems": 30
}
```

Daily alert: new marketing jobs from the last 7 days paying at least $80k:

```json
{
    "categories": ["marketing"],
    "postedWithinDays": 7,
    "minSalary": 80000,
    "onlyNew": true,
    "stateKey": "marketing-alert",
    "maxItems": 0
}
```

### Output example

One real job (trimmed). It was listed on three boards and merged into one record:

```json
{
    "id": "remoteok:1137475",
    "url": "https://remoteok.com/remote-jobs/remote-senior-react-full-stack-developer-lemon-io-1137475",
    "title": "Senior React Full stack Developer",
    "company": "Lemon.io",
    "companyUrl": null,
    "companyLogo": null,
    "category": "software-dev",
    "sourceCategory": null,
    "tags": ["dev", "engineer", "python", "aws", "javascript", "react", "node", "typescript", "full time", "full stack"],
    "jobType": "full-time",
    "locationRestriction": null,
    "salaryText": "$80,000 - $200,000 / year",
    "salaryMin": 80000,
    "salaryMax": 200000,
    "salaryCurrency": "USD",
    "salaryPeriod": "year",
    "postedAt": "2026-10-09T13:09:18.000Z",
    "expiresAt": null,
    "descriptionText": "Are you a talented Senior Developer looking for a remote job that lets you show your skills and get decent compensation? Look no further than Lemon.io …",
    "descriptionHtml": "<p>Are you a talented Senior Developer looking for a remote job …",
    "applyUrl": "https://remoteok.com/remote-jobs/remote-senior-react-full-stack-developer-lemon-io-1137475",
    "source": "Remote OK",
    "sourceUrl": "https://remoteok.com/remote-jobs/remote-senior-react-full-stack-developer-lemon-io-1137475",
    "sources": ["Remote OK", "We Work Remotely", "Working Nomads"],
    "sourceUrls": [
        "https://remoteok.com/remote-jobs/remote-senior-react-full-stack-developer-lemon-io-1137475",
        "https://weworkremotely.com/remote-jobs/lemon-io-senior-react-full-stack-developer-7",
        "https://www.workingnomads.com/job/go/1920195/"
    ],
    "scrapedAt": "2026-11-04T12:31:27.577Z"
}
```

More items: [`samples/sample-output.json`](samples/sample-output.json). The dataset has three views: **Overview**, **Salaries** and **Descriptions**. A `RUN_SUMMARY` record in the key-value store shows, per source, the status, jobs fetched, matched and output, and the request count.

### Pricing

Pay per event, no start fee:

| Event | Price | When |
|---|---|---|
| `job` | **$0.002** | One job saved to the dataset |

Example: **1,000 remote jobs ≈ $2.00**, and a daily alert that finds 30 new jobs costs about $0.06 a day. You only pay for jobs that are output. Duplicates merged across boards, jobs filtered out and (with `onlyNew`) jobs already seen cost nothing. The run stops cleanly at your **Maximum cost per run**.

### Sources and attribution

The jobs come from these boards' public feeds. **Every `applyUrl` goes to the original posting** on the board it came from. If you republish the jobs, credit the board and link back as its terms ask:

- **[Remote OK](https://remoteok.com)** ([API](https://remoteok.com/api)): mention Remote OK as the source and link back to the job on Remote OK (a followed link, no `nofollow`). Don't use the Remote OK logo without permission.
- **[We Work Remotely](https://weworkremotely.com)** ([RSS](https://weworkremotely.com/remote-jobs.rss)): link to the original posting.
- **[Jobicy](https://jobicy.com)** ([API](https://jobi.cy/apidocs)): credit Jobicy with a direct link to the source, and point every application button to the original job URL (`applyUrl`).
- **[Working Nomads](https://www.workingnomads.com)** ([API](https://www.workingnomads.com/api/exposed_jobs/)): link to the original posting.

**Remotive** and **Himalayas** are deliberately **not** included: their API terms forbid redistributing their jobs through other job boards or aggregators.

### Tips and limits

- **How much each board returns:** Remote OK about 100 newest jobs, Working Nomads about 30, Jobicy up to 100 per keyword from the last 7 days, We Work Remotely 100 on the main feed (10 per category) or the full category feeds when you pick categories A run with no filters returns about 300 unique jobs.
- **Keywords** are matched as whole words on title, tags and company (`go` does not match "Google"). They are also sent to Jobicy's search to fetch more relevant jobs, but every job is still checked locally, so results stay on topic. Jobicy needs keywords of 3+ characters; with a shorter keyword, Jobicy is read unfiltered and matched locally.
- **Candidate location** matches each board's own location text. Worldwide jobs also match a regional filter (you can take them from anywhere); enter only `Worldwide` to get worldwide jobs alone. Jobs with no location (common on Remote OK) are dropped when a location filter is set.
- **Minimum salary** compares the top of the range in USD per year (hourly × 2080, monthly × 12). Jobs without a parsed USD salary are **kept**, not dropped. Remote OK and Jobicy publish structured pay; for We Work Remotely and Working Nomads a pay range is extracted from the description when one is stated.
- **Categories** are normalized from the board's own category (or the title when the board has none, as on Remote OK), so expect some borderline calls.
- **De-duplication:** jobs with the same company and title (ignoring punctuation and suffixes like Inc/LLC/AB) posted within 14 days are merged. The richest posting is kept (salary first, then the longest description).
- **Max jobs** picks jobs in turns from each board, newest first, so one busy board doesn't crowd out the rest.
- **Only new jobs:** IDs are stored per `stateKey` in the named key-value store `remote-jobs-scraper-state`. Jobicy asks feeds not to be polled more than once an hour; a daily or hourly schedule is plenty.

### FAQ

**Is it legal?**
The Actor reads only public job feeds and APIs that the boards publish for this purpose. It collects no personal data: no applicant data, and e-mail addresses and phone numbers in job descriptions are replaced with `[email removed]` / `[phone removed]`. You are responsible for how you use the data, including each board's terms (see Sources and attribution) and data-protection laws such as the GDPR.

**Do I need a proxy?**
Usually not. All four feeds answered normally from Apify's datacenter IPs in testing. If a board returns HTTP 403/429, enable **Proxy configuration** with Apify Proxy (datacenter). **Residential proxies are not supported**: if you select them, the Actor falls back to Apify datacenter proxy and logs a warning.

**What happens if one board is down?**
The others still run and the run succeeds. The board's error is in the log and in `RUN_SUMMARY`. The run fails only if no board could be read at all.

**Why did I get fewer jobs than Max jobs?**
The boards only publish their newest postings, and your filters apply after fetching. `RUN_SUMMARY` shows, per board, how many jobs were fetched and how many matched.

### Integrations

Use the results anywhere through the [Apify API](https://docs.apify.com/api/v2) (JSON, CSV, Excel, RSS), the official JavaScript and Python clients, **Make**, **Zapier**, **n8n**, Google Sheets, Slack and webhooks. Run it from AI agents through the [Apify MCP server](https://mcp.apify.com). Schedule it in Apify Console for daily job alerts.

### Changelog

- **0.1** (November 2026): first release. Four sources (RemoteOK, We Work Remotely, Jobicy, Working Nomads), normalized schema, cross-source de-duplication, salary parsing, filters, only-new-jobs mode.

### More scrapers by the same developer

- [ATS Jobs Scraper: Greenhouse, Lever, Ashby & Workday](https://apify.com/rjmacarthy/ats-jobs-scraper)
- [Dice Jobs Scraper](https://apify.com/rjmacarthy/dice-jobs-scraper)
- [Reed.co.uk Jobs Scraper - UK Job Listings with Salary](https://apify.com/rjmacarthy/reed-jobs-scraper)
- [SEEK Jobs Scraper](https://apify.com/rjmacarthy/seek-jobs-scraper)
- [Workday Jobs Scraper](https://apify.com/rjmacarthy/workday-jobs-scraper)
- [AI Visibility Tracker: ChatGPT, Perplexity, Gemini, Claude](https://apify.com/rjmacarthy/ai-search-visibility-tracker)

Browse all my Actors on [my Apify Store profile](https://apify.com/rjmacarthy).

### Support

Found a bug or need another board or field? Open an issue on the **Issues** tab with your input, and we'll take a look.

# Actor input Schema

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

Job boards to read. Default: all four. Each is a public JSON API or RSS feed.

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

A job is kept if ANY keyword appears (whole word, case-insensitive) in its title, tags or company name, e.g. "python", "react", "product designer". Also sent to Jobicy search to fetch more matching jobs. Leave empty for all jobs. Max 10.

## `excludeKeywords` (type: `array`):

Drop jobs whose title, tags or company contain any of these words, e.g. "senior", "crypto".

## `categories` (type: `array`):

Keep only these normalized categories. Leave empty for all. Choosing categories also reads the matching deeper We Work Remotely category feeds.

## `locationRestriction` (type: `array`):

Keep jobs open to candidates in ANY of these places, matched on each source's location text: "Worldwide", "USA", "Canada", "UK", "Europe", "LATAM", "APAC" (synonyms like "Anywhere", "United States", "EMEA" are understood) or any country name. Jobs open worldwide match every region; use "Worldwide" alone for worldwide-only jobs. Jobs without a location are dropped when this is set.

## `minSalary` (type: `integer`):

Drop jobs whose top of range is below this, in USD per year (hourly x 2080, monthly x 12). Jobs without a parsed USD salary are kept. 0 = off.

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

Keep only jobs posted in the last N days. 0 = any age.

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

Maximum jobs to output (after filters and de-duplication). Jobs are picked in turns from each source, newest first. 0 = no limit.

## `onlyNew` (type: `boolean`):

Skip jobs already output by earlier runs with the same state key (stored in the named key-value store "remote-jobs-scraper-state"). Use it for scheduled job alerts: the first run outputs everything and becomes the baseline.

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

Name of this alert's memory. Use a different key for each scheduled search so they don't share seen jobs.

## `proxyConfiguration` (type: `object`):

Not needed normally: all sources are public feeds. Enable Apify Proxy (datacenter) if a source blocks or rate-limits the run. Residential proxies are not supported: if selected, the Actor falls back to Apify datacenter proxy.

## Actor input object example

```json
{
  "sources": [
    "remoteok",
    "weworkremotely",
    "jobicy",
    "workingnomads"
  ],
  "keywords": [
    "python"
  ],
  "excludeKeywords": [],
  "categories": [],
  "locationRestriction": [],
  "minSalary": 0,
  "postedWithinDays": 0,
  "maxItems": 15,
  "onlyNew": false,
  "stateKey": "default",
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

One item per job: title, company, category, location, salary, posting date, sources and apply link.

## `jobsFull` (type: `string`):

Every field, including tags, parsed salary and the description in HTML and plain text.

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

Per-source status, counts, request statistics and warnings.

# 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 = {
    "sources": [
        "remoteok",
        "weworkremotely",
        "jobicy",
        "workingnomads"
    ],
    "keywords": [
        "python"
    ],
    "maxItems": 15
};

// Run the Actor and wait for it to finish
const run = await client.actor("rjmacarthy/remote-jobs-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 = {
    "sources": [
        "remoteok",
        "weworkremotely",
        "jobicy",
        "workingnomads",
    ],
    "keywords": ["python"],
    "maxItems": 15,
}

# Run the Actor and wait for it to finish
run = client.actor("rjmacarthy/remote-jobs-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 '{
  "sources": [
    "remoteok",
    "weworkremotely",
    "jobicy",
    "workingnomads"
  ],
  "keywords": [
    "python"
  ],
  "maxItems": 15
}' |
apify call rjmacarthy/remote-jobs-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,rjmacarthy/remote-jobs-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/Bd2UREIb0IIcZqEUY/builds/aXqDW1oK8ElYo6vk3/openapi.json
