Skip to main content
Thirdwatchthirdwatch
Jobs & recruitment

Find Which Companies Actually Hire Remotely in Each Country

Map which employers really hire remotely in a given country using their own ATS boards, the board's remote flag, and city, region and country filters.

Jul 28, 2026 · 7 min read · 1,657 words
See the scraper →

Thirdwatch's Multi-ATS Jobs Scraper reads company career boards across eight applicant tracking systems and returns each posting with is_remote, workplace_type, city, region, country and country_code in one flat schema. That lets a recruiter build an employer-by-country map of who genuinely hires remotely — instead of trusting an aggregator's "Remote" tag, which usually means remote within one metro area.

Why scrape career boards for country-level remote hiring

Recruiters lose hours to the same failure: a role tagged "Remote" turns out to mean remote within commuting distance of the head office, or remote but payroll-restricted to a single country. By the time a candidate finds out, you have already spent goodwill. The question a talent partner actually needs answered is not "which roles are remote" but "which employers will hire a person sitting in Portugal, or Brazil, or the Philippines."

Remote work stopped being uniform after 2022 and the country spread is now the whole story. The Global Survey of Working Arrangements run by the WFH Research group measures paid work-from-home days across roughly 40 countries and finds averages that differ by more than a factor of two between them. Company policy varies at least as widely inside any single country, and no aggregator models that. The employer does, in the fields it fills in on its own board.

That makes the career board the primary source. It carries the employer's own remote flag, its own location breakdown, and the canonical apply URL — the three things you need to say "this company hires remotely, here, today."

How does this compare to the alternatives?

Three routes exist for building an employer-by-geography remote map, and they differ mostly in whether the remoteness signal is first-hand or inferred.

Approach Pricing Reliability Setup time Maintenance
Manual board-by-board review Free, costs analyst hours High but stale within days Hours per 50 companies Redo the whole pass every week
Remote job aggregator or generic scraping API Subscription Inferred remote tag, employer coverage is partial 1 hour Provider absorbs site changes; you inherit their tagging errors
Thirdwatch Multi-ATS Jobs Scraper Transparent pay-per-result First-hand fields from the employer's own board 5 minutes Thirdwatch tracks the eight ATS platforms

Aggregators are organised by role, which is the wrong axis for this job. You want the output keyed by employer and country, and you want to know when the remoteness signal is missing rather than have it guessed for you. The Multi-ATS Jobs Scraper actor page lists the full field map; the sections below cover the workflow.

How to map remote-eligible employers by country in 4 steps

Step 1: How do I assemble the list of company boards to check?

Give the actor career board URLs, or the shorter platform:slug form. A single boardUrls list can mix all eight supported systems — Workday, Greenhouse, Lever, SmartRecruiters, Workable, Ashby, Recruitee and BambooHR — so your target account list does not have to be grouped by vendor.

export APIFY_TOKEN="apify_api_xxxxxxxxxxxxxxxx"
import os, requests

ACTOR = "thirdwatch~multi-ats-jobs-scraper"
TOKEN = os.environ["APIFY_TOKEN"]

BOARDS = [
    "https://jobs.ashbyhq.com/linear",
    "https://jobs.lever.co/spotify",
    "https://job-boards.greenhouse.io/stripe",
    "https://bunq.recruitee.com",
    "https://apply.workable.com/blueground/",
    "workday:nvidia/wd5/NVIDIAExternalCareerSite",
]

def run(payload):
    r = requests.post(
        f"https://api.apify.com/v2/acts/{ACTOR}/run-sync-get-dataset-items",
        params={"token": TOKEN}, json=payload, timeout=900,
    )
    r.raise_for_status()
    return r.json()

Every row carries source_query, the exact input entry that produced it, so you can always trace a posting back to the board you asked for.

Step 2: How do I pull only the roles a board itself calls remote?

Turn on remoteOnly and narrow the geography with locationFilter. That filter is a case-insensitive substring matched against location, city, region, country and country_code, so "Germany", "Berlin" and "DE" are all valid inputs depending on how precise you want to be.

rows = run({
    "boardUrls": BOARDS,
    "remoteOnly": True,
    "locationFilter": "Germany",
    "includeDescription": True,
    "maxResults": 500,
    "maxResultsPerBoard": 100,
})

