RemoteOK Jobs Scraper - Remote Listings, Salaries & Details avatar

RemoteOK Jobs Scraper - Remote Listings, Salaries & Details

Pricing

from $1.50 / 1,000 dataset items

Go to Apify Store
RemoteOK Jobs Scraper - Remote Listings, Salaries & Details

RemoteOK Jobs Scraper - Remote Listings, Salaries & Details

Scrape RemoteOK jobs by keyword or job URL. Extract title, company, location, remote details, tags, salary when available, listing URL, description, and metadata into clean structured dataset rows.

Pricing

from $1.50 / 1,000 dataset items

Rating

0.0

(0)

Developer

Abot API

Abot API

Maintained by Community

Actor stats

0

Bookmarked

1

Total users

0

Monthly active users

12 hours ago

Last modified

Share

RemoteOK Scraper: Remote Job Listings, Filters & Sorting

RemoteOK Scraper turns RemoteOK into clean, structured job rows. Search current listings by keyword or paste RemoteOK listing and tag URLs directly, then filter by salary, company, location, posting age and tags, and sort the saved rows client-side. Every row carries normalized fields such as salary midpoint and posting age alongside the original listing data, and you can export straight to JSON, CSV or Excel, or read it through the API.

Why This Scraper?

  • Two ways to find jobs. Keyword search across current listings, or paste RemoteOK listing and tag URLs directly. No need to build search URLs by hand.
  • Salary, company, location, age and tag filters. Narrow a run to exactly the roles you care about before you pay for a row.
  • Normalized fields on every row. Salary midpoint, posting age in days, a cleaned plain-text description, and matched search tokens, in addition to the site's own fields.
  • Client-side sorting. Order saved rows by posting time, salary, or company name without a second pass.
  • Full-catalogue pulls across runs. Leave the page cap open and continue an interrupted or capped pull in a later run without re-saving (or re-billing for) jobs the previous run already collected.
  • Optional raw fields. Keep the complete original listing object alongside the normalized ones, or drop it to save space.
  • Optional MCP connector export. Pipe a summary of saved jobs into apps such as Notion, Linear, Airtable, or Apify.

Use Cases

  • Recruiting and sourcing: pull current remote roles by keyword, salary band, or tag to build candidate outreach lists.
  • Job board and job-alert apps: feed filtered, sorted listings into your own site, bot, or notification tool.
  • Market and compensation research: track salary ranges, posting volume, and tag trends for remote roles over time.
  • Lead generation: find companies actively hiring in a given location or stack, using the company and tag filters.
  • Competitive and industry analysis: monitor which companies and roles a segment of the remote job market is posting.

Data You Get

Sample shape: values are illustrative placeholders, not from a live record.

FieldExample
id / jobId"1000001" (the key used for de-duplication and resume)
slug"remote-senior-backend-engineer-sample-labs-1000001"
title"Senior Backend Engineer"
companyName"Sample Labs"
locationText"Remote, Worldwide"
postedAt"2024-01-15T09:00:00+00:00"
postedEpoch1705309200
postedAgeDays12
salary_min / salary_max90000 / 130000
salaryMid110000
hasSalarytrue
tags["python", "backend", "senior"]
tagCount3
descriptionText"We are a fully remote team looking for..."
url"https://remoteok.com/remote-jobs/remote-senior-backend-engineer-sample-labs-1000001"
applyUrl"https://remoteok.com/remote-jobs/remote-senior-backend-engineer-sample-labs-1000001"
sourceUrlthe search or URL-mode source this row came from
sourceType"search-term" (also "url")
searchTerm"python" (null for all jobs, or in url mode with no query term)
matchedSearchTokens["python"]
matchedTagFiltersmatched values from your tags filter, if set
companyLogoUrl"https://remoteok.com/assets/sample-logo.png"
scrapedAt"2024-01-27T10:00:00.000Z"

When includeRawFields is on (the default), the complete original listing object is also kept in raw.

How to Use

  1. Pick a mode: search (keyword) or url (paste RemoteOK links).
  2. Fill in the terms or URLs for that mode, and set the filters and sort order you want.
  3. Set Max items (per term or URL) and, optionally, Max pages, then click Start.
  4. Download the dataset as JSON, CSV or Excel, or read it through the API.

