# Remote Jobs Scraper – Remotive, Remote OK, Jobicy, Arbeitnow (`fetchsmith/remote-jobs-scraper`) Actor

Remote job postings from four public job boards (Remotive, Remote OK, Jobicy, Arbeitnow) in one normalized, newest-first schema, de-duplicated so the same job is never billed twice. Filter by keyword, company, location, salary and posted date; every row links back to the original posting.

- **URL**: https://apify.com/fetchsmith/remote-jobs-scraper.md
- **Developed by:** [Fetch Smith](https://apify.com/fetchsmith) (community)
- **Categories:** Jobs, Lead generation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 job postings

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 – Remotive, Remote OK, Jobicy & Arbeitnow in one de-duplicated feed

Four public remote-job boards, one normalized JSON schema, **de-duplicated across boards before you are charged**.

### What it does

Pulls live remote job postings from four documented, public, no-login job-board APIs and merges them:

| Source | What it covers | Salary data |
|---|---|---|
| [Remotive](https://remotive.com) | Remote tech, design, marketing, support roles, worldwide. Its public feed is small — **20 live postings total on 2026-09-21**, regardless of the `limit` you ask for | free-text range (`salaryText`) |
| [Remote OK](https://remoteok.com) | The ~100 most recent Remote OK postings | numeric USD range on many rows |
| [Jobicy](https://jobicy.com) | The 50 most recent Jobicy postings | numeric range + currency + period on many rows |
| [Arbeitnow](https://www.arbeitnow.com) | A general European board — **only** rows flagged remote are returned | none |

Every row says which board it came from (`source`, `sourceSite`) and links to the original posting (`url`).

### Cross-board de-duplication (and what it actually measures)

When a job is syndicated to more than one of these boards, a naive aggregator returns it twice — and on a pay-per-result Actor you pay for both copies. This Actor folds copies into one row (matched on normalized company + title, newest kept) **before** the charge is made: the extra boards show up as `alsoOn: ["remoteok","jobicy"]` with their links in `duplicateUrls`, so you keep the information without paying for it twice. Set `dedupe: false` to get one row per board copy.

**Measured honestly:** on a full four-board pull on 2026-09-21, **0 of 181** postings were cross-board duplicates — these four boards curate largely disjoint sets, so the overlap on any given day may be small or zero. Treat de-duplication as a guarantee that you will never be billed twice for one posting, not as a claim that the boards overlap heavily.

### Use cases

- **Job boards and newsletters** — one merged, deduplicated feed to republish or email, with the source link each board's terms require.
- **Recruiters and sourcers** — watch which companies are hiring remotely for a given stack (`searchKeyword: "rust"`, `salaryOnly: true`).
- **Market/comp research** — collect salary ranges across boards over time; `salaryMin`/`salaryMax`/`salaryCurrency`/`salaryPeriod` are normalized where the board publishes them.
- **Job-seeker automations** — a daily run with `postedAfter` set to yesterday gives exactly the new postings, nothing else.

### Input

| Field | Type | Notes |
|---|---|---|
| `sources` | array | Any subset of `remotive`, `remoteok`, `jobicy`, `arbeitnow`. Default: all four. An unknown name **fails the run** rather than being quietly dropped. |
| `searchKeyword` | string | Kept if the title, company, category or tags contain it (case-insensitive). Also passed to Remotive's and Jobicy's own search parameters. |
| `titleExcludeKeyword` | string | Drops postings whose title contains it, e.g. `Senior`. |
| `companyKeyword` | string | Substring match on company name. |
| `locationKeyword` | string | Substring match on the location/region field. |
| `postedAfter` / `postedBefore` | string | `YYYY-MM-DD`, UTC, **both bounds inclusive whole days**. A value that is not a real calendar date fails the run — see the FAQ. |
| `salaryOnly` | boolean | Keep only rows carrying a salary range or salary text. |
| `dedupe` | boolean | Default `true`. See above. |
| `includeDescription` | boolean | Adds `descriptionHtml`. Off by default — descriptions are large. |
| `maxPagesPerSource` | integer | Only affects Arbeitnow, the one paginated source (250/page). Default 2. |
| `maxResults` | integer | Stop after this many unique postings are pushed and charged. Default 100. |

### Output

One object per unique posting:

```json
{
  "source": "remotive",
  "sourceSite": "https://remotive.com",
  "sourceJobId": "2091141",
  "title": "Frontend Web Application Developer",
  "company": "KoboToolbox",
  "companyLogo": "https://remotive.com/job/2091141/logo",
  "url": "https://remotive.com/remote-jobs/design/frontend-web-application-developer-2091141",
  "location": "USA, Canada, Argentina, Mexico, Peru",
  "remote": true,
  "jobType": "full_time",
  "category": "Design",
  "tags": ["api", "django", "docker", "frontend", "python", "react"],
  "salaryText": "$90k - $105k",
  "salaryMin": null,
  "salaryMax": null,
  "salaryCurrency": null,
  "salaryPeriod": null,
  "publishedAt": "2026-09-18T16:43:22.000Z",
  "alsoOn": [],
  "duplicateUrls": [],
  "scrapedAt": "2026-09-21T09:40:00.000Z"
}
```

#### Location is a region, not a city

Remote boards publish the region a candidate must be in (`"Worldwide"`, `"USA, Canada"`, `"UK"`), not an office address, and some rows carry no location at all. `locationKeyword` therefore drops rows with an empty location — that is a filter on what the board actually published, not a geocoder.

#### Salary

`salaryMin`/`salaryMax` are only set when the board publishes numbers (Remote OK, Jobicy). Remote OK's zeros mean "not disclosed" and are normalized to `null`, not `0`. Remotive publishes a free-text range, kept verbatim in `salaryText`. **No currency conversion is performed** — `salaryCurrency` tells you what the number is in.

### Pricing

Pay per result: you are charged once per **unique** posting pushed to the dataset. No start fee, no per-run fee, nothing charged for duplicates, filtered-out rows or empty runs.

### FAQ

**What happens if I typo a date?** The run fails immediately with an error naming the field and the bad value. It is deliberate: if a bad `postedAfter` were ignored, the run would return (and bill for) every posting on all four boards instead of your window, and a warning line in a successful run is not something anyone reads. `2026-6-5`, `06/15/2026` and `2026-02-30` are all rejected — the last one because it is not a real date, even though JavaScript would silently roll it over to March 1.

**Are both date bounds inclusive?** Yes. `postedAfter` starts at 00:00:00.000Z of that day and `postedBefore` ends at 23:59:59.999Z, so a job posted at 14:00Z on your end date is included.

**How are duplicates detected?** Normalized company name + normalized job title (lowercased, punctuation collapsed). That catches the common syndication case ("Acme, Inc." on one board and "Acme Inc" on another). It will not merge two genuinely different openings that share a title at the same company — those stay separate rows.

**I set a small `maxResults` and got rows from only one board — is that a bug?** No. The merge is global newest-first across all four boards, so a small cap samples *recency*, not *boards*: whichever board happened to publish the freshest postings that minute fills the cap. Ask for at least 50 results to see all four represented, or run once per board with `sources` set to a single board if you need a guaranteed per-board slice.

**What if one board is down?** The run continues with the others and logs a warning naming the failed board. You are only charged for rows you actually receive.

**Does this need a login, API key or proxy?** No. All four endpoints are public and documented, and the Actor is HTTP-only — no headless browser.

**Why are Arbeitnow rows mostly German?** Arbeitnow is a European (largely German) board; only its postings flagged remote are returned here. Drop `arbeitnow` from `sources` if you want US-centric boards only.

### Sources and attribution

All four APIs are public and ask for credit in return. This Actor puts the source board and the original posting URL on every row so you can honour that downstream: **if you republish these postings, link back to [Remotive](https://remotive.com), [Remote OK](https://remoteok.com), [Jobicy](https://jobicy.com) and [Arbeitnow](https://www.arbeitnow.com), and point application buttons at the original job URL in the `url` field.** Remote OK's API terms require a followed link back; Jobicy's asks that apply buttons resolve to the original posting.

### Notes

- Public data only: no login, no personal data beyond what the boards publish publicly about a job.
- Feeds are snapshots of what each board serves at run time; Remote OK and Jobicy expose only their most recent postings, so historical `postedBefore` windows will thin out on those two.

### Related guides

- [Four public remote-job APIs with no key — and how small each feed really is](https://fetchsmith.com/blog/remote-job-board-json-apis-four-feeds) — measured feed sizes, Remotive's decorative `limit`, Remote OK's legal-notice row, and why the four boards don't overlap
- [FetchSmith blog](https://fetchsmith.com/blog) — data-source guides and API notes
- [All FetchSmith Actors](https://fetchsmith.com/tools)

# Actor input Schema

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

Which public job-board APIs to pull from. Any subset of: "remotive" (Remotive, remote tech/design/marketing roles, worldwide), "remoteok" (Remote OK, ~100 most recent postings, USD salary ranges on many rows), "jobicy" (Jobicy, 50 most recent, salary range + currency + period on many rows), "arbeitnow" (Arbeitnow, a general European board — only its rows flagged remote are returned). Leave empty for all four. An unknown name fails the run rather than being silently ignored.

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

Only keep postings whose title, company, category or tags contain this text (case-insensitive), e.g. "python" or "customer support". It is also passed to Remotive's and Jobicy's own search parameters, so those two boards filter server-side as well. Leave empty for all postings.

## `titleExcludeKeyword` (type: `string`):

Drop postings whose title contains this text (case-insensitive), e.g. "Senior" to exclude senior roles. Leave empty for no exclusion.

## `companyKeyword` (type: `string`):

Only keep postings whose company name contains this text (case-insensitive). Leave empty for all companies.

## `locationKeyword` (type: `string`):

Only keep postings whose location field contains this text (case-insensitive), e.g. "USA", "Europe", "Germany". Note that remote boards store a hiring REGION ("USA, Canada", "Worldwide", "UK") rather than a city, and some rows have no location at all — those are dropped when this filter is set. Leave empty for all locations.

## `postedAfter` (type: `string`):

Only keep postings published on or after this date, in YYYY-MM-DD (UTC). The whole day is included. A value that is not a real YYYY-MM-DD calendar date FAILS the run with an explanatory error — it is never ignored, because ignoring it would return and charge for jobs outside your window. Rows with no publication date are dropped when this filter is set.

## `postedBefore` (type: `string`):

Only keep postings published on or before this date, in YYYY-MM-DD (UTC). The whole day is included (ends 23:59:59.999Z). Same strict parsing as "Posted on or after": a typo fails the run instead of silently widening the result set.

## `salaryOnly` (type: `boolean`):

Keep only postings that carry a salary range or salary text. Remote OK and Jobicy supply numeric ranges on many rows; Remotive supplies a free-text range; Arbeitnow supplies none, so enabling this drops all Arbeitnow rows.

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

Fold postings that appear on more than one board into a single row (matched on normalized company + job title, newest kept). The row then lists the other boards in "alsoOn" and their links in "duplicateUrls". De-duplication happens BEFORE charging, so you are never billed twice for one job. Turn off to get one row per board copy.

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

Add the board's full job description HTML as "descriptionHtml". Off by default because descriptions are large and most pipelines only need the structured fields plus the link.

## `maxPagesPerSource` (type: `integer`):

Only affects Arbeitnow, the one paginated source (250 postings per page). The other three boards return their whole feed in one request.

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

Stop after this many unique postings have been pushed (and charged).

## Actor input object example

```json
{
  "sources": [
    "remotive",
    "remoteok",
    "jobicy",
    "arbeitnow"
  ],
  "salaryOnly": false,
  "dedupe": true,
  "includeDescription": false,
  "maxPagesPerSource": 2,
  "maxResults": 100
}
```

# Actor output Schema

## `dataset` (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 = {};

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

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

```

## MCP server setup

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