for r in rows[:5]:
    print(r["company"], "|", r["title"], "|", r["location"], "|", r["workplace_type"])

Set maxResultsPerBoard whenever your list mixes a 30-role startup with a 5,000-role enterprise, otherwise one Workday tenant will eat the entire budget before the smaller boards are read. Leave includeDescription on here — the description is where eligibility restrictions usually hide.

Step 3: How do I tell "no remote roles" apart from "this board has no remote flag"?

This is the trap, and it is the reason to run twice. is_remote is null when the board publishes no flag at all. With remoteOnly on, those rows are filtered out — so an employer whose ATS simply does not expose remoteness looks exactly like an employer with zero remote openings. Run once unfiltered and classify each company before you trust any filtered count.

from collections import defaultdict

full = run({
    "boardUrls": BOARDS,
    "remoteOnly": False,
    "includeDescription": True,
    "maxResults": 2000,
    "maxResultsPerBoard": 300,
})

by_company = defaultdict(list)
for r in full:
    by_company[(r["company"], r["platform"])].append(r)

for (company, platform), jobs in by_company.items():
    flags = [j["is_remote"] for j in jobs]
    if any(f is not None for f in flags):
        n = sum(1 for f in flags if f)
        status = f"flag published: {n}/{len(jobs)} remote"
    else:
        hits = sum(1 for j in jobs
                   if j["workplace_type"] == "remote"
                   or "remote" in (j["location"] or "").lower())
        status = f"NO FLAG - text match only: {hits}/{len(jobs)}"
    print(f"{company:20} {platform:16} {status}")

Companies in the second bucket are not evidence of anything. Fall back to workplace_type and the location string for them, and mark the result as lower confidence in whatever you hand to a sourcer.

Step 4: How do I turn the rows into an employer-by-country map?

Pivot on company and country. country_code is the cleanest key when the board publishes it, but it is frequently blank — several systems only fill location and country, so coalesce before you group.

import pandas as pd

df = pd.DataFrame(full)
df["geo"] = (df["country_code"].replace("", pd.NA)
               .fillna(df["country"].replace("", pd.NA))
               .fillna(df["location"])
               .fillna("unknown"))
df["remote"] = df["is_remote"].fillna(df["workplace_type"].eq("remote"))

grid = (df[df["remote"]]
        .pivot_table(index="company", columns="geo",
                     values="job_id", aggfunc="count", fill_value=0))
grid.to_csv("remote-eligible-employers-by-country.csv")
print(grid.head(20))

The CSV is the deliverable: rows are employers, columns are geographies, cells are live remote-flagged openings. Anything at zero across every column with a null flag needs the confidence caveat from step 3.

Step 5: How do I keep the map current without re-running everything manually?

Schedule it. Remote policy changes quietly — a company flips a whole department to hybrid and never announces it — so a weekly refresh keeps the map honest.

curl -X POST "https://api.apify.com/v2/schedules?token=$APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "remote-employer-map-weekly",
    "cronExpression": "0 6 * * 1",
    "timezone": "UTC",
    "isEnabled": true,
    "actions": [{
      "type": "RUN_ACTOR",
      "actorId": "thirdwatch~multi-ats-jobs-scraper",
      "runInput": {
        "boardUrls": ["https://jobs.ashbyhq.com/linear", "https://jobs.lever.co/spotify"],
        "remoteOnly": false,
        "includeDescription": true,
        "maxResults": 2000,
        "maxResultsPerBoard": 300
      }
    }]
  }'

Add an ACTOR.RUN.SUCCEEDED webhook and the refreshed dataset lands in your own store every Monday morning. Apify's schedules documentation covers the cron and webhook options.

Sample output

Every posting from every ATS comes back with the same keys, which is what makes the pivot in step 4 possible without per-platform cleanup. Two rows, trimmed:

