# Remote Jobs API: RemoteOK, WWR & Jobicy Feed (`impressionable_lupine/remote-jobs-api`) Actor

Remote jobs API: RemoteOK, We Work Remotely and Jobicy in one clean JSON feed. Filter by keyword, category, location, salary and date; cross-source dedupe; only-new mode for alerts. $0.001 per job.

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

## Pricing

$1.00 / 1,000 job delivereds

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 API: RemoteOK, We Work Remotely and Jobicy in one feed

**Remote Jobs API** gives you the newest remote jobs from **Remote OK**, **We Work Remotely** and **Jobicy** in one clean JSON feed. Each job has the same normalized fields, cross-posted duplicates are merged, and you can filter by keyword, category, location, salary and posting date.

- **Only-new mode.** Turn on `onlyNew` to get just the jobs you haven't seen yet, which makes daily job alerts, Slack bots and AI agents cheap to run.
- **No browser, no proxies, no logins.** The Actor reads only the boards' official public feeds, so runs finish in about 10–15 seconds and don't break when a website redesigns.
- **$0.001 per job ($1 per 1,000).** Failed sources, duplicates and already-seen jobs are free.
- **Default sources: Remote OK and Jobicy.** We Work Remotely is **opt-in**: add `"weworkremotely"` to `sources` to include it.

> Use it as a **remote jobs scraper** without the scraping: a **RemoteOK scraper**, a **We Work Remotely** RSS reader and a Jobicy API client behind one input form and one output schema.

### Why use this remote jobs API?

| | Remote Jobs API | Scraping each board yourself |
|---|---|---|
| Sources | Remote OK + Jobicy by default; We Work Remotely (12 feeds) on request, in one call | Three parsers and three schemas to maintain |
| Reliability | Official public feeds; if one source fails, the others still return | HTML scrapers break when a site redesigns |
| Duplicates | The same job posted on several boards is merged; `alsoListedOn` keeps the other links | You write the dedupe logic yourself |
| Alerts | `onlyNew` remembers what each saved search already returned | You build and run your own state database |
| Salary | Structured salary, plus conservative parsing of "Salary: $X – $Y" lines in descriptions | Inconsistent formats on every board |
| Compliance | Attribution and apply links built in, per each board's terms | Easy to get your API access revoked |
| Price | **$0.001 per job** | Your own servers and maintenance time |

#### Who uses it

- **Job boards and newsletters:** curate remote roles in a niche (Python, design, customer support, LATAM, and so on).
- **Job seekers and career coaches:** set up a daily alert for "new senior backend jobs open to Europe paying $100k+".
- **AI agents and assistants:** answer "find new remote data jobs I can do from Canada" with one tool call.
- **Recruiters and market researchers:** track which companies hire remotely, salary bands and demand by skill.

### How to use it

1. Click **Try for free**. The form is prefilled: engineering jobs open to Europe or the USA from the last 14 days.
2. Change the keywords, categories, locations or salary filters, or leave them empty to get every remote job.
3. Click **Start**. Results appear in the **Jobs** table in about 15 seconds. You can download them as JSON, CSV, Excel or HTML, or read them over the API.
4. For alerts, turn on **Only new jobs since the last run** and add a **Schedule** (for example, daily at 07:00).

### Input

All fields are optional.

| Field | Type | What it does |
|---|---|---|
| `keywords` | string\[] | Keep jobs that mention any of these words (title, company, tags, category, description). Set `keywordMode: "all"` to require every word. |
| `excludeKeywords` | string\[] | Drop jobs whose title or company contains any of these words, e.g. `intern`, `crypto`. |
| `categories` | string\[] | Cross-board categories, e.g. `Programming`, `Design`, `Marketing`, `Sales`, `Customer Support`, `DevOps`, `Data`, `Product`, `Finance`, `HR`, `Writing`, `Security`, `QA`. |
| `locations` | string\[] | Countries or regions the role must be open to: `USA`, `Europe`, `Germany`, `Canada`, `UK`, `LATAM`, `APAC`, `EMEA`. A job open to all of Europe matches `Germany`, and the other way round. |
| `includeWorldwide` | boolean | When `locations` is set, also keep jobs open worldwide or with no location limit. Default `true`. |
| `salaryMin` | integer | Minimum annualized salary (highest posted bound). Hourly and monthly pay is annualized; currencies are not converted. |
| `includeJobsWithoutSalary` | boolean | Keep jobs with no posted salary when `salaryMin` is set. Default `false`. |
| `postedWithinDays` | integer | Only jobs posted in the last N days. Default `30`. |
| `sources` | string\[] | Any of `remoteok`, `weworkremotely`, `jobicy`. Default: `["remoteok", "jobicy"]`. We Work Remotely is opt-in and runs only when you list `weworkremotely` explicitly. |
| `maxResults` | integer | Maximum jobs per run, newest first. Default `200`. |
| `onlyNew` | boolean | Skip jobs this saved search returned in earlier runs. History is kept in a named key-value store. |
| `stateKey` | string | Optional alert name. By default each combination of filters gets its own history. |
| `includeDescription` | boolean | Add the full plain-text `descriptionText`. Default `false`, which keeps output small. |
| `dedupeAcrossSources` | boolean | Merge the same job posted on several boards. Default `true`. |