Broad keyword search:

{
"mode": "search",
"searchTerms": ["all jobs"],
"sortBy": "posted-desc",
"maxItems": 20
}

Filtered keyword search:

{
"mode": "search",
"searchTerms": ["python", "devops"],
"tags": ["senior"],
"minSalary": 80000,
"requireSalary": true,
"sortBy": "salary-desc",
"maxItems": 30
}

Paste RemoteOK URLs:

{
"mode": "url",
"startUrls": [
{ "url": "https://remoteok.com/remote-dev-jobs" },
{ "url": "https://remoteok.com/remote-design-jobs" }
],
"requireSalary": true,
"maxItems": 15
}

Full-catalogue pull with no page cap:

{
"mode": "search",
"searchTerms": ["all jobs"],
"maxItems": 5000,
"maxPages": 0
}

Run it from your code

Python:

from apify_client import ApifyClient
client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("abotapi/remoteok-com-scraper").call(run_input={"mode": "search", "searchTerms": ["python"], "maxItems": 20})
for job in client.dataset(run["defaultDatasetId"]).iterate_items():
print(job["title"], job["companyName"], job["salaryMid"])

JavaScript:

import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('abotapi/remoteok-com-scraper').call({ mode: 'search', searchTerms: ['python'], maxItems: 20 });
const { items } = await client.dataset(run.defaultDatasetId).listItems();

Or connect it to Make, Zapier, n8n, Google Sheets or webhooks from the Integrations tab.

Caps and full-catalogue pulls

Max items and Max pages apply per search term or per URL, not to the whole run: a run with three search terms and maxItems: 20 can save up to 60 rows. Leave Max pages empty to walk every result page for a target; an explicit value stops after that many pages of that target regardless of maxItems.

For a full-catalogue pull (all current listings for a term or URL, not just the first page), leave Max pages empty and set Max items high enough (or paste multiple listing URLs, each with its own cap). Use Resume from a previous run to continue such a pull across separate runs: paste the prior run's ID (or its dataset ID) and this run skips every job id already saved there, appending only new jobs. It skips only the jobs in that one run's dataset, not in runs before it, so for a third pull point it at a dataset that holds everything collected so far, or de-duplicate by id on your side. A checkpoint is also saved automatically as the run progresses, so a run that was interrupted by a platform migration, or that failed and is then resumed with Resurrect, picks back up without re-saving (or re-billing for) jobs it had already pushed in that same run.

Interrupted runs: a job is marked as handled only after it has been saved, so a search term or URL that stops at its Max items cap leaves the rest of that page's jobs available to later search terms or URLs in the same run. If a run is interrupted (migration, abort or failure) between pages of one search term or URL, jobs of that term not yet saved are not marked, and a Resurrect or Resume from a previous run picks them up.

Send results into your apps (MCP connectors)

Optionally pipe the saved jobs into the apps you already use, via Model Context Protocol (MCP) connectors. This is an extra delivery step after the scrape: the Apify dataset is never changed.

