# Remote Jobs Aggregator (`real-estate-api/remote-jobs-aggregator`) Actor

Remote Jobs Aggregator is a Python tool designed to scrape and collect job listings across multiple remote job boards and company ATS portals. It extracts titles, companies, locations, salary ranges, and apply links into clean CSV or JSON formats.

- **URL**: https://apify.com/real-estate-api/remote-jobs-aggregator.md
- **Developed by:** [real-estate-api](https://apify.com/real-estate-api) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.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.
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

## Remote Jobs Scraper: Remote OK, Himalayas, Remotive and We Work Remotely in One Dataset (Free)

<p align="center">
  <img src="https://imgur.com/Tnbzu5g.png" alt="Remote Jobs Scraper" width="800" />
</p>

### Overview

Collect remote job listings from Remote OK, Himalayas, Remotive, and We Work Remotely in one unified dataset. Each record includes the job title, company, salary information, location and timezone restrictions, tags, application link, and original listing source.

Filter listings by keywords, Remotive category, or Himalayas country. No account or login is required.

Because each job board uses a different data structure, this scraper standardizes records, removes duplicates, and preserves source attribution. Remote OK, Himalayas, and Remotive require attribution when their listings are republished.

### Use Cases

- **Remote job boards and newsletters:** Collect remote vacancies and retain links to their original listings.
- **Recruiter sourcing:** Discover companies hiring remotely across multiple job boards.
- **B2B lead generation:** Identify companies with open roles, along with job titles and application links.
- **Salary research:** Compare available salary figures and salary descriptions across job boards.
- **Job-market analytics:** Analyze job tags, categories, location restrictions, and hiring trends.

### Filtering Options

Keyword filtering works differently across the four sources:

- **Remote OK:** Keywords are matched against job tags.
- **Himalayas:** Keywords are used for job searches, particularly when a country is specified or a keyword is provided.
- **Remotive:** Keywords are used to search listings. Because category filtering can be broad, keyword matches are checked again during processing.
- **We Work Remotely:** Listings are collected from selected category feeds and filtered locally by keyword.

Remotive category names use slugs such as `software-dev`. Enter the desired category slug directly.

When keywords are empty, the scraper collects Remote OK's latest listings, browses Himalayas listings, selects a Remotive feed, and retrieves listings from the chosen We Work Remotely categories.

### Input Parameters

| Parameter | Type | Default | Description |
|---|---|---|---|
| `sources` | array | All four sources | Job boards to collect from, in priority order. The first source takes precedence when duplicate listings are found. |
| `keywords` | array | Empty | Keywords used for source-specific searches and local filtering. |
| `remotiveCategory` | string | Empty | Remotive category slug, such as `software-dev`. |
| `wwrCategories` | array | `remote-programming-jobs` | We Work Remotely categories to collect. Each selected category is processed separately. |
| `country` | string | Empty | Country filter for Himalayas, using an ISO-2 code or country name, such as `US`. |
| `postedWithinDays` | integer | `30` | Exclude jobs older than the specified number of days. Listings without a posting date are retained. Range: 1–90. |
| `onlyWithSalary` | boolean | `false` | Keep listings containing numeric salary information or Remotive salary text. |
| `includeDescription` | boolean | `true` | Include the job description. |
| `descriptionFormat` | string | `text` | Description format: `text`, `markdown`, or `html`. |
| `outputFormat` | string | `normalized` | Use `normalized` for standardized records or `raw` to include the original source record in `rawJob`. |
| `dedupeAcrossSources` | boolean | `true` | Merge matching company and job-title records. Other source listings are stored in `alsoListedOn`. |
| `maxItems` | integer | `200` | Maximum total job records. Hard limit: 2,000 per run. |
| `maxItemsPerSource` | integer | `100` | Maximum records per job board. Hard limit: 1,000 per source. |
| `timeoutSecs` | integer | `300` | Maximum duration of the run, in seconds. |
| `requestTimeoutSecs` | integer | `30` | Timeout for an individual request, in seconds. |

### Example Input

```json
{
  "sources": [
    "remoteok",
    "himalayas",
    "remotive",
    "weworkremotely"
  ],
  "keywords": ["python"],
  "remotiveCategory": "software-dev",
  "wwrCategories": ["remote-programming-jobs"],
  "country": "US",
  "postedWithinDays": 30,
  "onlyWithSalary": false,
  "includeDescription": true,
  "descriptionFormat": "text",
  "outputFormat": "normalized",
  "dedupeAcrossSources": true,
  "maxItems": 200,
  "maxItemsPerSource": 100,
  "timeoutSecs": 300,
  "requestTimeoutSecs": 30
}
```

### Extracted Data

Each job record includes `sourceName` and `sourceUrl` for attribution. The job title is stored in `jobTitle`, and the description is stored in `jobDescription` to maintain compatibility with the dataset schema.

The `dedupeKey` combines a normalized company identifier and job-title identifier. When duplicates appear across sources, the first source in the configured priority order is retained. Additional listings are recorded in `alsoListedOn`.

#### Example Output

```json
{
  "id": "himalayas:JOB_LISTING_ID",
  "sourceName": "Himalayas",
  "sourceUrl": "SOURCE_LISTING_URL",
  "applyUrl": "APPLICATION_URL",
  "jobTitle": "Sales Recruiter",
  "company": "Toptal",
  "companySlug": "toptal",
  "employmentType": "Full Time",
  "seniority": ["Mid-level"],
  "locationRestrictions": ["Canada"],
  "timezoneRestrictions": [-8, -7, -6, -5, -4, -3.5],
  "salaryMin": null,
  "salaryMax": null,
  "salaryPeriod": "annual",
  "postedAt": "2026-10-11T18:46:27Z",
  "alsoListedOn": []
}
```

#### Field Reference

| Field | Type | Description |
|---|---|---|
| `id` | string | Source identifier combined with the listing identifier. |
| `sourceName` | string | Source board: Remote OK, Himalayas, Remotive, or We Work Remotely. |
| `sourceUrl` | string | Original listing URL on the source board. |
| `applyUrl` | string | Application link, when available. |
| `jobTitle` | string | Job title. |
| `company` | string | Hiring company. For We Work Remotely, the company may be extracted by splitting the listing title at the first `: `. |
| `companyLogo` | string | Company logo URL, when available. |
| `companySlug` | string | Company identifier used by Himalayas. |
| `category` | string | Job category or parent category. |
| `tags` | array | Job tags from Remote OK or Remotive. |
| `employmentType` | string | Employment type, normalized where possible. For example, `full_time` becomes `Full Time`. |
| `seniority` | array | Seniority labels from Himalayas. |
| `locationRestrictions` | array | Eligible locations or geographic restrictions supplied by the source. |
| `timezoneRestrictions` | array | Timezone offsets supplied by Himalayas. |
| `salaryMin` | number | Minimum salary, when available. |
| `salaryMax` | number | Maximum salary, when available. |
| `salaryCurrency` | string | Salary currency, when available. USD may be assigned to numeric Remote OK salary values. |
| `salaryPeriod` | string | Salary period, such as `annual`, when available. |
| `salaryText` | string | Free-text salary description from Remotive. |
| `postedAt` | string | Listing publication timestamp in UTC, when available. |
| `expiresAt` | string | Listing expiration timestamp in UTC, when available. |
| `jobDescription` | string | Job description in the selected format. |
| `dedupeKey` | string | Normalized company and job-title key used for deduplication. |
| `alsoListedOn` | array | Other sources containing the same company and job title. |
| `scrapedAt` | string | Timestamp indicating when the record was collected. |
| `rawJob` | object | Original source record, included only when `outputFormat` is `raw`. |

Missing salary values are preserved as empty or null values where appropriate. Remote OK salary values of `0` are treated as unavailable.

#### Dataset Views

The dataset contains two views:

- **Jobs:** Normalized job listings and their available details.
- **Errors:** Source-specific collection failures.

Error records can contain `source`, `input`, `error`, and `statusCode`. A failure on one source does not automatically cancel collection from the other sources.

### Collection and Rate Limits

Sources are processed sequentially to reduce unnecessary requests and respect their usage restrictions.

1. **Remote OK:** Processes one request per keyword, or one unfiltered request when no keywords are supplied. The initial JSON element containing the legal notice is skipped. Calls are spaced at least two seconds apart. The feed provides recent listings without pagination.

2. **Himalayas:** Uses browsing or search pagination depending on the selected filters. Each page contains up to 20 jobs. Requests are spaced at least one second apart. After an HTTP 429 response, the scraper waits 60 seconds and retries once before recording an error and continuing.

3. **Remotive:** Processes one request per keyword, without additional pages. Requests are spaced at least 30 seconds apart. Remotive recommends no more than four runs per day and restricts excessive request frequency. Listings may be delayed by approximately 24 hours.

4. **We Work Remotely:** Processes each selected category feed separately, with at least one second between requests. The feed's stated time-to-live is 60 minutes.

The `timeoutSecs` setting limits the total run duration. Records collected before a timeout are retained. The overall `maxItems` limit is applied after deduplication.

### Attribution and Source Restrictions

When republishing collected listings, retain `sourceName` and `sourceUrl` so readers can identify the original source.

| Source | Attribution and restrictions |
|---|---|
| Remote OK | Provide a dofollow link to the original listing and identify Remote OK by name. Do not use its logo. |
| Himalayas | Link to the original listing and identify Himalayas by name. Do not resubmit listings to Jooble, Neuvoo, Google Jobs, or LinkedIn Jobs. |
| Remotive | Link to the original listing and identify Remotive by name. Do not resubmit listings to Google Jobs, LinkedIn Jobs, Jooble, or Neuvoo. Respect its collection frequency restrictions. Using its listings to collect email addresses or sign-ups violates its stated terms. |
| We Work Remotely | Use the original listing link and respect the source's published feed guidance. The source's feed indicates a 60-minute time-to-live. |

The `companyLogo` field, when present, refers to the hiring company's logo rather than the logo of a job board.

### Free Usage Limits

- Maximum of 2,000 job records per run.
- Default overall limit of 200 records.
- Maximum of 1,000 records per source.
- Himalayas returns up to 20 jobs per page.
- Results depend on the available listings, selected filters, source restrictions, and configured limits.

### FAQ

#### How often can I run the Remotive source?

Remotive recommends no more than four runs per day and restricts excessive request frequency. This scraper spaces requests by at least 30 seconds and makes one request per keyword. Keep scheduled runs within the source's recommended daily limit.

#### Why is the job title stored as `jobTitle`?

The dataset reserves `title` and `description` for system-level use. Therefore, job titles are stored in `jobTitle`, and job descriptions are stored in `jobDescription`.

#### Does every listing include salary information?

No. Salary details vary by source. Remote OK may provide `0` when a numeric salary is unavailable, Himalayas may provide null values, and Remotive may provide a text description instead of numeric values. Enable `onlyWithSalary` to exclude listings without recognized salary information.

#### Can Himalayas return more than 20 jobs on one page?

No. The scraper processes pages of up to 20 jobs and continues using pagination until the configured limit is reached or no more results are available.

#### Can I resubmit these listings to Google Jobs or LinkedIn Jobs?

Himalayas and Remotive restrict the redistribution of their listings to certain job aggregators. Follow each source's terms, including the restrictions on resubmitting listings to Google Jobs, LinkedIn Jobs, Jooble, and Neuvoo.

#### Can I export the results?

Yes. The collected records are stored in the dataset for download and further analysis. The standardized output can be used for reporting, job-market research, and recruiting workflows.

### Trademark Notice

Remote OK, Himalayas, Remotive, We Work Remotely, and their respective names, logos, and trademarks belong to their owners. Their use in this documentation is solely to identify the sources of the collected information and does not imply affiliation, sponsorship, endorsement, or partnership.

### Disclaimer

This is an independent data collection tool. It is not affiliated with, endorsed by, sponsored by, or officially associated with Remote OK, Himalayas, Remotive, or We Work Remotely.

### Is Web Scraping Legal?

Collecting information that is already public is generally legal. Scraping is not the same thing as hacking, and there is no single law that bans it. The limits are the usual ones: do not break into accounts, do not take private or confidential data, and be careful with personal data and copyrighted works. A site's terms can also restrict how you access it. Facts such as a public name, price, or rating are not the same thing as republishing someone's article or photo.

This actor collects information from public pages. It does not log in and it does not bypass access controls.

For a longer explanation, see Apify's article [Is web scraping legal?](https://blog.apify.com/is-web-scraping-legal/).

# Actor input Schema

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

Job boards to fetch, in priority order. The first source wins when the same company and job title show up on more than one board.

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

Remote OK uses each keyword as a tag. Himalayas sends it as q on the search endpoint. Remotive sends it as search. We Work Remotely is filtered on the actor after the RSS download.

## `remotiveCategory` (type: `string`):

Remotive category slug, for example software-dev. The categories endpoint is blocked, so type the slug yourself. Remotive's own filter is loose, and the actor checks keywords again locally.

## `wwrCategories` (type: `array`):

We Work Remotely category RSS feeds. One request is made per selected feed.

## `country` (type: `string`):

Himalayas search country as ISO-2, a country name, or a slug, for example US. When this or keywords are set, Himalayas uses the search endpoint instead of the cursor browse feed.

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

Drop jobs whose posted time is older than this many days. Jobs with no posted time are kept.

## `onlyWithSalary` (type: `boolean`):

Keep a job only when it has a salary number or Remotive salary text.

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

Store the job description. Turn this off to leave jobDescription empty and keep the dataset smaller.

## `descriptionFormat` (type: `string`):

How jobDescription is stored when descriptions are included: plain text, markdown, or the original HTML.

## `outputFormat` (type: `string`):

Normalized rows use the shared job fields. Raw rows add the source payload under rawJob and still include sourceName and sourceUrl.

## `dedupeAcrossSources` (type: `boolean`):

Merge rows that share a company and job title. The earliest source in Sources is kept, and the other listings go into alsoListedOn.

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

Stop after this many job rows. The free cap is 2,000 jobs per run. Himalayas returns at most 20 jobs per request.

## `maxItemsPerSource` (type: `integer`):

Stop each board after this many jobs that pass the filters. The hard maximum is 1,000 per source.

## `timeoutSecs` (type: `integer`):

Stop the run after this many seconds and keep the rows already collected.

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

How long to wait for one HTTP response before recording an error for that request.

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

Optional proxy for requests. Leave useApifyProxy off unless a source blocks the default IP. Datacenter and residential proxies are available when you turn Apify Proxy on.

## Actor input object example

```json
{
  "sources": [
    "remoteok",
    "himalayas",
    "remotive",
    "weworkremotely"
  ],
  "keywords": [
    "python"
  ],
  "remotiveCategory": "software-dev",
  "wwrCategories": [
    "remote-programming-jobs"
  ],
  "country": "US",
  "postedWithinDays": 30,
  "onlyWithSalary": false,
  "includeDescription": true,
  "descriptionFormat": "text",
  "outputFormat": "normalized",
  "dedupeAcrossSources": true,
  "maxItems": 200,
  "maxItemsPerSource": 100,
  "timeoutSecs": 300,
  "requestTimeoutSecs": 30,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

Job title, company, source, location, salary, posted time, and source URL.

## `errors` (type: `string`):

Failed source requests with the request URL, error, and HTTP status.

## `dataset` (type: `string`):

Every job and error row from the run, without a table view applied.

# 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 = {
    "keywords": [
        "python"
    ],
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("real-estate-api/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 = {
    "keywords": ["python"],
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("real-estate-api/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 '{
  "keywords": [
    "python"
  ],
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call real-estate-api/remote-jobs-aggregator --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,real-estate-api/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/bOPXrhZhOjgugHUVt/builds/loknuNLaHaQzOw6vw/openapi.json