#### Input example: daily alert for Python and backend jobs open to Europe

```json
{
  "keywords": ["python", "backend"],
  "categories": ["Programming"],
  "locations": ["Europe"],
  "includeWorldwide": true,
  "salaryMin": 80000,
  "includeJobsWithoutSalary": true,
  "postedWithinDays": 7,
  "maxResults": 100,
  "onlyNew": true,
  "stateKey": "python-backend-europe"
}
```

### Output

Each dataset item is one job, newest first. Here is a real item from a local test run on 2026-10-20 (shortened):

```json
{
  "id": "weworkremotely:vertex-marketing-group-distribution-engineer",
  "source": "weworkremotely",
  "sourceName": "We Work Remotely",
  "sourceJobId": "vertex-marketing-group-distribution-engineer",
  "title": "Distribution Engineer",
  "company": "Vertex Marketing Group",
  "companyUrl": "https://vertexmg.com/",
  "location": "Anywhere in the World",
  "regions": ["Worldwide"],
  "category": "Back-End Programming",
  "tags": ["Python", "API", "DevOps", "Android", "iOS"],
  "employmentType": "Full-Time",
  "salaryMin": null, "salaryMax": null, "salaryCurrency": null, "salaryPeriod": null, "salarySource": null,
  "postedAt": "2026-10-20T11:32:37.000Z",
  "expiresAt": "2026-11-19T11:32:37.000Z",
  "url": "https://weworkremotely.com/remote-jobs/vertex-marketing-group-distribution-engineer",
  "applyUrl": "https://weworkremotely.com/remote-jobs/vertex-marketing-group-distribution-engineer",
  "excerpt": "Scale our distribution by code: a fleet of real devices…",
  "descriptionText": null,
  "attribution": {
    "sourceName": "We Work Remotely",
    "listingUrl": "https://weworkremotely.com/remote-jobs/vertex-marketing-group-distribution-engineer",
    "text": "Job via We Work Remotely: https://weworkremotely.com/remote-jobs/vertex-marketing-group-distribution-engineer",
    "html": "<a href=\"https://weworkremotely.com/remote-jobs/vertex-marketing-group-distribution-engineer\">View this job on We Work Remotely</a>"
  },
  "alsoListedOn": [],
  "isNew": true,
  "scrapedAt": "2026-10-20T18:23:01.078Z"
}
```

- **Salary fields.** `salaryMin`, `salaryMax`, `salaryCurrency` and `salaryPeriod` come from the source's structured fields (`salarySource: "feed"`). Some We Work Remotely and Remote OK posts only state pay in the text; for those, the Actor parses an explicit range such as "Salary: $140,000 – $175,000" and sets `salarySource: "description"`.
- **Cross-posted jobs.** `alsoListedOn` lists the same job on the other boards, each with `source`, `url` and `postedAt`.
- **Run summary.** The run's `OUTPUT` record (key-value store) holds the status, each source's health, counts, rejections per filter, billing and the `onlyNew` state.

The dataset has three ready-made views: **Jobs**, **Salaries** and **Cross-posted jobs**.

### Pricing

Pay per event: **`job-scraped` = $0.001 per job delivered** ($1 per 1,000 jobs). Platform usage is included.

| Run | Jobs delivered | Cost |
|---|---|---|
| Daily alert with `onlyNew` (typical) | 5–40 new jobs | $0.005–$0.04 per day |
| Full snapshot of all three boards | ~1,000–1,500 jobs | ~$1.00–$1.50 |
| Prefilled example | 50 jobs | $0.05 |

You are never charged for:

- failed sources;
- invalid records;
- merged duplicates;
- already-seen jobs (`onlyNew`).

Set **Max cost per run** (`maxTotalChargeUsd`) and the Actor stops cleanly at that budget: it pushes only jobs it can charge for and marks the run `budget_limit_reached`. If the budget can't cover even one job, the run ends immediately with `BUDGET_TOO_LOW` and fetches nothing.

