← blog  /  scrapers

Remote job APIs: five free feeds, and the catch in each

2026-11-03  ·  7 min read  ·  scrapers

Remote OK, We Work Remotely, Jobicy, Working Nomads and Arbeitnow publish their jobs without a key. What each feed holds, how far back it goes, the terms, and sixty lines of Python that put them in one list.

Five remote job boards publish their listings for anyone to read, with no key and no account: Remote OK, We Work Remotely, Jobicy, Working Nomads and Arbeitnow. Each does it differently, and each has one detail that bites if you take the answer at face value.

We read all five on 26 October 2026, after checking each site's robots.txt. The numbers below are from those answers.

The short version#

  • Remote OK: remoteok.com/api, JSON. 99 newest jobs, back to 13 August 2026. Salary on 22 of them.
  • We Work Remotely: RSS only, /remote-jobs.rss plus one feed per category. 96 jobs in the main feed, back to 17 September 2026.
  • Jobicy: jobicy.com/api/jobs, JSON, documented. Up to 200 a page, the last 7 days, 1,419 jobs in all that day. Salary on 683.
  • Working Nomads: /api/exposed_jobs/, JSON. 39 jobs, back to 28 September 2026.
  • Arbeitnow: /api/job-board-api, JSON, 300 a page. Mostly on-site jobs in Europe: 17 of the first 300 were marked remote.

