RemoteOK Jobs Scraper - Remote Listings, Salaries & Details
Pricing
from $1.50 / 1,000 dataset items
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
Maintained by CommunityActor stats
0
Bookmarked
1
Total users
0
Monthly active users
12 hours ago
Last modified
Categories
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.
| Field | Example |
|---|---|
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" |
postedEpoch | 1705309200 |
postedAgeDays | 12 |
salary_min / salary_max | 90000 / 130000 |
salaryMid | 110000 |
hasSalary | true |
tags | ["python", "backend", "senior"] |
tagCount | 3 |
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" |
sourceUrl | the 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"] |
matchedTagFilters | matched 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
- Pick a mode:
search(keyword) orurl(paste RemoteOK links). - Fill in the terms or URLs for that mode, and set the filters and sort order you want.
- Set Max items (per term or URL) and, optionally, Max pages, then click Start.
- 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 ApifyClientclient = 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.
- Authorize a connector once under Apify → Settings → Integrations (Notion, Linear, Airtable, or Apify).
- Select it in the "Pipe results into your apps" input field. (If the picker is empty, you haven't authorized a connector yet.)
- For Notion, also set
notionParentPageUrlto 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 shape | Example | Behaviour |
|---|---|---|
| Tag listing | https://remoteok.com/remote-react-jobs (also .json) | Jobs for that tag (in the default list) |
Homepage, /api, /remote-jobs | https://remoteok.com/ | The current feed of all jobs (in the default list) |
| Tag or location facets | https://remoteok.com/api?tag=python, https://remoteok.com/?tags=python, https://remoteok.com/remote-jobs?location=Remote | A 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 query | https://remoteok.com/remote-jobs?search=python | Applied as a keyword filter (in the default list) |
| Page and sort | https://remoteok.com/remote-python-jobs?page=2, ...?order_by=salary | page sets the start page; order_by=salary sorts by salary (both in the default list) |
| Location page | https://remoteok.com/remote-jobs-in-europe | Jobs whose location text contains the place; often 0 rows because most jobs have an empty or generic location (not in the default list) |
| Job page | https://remoteok.com/remote-jobs/remote-software-engineer-infrastructure-sodalis-inc-1137614 | That job, only if it is still in the current feed (older or closed jobs return 0 rows; not in the default list) |
| Short job link | https://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 page | https://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 hosts | https://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.
| Parameter | Type | Default | Description |
|---|---|---|---|
mode | string | search | search (keyword) or url (paste RemoteOK links). |
searchTerms | array | ["all jobs"] | Job keywords or tag phrases, such as python or senior+devops (search mode). |
startUrls | array | one example per supported URL shape | RemoteOK listing, tag, location, company or job URLs to read directly (url mode). |
tags | array | (none) | Keep only jobs whose tag list contains at least one of these values. |
companies | array | (none) | Keep only jobs whose company name contains one of these values. |
locations | array | (none) | Keep only jobs whose location text contains one of these values. |
minSalary | integer | (none) | Keep only jobs whose salary range reaches at least this amount. |
maxSalary | integer | (none) | Keep only jobs whose salary range starts at or below this amount. |
postedWithinDays | integer | (none) | Keep only jobs posted within this many days. |
sortBy | string | posted-desc | Client-side sort order applied after filtering: posted-desc, posted-asc, salary-desc, salary-asc, or company-asc. |
requireSalary | boolean | false | Keep only jobs where a salary range is present. |
includeDescriptionText | boolean | true | Add a cleaned plain-text version of the listing description. |
includeRawFields | boolean | true | Keep the full original job object alongside normalized fields, in raw. |
keywordFilter | string | (none) | Comma-separated keywords; keep only jobs whose title, company, location, description or tags contain at least one. |
includeUnverified | boolean | true | Turn off to keep only listings RemoteOK marks as verified. |
filterNonTech | boolean | false | Keep 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. |
maxResults | integer | (none) | Alias of maxItems: above 0 it replaces maxItems, 0 means unlimited. |
maxItems | integer | 20 | Maximum matching rows to save for each search term or start URL. A cap, not a guarantee. |
maxPages | integer | (none, walks every page) | Result pages to walk per search term or start URL. An explicit value is honored with no upper limit. |
resumeFromRunId | string | (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. |
incrementalMode | boolean | false | For 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. |
stateKey | string | (auto) | Incremental mode only: name of the baseline. Empty derives it from your search terms, URLs and filters (not from caps, sort or proxy). |
emitUnchanged | boolean | false | Incremental mode only: also deliver unchanged jobs (changeType UNCHANGED); each delivered row is billed like any other. |
emitExpired | boolean | false | Incremental 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. |
ignoreFieldsForChanges | array | (none) | Incremental mode only: field names left out of change detection. scrapedAt, scraped_at and postedAgeDays are always ignored. |
proxy | object | Apify Proxy, automatic group | Connection settings: Apify Proxy group, country, or your own proxy URLs (see Plan Requirement). |
mcpConnectors | array | (none) | Optional: send a summary of each saved job to apps you authorized under Integrations. |
notionParentPageUrl | string | (none) | Notion connector only: page under which job pages are created. |
maxNotifyJobs | integer | 50 | Cap on jobs written to each connector per run. Does not affect the dataset. |
maxNotifyListings | integer | (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.
Is it legal to scrape RemoteOK?
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... |
💬 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.