# RemoteOK Jobs Scraper — Remote Job Feed (`adderleydata/remoteok-jobs-scraper`) Actor

The latest remote jobs RemoteOK publishes, as structured data: title, company, tags, location with country where stated, employment type, RemoteOK salary figures, posted date, full text on request. One request per run. Incremental mode charges only for changes. No personal data.

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

## Pricing

from $1.30 / 1,000 posting saveds

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

### What does RemoteOK Jobs Scraper do?

RemoteOK Jobs Scraper reads RemoteOK's public API — the JSON feed RemoteOK publishes at `remoteok.com/api` for anyone to use — and returns the jobs in it as structured data you can load straight into a spreadsheet, a database or a model. RemoteOK lists remote jobs only.

One request returns the latest jobs RemoteOK publishes there: about a hundred, covering the last several weeks (on 26 September 2026 the feed held 99 jobs, posted between 1 August and 24 September). That is what the API offers; it is not RemoteOK's whole archive, and this Actor does not claim it is.

For each job you get the title, the company, RemoteOK's tags, the location as RemoteOK gives it with the country where the text names one, the employment type where a tag names one, the salary RemoteOK states (or the range the job's own text states), when it was posted, and the link to the job's page on RemoteOK — and, if you ask for it, the full text of the job.

What makes it different:

- **One request per run.** The whole feed, every job's text included, comes back in a single call to RemoteOK's API. No HTML parsing, no page-by-page crawling, nothing to break when a page is redesigned.
- **Text as RemoteOK means it.** RemoteOK's feed sends every accented letter, dash and emoji as the separate bytes that encode it — on 26 September 2026, 80 of the 99 job texts arrived that way, so "Mecánico" read as `MecÃ¡nico` — and a few job texts arrive as escaped HTML. The Actor reads both back before anything is filtered or saved, and leaves text that is already correct alone.
- **Incremental mode that tells the truth about a window.** Put the Actor on a schedule and each run returns only jobs that are new or changed, and charges only for those. It never reports a job as expired just because it has dropped out of RemoteOK's latest hundred.
- **One stable schema.** Every row has every field, every time. Unknown is `null`, never a missing key. The schema is versioned (`job.v1`) and every Adderley Data jobs Actor uses it, so RemoteOK sits in the same table as Greenhouse, Workable or SEEK.
- **No personal data.** RemoteOK's feed has no recruiter field, and `job.v1` has nowhere to put a person. Contact details inside job text are redacted by default. Logos are never returned.
- **Bounded cost.** You set a maximum number of results; the run stops there. It also stops at the spending limit you set on the run in Apify.

### What RemoteOK data can you extract?

| Field | What it holds |
| --- | --- |
| `id` | Stable across runs: `source:market:sourceJobId`. Use it as your primary key. |
| `title` | Job title as listed. |
| `company.name` | The hiring company, where the listing names one. |
| `advertiser.name` | The business that placed the listing — often a recruitment agency. Never a person. |
| `location.raw` | Location text as listed. Several locations are joined with `\|`. |
| `location.suburb` | Suburb, when the listing states one. |
| `location.city` | City or area, when the listing states one. |
| `location.region` | State or region, e.g. `VIC`. |
| `location.postcode` | Postcode, when the source provides it. |
| `location.country` | ISO 3166-1 alpha-2 country code. |
| `workArrangement` | `on_site`, `hybrid`, `remote` or `unknown`. |
| `employmentTypes` | Normalised: `full_time`, `part_time`, `contract`, `casual`, `temporary`, `internship`, `volunteer`. |
| `salary.raw` | The salary text exactly as shown, or null when the listing shows none. |
| `salary.min` | Lower bound as a number, when the text contains one. |
| `salary.max` | Upper bound as a number. Equal to min for a single figure. |
| `salary.currency` | ISO 4217. Taken from the text, otherwise the market default. |
| `salary.period` | `hour`, `day`, `week`, `month` or `year`; null when the text does not say. |
| `salary.includesSuper` | true / false when the text says so ("plus super", "inc. super"); otherwise null. |
| `classifications` | The source's category and subcategory pairs. |
| `teaser` | The short summary shown on the results page. |
| `bulletPoints` | Selling points shown on the results page. |
| `postedAt` | When the listing was posted, ISO 8601 UTC. |
| `updatedAt` | The source's own last-modified time, ISO 8601 UTC. Published by ATS and API sources; null where the site does not show one. |
| `expiresAt` | Expiry, ISO 8601 UTC, where the source states one. |
| `isPromoted` | true for paid placements. A listing shown both promoted and organic is returned once. |
| `url` | Link to the listing. |
| `description` | Null unless requested. `text`, optional sanitised `html`, and `contactsRedacted`. |
| `changeType` | Incremental runs: `NEW`, `UPDATED`, `REAPPEARED`, `EXPIRED` (or `UNCHANGED` if you ask for those). Otherwise null. |
| `firstSeenAt` | Incremental runs: when this monitor first saw the listing. |
| `contentHash` | SHA-256 over the fields that define a change. Compare it to detect edits yourself. |
| `scrapedAt` | When this row was produced, ISO 8601 UTC. |
| `source` | Source key, e.g. `seek`. |
| `market` | Market key, e.g. `au`, `nz`. |
| `sourceJobId` | The source's own identifier for the listing. |
| `company.sourceCompanyId` | The source's identifier for the company, when exposed. |
| `company.url` | The company's page on the source site, when exposed. |
| `advertiser.sourceAdvertiserId` | The source's identifier for the advertiser. |
| `schemaVersion` | Always `job.v1`. Breaking changes ship as `job.v2` in a new Actor version, never silently. |

