# Remote Jobs Aggregator — 6 Boards \[/w EMAILS💰] (`memo23/remote-jobs-aggregator`) Actor

One run = 2,000–3,000 unique live remote jobs from 6 boards — RemoteOK full-depth, WeWorkRemotely, Remotive, Himalayas, Jobicy, Arbeitnow — deduplicated into one clean dataset with salary, geo eligibility, and opt-in employer contact emails. $1.99/1K jobs. JSON or CSV out.

- **URL**: https://apify.com/memo23/remote-jobs-aggregator.md
- **Developed by:** [Muhamed Didovic](https://apify.com/memo23) (community)
- **Categories:** Jobs, AI, Agents
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.99 / 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/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

## Remote Jobs Aggregator — 6 Boards in One Dataset

Aggregate **six remote-job boards in a single run** — RemoteOK (full 1,000+ board), WeWorkRemotely, Remotive, Himalayas, Jobicy, and Arbeitnow — into one normalized, cross-board-deduplicated dataset: 2,000–3,000 unique live remote jobs with salary ranges, who-may-apply countries, full descriptions, and opt-in **employer contact emails**. Built for recruiters, job boards, researchers, and lead-gen teams who are tired of running five scrapers and merging CSVs by hand.

![How Remote Jobs Aggregator works](https://raw.githubusercontent.com/muhamed-didovic/muhamed-didovic.github.io/main/assets/how-it-works-remote-jobs-aggregator.png)

### How it works

1. **Run with defaults** — all six boards are fetched **in parallel** (pick any subset with the `sources` input).
2. Every job is normalized into one 30+ field schema; the same job posted on several boards collapses into **one row** (richest copy wins, the rest listed in `alsoFoundOn`, missing fields backfilled across boards).
3. Filters (keyword, tags, countries, salary, job type, posting age) run **before** rows count toward `maxItems` — you never pay for filtered-out rows.
4. Results are balanced round-robin across boards, freshest first, and exported as JSON, CSV, or Excel.

> 💰 **$1.99 per 1,000 unique jobs.** Six boards for roughly the price competitors charge for one.

***

### ✨ Why use this aggregator?

- **RemoteOK at full depth.** Most aggregators wrap RemoteOK's public API, which stops at the 100 newest jobs. This actor walks RemoteOK's complete live board — 1,000+ jobs — via the same feed its own infinite scroll uses.
- **Real cross-board dedupe.** Company+title matching collapses reposts into one row and records where else it appeared (`alsoFoundOn`) — the only honest way to count "unique live remote jobs".
- **One schema, six boards.** Salary ranges parsed to `{min, max, currency}`, dates to ISO, employment types unified to `FULL_TIME`/`CONTRACT`/…, geo eligibility as one `applicantCountries[]` array — no per-board special-casing in your code.
- **Fault-tolerant by design.** Boards are fetched independently; one being down is logged and skipped, never a failed run.
- **Employer contact emails (opt-in).** `enrichEmails` adds `contactEmail` + `contactWebsite` discovered from each hiring company's website — turning the merged feed into a lead list.
- **Balanced output.** Round-robin across boards stops any single high-volume source from drowning the rest of your dataset.

***

### What makes this richer than alternatives

| Capability | This actor | Typical remote-jobs aggregator |
|---|---|---|
| **Boards** | ✅ 6 (incl. WeWorkRemotely) | ⚠️ 3–4 easy APIs |
| **RemoteOK depth** | ✅ Full board (1,000+) | ❌ API's newest 100 |
| **Cross-board dedupe** | ✅ With `alsoFoundOn` + field backfill | ❌ Raw duplicates |
| **Salary** | ✅ Parsed `{min, max, currency}` | ⚠️ Raw strings |
| **Geo eligibility** | ✅ Unified `applicantCountries[]` | ⚠️ Per-board formats |
| **Contact emails** | ✅ Opt-in enrichment | ❌ |
| **Filter billing** | ✅ Filtered rows are free | ⚠️ Billed after scraping |
| **Source balance** | ✅ Round-robin | ❌ Whatever order boards return |

***

### Supported boards

| Board | Coverage per run | Notes |
|---|---|---|
| **RemoteOK** | ~1,000+ (full live board) | 10× past its public API's 100-job cap; full descriptions recovered from the page |
| **WeWorkRemotely** | ~100 newest | Full ~8KB descriptions, region + skills tags |
| **Himalayas** | paginated walk | Salary, seniority, location + timezone restrictions, real apply links |
| **Remotive** | ~20 newest | Their public API's hard cap — verified |
| **Jobicy** | ~50 newest | Industry, level, salary fields |
| **Arbeitnow** | remote rows only | European lean; non-remote rows are dropped |

Pick any subset with the `sources` input — e.g. `["remoteok", "weworkremotely"]` for just the two big independent boards.

***

### 🎯 Use cases

- **Job boards & newsletters** — one scheduled run mirrors six boards into your product, deduplicated and normalized.
- **Recruiting & sourcing** — every live listing for a skill or geography across the remote market, with employer emails on tap.
- **Market research** — salary distributions, tag frequency, and geo-eligibility trends across boards, not just one site's bias.
- **Lead generation** — each job is a company spending money on hiring right now; `contactEmail` makes them reachable.
- **Job alerts** — schedule daily with `timeFilter: "24h"` for a cross-board feed of only fresh postings.

***

### Quick start

Everything, defaults — up to 2,000 unique jobs from all six boards:

```json
{}
```

Senior engineering roles open to US applicants, posted this week, with emails:

```json
{
    "searchKeyword": "senior engineer",
    "countries": ["US"],
    "timeFilter": "7d",
    "enrichEmails": true
}
```

Just the two big independent boards:

```json
{
    "sources": ["remoteok", "weworkremotely"]
}
```

***

### Input configuration

| Field | Type | Default | Notes |
|---|---|---|---|
| `sources` | array | all six | Any subset of `remoteok`, `weworkremotely`, `remotive`, `himalayas`, `jobicy`, `arbeitnow` |
| `dedupe` | boolean | `true` | Cross-board dedupe with `alsoFoundOn` + field backfill |
| `searchKeyword` | string | `""` | Matched over title + company + categories/industry + location; every word must match |
| `tagsFilter` | array | `[]` | Any-of match over tags + categories |
| `countries` | array | `[]` | Names or ISO-2; `Anywhere` jobs always pass |
| `jobTypes` | array | `[]` | `FULL_TIME`, `CONTRACT`, … |
| `minSalary` | integer | `0` | USD/year, permissive (unlisted salaries kept) |
| `timeFilter` | select | `""` | `24h` / `7d` / `30d` / `90d` |
| `includeDescription` | boolean | `true` | Off = drop description fields |
| `cleanHtml` | boolean | `false` | On = plain-text description only |
| `enrichEmails` | boolean | `false` | Opt-in employer email discovery; billed per email found |
| `maxItems` | integer | `2000` | Filtered-out rows never count |

***

### Output overview

One dataset row per **unique** job, 30+ fields:

- **Identity:** `source`, `sourceJobId`, `url`, `applyUrl`, `alsoFoundOn[]`
- **Job:** `title`, `description` (+ `descriptionHtml`, `descriptionText`), `tags[]`, `categories[]`, `jobLevel`, `industry`, `employmentType[]`
- **Company:** `companyName`, `companyLogo`, `companyWebsite`, opt-in `contactEmail` / `contactWebsite` / `emailEnrichment`
- **Geography:** `applicantCountries[]`, `location`
- **Compensation:** `salary { currency, min, max, unit }`, `salaryText`
- **Timing:** `postedDate`, `validThrough`, `scrapedAt`

#### Sample row (real smoke-test data, truncated)

```json
{
    "type": "job",
    "source": "remoteok",
    "sourceJobId": "1128869",
    "url": "https://remoteok.com/remote-jobs/remote-senior-software-engineer-stellar-ai-1128869",
    "applyUrl": "https://remoteok.com/l/1128869",
    "title": "Senior Software Engineer",
    "companyName": "Stellar AI",
    "companyLogo": "https://r2.remoteok.com/jobs/….jpg",
    "companyWebsite": null,
    "description": "… full posting text, 2,221 chars …",
    "tags": ["developer", "ai"],
    "categories": ["Senior Software Engineer"],
    "jobLevel": null,
    "industry": "Startups",
    "employmentType": ["FULL_TIME"],
    "applicantCountries": ["United Kingdom", "Australia", "New Zealand"],
    "location": "United Kingdom, Australia, New Zealand",
    "salary": { "currency": "USD", "min": 90000, "max": 130000, "unit": "YEAR" },
    "salaryText": null,
    "postedDate": "2026-07-25T11:02:13.000Z",
    "validThrough": "2026-10-23T11:02:13.000Z",
    "alsoFoundOn": [{ "source": "himalayas", "url": "https://himalayas.app/companies/stellar-ai/jobs/…" }],
    "scrapedAt": "2026-08-12T04:44:19.102Z"
}
```

With `enrichEmails: true`, matching rows additionally carry `contactEmail`, `contactWebsite`, and a detailed `emailEnrichment` object.

***

### ❓ FAQ

**How many jobs will I get?**
With all six boards and no filters, typically 2,000–3,000 unique jobs after dedupe. `maxItems` caps the run; filtered-out rows never count toward it.

**How does deduplication work?**
Case-insensitive company+title matching across boards. The copy from the richest source is kept (RemoteOK → WeWorkRemotely → Himalayas → Remotive → Jobicy → Arbeitnow), every other appearance is recorded in `alsoFoundOn`, and missing fields (salary, website, longer description) are backfilled from the duplicates. Set `dedupe: false` to keep every board's copy.

**Why does RemoteOK contribute so many more jobs than Remotive?**
Board reality: this actor walks RemoteOK's full live board (1,000+), while Remotive's public API is hard-capped at its ~20 newest jobs (verified — the limit and category parameters don't lift it). Each board contributes what it actually exposes.

**Is one board being down a failed run?**
No — every board is fetched independently. A failing board is logged in the run stats and the rest continue.

**Why round-robin instead of newest-first?**
Some boards refresh timestamps aggressively; a global newest-first cap would fill your entire dataset from one source. Round-robin keeps every selected board represented, freshest first within each.

**Is `enrichEmails` guaranteed to find an email?**
No — it is best-effort discovery from the employer's website (seeded by `companyWebsite` when a board exposes it, otherwise name-based lookup with a business-precision guard). Billed per email found, never for misses.

**Can I schedule it?**
Yes — any Apify schedule. For a daily cross-board digest, set `timeFilter: "24h"` and schedule every morning.

***

### Support

- 🐛 **Issues tab** of this actor — the fastest way to reach me, usually answered within hours.
- 💬 Feature requests welcome: more boards, filters, or output fields ship on demand.

***

### Additional Services

- **Custom scrapers** for any job board, directory, or listing site — delivered as private Apify actors.
- **Managed data feeds** — scheduled runs, deduplication, delivery to your warehouse (BigQuery, Postgres, S3, Google Sheets).
- **Email enrichment at scale** across any of my actors.

Open an issue on this actor or message via my [Apify profile](https://apify.com/memo23) to discuss.

***

### Explore More Scrapers

Single-board actors, same schema philosophy and support:

- [RemoteOK Jobs Scraper](https://apify.com/memo23/remoteok-jobs-scraper) — the full-depth RemoteOK source, standalone.
- [WeWorkRemotely Jobs Scraper](https://apify.com/memo23/weworkremotely-jobs-scraper) — WWR with country-eligibility arrays.
- [Wellfound Jobs Scraper](https://apify.com/memo23/wellfound-jobs-scraper) — startup jobs with funding context.
- [Glassdoor Scraper](https://apify.com/memo23/glassdoor-scraper-ppr) — jobs + salaries + reviews.
- [Naukri Scraper](https://apify.com/memo23/naukri-scraper) — India's largest job market.

Browse the full catalog: [apify.com/memo23](https://apify.com/memo23)

***

### 🤖 For AI Agents & LLM Apps

- **Stable unified schema** — every field always present (null/\[] when unavailable), identical across all six boards, so tool-calling code never branches per source.
- **MCP-ready** — callable through Apify's MCP server; a minimal call is `{ "searchKeyword": "python", "maxItems": 20 }`.
- **Token-friendly modes** — `cleanHtml: true` for plain-text descriptions, `includeDescription: false` for compact metadata-only rows.
- **Deterministic** — exact client-side filtering and stable dedupe rules; same inputs, same selection logic.
- **Cheap in loops** — filtered queries bill only matching rows, so agents can iterate freely.

***

### ⚠️ Disclaimer

This actor is an independent tool, not affiliated with, endorsed by, or sponsored by Remote OK, WeWorkRemotely, Remotive, Himalayas, Jobicy, or Arbeitnow. It accesses only publicly available data — public pages, feeds, and APIs each board exposes to any visitor — and does not bypass authentication, paywalls, or access controls. You are responsible for how you use the data: comply with applicable laws (GDPR/CCPA where relevant), respect each board's terms of service, and attribute sources where their data guidelines require it. Job data comes from live sites and can change or contain employer-supplied errors; verify before making decisions based on it.

### SEO Keywords

remote jobs aggregator, remote jobs scraper, remote job listings data, remoteok scraper, weworkremotely scraper, remotive api, himalayas jobs api, jobicy scraper, arbeitnow api, remote jobs dataset, remote job leads with emails, remote hiring companies, remote jobs json csv export, multi board job scraper, remote work data, digital nomad jobs data

### License

Apache-2.0

# Actor input Schema

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

Boards to include in this run.

## `dedupe` (type: `boolean`):

**ON (default)** — the same job posted on several boards becomes ONE row (the richest source wins; the others are listed in `alsoFoundOn`, and missing fields are backfilled across boards).

**OFF** — emit every board's copy separately.

## `searchKeyword` (type: `string`):

Matched over title + company + categories/industry + location. Every word must match, e.g. `senior engineer`. For skill tags (`python`, `react`) use the Tags filter below.

## `tagsFilter` (type: `array`):

Keeps jobs carrying ANY of these tags or categories (case-insensitive substring, e.g. `python`, `design`, `devops`). Leave empty for no filter.

## `countries` (type: `array`):

Keeps jobs open to workers from at least ONE of the listed countries/regions. Full names (`United States`, `Europe`) or common ISO-2 codes (`US`, `GB`, `DE`). Jobs open to `Anywhere` always pass. Leave empty for no filter.

## `jobTypes` (type: `array`):

Post-filter on `employmentType[]`. Common values: `FULL_TIME`, `PART_TIME`, `CONTRACT`, `INTERNSHIP` (spellings like `Full-Time` work too). Empty = no filter.

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

Keeps rows whose parsed `salary.min` meets the threshold. **Permissive**: jobs without a listed salary are NOT dropped. Leave 0 for no filter.

## `timeFilter` (type: `string`):

Keeps only jobs posted within the selected window.

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

When **off**, the actor drops `description`, `descriptionHtml`, and `descriptionText` from every row for a much smaller dataset.

## `cleanHtml` (type: `boolean`):

When **on**, only the plain-text description is emitted; the HTML variants are stripped. Ignored when `includeDescription` is off.

## `enrichEmails` (type: `boolean`):

If enabled, tries to find a contact email for each employer by discovering the company's website (some boards expose it directly — then it's used as-is) and reading its contact/about pages. Adds contactEmail + contactWebsite columns plus a detailed emailEnrichment object. Best-effort, billed per contact email found; never charged for misses.

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

Hard cap on emitted rows. The merged cross-board feed holds ~2,000–3,000 unique live jobs; rows are emitted freshest-first. Filtered-out rows never count.

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

Maximum number of parallel HTTP tasks (board pagination and email-enrichment lookups).

## `proxy` (type: `object`):

Leave empty — the actor already routes all traffic through its own built-in residential proxy at no extra cost to you. Only set this if you want to use your own proxies.

## Actor input object example

```json
{
  "sources": [
    "remoteok",
    "weworkremotely",
    "remotive",
    "himalayas",
    "jobicy",
    "arbeitnow"
  ],
  "dedupe": true,
  "searchKeyword": "",
  "tagsFilter": [],
  "countries": [],
  "jobTypes": [],
  "minSalary": 0,
  "timeFilter": "",
  "includeDescription": true,
  "cleanHtml": false,
  "enrichEmails": false,
  "maxItems": 2000,
  "maxConcurrency": 10
}
```

# 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",
        "remotive",
        "himalayas",
        "jobicy",
        "arbeitnow"
    ],
    "tagsFilter": [],
    "countries": [],
    "jobTypes": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("memo23/remote-jobs-aggregator").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",
        "remotive",
        "himalayas",
        "jobicy",
        "arbeitnow",
    ],
    "tagsFilter": [],
    "countries": [],
    "jobTypes": [],
}

# Run the Actor and wait for it to finish
run = client.actor("memo23/remote-jobs-aggregator").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",
    "remotive",
    "himalayas",
    "jobicy",
    "arbeitnow"
  ],
  "tagsFilter": [],
  "countries": [],
  "jobTypes": []
}' |
apify call memo23/remote-jobs-aggregator --silent --output-dataset

```

## MCP server setup

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

```

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/MpxqZgN453dgtbKIA/builds/WOoIZX6biTJdfw9V1/openapi.json