Remotive has a public API too, but its robots.txt closes /api/* to every crawler, so we leave it out.

Remote OK#

The answer is a JSON list, and its first element is not a job. It is the terms: link back to the job on Remote OK, without nofollow, and name Remote OK as the source, or "we'll have to suspend API access". The jobs start at element 1.

  • 99 jobs, the newest first, from 24 October 2026 back to 13 August 2026. ?tag=python gives the newest of one tag.
  • salary_min and salary_max are US dollars a year. 0 means no salary. 22 of 99 had one.
  • location was empty on 53 of 99. Empty does not mean "anywhere".
  • 3 jobs had UTF-8 text decoded as Windows-1252, so ’ arrives as ’. fix() below puts it back.
  • The robots.txt asks for one second between requests (Crawl-delay: 1).

One detail matters if you show the descriptions to anyone. Each one ends: "Please mention the word ... and tag ... when applying". The tag is one letter followed by your own IP address in base64. We decoded it, and it was the address of the server that read the feed. Cut that sentence before you publish the text.

We Work Remotely#

There is no JSON API. There are RSS feeds: /remote-jobs.rss with the newest jobs (96 that day, back to 17 September 2026) and one feed per category, such as /categories/remote-programming-jobs.rss.

  • The title is "Company: Job title". Split it on the first ": ".
  • Each item has region, country, state, skills, category, type, pubDate and expires_at, 30 days after the date posted.
  • region said "Anywhere in the World" on 93 of 96 jobs. The description, which is HTML, often says more: it starts with the company's headquarters.
  • A job's own page also answered our script that day, but the feed gives every job's fields in one request.

Jobicy#

The only one of the five with real documentation. No key is needed. count (up to 200), geo, industry and tag filter the list, and nextCursor gives the next page.

  • The jobs of the last 7 days, published with a 3-hour delay: 1,419 that day, in 8 pages.
  • Salary on 683 of them, with salaryMin, salaryMax, salaryCurrency and salaryPeriod.
  • The terms: credit Jobicy, link to the source, and send applications to the job's own address. Poll a few times a day, and never more than once an hour.

Working Nomads#

www.workingnomads.com/api/exposed_jobs/ returns one list: 39 jobs, from 21 October 2026 back to 28 September 2026, each with title, company, category, tags (one comma-separated string), location and pub_date with its time zone. There is no paging and no filter: that list is all there is.

Arbeitnow#

www.arbeitnow.com/api/job-board-api?page=1 gives 300 jobs a page, with links.next for the next one. Most are on-site jobs in Europe: on the first page, Switzerland, Paris, London and Berlin came first.

  • remote is a true or false flag. It was true on 17 of 300. Another 11 said "Remote" in their location with the flag false, such as "Berlin/Remote". Decide which one you trust.
  • ?remote=true does not filter: it gave 300 jobs, 19 of them marked remote.
  • The terms: "a free public API for jobs, please do not abuse", and a link back is appreciated. Leave a few seconds between pages.

One list from all five#

The code reads the five feeds, puts every job in the same shape and drops a job already seen on an earlier board (the same company and title). It is standard library only.

Remote job feeds into one listpython
import json, re, time, urllib.request
import xml.etree.ElementTree as ET
from datetime import datetime, timezone

UA = {"User-Agent": "remote-jobs-reader/1.0 (you@example.com)"}

def get(url):
    with urllib.request.urlopen(urllib.request.Request(url, headers=UA), timeout=40) as r:
        return r.read()

def fix(s):
    """Remote OK sends some UTF-8 text decoded as Windows-1252: '’' back to '’'."""
    try:
        return s.encode("cp1252").decode("utf-8") if re.search("[ÂÃâ]", s) else s
    except UnicodeError:
        return s

def job(board, title, company, location, posted, url, salary=(None, None, None)):
    return dict(board=board, title=title.strip(), company=(company or "").strip(), location=location or None,
                salary_min=salary[0], salary_max=salary[1], currency=salary[2], posted=posted, url=url)

def remoteok():
    rows = json.loads(get("https://remoteok.com/api"))[1:]        # [0] is the terms notice, not a job
    out = []
    for j in rows:
        lo, hi = int(j.get("salary_min") or 0) or None, int(j.get("salary_max") or 0) or None   # 0 means none
        out.append(job("Remote OK", fix(j["position"]), fix(j["company"]), j["location"], j["date"][:10], j["url"],
                       (lo, hi, "USD" if lo or hi else None)))
    return out

def weworkremotely(feed="remote-jobs.rss"):
    items = ET.fromstring(get(f"https://weworkremotely.com/{feed}")).iter("item")
    out = []
    for i in items:
        company, _, title = i.findtext("title").partition(": ")
        posted = datetime.strptime(i.findtext("pubDate"), "%a, %d %b %Y %H:%M:%S %z").date().isoformat()
        out.append(job("We Work Remotely", title, company, i.findtext("region"), posted, i.findtext("link")))
    return out

def jobicy(pause=2):
    out, url = [], "https://jobicy.com/api/jobs?count=200"
    while url:
        page = json.loads(get(url))
        out += [job("Jobicy", j["jobTitle"], j["companyName"], j["jobGeo"], j["pubDate"][:10], j["url"],
                    (j.get("salaryMin"), j.get("salaryMax"), j.get("salaryCurrency"))) for j in page["jobs"]]
        url = page["nextCursor"] and f"https://jobicy.com/api/jobs?count=200&cursor={page['nextCursor']}"
        time.sleep(pause)
    return out

def workingnomads():
    return [job("Working Nomads", j["title"], j["company_name"], j["location"], j["pub_date"][:10], j["url"])
            for j in json.loads(get("https://www.workingnomads.com/api/exposed_jobs/"))]

def arbeitnow(pages=3, pause=3):
    out = []
    for n in range(1, pages + 1):
        for j in json.loads(get(f"https://www.arbeitnow.com/api/job-board-api?page={n}"))["data"]:
            if j["remote"]:                                     # most of this board is on-site work in Germany
                posted = datetime.fromtimestamp(int(j["created_at"]), timezone.utc).date().isoformat()
                out.append(job("Arbeitnow", j["title"], j["company_name"], j["location"], posted, j["url"]))
        time.sleep(pause)
    return out

def all_remote_jobs():
    seen, out = set(), []
    for read in (remoteok, weworkremotely, jobicy, workingnomads, arbeitnow):
        for j in read():
            key = (j["company"].lower(), j["title"].lower())   # the same job is often on two boards
            if key not in seen:
                seen.add(key)
                out.append(j)
    return out
Run itpython
jobs = all_remote_jobs()
print(len(jobs), "jobs:", dict(collections.Counter(j["board"] for j in jobs)))
with open("remote_jobs.jsonl", "w") as f:
    f.writelines(json.dumps(j, ensure_ascii=False) + "\n" for j in jobs)

That day it gave 1,679 jobs: 99 from Remote OK, 94 from We Work Remotely, 1,401 from Jobicy, 33 from Working Nomads and 52 from Arbeitnow.

before you publish them

Each board asks for credit and a link back to the job on its site, and Remote OK will cut off access without it. Keep the url of every job, send people there to apply, and do not use their logos.

Five boards, filtered, on a schedule#

Our Remote Jobs Scraper on Apify reads the same five feeds in one run. It removes duplicates across boards, cuts the base64 sentence from Remote OK descriptions, and filters by search words, words to exclude, location (USA also finds United States), date posted and salary. Every row keeps the job's address on its board. It costs $0.50 per 1,000 jobs on Apify's free plan.

written