How RemoteOK's fields fill the schema:

- `url` is the job's own page on RemoteOK, `https://remoteok.com/remote-jobs/<slug>`. The feed writes the address with capitals (`remoteOK.com`); the Actor writes it the ordinary way. `sourceJobId` is RemoteOK's job number, and `id` is `remoteok:global:<number>`.
- `company.name` and `advertiser.name` are the company name RemoteOK gives. RemoteOK publishes no company identifier or company page, so `company.sourceCompanyId`, `company.url` and `advertiser.sourceAdvertiserId` are `null`.
- `location.raw` is the location text as RemoteOK gives it, with the stray comma RemoteOK sometimes leaves at the end dropped. It is empty for many jobs (40 of 99 on 26 September 2026). `location.country` is filled only when the text names a country — "Remote - US", "Germany", "Remote UK", "London, London, England, United Kingdom" — and `city` and `region` only when the text's shape says which part is which ("Vancouver, BC, Canada"). A city on its own ("Melbourne", "Boston") could be in more than one country and gets no country. "Remote", "Remoto" and "Worldwide" name no place.
- `classifications` holds RemoteOK's tags, one per tag, in lower case and in RemoteOK's order, each once (`{"category": "golang", "subcategory": null}`).
- `employmentTypes` comes from a tag that names one: "full time" gives `full_time`, "part time" `part_time`, "contract", "contractor" and "freelance" `contract`, "internship" or "intern" `internship`, "temporary" `temporary`, "volunteer" `volunteer`. Most jobs carry no such tag, and their list is empty — nothing is guessed from the title.
- `salary` holds RemoteOK's own figures, in US dollars a year by RemoteOK's convention, or else the range the job's text states — the questions below explain both. `salary.includesSuper` is always `null`.
- `workArrangement` is `remote` for every job: RemoteOK lists only remote jobs.
- `postedAt` is RemoteOK's date for the job, to the second, in UTC. `updatedAt` and `expiresAt` are `null`: RemoteOK's feed publishes neither.
- `market` is always `global`, and `isPromoted` is always `false`: the feed does not mark paid placements.

### How much does it cost to scrape RemoteOK?

You pay per job saved to your dataset — **$1.50 per 1,000 jobs** on Apify's Starter plan — plus $0.005 each time a run starts. There is no monthly rental.

| Apify plan | Price | Per listing |
| --- | --- | --- |
| Free | $1.50 per 1,000 listings | $0.00150 |
| Starter (Bronze) | $1.50 per 1,000 listings | $0.00150 |
| Scale (Silver) | $1.40 per 1,000 listings | $0.00140 |
| Business (Gold) | $1.30 per 1,000 listings | $0.00130 |

Plus $0.005 per run start. Compute and proxy are included in these prices.

| What you run | Cost (USD, Starter plan) |
| --- | --- |
| 100 listings, one run | $0.15 |
| 1,000 listings, one run | $1.50 |
| 10,000 listings, one run | $15.01 |
| 50,000 listings, one run | $75.00 |
| A daily incremental monitor finding about 150 new or changed listings a day, for a month | $6.90 |