[
  {
    "platform": "ashby",
    "company": "ramp",
    "company_slug": "ramp",
    "job_id": "34413f8d-26bf-4bbc-8ade-eb309a0e2245",
    "title": "Security Engineer, Cloud",
    "department": "Engineering",
    "team": "Backend",
    "employment_type": "FullTime",
    "workplace_type": "hybrid",
    "is_remote": true,
    "location": "New York, NY (HQ), Remote (Canada), Remote (US)",
    "city": "New York City",
    "region": "NY",
    "country": "USA",
    "country_code": "",
    "posted_at": "2026-04-07T17:12:35.753000Z",
    "salary_min": 211400,
    "salary_max": 290600,
    "salary_currency": "USD",
    "salary_period": "year",
    "job_url": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245",
    "apply_url": "https://jobs.ashbyhq.com/ramp/34413f8d-26bf-4bbc-8ade-eb309a0e2245/application",
    "board_url": "https://jobs.ashbyhq.com/ramp",
    "source_query": "ashby:ramp",
    "scraped_at": "2026-07-27T22:56:42.869947Z"
  },
  {
    "platform": "recruitee",
    "company": "bunq",
    "job_id": "1284471",
    "title": "Backend Engineer",
    "department": "Engineering",
    "employment_type": "Full-time",
    "workplace_type": "onsite",
    "is_remote": false,
    "location": "Amsterdam, North Holland, Netherlands",
    "city": "Amsterdam",
    "region": "North Holland",
    "country": "Netherlands",
    "country_code": "NL",
    "posted_at": "2026-06-19T09:02:11Z",
    "job_url": "https://bunq.recruitee.com/o/backend-engineer",
    "apply_url": "https://bunq.recruitee.com/o/backend-engineer/c/new",
    "board_url": "https://bunq.recruitee.com",
    "source_query": "recruitee:bunq"
  }
]

Read the first row carefully — it is the exact ambiguity this workflow exists to expose. is_remote is true, workplace_type is hybrid, country_code is empty, and the location string names three separate arrangements. Only the location text tells you the remote option is US and Canada, not global. The second row is the clean case: an explicit false, a populated country_code, and a single city.

Common pitfalls

Boards without a remote flag return nothing under remoteOnly. This is the big one. is_remote is null on systems that publish no flag, and the filter drops those rows, so "no remote roles" and "this ATS does not expose remoteness" produce an identical empty result. Always run unfiltered first.

Field coverage varies sharply by ATS. On a mixed eight-board test run, posted_at was populated on every row, department and description on 88%, but city on only 48% and salary on none of them, because those particular employers published no pay ranges. Ashby and Lever carry the richest structured fields; Workday and BambooHR carry the least.

Very large Workday boards under-report. Workday returns a display-capped total, commonly 2,000, and paging past it silently restarts at page one. The actor stops at the reported total, so a huge enterprise board can return fewer roles than the employer actually has open.

A typo can look like an empty board. SmartRecruiters answers an unknown company slug with a normal, successful, empty response rather than a 404, so a mistyped slug is indistinguishable from a company with nothing open. Some BambooHR boards are access-restricted and are skipped with a log line.

Thirdwatch's actor surfaces is_remote as a true three-state value rather than collapsing missing data to false, logs which boards came back empty and why, and normalises every system into one schema so a null is visibly a null instead of a silent zero.

Related use cases

Frequently asked questions

How do I find companies that hire remotely in a specific country?

Read each employer's own ATS board and filter on the board's remote flag plus a country substring. The Multi-ATS Jobs Scraper takes a list of career board URLs, applies remoteOnly and locationFilter, and returns is_remote, workplace_type, city, region, country and country_code per posting.

Why does remoteOnly return zero results for some companies?

Because not every applicant tracking system publishes a remote flag. When a board publishes none, is_remote comes back null and remoteOnly filters every row out. That looks identical to a company with no remote roles, so run once without the filter before concluding anything.

Is a job marked remote actually open to candidates in my country?

Not always. Boards frequently mark a role remote while restricting eligibility to one country or timezone in the description. Combine is_remote with country_code and the description text before routing a candidate, and treat the location string as the stronger constraint.

Which ATS platforms publish the most reliable remote data?

Ashby and Lever boards carry the richest structured fields, including explicit remote flags and workplace type. Workday and BambooHR publish the least beyond title, location and apply URL, so remoteness on those boards usually has to be read out of the location string.

Can I check hundreds of companies in one run?

Yes. boardUrls accepts a mixed list of Workday, Greenhouse, Lever, SmartRecruiters, Workable, Ashby, Recruitee and BambooHR boards in one call. Use maxResultsPerBoard so a single large employer cannot consume the entire result budget for the run.

Related

Try it yourself

100 free credits, no credit card.

About 30 real searches. Add the MCP to Claude or Cursor in two minutes.