# Remote Jobs Aggregator API: Remote OK, WWR & Jobicy, Deduped (`fecklesspleb/remote-jobs-aggregator`) Actor

Remote jobs from Remote OK, We Work Remotely and Jobicy in one deduplicated JSON feed. Salary and keyword filters, only-new-jobs mode. $1 per 1,000 jobs.

- **URL**: https://apify.com/fecklesspleb/remote-jobs-aggregator.md
- **Developed by:** [Scott Williams](https://apify.com/fecklesspleb) (community)
- **Categories:** Jobs, Automation
- **Stats:** 2 total users, 1 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$1.00 / 1,000 jobs

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 Aggregator API: Remote OK, We Work Remotely and Jobicy in one deduplicated feed

Remote Jobs Aggregator pulls remote job listings from three boards that publish official public feeds (the Remote OK API, the We Work Remotely RSS feeds and the Jobicy API) and returns them as one clean JSON dataset. The same job posted on more than one board is merged into a single record, with the other copies listed in `alsoListedOn`. Filter by keyword, location, salary, date, We Work Remotely category, or Jobicy region and industry, and use monitoring mode to get only jobs you haven't seen. Each item carries the attribution line the source asks for. You pay $0.001 per job written to the dataset; filtered-out jobs and merged duplicates are free.

### Who it's for

- **Remote job boards and newsletters** that want a deduplicated daily feed with source attribution included.
- **Job seekers** who want one alert across three remote boards, filtered by stack and minimum salary.
- **Slack and Discord community owners** posting new remote roles to a channel.
- **Researchers** tracking remote hiring by category, region or published pay.

### What data you get

One dataset item per job:

| Field | What it holds |
|---|---|
| `id`, `uid` | The job's ID on its board, and a stable key `source:id`. |
| `source`, `sourceName` | `remoteok`, `weworkremotely` or `jobicy`, and the board's display name. |
| `title`, `companyName`, `companyLogo` | Job title, company and logo URL when the board gives one. |
| `location`, `isRemote` | Region or location text as published (e.g. `Anywhere`, `USA`). |
| `employmentType`, `employmentTypeRaw` | The board's job type normalized to `FULL_TIME`, `PART_TIME`, `CONTRACTOR`, `TEMPORARY`, `INTERN`, `VOLUNTEER`, `PER_DIEM`, `OTHER` (a value that fits none of them, such as `Permanent`) or null; and the board's own text, e.g. `Full-Time`. When a board lists several types the more specific one wins (`Full-Time, Contract` is `CONTRACTOR`). |
| `seniority`, `category`, `tags` | As published by the board (not every board has every field). |
| `postedAt`, `expiresAt` | ISO 8601 timestamps. |
| `url`, `applyUrl` | Job page and apply link. |
| `salaryMin`, `salaryMax`, `salaryCurrency`, `salaryPeriod`, `salaryText`, `salarySource` | Pay when the board gives it (`salarySource: "source"`) or when a range is found in the description (`"description"`). |
| `descriptionText` / `descriptionHtml` | Full description, if `includeDescription` is on. |
| `attribution` | The credit line the source asks for, e.g. "Job data from Jobicy (https://jobicy.com). Credit Jobicy and send Apply clicks to the job URL." |
| `alsoListedOn` | Other boards where the same job was found (when `dedupe` is on). |
| `scrapedAt` | When the run read the job. |

Sample item (real output, run without descriptions):

```json
{
  "id": "157680",
  "source": "jobicy",
  "sourceName": "Jobicy",
  "title": "Staff Software Engineer, Containers",
  "companyName": "Chainguard",
  "companyLogo": "https://jobicy.com/data/server-nyc0409/galaxy/mercury/2025/06/e4b6aacf-221.jpeg",
  "location": "Canada, Germany, UK, USA",
  "isRemote": true,
  "employmentType": "FULL_TIME",
  "employmentTypeRaw": "Full-Time",
  "seniority": "Senior",
  "category": "Software Engineering",
  "tags": [],
  "postedAt": "2026-10-28T16:55:29+00:00",
  "expiresAt": null,
  "url": "https://jobicy.com/jobs/157680-staff-software-engineer-containers",
  "applyUrl": "https://jobicy.com/jobs/157680-staff-software-engineer-containers",
  "salaryMin": 205000,
  "salaryMax": 231000,
  "salaryCurrency": "USD",
  "salaryPeriod": "YEAR",
  "salaryText": null,
  "salarySource": "source",
  "attribution": "Job data from Jobicy (https://jobicy.com). Credit Jobicy and send Apply clicks to the job URL.",
  "uid": "jobicy:157680",
  "alsoListedOn": [],
  "scrapedAt": "2026-10-29T01:03:49.025052+00:00"
}
```

### How to use it

Every input is optional. An empty input runs in default mode: it returns the 8 newest jobs from all three boards, without descriptions, and the log says `Default mode: no sources or filters given, returning up to 8 of the newest jobs from remoteok, weworkremotely, jobicy without descriptions`. That costs at most $0.008. Raise **Max jobs in total** (0 = no limit) and turn on **Include description** for a full pull. Click **Start**, then export from the **Output** tab.

| Input field | Label | Notes |
|---|---|---|
| `sources` | Sources | `remoteok`, `weworkremotely`, `jobicy`. Empty = all. |
| `keywords` | Keywords (any) | Matches title, tags or category, case-insensitive. |
| `excludeKeywords` | Exclude title keywords | Drops jobs whose title contains any of these. |
| `searchInDescription` | Match keywords in description too | Default `false`. |
| `locations` | Locations (any) | Matches location or region text, e.g. `USA`, `Europe`, `Anywhere`. |
| `requireSalary` | Only jobs with a salary | Default `false`. |
| `minSalaryUsd` | Minimum yearly salary (USD) | Compares the top of the range, converted to a rough yearly USD figure. Jobs without a salary are dropped when this is set. |
| `postedWithinDays` | Posted within last N days | 1–365. Jobs without a posting date are kept. |
| `wwrCategories` | We Work Remotely categories | `programming`, `full-stack`, `front-end`, `back-end`, `devops`, `design`, `product`, `customer-support`, `sales-marketing`, `management-finance`, `other`. Empty = all. |
| `jobicyGeo` | Jobicy region | e.g. `usa`, `europe`, `canada`. |
| `jobicyIndustry` | Jobicy industry | e.g. `dev`, `marketing`, `design-multimedia`. |
| `dedupe` | Merge duplicates across boards | Default `true`. Same company and title (normalized) = one job; the newest copy is kept. |
| `onlyNewJobs` | Only new jobs (monitoring mode) | Default `false`. Seen jobs are remembered for 120 days. |
| `stateStoreName` | State store name | Default `remote-jobs-aggregator-state`. Also stores the Jobicy hourly limit. |
| `includeDescription` | Include description | Default `false` (was `true` before GAP8). |
| `descriptionFormat` | Description format | `text`, `html` or `both`. |
| `maxTotalJobs` | Max jobs in total | Default 8. 0 = no limit. With a cap and no filters, each board stops after twice the cap, so capped runs are fast. |

Example input (Python or backend jobs paying at least $100k a year, posted in the last week, only new ones):

```json
{
  "sources": [
    "remoteok",
    "weworkremotely",
    "jobicy"
  ],
  "keywords": [
    "python",
    "backend"
  ],
  "minSalaryUsd": 100000,
  "postedWithinDays": 7,
  "onlyNewJobs": true
}
```

Things to know:

- **Jobicy hourly limit.** Jobicy asks API users to poll at most once an hour. If Jobicy was read less than 60 minutes ago (tracked in the state store), it is skipped for that run with a log message, and the other boards still run.
- **Salary periods.** Some boards publish hourly or monthly pay. Check `salaryPeriod` before comparing numbers. Remote OK's salary fields are meant to be yearly USD, but some employers enter an hourly rate there; a Remote OK range whose top is $500 or less is labelled `HOUR`, everything else `YEAR`.
- **Text encoding.** Remote OK sends some titles, company names, locations and descriptions with garbled characters (for example `â€™` instead of an apostrophe, or `MÃ©xico`). The Actor repairs these before writing the item; text that is already correct is left as is.
- **Rough salary conversion.** `minSalaryUsd` converts to a yearly figure with fixed rough exchange rates (USD, EUR, GBP, CAD, AUD); it is a filter, not an accurate conversion.
- `RUN_SUMMARY` in the default key-value store shows jobs pushed, duplicates merged, HTTP requests, `stoppedByCostLimit` and per-source counts.

### Pricing

This Actor uses pay-per-event pricing: **$0.001 per job** written to the dataset. That is **$1.00 per 1,000 jobs**. There is no separate start fee in this Actor's pricing (as listed on October 28, 2026; the Pricing tab on the Store page is the source of truth).

You are charged only for jobs that end up in your dataset. Jobs removed by your filters, jobs skipped in monitoring mode and duplicates are not charged.

| Example | Jobs | Cost |
|---|---:|---:|
| Default run (empty input) | 8 | $0.008 |
| Daily new remote jobs, filtered to one stack | 40 | $0.04 |
| One full pull of all three boards, `maxTotalJobs` 0 (illustration) | 600 | $0.60 |
| A month of daily monitoring at 40 new jobs a day | 1,200 | $1.20 |

Rows are illustrations, not measured board sizes.

Worked example: 1,000 jobs × $0.001 = **$1.00**.

To cap spend, set **Maximum cost per run** in the run options (or `maxTotalChargeUsd` via the API). The Actor stops cleanly when the next job would go over the limit and records `stoppedByCostLimit: true` in the run summary.

### How it compares

Prices and descriptions are from the public Apify Store listings on October 28, 2026.

| Option | Listed price | What it is good at |
|---|---|---|
| This Actor | $0.001 per job | Three boards with official public feeds, cross-board dedupe, salary and region filters, attribution field, only-new-jobs mode. |
| [memo23/remote-jobs-aggregator](https://apify.com/memo23/remote-jobs-aggregator) | $0.00199 free plan / $0.00189 paid, plus extra data | Remote OK plus six more boards, and company emails. A better fit if you need more boards or contact data. |
| benthepythondev's remote jobs Actor | See listing | Six boards with a worldwide vs US-only filter. |
| [inlifeprojects/remoteok-jobs-scraper](https://apify.com/inlifeprojects/remoteok-jobs-scraper) | $0.001 per job | Remote OK only. |
| The boards' own feeds | Free | Remote OK, We Work Remotely and Jobicy all publish free feeds. Calling them yourself is fine if you handle dedupe, salary parsing, the Jobicy rate limit and attribution. |

We deliberately read only boards that publish a public feed or API for reuse. Other remote boards are not included.

### Integrations and API

The actor ID is `fecklesspleb/remote-jobs-aggregator` (in API URLs: `fecklesspleb~remote-jobs-aggregator`).

**n8n, Make and Zapier.** Each has an Apify integration with a "run Actor" action. Pick `fecklesspleb/remote-jobs-aggregator`, paste the input JSON shown below, then read the results from the run's default dataset in the next step. To process only new jobs on a schedule, turn on the monitoring option and run it from a schedule (Apify Schedules or a trigger in your automation tool).

**Webhooks and schedules.** Apify Schedules can run the Actor on a cron, and a webhook on "run succeeded" can call your own endpoint with the run ID.

**HTTP API.** Run the Actor and get the items back in one call. This waits for the run to finish, so use it for small runs:

```bash
curl -X POST "https://api.apify.com/v2/acts/fecklesspleb~remote-jobs-aggregator/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"sources": ["remoteok", "weworkremotely", "jobicy"], "keywords": ["python", "backend"], "minSalaryUsd": 100000, "postedWithinDays": 7, "onlyNewJobs": true}'
```

**Python** (`pip install apify-client`):

```python
from decimal import Decimal
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("fecklesspleb/remote-jobs-aggregator").call(
    run_input={
        "sources": [
            "remoteok",
            "weworkremotely",
            "jobicy"
        ],
        "keywords": [
            "python",
            "backend"
        ],
        "minSalaryUsd": 100000,
        "postedWithinDays": 7,
        "onlyNewJobs": True
    },
    max_total_charge_usd=Decimal("5"),  # optional spend cap
)
## apify-client 3.x returns a Run object; on 1.x/2.x use run["defaultDatasetId"]
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)
```

**JavaScript / Node.js** (`npm install apify-client`):

```js
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('fecklesspleb/remote-jobs-aggregator').call(
    {
        "sources": [
            "remoteok",
            "weworkremotely",
            "jobicy"
        ],
        "keywords": [
            "python",
            "backend"
        ],
        "minSalaryUsd": 100000,
        "postedWithinDays": 7,
        "onlyNewJobs": true
    },
    { maxTotalChargeUsd: 5 }, // optional spend cap
);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items);
```

Results can also be exported from the dataset as JSON, CSV, Excel, XML or RSS from the Apify Console or the dataset API.

### FAQ

**Is there a Remote OK API I can use without writing code?**
Yes: Remote OK publishes a free JSON API, and this Actor wraps it together with We Work Remotely and Jobicy, so you can call one endpoint and get filtered, deduplicated jobs. Remote OK asks that you link back to the job URL and mention Remote OK as the source when you show its jobs. The `attribution` field carries that note on every item.

**How are duplicates detected?**
Two jobs are treated as the same when the normalized company name and title match. The newest copy is kept and the others are listed in `alsoListedOn`. Turn `dedupe` off to keep every copy.

**How do I get only worldwide or only US jobs?**
Use `locations` (e.g. `Anywhere`, `Worldwide`, `USA`), which matches each board's location text. For Jobicy, `jobicyGeo` filters at the source. Boards word locations differently, so test your list on a small run first.

**Why did Jobicy return nothing?**
Most likely it was read less than an hour ago and skipped to respect Jobicy's hourly limit. The log says so. Run again after an hour.

**Can I post new remote jobs to Slack or a Google Sheet?**
Yes. Schedule the Actor with `onlyNewJobs` on, and use the Apify integration in n8n, Make or Zapier to send each new item on.

**Do I need an API key or login?**
No. The Actor reads public endpoints and needs no account on the source site. You only need an Apify account to run it. It obeys robots.txt and rate-limits requests per host. robots.txt is checked again on every redirect, so a URL that redirects to a disallowed path or another host is not followed. If a host's robots.txt can't be read (a server error, a network failure or a page that isn't a robots file), that host is skipped for the run and `RUN_SUMMARY` says why. A missing robots.txt means no rules.

### Related Actors

- [ATS Jobs Scraper](https://apify.com/fecklesspleb/ats-jobs-scraper): jobs from Greenhouse, Lever, Ashby, Workable, Recruitee and Workday boards in one schema.
- [Greenhouse Jobs Scraper](https://apify.com/fecklesspleb/greenhouse-jobs-scraper): jobs from any Greenhouse board.
- [Lever Jobs Scraper](https://apify.com/fecklesspleb/lever-jobs-scraper): jobs from any Lever board.
- [Ashby Jobs Scraper](https://apify.com/fecklesspleb/ashby-jobs-scraper): jobs from any Ashby board.
- [Workday Jobs Scraper](https://apify.com/fecklesspleb/workday-jobs-scraper): jobs from any myworkdayjobs.com career site.
- [Hacker News Scraper](https://apify.com/fecklesspleb/hacker-news-scraper): Hacker News stories and comments, plus parsed "Who is hiring?" job posts.

# Changelog

This Actor's version history is a separate document: https://apify.com/fecklesspleb/remote-jobs-aggregator/changelog.md

# Actor input Schema

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

Which job boards to include. Leave empty for all.

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

Keep jobs whose title, tags or category contain at least one of these words (case-insensitive). Leave empty to keep all.

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

Drop jobs whose title contains any of these words.

## `searchInDescription` (type: `boolean`):

Also match keywords against the full job description.

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

Keep jobs whose location or region text contains one of these strings, for example 'USA', 'Europe', 'Anywhere'.

## `requireSalary` (type: `boolean`):

Keep only jobs that state pay (from the board or found in the description).

## `minSalaryUsd` (type: `integer`):

Keep jobs whose top of range, converted to a rough yearly USD figure, is at least this. Jobs without a salary are dropped when this is set.

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

Keep only jobs posted in the last N days. Jobs without a posting date are kept.

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

Limit We Work Remotely to these categories. Empty means all.

## `jobicyGeo` (type: `string`):

Optional Jobicy geo filter, for example 'usa', 'europe', 'canada'.

## `jobicyIndustry` (type: `string`):

Optional Jobicy industry filter, for example 'dev', 'marketing', 'design-multimedia'.

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

The same job is often posted on several boards. Keep one record (newest) and list the other copies in 'alsoListedOn'.

## `onlyNewJobs` (type: `boolean`):

Return only jobs not returned by a previous run with this option on, so scheduled runs give a clean feed and you pay only for new jobs.

## `stateStoreName` (type: `string`):

Named key-value store for 'Only new jobs' and the Jobicy hourly limit.

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

Include the full job description. Off by default to keep runs small; turn it on to get descriptionText / descriptionHtml.

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

Plain text, HTML, or both.

## `maxTotalJobs` (type: `integer`):

Stop after this many jobs. Default 8, so a run with no input is small and fast. 0 means no limit. The run also stops at your 'Maximum cost per run'.

## Actor input object example

```json
{
  "sources": [
    "remoteok",
    "weworkremotely",
    "jobicy"
  ],
  "searchInDescription": false,
  "requireSalary": false,
  "dedupe": true,
  "onlyNewJobs": false,
  "stateStoreName": "remote-jobs-aggregator-state",
  "includeDescription": false,
  "descriptionFormat": "text",
  "maxTotalJobs": 8
}
```

# Actor output Schema

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

No description

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

No description

# 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"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("fecklesspleb/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",
        "jobicy",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("fecklesspleb/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",
    "jobicy"
  ]
}' |
apify call fecklesspleb/remote-jobs-aggregator --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,fecklesspleb/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/9YeXLTlW2qSxLZkjd/builds/LRWfyIiSnen0oeDte/openapi.json