A run saves at most about a hundred jobs, the size of RemoteOK's feed, so the first line of the table is close to the cost of the largest run there is, and the larger lines apply only to many runs added together. Descriptions cost nothing extra: they arrive in the same request as the listing. Use incremental mode for anything you run more than once — after the first run you pay only for what is new or changed. As an estimate from one reading: the 99 jobs in the feed on 26 September 2026 had been posted over eight weeks, about two a day, so a daily monitor of the whole feed would save a few jobs a run, not the 150 a day in the table.

### How to scrape RemoteOK jobs

1. Open the Actor in Apify Console and go to the **Input** tab.
2. Leave every filter empty for the whole feed, or narrow it: **Title keywords**, **Tags**, **Locations**, **Posted within (days)**, **Minimum salary (US dollars a year)**.
3. Set **Maximum results**. This is also your cost cap.
4. Press **Start**. The run makes one request, so it is short. When it finishes, open the **Output** tab and export as JSON, CSV, Excel, XML or HTML, or read the dataset through the Apify API.

Every filter is applied to the one response, so filters never add requests. RemoteOK's own `?tag=` parameter is not used: it did not filter reliably when this Actor was built.

### Input

| Field | Type | Default | What it does |
| --- | --- | --- | --- |
| `keywords` | array | — | Keep jobs whose title contains every word of any keyword, in any order — "engineer data" matches "Senior Data Engineer". Leave empty for all titles. |
| `tags` | array | — | Keep jobs carrying any of these RemoteOK tags, e.g. "dev", "golang", "customer support", "full time". A whole tag is matched, without regard to case, and "full-time" is the same as "full time". Leave empty for all tags. |
| `locations` | array | — | Keep jobs whose location names this country ("Germany", "UK", "United States") or has this text as a whole word or phrase ("Texas", "London", "Europe", "LATAM"). "Worldwide", "Anywhere", "Remote" or "Global" keep jobs whose location names no place — empty, or only a word such as "Remote". Leave empty for all locations. |
| `postedWithinDays` | integer | — | Keep jobs RemoteOK dates within the last N days, counted back from the start of the run. Leave empty for any time — the feed holds the last several weeks. |
| `salaryMinUsd` | integer | — | Keep jobs whose RemoteOK salary reaches this many US dollars a year: the top of the range, or the one figure given. Jobs RemoteOK gives no salary for are left out when this is set. Leave empty for any pay. |
| `maxResults` | integer | `100` | The run stops once this many jobs are saved, newest first. You are charged per job saved, so this is also your cost cap. RemoteOK's feed holds about a hundred jobs. |
| `includeDescription` | boolean | `false` | On: the full job text comes back with each row, read from the same request as the listing, so it costs no extra requests. Off: listing fields only. |
| `descriptionFormat` | `text`, `text_and_html` | `"text"` | Plain text, or plain text plus sanitised HTML. |
| `redactContacts` | boolean | `true` | On by default: email addresses and phone numbers inside description text are replaced with \[redacted]. This Actor never outputs recruiter names or contact fields. |
| `incremental` | boolean | `false` | Remember what earlier runs saw and save only jobs that are new or changed. Unchanged jobs are skipped and not charged. Put the Actor on a schedule with this on. |
| `stateKey` | string | — | Optional name for this monitor, e.g. "remote-engineering". Runs with the same key share memory. Left empty, a key is derived from the filters themselves. |
| `emitExpired` | boolean | `false` | Kept so one input works across Adderley Data Actors; it has no effect here. RemoteOK's feed holds only its latest jobs, so a job that leaves it may still be open, and this Actor never saves an EXPIRED row. |
| `emitUnchanged` | boolean | `false` | Incremental mode only. Saves (and charges for) every job, labelled UNCHANGED where nothing moved. |
| `proxyConfiguration` | object | `{"useApifyProxy":true}` | Apify Proxy, automatic group, is the default and is what this Actor is tested with. |
| `maxConcurrency` | integer | `4` | Parallel requests. A run makes one request, so this changes nothing in practice; the default is the modest one every Adderley Data Actor uses. |
| `maxRequestsPerMinute` | integer | `90` | An upper bound on request rate across the whole run. If RemoteOK answers 429 (too many requests), the Actor waits as long as RemoteOK asks, or 30 seconds, and asks again from the same address, at most twice. |