What gets written to the connector: a condensed, human-readable summary of each job, not the full JSON. Each row becomes one entry with a title and its key fields flattened to plain text. The complete record always stays in the Apify dataset.

  1. Authorize a connector once under Apify → Settings → Integrations (Notion, Linear, Airtable, or Apify).
  2. Select it in the "Pipe results into your apps" input field. (If the picker is empty, you haven't authorized a connector yet.)
  3. For Notion, also set notionParentPageUrl to the page where job pages should be created.

The connection is mediated by Apify's MCP proxy, so this actor never sees your third-party credentials. Connector failures never change the dataset output. Leave the field empty to skip.

Input Parameters

Supported URL shapes (URL mode). Each pasted URL is read on its own; facets in the URL win over the run-level filters.

URL shapeExampleBehaviour
Tag listinghttps://remoteok.com/remote-react-jobs (also .json)Jobs for that tag (in the default list)
Homepage, /api, /remote-jobshttps://remoteok.com/The current feed of all jobs (in the default list)
Tag or location facetshttps://remoteok.com/api?tag=python, https://remoteok.com/?tags=python, https://remoteok.com/remote-jobs?location=RemoteA single tag is fetched from RemoteOK's own tag feed for that tag (about 100 jobs); two or more tags, and location, are applied as filters on the current feed (location is a text match on the job's location field, which is often empty). The single-tag form is in the default list; the location form is not (it returns rows only when a job's location text matches)
Search queryhttps://remoteok.com/remote-jobs?search=pythonApplied as a keyword filter (in the default list)
Page and sorthttps://remoteok.com/remote-python-jobs?page=2, ...?order_by=salarypage sets the start page; order_by=salary sorts by salary (both in the default list)
Location pagehttps://remoteok.com/remote-jobs-in-europeJobs whose location text contains the place; often 0 rows because most jobs have an empty or generic location (not in the default list)
Job pagehttps://remoteok.com/remote-jobs/remote-software-engineer-infrastructure-sodalis-inc-1137614That job, only if it is still in the current feed (older or closed jobs return 0 rows; not in the default list)
Short job linkhttps://remoteok.com/l/1137614 (or https://remoteok.com/1137614)That job, only if it is still in the current feed (not in the default list)
Company pagehttps://remoteok.com/remote-companies/sodalis-inc (or /company/sodalis-inc)That company's jobs in the current feed; 0 rows when the company has none right now (not in the default list)
Other hostshttps://www.remoteok.com/..., https://remoteok.io/...Treated as remoteok.com (the www form is in the default list)

The company directory (/remote-companies) and any other URL shape are skipped with a warning in the log; if every URL is skipped the run fails instead of finishing with zero rows.

Behaviour change: URL mode with RemoteOK URLs left untouched now reads the tag, homepage, API, facet, search, page, sort and host examples above that return rows from the current feed (it used to have no default); location, job and company pages are left out of the default because they only return rows when a matching job is in the feed. Replace the list with your own URLs to scrape only those. Plain names work in search terms and tags: React Native matches the react-native tag.

ParameterTypeDefaultDescription
modestringsearchsearch (keyword) or url (paste RemoteOK links).
searchTermsarray["all jobs"]Job keywords or tag phrases, such as python or senior+devops (search mode).
startUrlsarrayone example per supported URL shapeRemoteOK listing, tag, location, company or job URLs to read directly (url mode).
tagsarray(none)Keep only jobs whose tag list contains at least one of these values.
companiesarray(none)Keep only jobs whose company name contains one of these values.
locationsarray(none)Keep only jobs whose location text contains one of these values.
minSalaryinteger(none)Keep only jobs whose salary range reaches at least this amount.
maxSalaryinteger(none)Keep only jobs whose salary range starts at or below this amount.
postedWithinDaysinteger(none)Keep only jobs posted within this many days.
sortBystringposted-descClient-side sort order applied after filtering: posted-desc, posted-asc, salary-desc, salary-asc, or company-asc.
requireSalarybooleanfalseKeep only jobs where a salary range is present.
includeDescriptionTextbooleantrueAdd a cleaned plain-text version of the listing description.
includeRawFieldsbooleantrueKeep the full original job object alongside normalized fields, in raw.
keywordFilterstring(none)Comma-separated keywords; keep only jobs whose title, company, location, description or tags contain at least one.
includeUnverifiedbooleantrueTurn off to keep only listings RemoteOK marks as verified.
filterNonTechbooleanfalseKeep only technical roles: the job title must name an engineering, developer, devops, data, security, QA or similar role. Sales, recruiting, marketing, support and coordinator roles are dropped even when RemoteOK tags them with technology words.
maxResultsinteger(none)Alias of maxItems: above 0 it replaces maxItems, 0 means unlimited.
maxItemsinteger20Maximum matching rows to save for each search term or start URL. A cap, not a guarantee.
maxPagesinteger(none, walks every page)Result pages to walk per search term or start URL. An explicit value is honored with no upper limit.
resumeFromRunIdstring(none)A previous run ID or dataset ID to continue a full-catalogue walk across separate runs; jobs already collected there (matched by id) are skipped.
incrementalModebooleanfalseFor scheduled runs: deliver only NEW, UPDATED and REAPPEARED jobs compared with the previous run of the same tracked scope. Rows gain changeType, changedFields, firstSeenAt, lastSeenAt. Unchanged jobs are not delivered or billed.
stateKeystring(auto)Incremental mode only: name of the baseline. Empty derives it from your search terms, URLs and filters (not from caps, sort or proxy).
emitUnchangedbooleanfalseIncremental mode only: also deliver unchanged jobs (changeType UNCHANGED); each delivered row is billed like any other.
emitExpiredbooleanfalseIncremental mode only: deliver a row with changeType EXPIRED for each tracked job that left the feed. Reported only when the run covered the whole tracked scope (no item cap hit, no failed or skipped search, not resumed, not starting past page 1). Each delivered row is billed like any other, and EXPIRED rows are delivered and billed in addition to the Max items cap.
ignoreFieldsForChangesarray(none)Incremental mode only: field names left out of change detection. scrapedAt, scraped_at and postedAgeDays are always ignored.
proxyobjectApify Proxy, automatic groupConnection settings: Apify Proxy group, country, or your own proxy URLs (see Plan Requirement).
mcpConnectorsarray(none)Optional: send a summary of each saved job to apps you authorized under Integrations.
notionParentPageUrlstring(none)Notion connector only: page under which job pages are created.
maxNotifyJobsinteger50Cap on jobs written to each connector per run. Does not affect the dataset.
maxNotifyListingsinteger(none)Same cap as maxNotifyJobs; when set it wins.

Notifications and webhooks

There is no custom webhook input. To get a call when a run finishes, use Apify's built-in webhooks and integrations on the run or on a schedule; they deliver the dataset to your own endpoint with Apify's signed payloads.

Output Example

Sample shape: values are illustrative placeholders, not from a live record.

{
"id": "1000001",
"jobId": "1000001",
"slug": "remote-senior-backend-engineer-sample-labs-1000001",
"title": "Senior Backend Engineer",
"position": "Senior Backend Engineer",
"company": "Sample Labs",
"companyName": "Sample Labs",
"location": "Remote, Worldwide",
"locationText": "Remote, Worldwide",
"descriptionText": "We are a fully remote team looking for a senior backend engineer to join our platform team.",
"tags": ["python", "backend", "senior"],
"tagCount": 3,
"url": "https://remoteok.com/remote-jobs/remote-senior-backend-engineer-sample-labs-1000001",
"applyUrl": "https://remoteok.com/remote-jobs/remote-senior-backend-engineer-sample-labs-1000001",
"sourceUrl": "https://remoteok.com/remote-python-jobs",
"sourceType": "search-term",
"searchTerm": "python",
"postedAt": "2024-01-15T09:00:00+00:00",
"postedEpoch": 1705309200,
"postedAgeDays": 12,
"salary_min": 90000,
"salary_max": 130000,
"salaryMid": 110000,
"hasSalary": true,
"companyLogoUrl": "https://remoteok.com/assets/sample-logo.png",
"scrapedAt": "2024-01-27T10:00:00.000Z",
"scraped_at": "2024-01-27T10:00:00.000Z",
"source": "remoteok",
"source_url": "https://remoteok.com/remote-jobs/remote-senior-backend-engineer-sample-labs-1000001",
"company_name": "Sample Labs",
"company_slug": "sample-labs",
"company_url": "https://remoteok.com/company/sample-labs",
"company_logo_url": "https://remoteok.com/assets/sample-logo.png",
"type": null,
"work_mode": "remote",
"experience_level": null,
"currency": "USD",
"salary_period": "year",
"markets": ["python", "django"],
"posted_at": "2024-01-15T09:00:00+00:00",
"expires_at": null,
"data_source": "remoteok-api",
"data_source_url": "https://remoteok.com/api",
"verified": true,
"matchedSearchTokens": ["python"],
"matchedTagFilters": [],
"raw": {}
}

Run summary

The dataset has one Jobs overview view. Every run also writes an OUTPUT record to the key-value store with the total saved, a per-search or per-URL entry (status, matched and saved counts) and the filters applied.

Plan Requirement

Requests go through Apify Proxy, using the Proxy & connection setting (default: Apify Proxy, automatic group). In a local check through Apify Proxy, both the default (datacenter) connection and the residential group loaded RemoteOK's public feed, so the default needs no change. Apify Proxy access requires a paid Apify plan.

If the default connection is refused, the actor retries on other connections and logs "Served over the backup connection (the default connection was unavailable)." when it was served that way. If residential access is unavailable on your plan, the log shows "The selected connection is unavailable to this run. Apify free plans do not include residential proxies. Continuing on a backup connection; some pages may still be unavailable." If a single search term or URL cannot be loaded, the run logs "Could not load this search" and continues with the rest.

If nothing could be loaded, the run fails with one of these messages instead of returning an empty dataset:

  • "The proxy connection was refused for this account, so no page could be read. Your Apify plan does not include proxy access; a paid plan is required: https://apify.com/pricing. If you entered your own proxy URLs, check that their credentials are valid."
  • "No results: residential proxies are unavailable on this run (Apify free plans do not include them), and the backup connection could not load the requested RemoteOK pages. Upgrade at https://apify.com/pricing or supply your own proxy URLs, then try again."
  • "Could not load any results page (the requests failed), so nothing was collected. This is a connection problem, not an empty search. Please try again later."

You can also supply your own proxy URLs in the same field.

FAQ

How much does it cost?

Pay per event. The Pricing tab shows the current rates. Two events are charged: the actor start (once per run) and each dataset item saved (once per job row pushed to the dataset). Rows that are skipped, filtered out or already saved by a resumed run are not charged. Use Max items to cap the job rows per search term or URL; with emitExpired on, EXPIRED rows for tracked jobs that left the feed are delivered and billed in addition to that cap.

This actor collects only publicly available job listing data. You are responsible for how you use it: follow RemoteOK's terms and the laws that apply to you, and get legal advice if you plan commercial redistribution.

Why did a search term or URL return fewer jobs than I expected within a mixed run?

A job counts as handled only after it has been saved. Search terms or URLs that stop at their Max items cap do not hide the rest of that page from later terms or URLs in the same run. A later term or URL still skips jobs that an earlier one in the same run already saved, so overlapping terms return each job once. Jobs that do not pass your filters never count as handled. Resume from a previous run only skips jobs actually found in the prior run's dataset.

Can I get only new job listings on a schedule?

There is no built-in change-detection mode. For a recurring pull, pass the previous run's ID (or dataset ID) into Resume from a previous run: this run then skips every job id saved in that one run and appends the rest. It does not remember runs before that one, so a job saved two runs ago can be saved again. For a clean recurring feed, de-duplicate by id on your side. You also have to pass the latest run ID into the next run's input yourself.

Why did my run fail instead of returning an empty dataset?

The run stops with a clear error when no search terms or URLs are supplied, when Resume from a previous run points to a run or dataset your account cannot read, when every pasted URL is unsupported, or when no results page could be loaded at all (see the messages under Plan Requirement). If only some search terms or URLs fail to load, the run logs a warning, saves the rest, and lists the failed ones in the OUTPUT summary. Check the URLs, then run it again.

Can I use it with AI agents or MCP?

Yes. Call it from any Apify integration or MCP client, and use the connector field to push saved jobs into Notion, Linear or Airtable.

🔗 Want more jobs data?

Pair this actor with these related scrapers from the same team:

💼 WeWorkRemotely Scraper
Extract current remote job listings from weworkremotely.com. Use keyword search or WWR...
💼 CareerBuilder Scraper
Scrape CareerBuilder.com job listings by search or URL. Returns title, company profile...
💼 Wellfound Jobs Scraper
Scrape startup jobs from Wellfound.com by keyword, location, role, or remote status...
💼 Jora Jobs Scraper
Scrape Jora.com job listings across supported countries by keyword, filters, or URL...
💼 Monster Jobs Scraper
Scrape Monster.com job listings from search and direct job URLs. Extract full...
💼 Workday Jobs Scraper
Scrape job postings from any Workday-hosted careers site on Myworkdayjobs.com. Paste...

👉 Browse all abotapi scrapers

💬 Support

  • 🐞 Found a bug or a missing field? Open a ticket on the Issues tab. We usually reply within hours.
  • 🛠️ Need another site, extra fields or a private build? Email contact@abotapi.com.
  • ⭐ Enjoying it? A quick review on the actor page helps other users find it.