### API and integrations

Copy-paste ready: set `APIFY_TOKEN` to your own token from Apify Console → Settings → API & Integrations.

**HTTP (one call, returns the jobs):**

```bash
curl -X POST "https://api.apify.com/v2/acts/impressionable_lupine~remote-jobs-api/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"keywords":["react"],"locations":["USA"],"postedWithinDays":3,"maxResults":50}'
```

**JavaScript:**

```js
import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('impressionable_lupine/remote-jobs-api').call({ categories: ['Design'], onlyNew: true, stateKey: 'design-alert' });
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

**Python:**

```python
from apify_client import ApifyClient
client = ApifyClient("<APIFY_TOKEN>")
run = client.actor("impressionable_lupine/remote-jobs-api").call(run_input={"keywords": ["data engineer"], "salaryMin": 120000})
jobs = client.dataset(run["defaultDatasetId"]).list_items().items
```

Results also flow to Zapier, Make, n8n, Google Sheets, Slack and webhooks through Apify integrations. A common setup is a daily schedule with `onlyNew`, plus a webhook that posts the new jobs to Slack.

### Use with AI agents (MCP)

The Actor works as a tool through the [Apify MCP server](https://mcp.apify.com) in Claude, Cursor, VS Code and any MCP client:

```json
{
  "mcpServers": {
    "apify": {
      "url": "https://mcp.apify.com/?tools=impressionable_lupine/remote-jobs-api",
      "headers": { "Authorization": "Bearer <APIFY_TOKEN>" }
    }
  }
}
```

Example prompt: *"Use remote-jobs-api to find remote product designer jobs open to Europe posted in the last 3 days, with salary above 70k, and list company, salary and link."*

Agents get a flat, predictable schema, a cost of $0.001 per result, and an `attribution.text` string they can quote as-is.

### Attribution (required by the sources)

Each source lets you reuse its jobs **only with attribution**. Every item includes an `attribution` object and a source `url`, so complying takes one line of code:

- **Remote OK.** Show "Remote OK" as the source and link to the job's `url` with a normal, followed link (no `rel="nofollow"`). Do not use the Remote OK logo.
- **We Work Remotely** (opt-in, only when you add `weworkremotely` to `sources`). Credit We Work Remotely and send applicants to the weworkremotely.com listing (`applyUrl`). Don't brand your app with the WWR name. WWR's API terms restrict use in job search services, so it is off by default; check that your use fits WWR's terms before you enable it.
- **Jobicy.** Credit Jobicy with a direct link to the listing, and point apply buttons to the feed's job URL. Don't present the jobs as your own.

Easiest option: render `attribution.html` next to each job, and use `applyUrl` for your "Apply" button. Details are in `COMPLIANCE.md` in the source code.

### Limitations

- **Coverage follows the public feeds.**
  - Remote OK's API returns its latest ~100 jobs.
  - Jobicy's public feed covers the last 7 days.
  - We Work Remotely's RSS feeds carry the current listings in each category (only when `weworkremotely` is requested; it is off by default).
  - Older jobs are not available, so for history, schedule the Actor and keep the datasets.
- **Remotive and Himalayas are not included.** Their API terms forbid redistributing jobs through aggregators. If you pass them in `sources`, the Actor ignores them and adds a warning.
- **Salary.** Many remote jobs don't publish pay. Currencies are not converted, and `salaryMin` compares numbers in each job's own currency.
- **Locations.** Region matching uses the boards' free-text location labels; "Anywhere" and unspecified jobs count as worldwide.
- **Dedupe.** Jobs are merged on company plus normalized title. Two genuinely different roles with identical titles at the same company would be merged; turn off `dedupeAcrossSources` if that matters to you.
- **Polling.** Please don't schedule more often than hourly. The feeds refresh a few times a day.

### FAQ

**Is this a RemoteOK scraper?**
It returns everything a RemoteOK scraper would, but through Remote OK's official public API. That's faster and more reliable, and it's allowed when you credit Remote OK.

**Do I need an API key or proxies?**
You only need an Apify account. The Actor uses no proxies and no logins.

**How does "only new jobs" work?**
The Actor stores the IDs (and cross-board keys) of jobs it returned in a named key-value store (`remote-jobs-api-state`) under a key derived from your filters, or your `stateKey`. The next run skips those jobs, and you pay only for new ones. History is kept for 120 days.

**What if one job board is down?**
The other sources still return results and the run status is `partial`. The run fails only if every source fails, and then nothing is charged.

**Can I get full job descriptions?**
Yes. Set `includeDescription: true` (plain text, up to `descriptionMaxChars`). Keyword search always looks at descriptions, even when they aren't returned.

**Can I use this to power my own job board?**
Yes, as long as you keep each source's attribution and apply links (see Attribution). Check each board's terms for your specific use.

**Can I suggest a source?**
Yes. Open an issue on the Actor page. We only add sources whose terms allow this kind of use.

# Actor input Schema

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

Job must contain at least one of these words or phrases (title, company, tags, category and description). Example: "python", "product designer", "customer support". Leave empty for all jobs.

## `keywordMode` (type: `string`):

"any" = at least one keyword must match; "all" = every keyword must match.

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

Drop jobs whose title or company contains any of these words, e.g. "intern", "crypto".

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

Match against the source category and tags, e.g. "Programming", "Design", "Marketing", "Customer Support", "DevOps", "Sales", "Finance".

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

Countries or regions candidates must be allowed to work from, e.g. "USA", "Europe", "Germany", "Canada", "LATAM", "APAC", "EMEA". Region names include their countries and vice versa.

## `includeWorldwide` (type: `boolean`):

When locations are set, also keep jobs open worldwide or with no location restriction.

## `salaryMin` (type: `integer`):

Keep jobs whose highest posted salary, annualized, is at least this amount. Posted currency is kept as-is (no FX conversion). Leave empty for no salary filter.

## `includeJobsWithoutSalary` (type: `boolean`):

Only used with minimum salary. Many remote jobs do not publish pay; turn on to keep them.

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

Only jobs published in the last N days. Jobicy's public feed covers the last 7 days.

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

Job boards to include. Default: Remote OK and Jobicy. We Work Remotely is opt-in: add it here explicitly to include it.

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

Maximum jobs returned (and charged) per run. Newest first.

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

Remember returned jobs in a named key-value store and skip them next time. Ideal for scheduled alerts: you only pay for jobs you have not seen.

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

History key for "Only new". Default: derived from your filters, so each saved search has its own history. Set the same name on several tasks to share one history.

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

Named key-value store that keeps the "Only new" history (letters, digits, hyphens).

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

Add plain-text job descriptions (descriptionText). Off by default for smaller, faster output; keyword search always uses the description.

## `descriptionMaxChars` (type: `integer`):

Truncate descriptionText to this many characters.

## `searchDescription` (type: `boolean`):

Turn off to match keywords only in title, company, tags and category.

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

One record per job (company + normalized title). Other listings are kept in alsoListedOn.

## `jobicyMaxPages` (type: `integer`):

Upper bound on Jobicy requests per run.

## Actor input object example

```json
{
  "keywords": [
    "engineer",
    "developer"
  ],
  "keywordMode": "any",
  "locations": [
    "Europe",
    "USA"
  ],
  "includeWorldwide": true,
  "includeJobsWithoutSalary": false,
  "postedWithinDays": 14,
  "sources": [
    "remoteok",
    "jobicy"
  ],
  "maxResults": 50,
  "onlyNew": false,
  "stateStoreName": "remote-jobs-api-state",
  "includeDescription": false,
  "descriptionMaxChars": 5000,
  "searchDescription": true,
  "dedupeAcrossSources": true,
  "jobicyMaxPages": 10
}
```

# Actor output Schema

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

Normalized jobs, newest first, with attribution.

## `jobsJson` (type: `string`):

No description

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

OUTPUT record: status, per-source health, counts, filters, billing and onlyNew state.

# 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": [
        "engineer",
        "developer"
    ],
    "locations": [
        "Europe",
        "USA"
    ],
    "postedWithinDays": 14,
    "sources": [
        "remoteok",
        "jobicy"
    ],
    "maxResults": 50
};

// Run the Actor and wait for it to finish
const run = await client.actor("impressionable_lupine/remote-jobs-api").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": [
        "engineer",
        "developer",
    ],
    "locations": [
        "Europe",
        "USA",
    ],
    "postedWithinDays": 14,
    "sources": [
        "remoteok",
        "jobicy",
    ],
    "maxResults": 50,
}

# Run the Actor and wait for it to finish
run = client.actor("impressionable_lupine/remote-jobs-api").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": [
    "engineer",
    "developer"
  ],
  "locations": [
    "Europe",
    "USA"
  ],
  "postedWithinDays": 14,
  "sources": [
    "remoteok",
    "jobicy"
  ],
  "maxResults": 50
}' |
apify call impressionable_lupine/remote-jobs-api --silent --output-dataset

```

## MCP server setup

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

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/TqEgvOArfTN8F7U6b/builds/zHBSorlndaR15ofLd/openapi.json