A typical input:

```json
{
  "tags": [
    "dev"
  ],
  "maxResults": 100
}
```

How the filters decide, so the results are predictable:

- **Title keywords** keep a job when its title contains every word of any one keyword, in any order.
- **Tags** keep a job carrying any of the tags given. A whole tag is matched, without regard to case, and "full-time", "full\_time" and "full time" are the same tag; "engine" does not match "engineer".
- **Locations** keep a job when the location names the country given, by name or code ("Germany", "UK", "USA"), or has the text given as a whole word or phrase ("Texas", "London", "Europe", "LATAM" — "US" is not found inside "Austin"). "Worldwide", "Anywhere", "Remote" and "Global" keep jobs whose location names no place: empty, or only a word such as "Remote" or "Remoto". Every RemoteOK job is remote, so "Remote" does not mean every job. A region is matched by its words, not worked out from a country: "Europe" does not keep a job in "Germany".
- **Posted within (days)** counts back from the start of the run. A job with no date is kept.
- **Minimum salary** reads RemoteOK's own salary figures: the top of the range, or the one figure given. A job whose pay appears only in its text is not kept by this filter.

### Output

One row per job. This is a synthetic example in the exact shape the Actor returns:

```json
{
  "schemaVersion": "job.v1",
  "id": "remoteok:global:9100012",
  "source": "remoteok",
  "market": "global",
  "sourceJobId": "9100012",
  "url": "https://remoteok.com/remote-jobs/remote-senior-backend-engineer-go-example-robotics-co-9100012",
  "title": "Senior Backend Engineer (Go)",
  "company": {
    "name": "Example Robotics Co",
    "sourceCompanyId": null,
    "url": null
  },
  "advertiser": {
    "name": "Example Robotics Co",
    "sourceAdvertiserId": null
  },
  "location": {
    "raw": "Austin, Austin, Texas, United States",
    "suburb": null,
    "city": "Austin",
    "region": "Texas",
    "postcode": null,
    "country": "US"
  },
  "workArrangement": "remote",
  "employmentTypes": [
    "full_time"
  ],
  "salary": {
    "raw": "USD 150,000 - 185,000 a year",
    "min": 150000,
    "max": 185000,
    "currency": "USD",
    "period": "year",
    "includesSuper": null
  },
  "classifications": [
    {
      "category": "dev",
      "subcategory": null
    },
    {
      "category": "engineer",
      "subcategory": null
    },
    {
      "category": "golang",
      "subcategory": null
    },
    {
      "category": "senior",
      "subcategory": null
    },
    {
      "category": "backend",
      "subcategory": null
    },
    {
      "category": "full time",
      "subcategory": null
    }
  ],
  "teaser": null,
  "bulletPoints": [],
  "postedAt": "2026-09-24T16:00:06.000Z",
  "updatedAt": null,
  "expiresAt": null,
  "isPromoted": false,
  "description": null,
  "changeType": "NEW",
  "firstSeenAt": "2026-09-25T19:30:12.000Z",
  "contentHash": "86e23927f8cf8bff14a35d94f5e6787402e3b7048390b9a0b6887633a3e800ca",
  "scrapedAt": "2026-09-25T19:30:12.000Z"
}
```

The Output tab has two table views: **Overview** (the fields most people want, flattened) and **Changes** (for incremental runs).

### Incremental mode: new and changed RemoteOK jobs on a schedule

Turn on **Incremental mode** and run the same input on a schedule — hourly or daily. The Actor keeps a small record of what it has seen and every row tells you what happened:

| `changeType` | Meaning |
| --- | --- |
| `NEW` | First time this monitor has seen the job |
| `UPDATED` | Seen before, and the title, company name, location, tags, salary or any word of the job's text has changed |
| `UNCHANGED` | Only if you turn on **Also save unchanged jobs** |

How it behaves, so there are no surprises:

- **`EXPIRED` is never reported.** RemoteOK's feed is a window onto its latest jobs, not the whole of them. A job leaves the window when newer jobs push it out, whether or not it is still open on RemoteOK, so its absence proves nothing. Other Adderley Data Actors, which read whole job boards, report `EXPIRED`; this one never does, and never reports `REAPPEARED` either. **Report expired jobs** has no effect here.
- The first run returns everything as `NEW`. From the second run you pay only for the difference.
- A job that leaves the window and comes back unchanged is not saved or charged again.
- A change is found by comparing the job itself. An edit to its text counts: a corrected typo is reported as `UPDATED` and charged like any other change. A change of formatting alone does not count, and neither do the order of the jobs or of a job's tags, nor a new date on a job that is otherwise the same, nor RemoteOK's note to applicants at the end of every text (see below), whose tag changes with the address the feed is requested from.
- The feed covers several weeks, so a daily schedule sees every job that stays in it for a day or more.
- Runs share memory when they share a **State key**. Leave it empty and the key is derived from the filters themselves, so the same input always continues the same monitor. Name it (`remote-engineering`) if you want to change filters later without starting again.

### Descriptions and contact details

Full descriptions are off by default. Turn on **Include full descriptions** and each row carries `description.text` (and sanitised `description.html` if you choose that format): the job's text as RemoteOK publishes it. Because RemoteOK returns the text in the same response as the listing, this costs no extra requests and no extra time.

RemoteOK ends every job's text with a note to applicants: "Please mention the word … and tag … when applying to show you read the job post completely." The word belongs to the job, and the tag encodes the network address the feed was requested from — on the Apify platform, the proxy's. The note is part of what RemoteOK publishes, so it stays in the text; the Actor leaves it out only when comparing runs.

Job text sometimes contains a recruiter's email address or phone number. With **Redact contact details** on — the default — those are replaced with `[redacted]` and `description.contactsRedacted` is `true`; so is a personal profile address (linkedin.com/in/…). Neither `description.text` nor the optional HTML carries the address behind a link: the HTML keeps each link's words and drops its address, and drops images. The Actor never returns recruiter names or contact details as fields, under any setting, and never returns a company logo. If your use case is contacting individuals, this is the wrong tool.

### What people use it for

- **Remote job boards and alerts.** A clean feed of new remote jobs, deduplicated, labelled by change and linked back to RemoteOK.
- **Hiring signals.** Which companies are hiring remotely, for which skills, at which pay — a daily monitor is one scheduled run.
- **Pay research.** RemoteOK's salary figures as numbers, beside the ranges job texts state themselves.
- **Skills and tag trends.** RemoteOK's tags over time, as a table.
- **Research and teaching.** A clean, repeatable dataset with a documented schema.

### Using the API

Run it from code with the Apify client, using your own API token:

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

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('adderleydata/remoteok-jobs-scraper').call({"tags":["dev"],"maxResults":100});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items.length, items[0]?.url);
```

```python
import os
from apify_client import ApifyClient

client = ApifyClient(os.environ["APIFY_TOKEN"])
run = client.actor("adderleydata/remoteok-jobs-scraper").call(run_input={"tags":["dev"],"maxResults":100})
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item["title"], item["company"]["name"], item["url"])
```

Schedules, webhooks and the Make, Zapier, n8n and Google Sheets integrations all work the way they do for any Apify Actor. The Actor runs with limited permissions and is priced per event, so AI agents can call it through Apify's MCP server as well.

### Is it legal to scrape RemoteOK?

The Actor reads RemoteOK's public API — the feed RemoteOK publishes for others to use, with no login and no key — once per run, and returns facts about job postings. It does not log in, does not solve CAPTCHAs, does not get round any limit RemoteOK sets and does not collect personal information. If RemoteOK answers that it is getting too many requests, the Actor waits as long as RemoteOK asks and asks again from the same address, at most twice; it never switches address to get round a limit.

RemoteOK asks everyone who uses data from its API or its site to link back to RemoteOK where the data is used, and to mention Remote OK as the source. Every row carries `url`, the job's page on RemoteOK, for that link. RemoteOK also asks that its logo not be used without its permission; the Actor never returns logos.

What you do with the data is your responsibility. Each job's text is its advertiser's copyright — analyse it, do not republish it. If your project touches personal information, privacy law applies to you wherever you are. This is general information, not legal advice.

### Questions

**Why does a run return about a hundred jobs at most?** That is what RemoteOK's API holds: its latest jobs, covering the last several weeks. A larger **Maximum results** does not change it.

**Why is a job that disappeared not reported as expired?** Because the feed is a window. A job drops out of it when newer jobs push it out, whether or not it has been filled. The incremental mode section above says what is reported instead.

**Why is the salary in US dollars a year?** RemoteOK gives each job two salary figures and no currency or period, and shows them on its pages as US dollars a year. That is RemoteOK's convention, not something each job states: where a job's own text gives its pay in another currency or by the hour, RemoteOK's figures are a yearly US-dollar equivalent (on 26 September 2026, a job whose text gave a yearly range in euros, and another paid by the hour, both carried yearly dollar figures). The Actor returns RemoteOK's figures as given — `min`, `max`, `currency` `USD`, `period` `year` — with `raw` saying them in words ("USD 70,000 - 80,000 a year"; "From USD 70,000 a year" when only a minimum is given). Figures that cannot be a year's pay (one job carried 30 and 36) are not returned as a salary. Where RemoteOK gives no figures, a range the job's text states is read from that sentence, in the currency and period the text states, and the sentence is kept in `salary.raw`. Otherwise `salary` is empty; nothing is estimated.

**Why is the location empty, or the country missing?** Many RemoteOK jobs state no location (40 of 99 on 26 September 2026). Where there is one, the country is filled only when the text names it; a city alone does not say which country it is in.

**Why does the Actor save fewer jobs than the feed holds?** Filters aside, a job the feed gives without a link to its own page is left out, because every row links to the job on RemoteOK (one of 99 on 26 September 2026, whose title RemoteOK had cut short). Nothing else is dropped.

**Why does every description end with a note about mentioning a word?** RemoteOK adds it to every job's text, so the Actor keeps it. The descriptions section above says what it holds.

**Does it need a RemoteOK login or API key?** No. The API is public.

**Can I get recruiter emails or phone numbers?** No, by design.

**How current is the data?** It is read from RemoteOK while your run is in progress. `postedAt` is RemoteOK's date for the job; `scrapedAt` records when the row was produced.

**The field I need is not there.** Open an issue on the **Issues** tab. Fields are added to the schema without breaking existing ones.

### Support

Use the **Issues** tab on this page. We read it every day. Include the run ID and what you expected to see.

### Other Adderley Data Actors

Every Actor in a vertical returns the same fields, so adding a source needs no new code on your side.

- [Ashby Jobs Scraper — Company Job Boards](https://apify.com/adderleydata/ashby-jobs-scraper) — same `job.v1` fields
- [BambooHR Jobs Scraper — Company Job Boards](https://apify.com/adderleydata/bamboohr-jobs-scraper) — same `job.v1` fields
- [Breezy HR Jobs Scraper — Company Job Boards](https://apify.com/adderleydata/breezy-jobs-scraper) — same `job.v1` fields
- [Career Site Jobs Scraper — Greenhouse, Lever, Workday](https://apify.com/adderleydata/career-site-jobs-scraper) — same `job.v1` fields
- [Dayforce Jobs Scraper — Company Job Boards](https://apify.com/adderleydata/dayforce-jobs-scraper) — same `job.v1` fields
- [Greenhouse Jobs Scraper — Company Job Boards](https://apify.com/adderleydata/greenhouse-jobs-scraper) — same `job.v1` fields
- [JazzHR Jobs Scraper — Company Job Boards](https://apify.com/adderleydata/jazzhr-jobs-scraper) — same `job.v1` fields
- [JobAdder Jobs Scraper — Agency and Employer Job Boards](https://apify.com/adderleydata/jobadder-jobs-scraper) — same `job.v1` fields
- [Lever Jobs Scraper — Company Job Boards](https://apify.com/adderleydata/lever-jobs-scraper) — same `job.v1` fields
- [Personio Jobs Scraper — Company Job Boards](https://apify.com/adderleydata/personio-jobs-scraper) — same `job.v1` fields
- [Pinpoint Jobs Scraper — Company Job Boards](https://apify.com/adderleydata/pinpoint-jobs-scraper) — same `job.v1` fields
- [Recruitee Jobs Scraper — Company Job Boards](https://apify.com/adderleydata/recruitee-jobs-scraper) — same `job.v1` fields
- [Rippling Jobs Scraper — Company Job Boards](https://apify.com/adderleydata/rippling-jobs-scraper) — same `job.v1` fields
- [Workable Jobs Scraper — Company Job Boards](https://apify.com/adderleydata/workable-jobs-scraper) — same `job.v1` fields
- [Workday Jobs Scraper — Company Job Boards](https://apify.com/adderleydata/workday-jobs-scraper) — same `job.v1` fields

### About

Made by Adderley Data, Melbourne — https://adderleydata.com. Not affiliated with, endorsed by or sponsored by Remote OK. Remote OK is a name and trade mark of its owner, used here only to describe what this Actor reads; its logo is not used.

# Changelog

This Actor's version history is a separate document: https://apify.com/adderleydata/remoteok-jobs-scraper/changelog.md

# Actor input Schema

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

Keep jobs whose title contains every word of any keyword, in any order — "engineer data" matches "Senior Data Engineer". Leave empty for all titles.

## `tags` (type: `array`):

Keep jobs carrying any of these RemoteOK tags, e.g. "dev", "golang", "customer support", "full time". A whole tag is matched, without regard to case, and "full-time" is the same as "full time". Leave empty for all tags.

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

Keep jobs whose location names this country ("Germany", "UK", "United States") or has this text as a whole word or phrase ("Texas", "London", "Europe", "LATAM"). "Worldwide", "Anywhere", "Remote" or "Global" keep jobs whose location names no place — empty, or only a word such as "Remote". Leave empty for all locations.

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

Keep jobs RemoteOK dates within the last N days, counted back from the start of the run. Leave empty for any time — the feed holds the last several weeks.

## `salaryMinUsd` (type: `integer`):

Keep jobs whose RemoteOK salary reaches this many US dollars a year: the top of the range, or the one figure given. Jobs RemoteOK gives no salary for are left out when this is set. Leave empty for any pay.

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

The run stops once this many jobs are saved, newest first. You are charged per job saved, so this is also your cost cap. RemoteOK's feed holds about a hundred jobs.

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

On: the full job text comes back with each row, read from the same request as the listing, so it costs no extra requests. Off: listing fields only.

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

Plain text, or plain text plus sanitised HTML.

## `redactContacts` (type: `boolean`):

On by default: email addresses and phone numbers inside description text are replaced with \[redacted]. This Actor never outputs recruiter names or contact fields.

## `incremental` (type: `boolean`):

Remember what earlier runs saw and save only jobs that are new or changed. Unchanged jobs are skipped and not charged. Put the Actor on a schedule with this on.

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

Optional name for this monitor, e.g. "remote-engineering". Runs with the same key share memory. Left empty, a key is derived from the filters themselves.

## `emitExpired` (type: `boolean`):

Kept so one input works across Adderley Data Actors; it has no effect here. RemoteOK's feed holds only its latest jobs, so a job that leaves it may still be open, and this Actor never saves an EXPIRED row.

## `emitUnchanged` (type: `boolean`):

Incremental mode only. Saves (and charges for) every job, labelled UNCHANGED where nothing moved.

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

Apify Proxy, automatic group, is the default and is what this Actor is tested with.

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

Parallel requests. A run makes one request, so this changes nothing in practice; the default is the modest one every Adderley Data Actor uses.

## `maxRequestsPerMinute` (type: `integer`):

An upper bound on request rate across the whole run. If RemoteOK answers 429 (too many requests), the Actor waits as long as RemoteOK asks, or 30 seconds, and asks again from the same address, at most twice.

## Actor input object example

```json
{
  "maxResults": 20,
  "includeDescription": false,
  "descriptionFormat": "text",
  "redactContacts": true,
  "incremental": false,
  "emitExpired": false,
  "emitUnchanged": false,
  "proxyConfiguration": {
    "useApifyProxy": true
  },
  "maxConcurrency": 4,
  "maxRequestsPerMinute": 90
}
```

# Actor output Schema

## `postings` (type: `string`):

One row per posting in the job.v1 schema; every key is always present and unknown is null. In incremental runs each row carries a changeType of NEW, UPDATED, REAPPEARED or EXPIRED.

## `runSummary` (type: `string`):

Requests made, first-attempt success, rows saved and skipped, rows that failed validation, and contact details redacted.

# 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 = {
    "maxResults": 20,
    "proxyConfiguration": {
        "useApifyProxy": true
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("adderleydata/remoteok-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 = {
    "maxResults": 20,
    "proxyConfiguration": { "useApifyProxy": True },
}

# Run the Actor and wait for it to finish
run = client.actor("adderleydata/remoteok-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 '{
  "maxResults": 20,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}' |
apify call adderleydata/remoteok-jobs-scraper --silent --output-dataset

```

## MCP server setup

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