Job Feed: 110+ Job Boards & Career Pages in 60+ Countries avatar

Job Feed: 110+ Job Boards & Career Pages in 60+ Countries

Pricing

$0.50 / 1,000 unique job in aggregated feeds

Go to Apify Store
Job Feed: 110+ Job Boards & Career Pages in 60+ Countries

Job Feed: 110+ Job Boards & Career Pages in 60+ Countries

One deduplicated, newest-first job feed from 110+ job boards in 60+ countries, 40+ ATS career systems and any company career page. Deepest in Switzerland, Germany and Austria. Daily new-jobs-only runs, radius filter, agency flag, and MCP for AI agents.

Pricing

$0.50 / 1,000 unique job in aggregated feeds

Rating

0.0

(0)

Developer

NanoScrape

NanoScrape

Maintained by Community

Actor stats

0

Bookmarked

3

Total users

2

Monthly active users

5 hours ago

Last modified

Share

Job Feed - Job Aggregator for Recruiters, Job Seekers and AI Agents

One deduplicated, newest-first job feed from 110+ job boards (LinkedIn, Indeed, StepStone, Jobs.ch, Arbeitsagentur.de, USAJOBS and many more), 40+ ATS career systems and any company career page. One input, one schema, one stable job_id. No login needed.

Who it is for

  • Recruiters and agencies (deepest coverage in CH, DE and AT) who want new roles at direct employers, hiring signals and a daily list instead of ten browser tabs.
  • Job seekers who want a daily alert for a role, city and radius.
  • HR-tech and AI-agent builders who need one normalized job schema, a stable key to upsert on, and an MCP tool.

What you get

  • One feed across boards, deduplicated, newest first, each row with a stable job_id (same job from any board hashes the same).
  • onlyNewJobs: a daily schedule delivers (and bills) only jobs you have not received before.
  • A radius filter (location + radiusKm) that also drops rows boards return too far away.
  • An agency flag on every company (company.is_agency) and excludeAgencies to drop staffing firms and job aggregators.
  • Company career pages: add companyUrls and get that employer's own openings, including company, career page and employee range.

For recruiters: a copy-paste search

{"countries":["CH"],"keywords":["Pflegefachfrau","Pflegefachmann"],"location":"Zürich","radiusKm":30,"postedWithinDays":7,"excludeAgencies":true,"maxResultsTotal":200}

Measured (local run, October 2026): 98 unique direct-employer jobs from 6 boards (jobs.ch, arbeit.swiss, jobscout24, Indeed, LinkedIn, Glassdoor); 138 board rows merged to 98, 10 agency postings left out, 24 jobs outside 30 km dropped. Cost about $0.38 for the run ($0.33 sub-actors, $0.05 job-feed), about $0.004 per job. Weekly that is about $1.65 a month, daily at most about $11; with onlyNewJobs the job-feed part drops to new jobs only while board scraping still applies.

How to start

  1. Pick countries (for example ["CH"]), keywords and a location.
  2. Start; the prefilled input (Zürich, Software Engineer, 30 km, last 7 days) works as is.
  3. Schedule it daily with "onlyNewJobs": true, and add "excludeAgencies": true if you only want direct employers.

What it costs

Pay-per-event, no subscription. job-feed bills $0.0005 per unique job it delivers. The boards and career-site scrapers it calls are separate actors, billed separately to your Apify account at their own rates, and included in the totals below. Rows job-feed drops before delivery (caps, filters, duplicates) are not billed by job-feed, though a board may have billed its own event for them.

SearchPer runPer month
CH, Pflegefachfrau/-mann, Zürich, 30 km, excludeAgencies, 98 unique jobs (measured, October 2026)about $0.38about $1.65 weekly, at most about $11 daily
CH, Laborant, Basel, 6 boards plus jobbasel-ch, 25 unique jobs (measured, October 2026)about $0.11about $3.30 daily (30 runs)
50-company career-site list (careerSites, known ATS, about 12 open roles each, about 600 jobs), weekly (estimate)about $0.90about $3.60 (4 runs)

The measured runs use list prices from each board's own rate (for example jobs-ch $0.00287 per row, arbeit-swiss and jobscout24-ch $0.003, career-site-jobs $0.001 per job) plus job-feed's $0.0005 per unique job; the career-site row is an estimate. See Supported boards for board prices and Billing and cost model for the cost_report of each run. On custom pages (LLM extractor, about $0.012 per job plus $0.01 per company) the same 50 companies cost around $8 for the first run (estimate). maxResultsTotal caps spend.

Use cases

  • Agency sourcing of new roles at direct employers. countries: ["CH","DE"], your keywords, excludeAgencies: true, onlyNewJobs: true, daily. Staffing firms and aggregators (DACH agency registry) are dropped.
  • Hiring-signal leads for sales. Run a role keyword (for example "DevOps Engineer") with includeCompanyDetails, and rows carry the company name, domain, career_page_url and, where the source reports it, an employee range (employee_count_from, employee_count_to).
  • A daily alert for a job seeker. One city, radiusKm: 25, postedWithinDays: 1, onlyNewJobs: true, scheduled each morning.
  • Life-science niche search. Combine regional boards (includeRegionalBoards: "nearby" for a Basel or Zürich location) with a list of employer career pages in careerSites, so small cantonal boards and employer ATS pages land in one feed.
  • Feeding an AI agent. Connect the actor over MCP (below) and let your agent search jobs by country, keyword and location; job_id tells it what it has already shown.

Example output

One row from the Pflege run, trimmed:

{
"job_id": "edf36ab3f7686037",
"title": "Pflegefachperson für die Viszeralchirurgie / Gastroenterologie 50 -100%",
"company": {"name": "Universitätsspital Zürich", "is_agency": false, "agency_type": null},
"location": {"raw": "Zurich", "city": "Zürich", "region": "ZH", "country": "CH"},
"employment_type": "permanent",
"workload_pct": {"min": 50, "max": 100},
"posted_date": "2026-10-16T12:31:19.714Z",
"language": "de",
"matched_query": "Pflegefachmann",
"apply_url": "https://www.jobscout24.ch/en/job/89adc54d-9f7b-4fca-8557-716fd099eab0/",
"sources": [{"platform": "jobscout24-ch", "url": "https://www.jobscout24.ch/en/job/89adc54d-9f7b-4fca-8557-716fd099eab0/"}],
"_platform": "jobscout24-ch"
}

Use with AI Agents (MCP)

Connect this actor to any MCP-compatible AI client.

Apify MCP server URL:

https://mcp.apify.com?tools=santamaria-automations/job-feed

For clients that only speak stdio, use mcp-remote:

{
"mcpServers": {
"job-feed": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://mcp.apify.com?tools=santamaria-automations/job-feed", "--header", "Authorization: Bearer YOUR_APIFY_TOKEN"]
}
}
}

How it works

Job Feed is an orchestrator over three tiers of scrapers:

  1. 110+ job boards, each with a scraper built per site (LinkedIn, Indeed, StepStone, Jobs.ch, Arbeitsagentur.de, USAJOBS and many more). Selected via platforms or countries.
  2. career-site-jobs - direct extraction from 40+ ATS career systems (Greenhouse, Lever, Workday, Ashby, Teamtailor, Personio, SmartRecruiters, BambooHR, iCIMS, Recruitee, and more). Activated when a companyUrls entry matches a known ATS, or explicitly with includeCareerSites: true.
  3. website-job-extractor - reads any other company website: it finds the careers page, detects the ATS, and falls back to LLM extraction plus a browser retry for JS-rendered sites.

More boards and career systems are added continuously.

Table of contents

Quick start

{
"countries": ["DE", "AT", "CH"],
"keywords": ["Software Engineer"],
"maxResultsPerQueryPerPlatform": 25
}

That fires every Tier 1 platform in DACH plus Indeed, Glassdoor and LinkedIn per country (includeGlobalBoards, on by default). Reports are in the OUTPUT record, see Terminal items.

Constraints

  • Platforms per run: 110+ selectable in the platforms picker; the orchestrator gates concurrency against a RAM budget, capped by maxTotalMemoryMbytes if you set it (see RAM and parallelism). Browser-tier actors use up to 4 GB each and queue when budget is tight.
  • Sub-actor timeout: 60-3600 seconds per platform (default 600).
  • titleFilter: comma-separated substring list, case-insensitive OR match. Ten boards also filter natively before detail fetch at no extra cost, and every row is then checked against the same rule here (see Platform selection); the rest are filtered client-side, counted as dropped_title_filter.
  • postedWithinDays precision: accepts fractional days: 0.5 = last 12 hours, 0.042 is roughly 1 hour, use 1.0/24 for exactly 1 hour.
  • Dedup mode: streaming (default, rows flow as each platform finishes) or batch (buffer until all platforms complete, then emit once per unique job_id with all sources inline). See Dedup mode.

Supported platforms

The registry tracks 120+ platforms. 110+ are healthy and selectable in the platforms picker: 110+ job boards plus 3 company-website sources (career-site-jobs, website-job-extractor, website-job-extractor-browser). The rest are parked or deliberately excluded and never selectable (mostly aggregator wrappers over sources this fleet already scrapes first-party, such as Adzuna, Jooble, Careerjet, and Monster). See Coming soon for what is in the build queue.

Every board has a tier: Tier 1 boards run automatically when their country is in countries[]; Tier 2 boards run only when named in platforms[]. The full list, with each board's standalone Apify actor, RAM and pricing, is in Supported boards.

Country presets

countries: ["XX"] turns on every healthy Tier 1 board for that market (table below). It is additive across countries, and additive with anything you list in platforms[]. Tier 2 boards are not in the preset and need to be named explicitly (or added with includeRegionalBoards). When platforms[] is also set, countries still add their preset and the global boards, and run_summary warnings name every board added that way. GB is accepted as an alias for UK. Countries not in the table have no preset, use platforms instead.

includeGlobalBoards (default true) additionally runs Indeed, Glassdoor, and LinkedIn once per selected country: countries: ["DE","AT"] fires two Indeed runs, one scoped to DE and one to AT. Boards you name in platforms[] are not added twice. Set it to false to run only the preset.

CountryPreset (Tier 1, automatic)
Germany (DE)arbeitsagentur-de, jobware-de, meinestadt-de, stellenanzeigen-de, stepstone-de
Austria (AT)ams-at, karriere-at, stepstone-at
Switzerland (CH)arbeit-swiss, jobs-ch, jobscout24-ch
United Kingdom (UK / GB)nhs-jobs, reed-uk
United States (US)usajobs-us, wellfound-us, workatastartup-us
Canada (CA)jobbank-ca
Australia (AU)seek
Belgium (BE)forem-be, vdab-be
Czech Republic (CZ)prace-cz
Denmark (DK)jobindex-dk
Spain (ES)infojobs-es, sepe-es
France (FR)france-travail, hellowork-fr, welcome-to-the-jungle
Greece (GR)kariera-gr
Croatia (HR)mojposao-hr
Hungary (HU)profession-hu
Ireland (IE)irishjobs-ie
Italy (IT)subito-it-jobs
Netherlands (NL)nationale-vacaturebank-nl, werk-nl
Norway (NO)finn-no
Poland (PL)justjoinit, nofluffjobs-eu, pracuj-pl
Portugal (PT)net-empregos-pt
Romania (RO)anofm-ro, ejobs-ro
Russia (RU)hh-ru, trudvsem-ru
Sweden (SE)arbetsformedlingen-se
Slovakia (SK)profesia-sk
Ukraine (UA)dcz-gov-ua

Tier 2 boards (opt-in, name them in platforms[]):

  • DE: stellenmarkt-de, nicejob-de. AT: westjob-at. PL: praca-gov-pl. UA: work-ua.
  • CH: jobbasel-ch, ostjob-ch, jobzueri-ch, jobbern-ch, jobmittelland-ch, zentraljob-ch, myjob-ch, schaffu-ch, sozjobs-ch, ictjobs-ch, swissdevjobs-ch, carriera-ch, jobup-ch, alpha-topjobs-ch, lavoro-cdt-ch, ge-ch-emploi.
  • UK: totaljobs-uk, tes-uk, jobserve-uk, charityjob-uk, guardianjobs-uk, teaching-vacancies-uk, targetjobs-uk, jobs-ac-uk, escape-the-city, cwjobs-uk, prospects-uk, hays-uk, milkround-uk.
  • US: healthecareers-us, levels-fyi-us, rigzone-us, wayup-us, snagajob-us, craigslist-jobs, dribbble-jobs, biospace, higheredjobs.
  • No country preset: naukri (India), jobkorea-kr (South Korea), bayt-me, and the international or remote boards (linkedin, glassdoor, efinancialcareers, jobrapido, computrabajo, idealist, ycombinator, michaelpage-intl, remoteok, remotive, weworkremotely, dice, builtin, cryptojobslist, web3career, e-fellows, euraxess).
  • Research jobs (EU-wide): euraxess (PhD, postdoc, research staff, lab technicians) belongs to no preset and is never added by includeGlobalBoards; name it in platforms[]. countries are forwarded to its own country filter (OR-combined), location as is. countries also activates that country's preset, so add "includeGlobalBoards": false and pick boards yourself for EURAXESS alone, or use location with a country name.

That is 40+ Tier 1 boards across 25+ countries, plus the three global boards, plus career-site-jobs (tied to no country: use includeCareerSites or companyUrls).

Supported boards

Every source selectable in the platforms picker today (113 of them: 110 job boards plus 3 company-website sources: career-site-jobs for 40+ ATS systems, website-job-extractor and website-job-extractor-browser for any other company website). Each links to its standalone Apify actor, which you can also run on its own. RAM is each actor's own actor.json ceiling, the value job-feed budgets against (see RAM and parallelism). Prices are the live pay-per-event rates of each actor; "-" means no priced event.

BoardApify actorCountryRAM (MB)Pricing per event
alpha-topjobs-chalpha-topjobs-ch-scraperCH128start $0.001
ams-atams-at-scraperAT256start $0.05, item $0.005
anofm-roanofm-ro-scraperRO128SERP $0.003, detail $0.005, start $0.001
arbeit-swissarbeit-swiss-scraperCH512start $0.0001, item $0.003
arbeitsagentur-dearbeitsagentur-de-scraperDE512SERP $0.008, detail $0.02, start $0.05
arbetsformedlingen-searbetsformedlingen-se-scraperSE256SERP $0.003, detail $0.005, start $0.001
bayt-mebayt-scraperME128SERP $0.003, detail $0.005, company $0.005, start $0.001
biospacebiospace-scraperUS128SERP $0.003, detail $0.005, company $0.005, start $0.001
builtinbuiltin-scraperREMOTE128start $0.001, item $0.003
career-site-jobscareer-site-jobs-scraperANY128start $0.001, career-job-result $0.001
carriera-chcarriera-ch-scraperCH128SERP $0.003, detail $0.005, start $0.001
charityjob-ukcharityjob-uk-scraperUK128SERP $0.0014, detail $0.003, company $0.005, start $0.001
computrabajocomputrabajo-scraperINTL128start $0.001, item $0.003
craigslist-jobscraigslist-jobs-scraperUS128SERP $0.003, detail $0.005, start $0.001
cryptojobslistcryptojobslist-scraperREMOTE128start $0.001, item $0.003
cwjobs-ukcwjobs-uk-scraperUK128SERP $0.0012, detail $0.0025, company $0.005, start $0.001
dcz-gov-uadcz-gov-ua-scraperUA128SERP $0.003, detail $0.005, start $0.001
dicedice-scraperREMOTE128start $0.001, item $0.003
dribbble-jobsdribbble-jobs-scraperUS128SERP $0.003, detail $0.005, start $0.001
e-fellowse-fellows-scraperREMOTE128SERP $0.0015, detail $0.003, start $0.001
efinancialcareersefinancialcareers-scraperINTL256SERP $0.003, detail $0.005, start $0.001
ejobs-roejobs-ro-scraperRO128SERP $0.003, detail $0.005, company $0.002, start $0.001
escape-the-cityescape-the-city-scraperUK128SERP $0.003, detail $0.005, company $0.005, start $0.001
euraxesseuraxess-scraperINTL128SERP $0.003, detail $0.005, start $0.001
finn-nofinn-no-scraperNO256SERP $0.003, detail $0.005, start $0.001
forem-beforem-be-scraperBE128SERP $0.003, detail $0.005, start $0.001
france-travailfrance-travail-scraperFR256SERP $0.003, detail $0.005, start $0.001
ge-ch-emploige-ch-emploi-scraperCH128SERP $0.003, detail $0.005, start $0.001
glassdoorglassdoor-scraperINTL128SERP $0.003, detail $0.002, company $0.005, start $0.001
guardianjobs-ukguardianjobs-scraperUK128SERP $0.003, detail $0.005, company $0.005, start $0.001
hays-ukhays-scraperUK128SERP $0.003, detail $0.005, start $0.001
healthecareers-ushealthecareers-scraperUS128SERP $0.003, detail $0.005, company $0.005, start $0.001
hellowork-frhellowork-fr-scraperFR128SERP $0.003, detail $0.005, start $0.001
hh-ruhh-ru-scraperRU128SERP $0.003, detail $0.005, company $0.005, start $0.001
higheredjobshigheredjobs-scraperUS128SERP $0.003, detail $0.005, company $0.005, start $0.001
ictjobs-chictjobs-ch-scraperCH128start $0.0001, item $0.003
idealistidealist-scraperINTL128start $0.001, item $0.003
indeedindeed-http-scraperINTL256SERP $0.003, start $0.001, job-with-description $0.005, company-enriched $0.005
infojobs-esinfojobs-scraperES128SERP $0.003, detail $0.005, company $0.002, start $0.001
irishjobs-ieirishjobs-ie-scraperIE128SERP $0.003, detail $0.005, start $0.001
jobbank-cajobbank-ca-scraperCA128SERP $0.003, detail $0.005, company $0.005, start $0.001
jobbasel-chjobbasel-ch-scraperCH512start $0.0001, item $0.003
jobbern-chjobbern-ch-scraperCH512start $0.0001, item $0.003
jobindex-dkjobindex-dk-scraperDK128SERP $0.003, detail $0.005, start $0.001
jobkorea-krjobkorea-kr-scraperKR128SERP $0.003, detail $0.005, start $0.001
jobmittelland-chjobmittelland-ch-scraperCH512start $0.0001, item $0.003
jobrapidojobrapido-scraperINTL128SERP $0.003, detail $0.005, start $0.001
jobs-ac-ukjobs-ac-uk-scraperUK128SERP $0.0016, detail $0.003, company $0.005, start $0.001
jobs-chjobs-ch-scraperCH128start $0.001
jobscout24-chjobscout24-ch-scraperCH256start $0.0001, item $0.003
jobserve-ukjobserve-uk-scraperUK128SERP $0.003, detail $0.005, company $0.005, start $0.001
jobup-chjobup-ch-scraperCH128start $0.001
jobware-dejobware-de-scraperDE512SERP $0.0037, detail $0.0088, start $0.005
jobzueri-chjobzueri-ch-scraperCH512start $0.0001, item $0.003
justjoinitjustjoinit-scraperPL128SERP $0.003, detail $0.005, start $0.001
kariera-grkariera-gr-scraperGR128SERP $0.003, detail $0.005, start $0.001
karriere-atkarriere-at-scraperAT128SERP $0.003, detail $0.005, start $0.001
lavoro-cdt-chlavoro-cdt-ch-scraperCH128SERP $0.003, detail $0.005, start $0.001
levels-fyi-uslevels-fyi-scraperUS128SERP $0.003, detail $0.005, company $0.005, start $0.001
linkedinlinkedin-scraperINTL128start $0.001, item $0.001
meinestadt-demeinestadt-de-scraperDE512SERP $0.0035, detail $0.0085, start $0.005
michaelpage-intlmichaelpage-scraperINTL128SERP $0.003, detail $0.005, start $0.001
milkround-ukmilkround-uk-scraperUK128SERP $0.003, detail $0.005, company $0.005, start $0.001
mojposao-hrmojposao-hr-scraperHR128SERP $0.003, detail $0.005, start $0.001
myjob-chmyjob-ch-scraperCH512start $0.0001, item $0.003
nationale-vacaturebank-nlnationale-vacaturebank-nl-scraperNL256SERP $0.003, start $0.001
naukrinaukri-scraperIN4096SERP $0.012, start $0.005
net-empregos-ptnet-empregos-pt-scraperPT128SERP $0.003, detail $0.005, start $0.001
nhs-jobsnhs-jobs-scraperUK128SERP $0.003, detail $0.005, start $0.001
nicejob-denicejob-de-scraperDE512start $0.0001, item $0.003
nofluffjobs-eunofluffjobs-eu-scraperPL128SERP $0.003, detail $0.005, company $0.002, start $0.001
ostjob-chostjob-ch-scraperCH512start $0.0001, item $0.003
praca-gov-plpraca-gov-pl-scraperPL128SERP $0.003, detail $0.005, start $0.001
prace-czprace-cz-scraperCZ128SERP $0.003, detail $0.005, start $0.001
pracuj-plpracuj-pl-scraperPL128SERP $0.003, detail $0.005, start $0.001
profesia-skprofesia-sk-scraperSK128SERP $0.003, detail $0.005, start $0.001
profession-huprofession-hu-scraperHU128SERP $0.003, detail $0.005, start $0.001
prospects-ukprospects-uk-scraperUK128SERP $0.003, detail $0.005, company $0.005, start $0.001
reed-ukreed-uk-scraperUK128SERP $0.003, detail $0.005, start $0.001
remoteokremoteok-scraperREMOTE128start $0.001, item $0.003
remotiveremotive-scraperREMOTE128start $0.001, item $0.003
rigzone-usrigzone-us-scraperUS128SERP $0.003, detail $0.005, company $0.005, start $0.001
schaffu-chschaffu-ch-scraperCH512start $0.0001, item $0.003
seekseek-scraperAU128start $0.001, item $0.0025
sepe-essepe-es-scraperES128SERP $0.003, detail $0.005, start $0.001
snagajob-ussnagajob-us-scraperUS2048SERP $0.012, detail $0.015, company $0.008, start $0.005
sozjobs-chsozjobs-ch-scraperCH256start $0.0001, item $0.003
stellenanzeigen-destellenanzeigen-de-scraperDE512SERP $0.0025, detail $0.0075, start $0.005
stellenmarkt-destellenmarkt-de-scraperDE128SERP $0.003, detail $0.005, start $0.001
stepstone-atstepstone-at-scraperAT512SERP $0.0037, detail $0.008, start $0.001
stepstone-destepstone-de-scraperDE128start $0.001
subito-it-jobssubito-it-jobs-scraperIT128SERP $0.003, company $0.005, start $0.001, phone-reveal-result $0.001
swissdevjobs-chswissdevjobs-ch-scraperCH128start $0.0001, item $0.003
targetjobs-uktargetjobs-uk-scraperUK128SERP $0.003, detail $0.005, company $0.005, start $0.001
teaching-vacancies-ukteaching-vacancies-uk-scraperUK128SERP $0.003, detail $0.005, company $0.005, start $0.001
tes-uktes-uk-scraperUK128SERP $0.004, detail $0.006, company $0.005, start $0.001
totaljobs-uktotaljobs-scraperUK128SERP $0.003, detail $0.005, start $0.001
trudvsem-rutrudvsem-ru-scraperRU128SERP $0.003, detail $0.005, start $0.001
usajobs-ususajobs-federal-scraperUS128SERP $0.003, detail $0.005, start $0.001
vdab-bevdab-be-scraperBE128SERP $0.003, detail $0.005, start $0.001
wayup-uswayup-us-scraperUS128detail $0.005, start $0.001
web3careerweb3career-scraperREMOTE256start $0.001, item $0.003
website-job-extractorwebsite-job-extractorANY256job-result $0.004, company-enriched $0.01, start $0.0001, job-result-managed $0.012
website-job-extractor-browserwebsite-job-extractor-browserANY2048browser-job-result $0.008, browser-company-enriched $0.05, browser-page-processed $0.005, start $0.0001, browser-job-result-managed $0.024
welcome-to-the-junglewelcome-to-the-jungle-scraperFR128SERP $0.001, detail $0.001, start $0.001
wellfound-uswellfound-jobs-scraperUS128SERP $0.006, detail $0.01, company $0.005, start $0.001
werk-nlwerk-nl-scraperNL128SERP $0.003, detail $0.005, start $0.001
westjob-atwestjob-at-scraperAT512start $0.0001, item $0.003
weworkremotelyweworkremotely-scraperREMOTE128item $0.003, start $0.001
work-uawork-ua-scraperUA2048SERP $0.003, detail $0.005, company $0.005, start $0.001
workatastartup-usworkatastartup-scraperUS128SERP $0.003, detail $0.005, company $0.005, start $0.001
ycombinatorycombinator-scraperINTL128hn-item-result $0.0005, start $0.001
zentraljob-chzentraljob-ch-scraperCH512start $0.0001, item $0.003

Regenerate this table with python3 actors/jobs/job-feed/scripts/gen_board_table.py.

RAM and parallelism

Your Apify account has one memory limit shared by every run on it, not just those job-feed starts, so job-feed budgets sub-actor RAM instead of ignoring it. It first tries to detect the memory actually available (GET /v2/users/me/limits, then GET /v2/users/me). If you set maxTotalMemoryMbytes, the lower of your value and the detected figure wins. If detection is not possible it uses a flat 4096 MB, which fits every plan including Free. job-feed itself adds 256 MB on top.

Each selected board requests the RAM shown in the table above. Boards that fit start immediately, the rest wait, and as RAM frees up the largest waiting request that fits is admitted first so big browser-tier boards are not starved behind small ones. A board whose own need exceeds the whole budget fails fast with insufficient memory budget.

Coming soon

Built and launching shortly: none right now. Every healthy registry platform is already public and selectable (113 of 113).

The job-platform-actors session ships an average of 5+ new platforms per week. Highest-priority job platforms currently in the build queue (auto-generated from docs/build-queue/pipeline.json, top 5 by score, deduped against the registry, corporate single-employer pages excluded -- those are covered by career-site-jobs):

RankPlatformCountryHostAnti-botNotes
1simplyhired-us🇺🇸 USsimplyhired.com3.0Indeed-owned aggregator
2teamblind🇺🇸 USteamblind.com1.5Anonymous tech community + jobs
3kaiser-permanente-jobs🇺🇸 USjobs.kp.org3.0
4boss-zhipin-cn🏳️ CNzhipin.com3.0
551job-cn🏳️ CN51job.com3.0

719 more jobs-platforms queued behind these (76 require Camoufox / browser-tier). Regenerate this table with python actors/jobs/job-feed/scripts/gen_coming_soon.py.

Limitations

  • Some boards sort by relevance only. Without a native date sort, sortBy: "newest" is a best-effort freshness filter over relevance-ordered results.
  • remoteco is parked. remote.co hides employer data behind a paywall, so it is not selectable (see Coming soon).
  • wellfound-us costs more per result ($0.006 per listing, plus $0.010 with details): every request uses an anti-bot proxy.
  • URL-only platforms need a URL. career-site-jobs, website-job-extractor, and website-job-extractor-browser do nothing without companyUrls, careerSites, or searchUrls. If your whole selection is made of them and none is supplied (for example includeCareerSites: true alone), the run is rejected at input validation: exit code 2, statusMessage starting "FAILED: ", nothing dispatched or billed.
  • The browser retry has no guaranteed win. It re-runs a zero-job URL through an extractor roughly 3-5x more expensive. It can still return zero jobs (login wall, geofence). See Company career pages.

Input reference

The input schema carries a tooltip for every field. This section covers defaults and the gotchas.

Platform selection

  • countries[] - ISO codes with a preset (see Country presets). Activates each country's Tier 1 boards.
  • platforms[] - hand-picked board IDs. Additive to countries. Tier 2 boards run only when named here.
  • includeGlobalBoards - default true, see Country presets.
  • keywords[] - search terms, each forwarded to every selected board. Results are deduplicated across keywords and boards, and matched_query on each row records which keyword found it. Aliases searchQueries, search_queries, queries fill keywords when it is empty.
  • location - city, region, or state, forwarded in whatever form each board accepts. See Location and language.
  • radiusKm - search radius, default 25, max 200, clamped to each board's own maximum.
  • enforceRadius - default true. Active only when location is set and radiusKm > 0. Some boards ignore the radius (linkedin, jobscout24-ch, karriere-at and others), so job-feed drops rows clearly farther than radiusKm (plus a 10% or 3 km margin) from the geocoded location, before any charge or onlyNewJobs bookkeeping, using the row's coordinates or its location.city. Rows are KEPT when city or coordinates are missing or unknown to the CH/DE/AT/LI place table, the row's country is outside those four, the location is country-level (Schweiz), or the job is remote. A canton or Land (Vaud, Steiermark) is dropped only when its whole area lies beyond the radius. If the search location cannot be resolved or is outside CH/DE/AT/LI, the filter is skipped. run_summary.radius_filter reports {applied, reason, center, radius_km, dropped, kept_unknown}.
  • sortBy - "newest" (default, best for scheduled scrapes), "relevance", "salary", or "" (forward nothing, board default order). A board that does not know the value ignores it.
  • titleFilter - comma-separated substrings, case-insensitive OR match on title, for example "sales manager, account executive". Matching is by substring inside each word, so labor also finds Chemielaborant; the words Collaborator, Collaborateur and Kollaboration are excluded for terms shorter than collabor/kollabor. Ten boards filter natively before detail fetch at no extra cost (arbeitsagentur-de, stepstone-de, france-travail, pracuj-pl, indeed, linkedin, sepe-es, prospects-uk, milkround-uk, nationale-vacaturebank-nl); the filter is forwarded to them, and every row from every board is then checked against the same rule here after fetch, counted as dropped_title_filter (the part from native boards also as dropped_title_filter_native_board).
  • postedWithinDays - fractional days (0 = off, 0.5 = 12 hours). Boards with a native age filter get it forwarded (rounded up to their unit), for the rest rows outside the window are dropped after normalization, counted as dropped_stale.
  • searchUrls[] - direct search URLs for advanced use, routed to the board or ATS that matches the URL.
  • jobUrls[] - single job posting URLs for an alive check plus full data, see Direct job URLs.

Direct job URLs (jobUrls)

Paste single job posting URLs (max 500) to check whether they are still online and to get the full job data. Each URL is routed by host to a board that takes detail URLs; one sub-actor run per board.

{ "jobUrls": ["https://www.seek.com.au/job/91878509", "https://www.idealist.org/en/nonprofit-job/abc123-job-title-org-name-city"] }
  • Boards supported now: seek (incl. JobStreet and JobsDB) and idealist. Other URLs are not run and not charged; they are listed in run_summary.job_urls.unsupported with no_detail_support, unknown_host or invalid_url. If nothing can be routed the run stops with exit code 1 before any charge.
  • jobUrls alone runs no keyword search. Combined with countries, platforms or searchUrls it adds the lookups to that run.
  • Output: normal job rows, each with job_status (open, expired, unknown) and input_refs of {source: "jobUrls", field: "jobUrls", index, url}. URLs that return no row get a plain HTTP check (404/410 or a redirect to a listing page = expired), reported in run_summary.job_urls.status_only, never as a row.
  • Cost: the normal sub-actor rows, nothing extra.

Result caps

Three caps, applied from the inside out:

  1. maxResultsPerQueryPerPlatform - raw rows per keyword per board. Alias maxResultsPerQuery. When above 0 it wins over cap 2.
  2. maxResultsPerPlatform - raw rows per board across all keywords, before dedup. Alias maxResults. With several keywords and cap 1 at 0, each keyword gets ceil(cap / keywords) (3 keywords against 10 gives 4 each, so up to 12 raw rows). 0 = each board's own default.
  3. maxResultsTotal - unique jobs delivered after dedup, across all boards. Once reached, job-feed stops accepting and billing further jobs. Sub-actor calls already running still finish, but their extra rows are dropped and never billed by job-feed. 0 = unlimited.

To avoid paying for rows it would then discard, job-feed also clamps the per-query cap sent to each board to max(1, floor(maxResultsTotal / (boards * keywords))). If a run mixes boards with companyUrls, the boards see half of maxResultsTotal for this split.

On a companyUrls or careerSites run, maxResultsPerPlatform is a per-extractor budget split across its companies with max(1, floor(cap / companies)). Six rows over 2 companies gives 3 each (forwarded as maxJobsPerCompany to career-site-jobs and maxResultsPerCompany to the extractors). With more companies than the cap, every company still gets 1, so the total can run slightly over budget in that one case. Rows beyond a company's share are trimmed, never billed by job-feed, and counted as dropped_over_cap (see Company career pages).

Content options

  • includeJobDetails - fetch the full description, salary, and requirements from each detail page. Some boards charge more per detail than per SERP row. Off by default. With companyUrls or careerSites it also reads each posting's employer page, not only board detail pages (job-detail-result event per posting, once priced).
  • detailCountries - ISO-2 codes. With includeJobDetails, career-site-jobs reads detail pages only for rows of those countries; other rows are still returned and charged. Invalid entries are ignored with a warning.
  • maxResultsPerCompany - per career site: -1 = every job (career-site-jobs: up to 5000 per company), a positive number caps each company, 0 keeps the current behaviour (500 per company when no cap is set).
  • includeContactData - fill contacts[] (recruiter name, email, phone) where the source has them. Implies includeJobDetails. About 28 boards expose contacts.
  • includeCompanyDetails - fetch company profiles (about, industry, size_range, social_urls, active_jobs, and more). Implies includeJobDetails. About 28 boards support it.
  • includeCompaniesWithoutJobs - default true, adds free zero-job company rows, see Company career pages.
  • minRelevanceScore - 0-100, default 0 (keep all). Each keyword is scored as the share of its tokens found in the title (case and umlaut folded), and a row scores as its best keyword. 50 drops rows where fewer than half of the best keyword's tokens match, 100 keeps only rows where every token of some keyword matches.
  • excludeAgencies - default false. Drops rows whose company.is_agency is true (staffing, executive search, job aggregators in the DACH agency registry at high or medium confidence, or a name with Zeitarbeit, Personalvermittlung, Personalberatung and similar) before they are charged or marked seen by onlyNewJobs. licence_only firms are never dropped. The count is agencies_excluded in run_summary.
  • includeReportRowsInDataset - default false, see Terminal items in the OUTPUT record.

Company career pages

Three sub-actors can read a company career page:

ActorReadsBrowser or LLMRoughly
career-site-jobsPages on a known job system (Greenhouse, Workday, Lever, Workable, and 35+ more)No$0.001/job
website-job-extractorAny career page over plain HTTPLLM, managed by default$0.012/job managed, plus $0.01/company
website-job-extractor-browserAny career page, including JS-only appsReal browser and LLMroughly 3-5x the HTTP rate

career-site-jobs is always cheapest when it applies. Inputs:

  • companyUrls[] - one career-page URL per company. A URL matching a known ATS fingerprint goes to career-site-jobs directly (regardless of platforms, no API key needed), anything else to the extractor. Via the API an item may also be an object {"url": "...", "country": "DE", "company": "..."} (country and company optional; country works like on careerSites).
  • careerSites[] - companies whose job system you already know, for example from next_run_input. Each entry is a plain URL (auto-detected) or {"url": "...", "ats": "...", "company": "..."}; optional website, employer (shared group boards such as vitrea-gesundheit.onlyfy.jobs) and country (ISO alpha-2 default for rows without a location). When you set countries and an entry has no country of its own, job-feed sends your countries to career-site-jobs on every company query (one country as country, several as countries): rows of known other countries are dropped before they are charged, rows with an unresolved location are kept with country null. The same applies to companyUrls and directory-sourced companies. A per-URL country (on companyUrls objects or careerSites) also applies to rows read by the website extractor, including handover and fallback rows: rows whose location country is known and different are dropped before they are charged and counted in dropped_off_country; rows with an unknown country are kept. Your own location is forwarded unchanged. Supported ats values are in the input schema. Works on its own.
  • companyList - pick employers from the built-in company directory instead of listing URLs. Filters (all optional, AND across fields, OR within a list): regions (ch-de, de), industries, subIndustries, countries, headcount (1000+, 250-1000, 50-250, <50, unknown), plus limit (default 50, maximum 300, taken in alphabetical order of domain). An empty object, or one that matches nothing, is ignored with a warning when other inputs select platforms; on its own it ends the run before anything is dispatched or billed (Invalid input: companyList ..., exit code 1). Rows from these companies carry input_refs entries {source: "companyList", field: "companyList", domain, index} (index is the position in the expanded list). Companies on a job system career-site-jobs reads go there, the rest go to the extractor like companyUrls; companies you already list yourself are skipped. The directory is an embedded snapshot (dated in run_summary.company_list.snapshot_date, currently 2026-10-21) that refreshes with each build. See Company lists.
  • includeCareerSites - default false. Turns on career-site-jobs without a matching URL. It has no keyword search: with no companyUrls, careerSites or ATS-matching searchUrls the run is rejected, see Limitations.
  • includeJobDetails (alias includeFullPostingText), maxFullTextChars (default 12000, 500 to 50000), fullTextLlmFill - for pages read by the extractor, each posting's detail page is read (text incl. PDFs in description_md, deadline, salary, contact, workload, languages). fullTextLlmFill: false skips the LLM fill call.

keywords, location, postedWithinDays and sortBy are forwarded to all three and applied server-side before the per-company cap and before charging. When a company's jobs legitimately do not match, its per_platform[] entry carries a company_notes[] line and it is not retried in the browser.

Zero-job company rows. A company read successfully with no open roles (or none matching) is one dataset row with _type: "company_no_matching_jobs": the company (name, domain, url, career_page_url, ats_detected), jobs_on_site (a number only when the site-wide total is known, else null), jobs_matching: 0, a reason (no_jobs_found, filtered_out_by_filters, no_location_match_on_site), a note, the filters and scraped_at. Never charged, not counted in unique_jobs. Turn includeCompaniesWithoutJobs off for job rows only, or filter on _type.

Picking HTTP or browser. For URLs matching no known ATS, naming only website-job-extractor in platforms[] means HTTP only (a JS-rendered page returns a zero-row reason); only website-job-extractor-browser sends every such URL to a real browser; both or neither (default) means HTTP first, then a scoped browser retry for companies flagged JS-rendered or with an unexplained zero. Companies that returned jobs or whose zero your filters explain are never re-sent. The legacy enableBrowserFallback: true forces the default mode. The retry gets max(subactorTimeoutSecs, 1800) seconds (capped by the run's remaining time minus 90 s); on timeout rows already pushed are kept as TIMED-OUT, and the zero-row reason is replaced by the retry's outcome.

Company lists

companyList reads every matching employer of the built-in directory (about 1,100 Swiss and German employers), so you need no career URLs. Use it for sector sweeps.

{ "companyList": { "regions": ["ch-de"], "industries": ["biotech"], "limit": 50 }, "maxResultsPerCompany": 20 }

Output: normal job rows with an input_refs entry {source: "companyList", domain, index}, and run_summary.company_list = {matched, used, capped_at, snapshot_date}. Cost: the lookup is free, each company costs the normal sub-actor price.

Details: limit default 50, maximum 300; a filter matching nothing is ignored with a warning, or ends the run before billing if it is the only input; the snapshot refreshes with each build.

Handing a job system to the next run. Rows read through a known ATS carry ats_system and ats_url. The OUTPUT record's next_run_input moves each company whose job system was found from companyUrls to a careerSites entry, for example {"url": "https://kenvue.wd1.myworkdayjobs.com/en-US/kenvue_careers", "ats": "workday", "company": "kenvue.com", "website": "https://www.kenvue.com/careers"}. It carries only {companyUrls, careerSites}: merge them into your input and keep your own limits, otherwise each site is read in full. Example, Chewy (264 roles): about $3.20 via the managed extractor, then about $0.27 per run via career-site-jobs.

When career-site-jobs cannot read a company. If it ends unreadable (invalid_query, board_not_found, unsupported_platform, blocked, parse_error, connection_error, or a failed sub-run), job-feed falls back to the extractor in the same run; those rows are deduplicated, capped and billed like any extractor row. run_summary.csj_fallback reports {attempted, recovered, rows, companies, skipped_reason}. Turn off with csjFallbackToExtractor: false. The reverse also runs: when the extractor recognises a supported job system and finds 0 jobs, job-feed sends the company to career-site-jobs once (run_summary.wje_handover; turn off with sameRunHandover: false). A careerSites entry failing for a structural reason moves back to next_run_input.companyUrls (listed in handover_demoted); temporary failures keep it. With handoverSharedBoards on (default), shared group boards are handed over with the employer found. A bare vendor root names no company (job-boards.greenhouse.io/, career2.successfactors.eu, app.jazz.co, myworkdayjobs.com, a Prospective careercenter without its id): it is never sent to career-site-jobs, so it is not charged, and it is listed in companies_not_read as vendor_root with the reason. If the careerSites entry carries the company's website (or a domain in company), that site is read by the extractor instead. A careerSites entry without a url is ignored with a warning; demo tenants are never handed over. If a sub-actor reports a different job-system list version, run_summary.manifest_mismatch lists it. Any other unexplained zero (for example js_rendered_no_browser) is listed in run_summary.companies_not_read, so no requested company disappears silently.

Managed LLM key

Without your own openrouterApiKey, job-feed forwards no key into website-job-extractor runs. It tags the run with NanoScrape's managed key profile, and the extractor resolves the key server-side and bills at its own managed rate, see that actor's README for current prices and caps. Supplying openrouterApiKey (or the geminiApiKey / groqApiKey fallbacks) switches the run to standard billing on your own account.

Location and language

location is free text and most boards accept it as-is. Boards with a closed list, or no location field, are resolved by job-feed when possible:

  • swissdevjobs-ch takes only its own city names ("Zurich", not "Zürich"); job-feed matches case- and accent-insensitively. arbeit-swiss needs a 2-letter canton code, resolved from any canton or main city. sozjobs-ch (7 German-Swiss macro-regions, French and Italian cantons intentionally no match), alpha-topjobs-ch (6 JobCloud region IDs, Ticino none) and lavoro-cdt-ch (Ticino only: Lugano, Mendrisio, Locarno, Chiasso) use closed lists resolved via the canton. healthecareers-us needs a full US state name (abbreviations and "City, ST" are resolved), craigslist-jobs regional site slugs for major metros. jobs-ch, ictjobs-ch and the three company-site actors take free text. ge-ch-emploi has no location field and lists only Geneva roles.

When a mapping fails, or the board has no location field, job-feed logs a warning and post-filters that board's rows instead: rows whose own location text does not contain your location (or have none) are dropped, never billed, and counted as dropped_off_location. Rows flagged remote are kept when they name no country or a matching one. This only triggers when you set location, never on top of a successful native filter. The three company-site actors are always post-filtered as a safety net.

language on each row is inferred. Swiss listings use a canton heuristic (Geneva, Vaud, Fribourg, Neuchatel, Jura, Valais to fr, Ticino to it, else de); otherwise a country-to-language table (DE/AT to de, FR to fr, US/GB/CA/AU/IE/IN/PK to en, and about 20 more); otherwise a conservative stopword guess over title and description. With no signal it is "", never a default "de".

Dedup mode

  • streaming (default) - rows are pushed as each board finishes. The first board to report a job_id owns the row, later reports go into sources_manifest under additional_sources_by_job_id. sources[] has length 1. Best for real-time use, fastest first result, and aborts (you pay only for rows already delivered).
  • batch - rows are buffered until all boards finish, then emitted once per job_id with every reporting board in sources[]. No partial delivery if the run is killed, slower first result, more memory.

Billing is identical: one event per unique job_id. See How job IDs and dedup work.

Only new jobs

Off by default. Set onlyNewJobs: true on a scheduled run and job-feed skips jobs an earlier run already delivered: they are not pushed and not charged the orchestrator-unique-job fee. Delivered job_ids are kept in a named key-value store on your own account (seenJobsStoreName, default job-feed-seen-<hash of your search input>) and forgotten after seenJobsTtlDays (default 30, max 180). The first run delivers everything. run_summary.only_new shows seen_skipped, new_pushed, store_size and ttl_days.

You still pay the sub-actor fees for repeats, because the platforms run before job-feed knows a job is old. postedWithinDays stays your main cost control.

Job lifecycle across runs

trackJobLifecycle: true on a scheduled run with a fixed input tells you how long each job has been online and which jobs closed since the last run.

{ "countries": ["CH"], "keywords": ["data engineer"], "trackJobLifecycle": true, "seenJobsStoreName": "my-ch-data-jobs" }

Output: each row gets lifecycle: {first_seen_at, last_seen_at, days_online, runs_seen}; the OUTPUT record gets closed_jobs (job_id, title, company, url, first_seen_at, last_seen_at, days_online). Cost: free, only small KV storage on your account.

Details:

  • Closed jobs are never pushed or charged; at most 500 are listed (closed_jobs_truncated for the rest).
  • Closed means "not seen in a complete, uncapped run of the same search". Closed detection runs only if the search settings (including postedWithinDays) equal the last tracked run's. It is checked per platform: jobs of a platform that failed, was partial or hit maxResultsTotal, maxResultsPerQueryPerPlatform or maxResultsPerPlatform are never closed. When no platform was covered completely, run_summary.lifecycle.closed_detection is skipped_incomplete and closed_count is 0; closed_detection_sources_skipped lists the platforms left out. A platform with no cap set by you is judged against the sub-actor's own default result limit, and counts as incomplete if that limit is unknown. Jobs stored by builds before 0.9.148 are never closed by absence. Builds 0.9.144 to 0.9.147 could list still-open jobs as closed on capped runs.
  • History sits in the onlyNewJobs store (LIFECYCLE-* records); unseen entries drop after 120 days, newest 10,000 jobs kept. Deleting the store resets it (and the onlyNewJobs list).

Company hiring summary

Free, off by default. Set companySummary: true to get one summary per company from the jobs of this run: open roles, boards, top countries and cities, roles by function and seniority, newest and oldest posting, median days since posted. It is written to the key-value store only (no dataset rows, no extra charge): the top 200 companies are in the OUTPUT record under company_summary, up to 5,000 in the COMPANY_SUMMARY record. Add companySummaryStoreName to keep a history and get trend per company. With onlyNewJobs, open_roles counts only this run's delivered rows.

{ "countries": ["CH"], "keywords": ["QA"], "companySummary": true, "companySummaryStoreName": "my-hiring-trends" }

Company hiring signals (outputMode companies)

Set outputMode to companies to get one company_hiring row per companyUrls entry instead of job rows. It answers "is this company hiring, and for what?" for a list of websites.

{
"companyUrls": [
{"url": "https://www.example.com", "company": "Example GmbH", "country": "DE", "ref": "ChIJN1t_tDeuEmsRUsoyG83frY4"}
],
"outputMode": "companies",
"maxResultsPerCompany": 5,
"includeGlobalBoards": false
}

ref is an optional string (up to 200 characters) on a companyUrls object. It is echoed unchanged as ref on the company row, as company.ref on every job row of that URL (jobs mode too), and in input_refs[].ref, including rows found through a same-run handover, the career-site-jobs to extractor fallback and the browser retry.

FieldMeaning
refThe ref you sent (null if none).
company, websiteAs sent.
hiring_checkedfalse only for not_read.
hiring_statushiring, no_open_roles or not_read.
hiring_open_roles_countThe site-wide number of open roles when the reader reports one for this request, else the distinct roles read. null for not_read.
hiring_open_roles_count_is_cappedtrue only when no site total is known and the roles read equal maxResultsPerCompany.
hiring_rolesUp to maxResultsPerCompany titles.
hiring_roles_detailSame order and length as hiring_roles: {title, location_raw, city, postal_code, country} (null for unknown parts). [] unless hiring.
hiring_evidencehiring: ats_board, careers_page or job_board (in that precedence). no_open_roles: ats_zero, careers_page_empty or no_careers_page. null for not_read.
hiring_sourcesATS names (greenhouse, lever, workday, ...) or website.
hiring_jobs_urlThe careers page or ATS board, else null.
hiring_checked_atISO 8601 time.
not_read_reasonWhy the company was not read, else null. A repeated URL keeps its first entry; the repeat gets not_read with duplicate_url.

How the mode behaves:

  • Only companyUrls are read. countries, platforms, searchUrls, jobUrls, careerSites, companyList, includeCareerSites and the job boards are ignored with a warning. Use the per-URL country to keep only roles in that country; it is applied before any charge.
  • includeCompaniesWithoutJobs is implied. No job rows and no company_no_matching_jobs rows are pushed. onlyNewJobs and trackJobLifecycle are ignored with a warning. companySummary still works.
  • Billing is unchanged: the orchestrator-unique-job event is charged once per unique role read (at most maxResultsPerCompany per company), after the company row has landed. The company_hiring row itself is free. run_summary.companies_mode reports companies, hiring, no_open_roles, not_read and roles_charged, which equals the events charged. Sub-actor fees are billed as usual.
  • hiringBrowserFallback (default false) decides whether career sites that need a browser (JS-rendered) are retried. A browser read costs about $0.18 per place versus $0.01-0.03 for normal reads, so by default those places get not_read with not_read_reason needs_browser, hiring_checked false and no charge. run_summary.companies_mode.needs_browser counts them (a subset of not_read). Set it to true to retry them in a browser. It is ignored in jobs mode.
  • A homepage that was fetched fine but has no careers, jobs or job-system link is no_open_roles with hiring_evidence no_careers_page (count 0, no reason). If the fetch failed or is unknown, it stays not_read (dns_error means the domain does not resolve). None of these is retried in a browser.
  • Websites on directories, social and profile sites (jameda.de, facebook.com, yelp.*, local.ch, ...) are not read and cost nothing: not_read with not_read_reason directory_profile_url. In jobs mode they are skipped with a warning.
  • not_read_reason charge_limit means the source was not started because your max charge was used up.
  • not_read_reason ats_handover_available means the site uses a job system we could not read in this run. When the same-run handover to the job-system reader succeeds, the company is read and gets hiring or no_open_roles instead.
  • In companies mode maxResultsPerCompany is at most 50 (also the number of titles in hiring_roles); higher values and -1 are lowered to 50 with a warning. If two entries resolve to the same board, both list the role and it is charged once. hiring_jobs_url falls back to the listing page the roles were read from.
  • A company with roles is pushed as soon as its sub-actor result is processed. Companies without roles are pushed when the run's fallback and browser passes are done, because those passes can still find roles.
  • With a country, keyword, location, posted-within or title filter active, the reader's site total would count roles you filtered out, so hiring_open_roles_count is then the roles read and hiring_open_roles_count_is_capped tells whether the cap was hit.

Advanced concurrency

  • maxParallelPlatforms - sub-actor runs kept in flight at once (default 8, 1-32). Apify's concurrent-run limit is account-wide, so job-feed lowers this cap to fit it (a free account runs 3 at a time). A 402 concurrent-runs-limit-exceeded on start is retried with backoff.
  • perHostConcurrency - concurrent runs sharing a host (default 2, max 6).
  • globalConcurrency - legacy in-flight cap (default 0, max 16); the lower of it and maxParallelPlatforms wins.
  • maxTotalMemoryMbytes - sub-actor RAM budget, 0 = 4096 MB. See RAM and parallelism.
  • subactorTimeoutSecs - per-board timeout (default 600, 60-3600). The scoped browser retry has its own longer budget.

Partial failure and statusMessage

statusMessage summarizes the run ("14/14 platforms succeeded", failed boards, the dedup summary, a reason when a healthy board returned nothing, "+ 2 companies with no matching jobs", any dropped_over_cap). Partial failure still ends SUCCEEDED with every job the working boards returned.

A board job-feed stops waiting on is gracefully aborted and its pushed rows are kept through normal dedup and billing. Its per_platform[] entry reports status: "TIMED-OUT" with the salvaged raw_results and a reason; it counts as neither succeeded nor failed. Near the run timeout, boards that cannot usefully start are skipped (SKIPPED_DEADLINE) and running boards are stopped shortly before the deadline (run_summary.stopped_for_deadline, deadline_stopped_boards), see Run timeout.

Two cases end FAILED with statusMessage starting "FAILED: ": exit code 2, selection rejected at validation (only URL-only platforms and no URL), nothing billed; exit code 1, every dispatched platform failed and nothing was delivered. A board that only finished through a salvaged TIMED-OUT run does not count as succeeded, but if it delivered and billed a real row the run is not failed on that basis.

LLM keys

All optional, used only when a URL is routed to website-job-extractor or its browser variant. Leave them blank for managed mode (see Managed LLM key). The forced primary model is deepseek/deepseek-chat-v3-0324.

  • openrouterApiKey - primary key, standard BYOK billing on your account.
  • geminiApiKey - cross-provider fallback, used only if the whole OpenRouter side fails.
  • groqApiKey - second fallback (Llama-3.3-70b), after Gemini.

Output reference

Key fields only. The dataset schema (Output tab in the Console) lists every field, and coverage varies per board. Rough coverage: salary about 70 boards, description_text about 60, expires_at about 45, requirements about 30, experience_level about 25, skills about 20, contacts about 28, workload_pct CH and SE boards, and technologies only about 6 (swissdevjobs-ch, workatastartup-us, nofluffjobs-eu, justjoinit, welcome-to-the-jungle). location.postal_code appears only where the board emits one.

Job-level fields

FieldTypeDescription
job_idstringFirst 16 hex chars of SHA-256 over folded title, company, city. Stable across runs and boards. Same-board collisions get an 8-hex suffix.
job_id_aliasesstring[]job_id plus the ids of rows merged into this posting when it was pushed. A merged posting keeps the first arriving row's id, so it can return under another job_id next run: check [job_id, ...job_id_aliases] against what you have seen. Rows merged later are in sources_manifest.additional_job_id_aliases_by_job_id.
titlestringJob title, HTML stripped.
companyobjectSee below.
locationobjectraw, city, region, country (ISO alpha-2, UK always GB), remote (onsite, hybrid, remote), postal_code.
employment_typestringClosed enum: permanent, temporary, contract, internship, apprenticeship, or "". A raw board value that maps to none of these becomes "".
workload_pctobject{min, max} as 0-100, for CH and SE boards.
salaryobject{min, max, currency, period, text, source}, never currency-converted. source is stated or hidden_field (a band for an ad that hides its salary).
posted_date, expires_atstringISO8601 posting date and application deadline.
job_statusstringCanonical posting status: open, expired or unknown (when the board reports none). Mapped from the board's job_status, is_active, expired or is_expired.
job_url_statusstringCareer-site-jobs only: advertisement_off_apply_url means job_url is the apply page; portal_frontend_off means it does not open.
description_text, description_html, description_mdstringFull description in three forms. Needs includeJobDetails on most boards.
requirements, skills, technologiesstring[]Structured lists where the board provides them.
department, experience_level, education_level, industrystringOrganizational and seniority metadata, board-dependent.
apply_urlstringApplication URL.
contactsobject[]Recruiter contacts (first_name, last_name, email, phone, position, raw, source_platform), needs includeContactData. source_platform is the board a contact came from and survives merging of duplicate rows.
matched_querystringThe keyword that surfaced the row. If a board decorates it (accountant in Dubai), job-feed rewrites it back to your plain keyword.
relevance_score, relevance_matched_tokensfloat, string[]Share of keyword tokens found in the title (0.0-1.0, 1.0 when no keywords were given), and which ones.
sourcesobject[]Origin list (platform, url, source_id, first_seen, ...). One entry in streaming mode, all in batch.
first_seen, last_seen, content_hashstringRun timestamps and a hash over mutable fields for cross-run change detection.
lifecycleobjectOnly with trackJobLifecycle: first_seen_at, last_seen_at, days_online, runs_seen across runs. See Job lifecycle across runs.
languagestringInferred listing language, "" when unknown.
_platformstringThe board that first reported the row.
application_deadline, reference_number, is_easy_apply, street_address, working_hours, languagesmixedDetail fields from the website extractor with includeJobDetails. Omitted when not reported.
updated_at, start_date, end_date, starts_immediately, work_forms, locations, hours_per_weekmixedSource dates and work details when a board reports them. updated_at is the source's refresh date (jobbasel-ch), posted_date stays the first publication. start_date, end_date (ISO), starts_immediately and work_forms come from arbeit-swiss; an empty work_forms means none, absent means not reported. locations lists every city of a multi-city ad (stepstone-de, stellenanzeigen-de). hours_per_week is {min, max} (stellenanzeigen-de); the text as written is in working_hours. Omitted when not reported.
hiring_organizationstringThe employing entity when it differs from the board's company (for example company Johnson & Johnson, hiring_organization Cilag AG), as reported by career-site-jobs or another sub-actor. Omitted when empty. On a shared group board each row keeps its own company.name; the group entry you gave stays in input_refs.
input_refsobject[]Only on rows that came from a careerSites or companyUrls entry: which entries, as {source, index, url, company, employer} (source is careerSites or companyUrls, index is the 0-based position in that input array, url, company and employer are your values verbatim, null when not given). An array because the same job found through two entries lists both. Use this, not company.name, to attribute rows to your input: company.name is the employer's display name (detected names replace your label, boards shared by several brands carry the brand). Omitted on board and keyword rows.
ats_system, ats_url, ats_tenant, career_page_urlstringJob-system provenance (for example workday, greenhouse, phenom), empty on board rows. ats_tenant and career_page_url come from the extractors only, and ats_url is a best-effort match for career-site-jobs. Distinct from company.ats_detected, a best-effort guess made for every board. See Company career pages.

Company sub-fields

FieldTypeDescription
namestringCompany name. On career pages: your careerSites label, else the page's declared name, else a cleaned brand from the domain (new.abb.com becomes ABB). Every row of one company, including its zero-job row, uses the same name, and company.domain keeps the real domain.
url, domain, logo_urlstringOwn website, registrable domain, logo.
about, industry, size_rangestringProfile text, sector, headcount bucket.
employee_count_from, employee_count_tointegerHeadcount range bounds, to null if open-ended.
rating, review_countfloat, integerEmployer rating (scale varies) and review count.
founding_yearintegerFour-digit year.
benefitsstring[]Perks list.
phone, email, headquartersstringCompany-level contact data.
addressobject{street, city, region, postcode, country}.
profile_url, career_page_urlstringBoard's company profile (first reporter only) and the company's own careers page.
ats_detectedstringATS recognized from apply_url.
social_urlsobjectLinks keyed by network.
active_jobs_count, active_jobsinteger, object[]Open-role count, and up to 20 {title, url} entries.
recruitersobject[]Recruiter roster (dice only): name, title, email, profile_url.
is_agencybooleanAlways present. True when the company is in the DACH agency registry with high or medium confidence, or its name has an agency keyword (Zeitarbeit, Personalvermittlung, Personalberatung, Staffing, Recruit).
agency_typestring, nullstaffing, executive_search, rpo, job_aggregator, engineering_services or placeholder; null when unknown.
agency_confidencestring, nullhigh, medium, licence_only or keyword. licence_only firms only hold a staffing licence, often as a side business.
is_agency_signalbooleanTrue for any registry hit, including licence_only, or a keyword hit. Use it for scoring, not hiding.
platform_extrasobjectPassthrough of board-specific company_* fields.

Terminal items in the OUTPUT record

Every run produces four _type-tagged items: run_summary, cost_report, relevance_report, sources_manifest. By default they live only in the run's OUTPUT key-value-store record, next to a live-updated subruns[] manifest (one entry per board: actor, build, run_id, console_url, dataset_id, status, timing, memory, charged amount, charged_event_counts). If the run had companyUrls or careerSites, OUTPUT also carries next_run_input. Set includeReportRowsInDataset: true to also append the four items to the dataset; if you export to CSV or Excel, drop rows with a non-empty _type (or keep only rows that have a job_id), since they do not share the job schema.

  • run_summary - totals, dedup_summary, completeness, and per_platform[] with platform_id, status, raw_results, elapsed_ms, plus when relevant country (global-board sub-runs), reason, dropped_over_cap, and dropped_off_location, agencies_excluded when excludeAgencies is on, and radius_filter when enforceRadius ran. companies_no_openings lists companies whose career page was read and says there are no vacancies (they are not in companies_not_read). llm_failures (companies, calls, entries, and a rerun.companyUrls fragment to paste) lists companies where extraction calls failed, including partial failures (error_category "partial"); a company with 0 jobs and failed calls also stays in companies_not_read with reason llm_failed. Both need a website-job-extractor build that reports jobs_status. Also companies_not_read (with companies_not_read_count), the list of requested careerSites and companyUrls entries that gave no jobs because they could not be read; also companies_without_jobs, companies_partially_read (company, url, platform_id, reason, rows, input_refs) lists career sites that returned rows but were cut short by the time budget or a wind down, never also in companies_not_read; and on early stops stopped_for_deadline and deadline_stopped_boards. Company career-site rows now get location.country from the location text when it names one ("CH", "CHE Baden", "Schweiz - Burgdorf", "Brazil: Rio de Janeiro"); rows with no location stay empty and are counted in company_rows_without_location. Rows from a companyList entry use the directory name instead of a host-derived one.
  • cost_report - total_cost_usd and a per_platform[] breakdown, see Billing and cost model.
  • relevance_report - per-board on-topic rate, a hint to drop boards that return off-topic rows.
  • sources_manifest - cross-board origins for jobs seen on several boards (streaming mode). It also carries additional_job_id_aliases_by_job_id (job_id to the ids of rows merged after that row was pushed). In streaming mode it also carries additional_input_refs_by_job_id for a job already delivered that a second careerSites or companyUrls entry found later.

input_refs (same {source, index, url, company, employer} objects) also appears on every companies_not_read entry, no-match row, and csj_fallback.companies[] and wje_handover.companies[] entry. The OUTPUT record's next_run_input_refs ({companyUrls: [...], careerSites: [...]}) maps each item of next_run_input (same index) to the input entries it descends from; it is kept outside next_run_input so that fragment stays pasteable.

How job IDs and dedup work

job_id is the first 16 hex characters of SHA-256(foldText(title) | foldText(company) | foldText(city)). A job with no city uses region and country instead, so remote listings do not collide on an empty city. The same job at the same company in the same city hashes identically from any board, today or next month. Upsert on it.

foldText normalizes case, whitespace, umlauts, gender suffixes ((m/w/d), (f/m/x), (a), :in), trailing workload markers (", 80-100%"), and dash variants.

When matching the same job across boards, titles that differ only by gender tags anywhere in the title ((m/w/d), (all genders)), inclusive endings (*in, :in, /in, Innen, French (e)), pensum (100% against 80-100%), a job reference number, (unbefristet) or a trailing city are treated as one title, while two explicit pensum ranges that do not overlap (50% against 100%), Senior against Junior, and trainee or apprentice titles stay separate rows; job_id and the shown title are unchanged. Cross-board matches are folded into one row. Same-board postings with identical folded title, company, and city are never merged: every posting after the first gets an 8-hex suffix from its own source_id (b39d05cf0914e480 and b39d05cf0914e480-2e000388). Two distinct USAJOBS "Accountant (Staff)" postings at Fort Belvoir are a real case.

Every run's run_summary and statusMessage carry a line like

12 rows from boards → 11 unique jobs (1 merged: arbeit-swiss ×1)

The parenthetical lists boards that contributed merged rows, most first. On a companyUrls run it can also show trimmed rows: 29 rows from boards → 12 unique jobs (0 merged, 17 over cap).

Billing and cost model

job-feed bills one event, orchestrator-unique-job ($0.0005), per unique job delivered after dedup and maxResultsTotal. Rows dropped before delivery (caps, titleFilter, postedWithinDays, location post-filter, onlyNewJobs repeats, zero-job rows) are never billed by job-feed.

Every board job-feed calls is a separate actor with its own pay-per-event rates (SERP scans, detail fetches, contacts, LLM extraction), billed independently to the same Apify account at the same rate as running it standalone. When you set a max charge, it also limits the sub-actors job-feed starts for you: each gets a share of what is left, and a source that would get too little is skipped (run_summary.charge_budget_exhausted, subruns_skipped_for_budget). A sub-actor may bill its own events for rows job-feed later trims. On companyUrls and careerSites runs the sub-actor's company fee applies whenever the career site was read successfully, even with no open jobs.

The cost_report in the OUTPUT record separates what compute cost from what you were billed:

  • per_platform[].user_charged_usd - the sub-run's usageTotalUsd from Apify (compute cost, not the event bill, on the actor owner's own account), else events times list price capped at its max charge. Re-read 3 times, 10 s apart. charges_final: false means the budget ran out and the figure may be a slight undercount.
  • per_platform[].price_basis - "" normally, "free_tier" (tiered event priced at the conservative FREE tier) or "actor_fallback" (no pricing info). Either means a conservative estimate. estimated_cost_usd is a registry-based fallback; platform_compute_usd is the sub-run's CU cost for internal monitoring and not part of your bill.
  • orchestrator_charged_events, subactor_cost_total_usd, orchestrator_cost_usd (unique_jobs x $0.0005), total_cost_usd (their sum) and cost_per_unique_job_usd.
  • overlap_matrix / redundancy_hint - which boards return the same jobs, and which to drop without losing coverage.

Rate card

EventTriggerPrice
orchestrator-unique-jobEach unique job emitted after dedup and the optional maxResultsTotal cap$0.0005

Sub-actor events are billed by the sub-actors on top, see Supported boards and the worked examples in What it costs. A real DE-Munich Software Engineer run over 4 boards (25 SERP rows each, no detail fetch) gave 85 unique jobs, $0.478 sub-actor subtotal plus $0.042 orchestrator fee: $0.522 total, $0.006 per unique job.

Examples

All inputs are literal and can be pasted as is.

  • Single country: {"countries": ["CH"], "keywords": ["software engineer"]} fires 6 boards, the CH preset (jobs-ch, arbeit-swiss, jobscout24-ch) plus Indeed, Glassdoor and LinkedIn scoped to CH.
  • Several countries: {"countries": ["DE", "AT"], "keywords": ["software engineer"], "maxResultsPerPlatform": 5} fires 14 boards. The cap of 5 splits as [3, 2] across the two countries for the global boards (AT first), while ordinary boards get the full 5.
  • Preset plus a board: {"countries": ["DE"], "platforms": ["wellfound-us"], "keywords": ["backend engineer"], "includeGlobalBoards": false}. platforms is additive: 5 DE boards plus wellfound-us. Without includeGlobalBoards: false, 9 in total.
  • Several keywords: {"platforms": ["swissdevjobs-ch", "ycombinator"], "keywords": ["react", "engineer"]}. Each row keeps the keyword that surfaced it in matched_query.
  • Company career pages: {"companyUrls": ["https://boards.greenhouse.io/stripe", "https://jobs.lever.co/figma", "https://www.amazon.jobs/en/search?base_query=software+engineer"], "keywords": ["backend"]}. The first two match ATS fingerprints (career-site-jobs), the third goes to website-job-extractor in managed mode.
  • Location: {"countries": ["CH"], "keywords": ["engineer"], "location": "Zürich"}. jobs-ch takes the text, arbeit-swiss resolves canton ZH, jobscout24-ch its own region enum. A board that cannot honor it (for example ge-ch-emploi, Geneva only) is post-filtered: mismatches are dropped uncharged and reported as dropped_off_location.
  • Newest, last day: {"countries": ["UK"], "keywords": ["product manager"], "postedWithinDays": 1, "sortBy": "newest"}.
  • Empty input {} applies the code defaults countries: ["DE"], keywords: ["Software Engineer"], maxResultsPerQueryPerPlatform: 10, maxResultsTotal: 50 (Apify's automated QA runs {}, and a schema prefill is only a Console hint). That is 8 boards sharing 50 rows, each capped at 6.
  • Per-company cap: {"companyUrls": ["https://boards.greenhouse.io/stripe", "https://jobs.lever.co/figma"], "maxResultsPerPlatform": 6} gives floor(6/2)=3 per company. Rows over the cap are dropped, reported as dropped_over_cap and never billed by job-feed, though a sub-actor may have billed its own events for them.
  • Tight budget: {"countries": ["DE"], "keywords": ["software engineer"], "includeGlobalBoards": false, "maxResultsPerQueryPerPlatform": 5, "maxResultsTotal": 20, "maxTotalMemoryMbytes": 700, "maxParallelPlatforms": 2}. Boards that do not fit the RAM budget queue, largest fitting request first.

FAQ

Why fewer results than my cap? Caps are upper bounds. A board may have fewer matches, and dedup and maxResultsTotal trim further. A SUCCEEDED board with raw_results: 0 always carries a reason in its per_platform[] entry.

Why does my company career page return zero jobs? A URL matching a known ATS went to career-site-jobs (check its reason). Otherwise it went to website-job-extractor, and a JS-only page may need the browser variant, see Company career pages.

Does a partial failure fail the run? No. It ends SUCCEEDED. Only a total failure, or a selection of URL-only platforms with no URL, ends FAILED, see Partial failure and statusMessage.

How do I avoid duplicates across runs? Use onlyNewJobs: true (Only new jobs), or upsert on the stable job_id in your own storage.

Where are run_summary and the other reports? In the OUTPUT key-value-store record, unless includeReportRowsInDataset is on, see Terminal items in the OUTPUT record.

Does it work on the Free plan? Yes. Boards that do not fit the RAM budget queue instead of failing, see RAM and parallelism.

How long does a run take? Under a minute for a few lean boards, several minutes for a large preset. On timeout job-feed keeps collected rows, see Run timeout.

Troubleshooting

SymptomLikely cause and fix
FAILED, exit code 2, nothing dispatchedOnly URL-only platforms selected with no companyUrls, careerSites or searchUrls. Add a URL or select keyword boards too.
FAILED, exit code 1Every dispatched board failed. Read statusMessage for each reason.
career-site-jobs is SKIPPED_NO_INPUT but the run SUCCEEDEDMixed selection without a URL for it. Expected.
Fewer rows than expected from a presetincludeGlobalBoards off, or a board returned 0 that day. Check per_platform[].reason.
Extractor returned 0 on a custom pageJS-rendered (js_rendering_suspected). Add website-job-extractor-browser to platforms.
Two jobs share a job_id prefixSame-board collision, disambiguated by the -XXXXXXXX suffix. Both are real.
location seems ignored on a boardClosed location list or no field. Look for location does not match in the log.
titleFilter or postedWithinDays seems not to applyFiltered client-side on most boards. Check dropped_title_filter and dropped_stale.
A cap seems ignoredMisspelled field. Look for WARN input contains unrecognized keys.
user_charged_usd is 0 or low on a board that returned jobsApify charged-event lag. Check charges_final, re-fetch later; estimated_cost_usd is the fallback.
A board you expected is not in the presetIt is Tier 2 for that country. Add it via platforms[].

Regional boards

includeRegionalBoards (default off) adds the small tier-2 regional boards to a countries run. nearby adds the healthy boards whose canton contains your location (Basel gives Jobbasel, Zurich gives Jobzueri, Lugano gives Carriera and Lavoro CDT). all adds every healthy tier-2 board of the selected countries, including the profession boards. If the location cannot be found, or lies outside the selected countries, nothing is added and a warning is returned. The boards added are listed in run_summary.platform_selection.regional_boards_added. Boards you name in platforms are never run twice.

Registry

Place data for enforceRadius comes from GeoNames (CC BY 4.0): places of DE and AT with 500+ inhabitants and all populated places of CH and LI, about 940 KB in registry/places.json, plus canton and Land bounding boxes in registry/regions.json (see registry/NOTICE-geonames.txt).

registry/platforms.json is the single source of truth. Status values: healthy (in country defaults), degraded and unvalidated (only when named explicitly), parked (pulled, never runs), excluded (deliberately never selectable). A platform can also carry release: "coming_soon": built and routed, but its Apify actor is not public yet, so it is excluded from the platforms enum and country defaults.

To add or remove a board, run from actors/jobs/job-feed/ (then apify push): scripts/refresh_registry_from_apify.py (refreshes subactor_registry.json), scripts/sync_platform_enum.py (regenerates the platforms enum), scripts/gen_coming_soon.py and scripts/gen_board_table.py (regenerate Coming soon and Supported boards).

When a job row carries company.url or company.domain (emitted by roughly 15 platforms in SERP mode and most platforms in detail mode), you can feed those URLs directly into contact-extraction actors to enrich with decision-maker emails and phone numbers:

Run job-feed from code

job-feed is santamaria-automations/job-feed (actor ID zoDyIgISr7yFPyLJd). Runs can take minutes with several boards, so prefer start() plus a webhook over blocking.

curl -X POST "https://api.apify.com/v2/acts/santamaria-automations~job-feed/runs?token=$APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"countries":["CH"],"keywords":["Laborant"],"location":"Basel","onlyNewJobs":true}'

The response carries id and defaultDatasetId immediately. Poll GET /v2/actor-runs/{runId} until status is SUCCEEDED, then read GET /v2/datasets/{defaultDatasetId}/items. The same actor is also addressable as POST /v2/acts/zoDyIgISr7yFPyLJd/runs.

import { ApifyClient } from 'apify-client';
const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('santamaria-automations/job-feed').call({
countries: ['CH'], keywords: ['Laborant'], location: 'Basel', onlyNewJobs: true,
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
const output = await client.keyValueStore(run.defaultKeyValueStoreId).getRecord('OUTPUT');
from apify_client import ApifyClient
client = ApifyClient(token=os.environ["APIFY_TOKEN"])
run = client.actor("santamaria-automations/job-feed").call(run_input={
"countries": ["CH"], "keywords": ["Laborant"], "location": "Basel", "onlyNewJobs": True})
items = client.dataset(run["defaultDatasetId"]).list_items().items
output = client.key_value_store(run["defaultKeyValueStoreId"]).get_record("OUTPUT")

Get notified when a run finishes

In the Console, open https://console.apify.com/actors/zoDyIgISr7yFPyLJd/integrations, add an HTTP webhook on "Run succeeded" (and failed, timed out, aborted if you want them), and point it at YOUR_WEBHOOK_URL. The default payload template works; resource.defaultDatasetId and resource.defaultKeyValueStoreId are what you need to fetch the rows and the OUTPUT record. Apify event types are ACTOR.RUN.CREATED, ACTOR.RUN.SUCCEEDED, ACTOR.RUN.FAILED, ACTOR.RUN.ABORTED, ACTOR.RUN.TIMED_OUT and ACTOR.RUN.RESURRECTED. Acknowledge with a 200 immediately, then process.

To attach a webhook to a single run, pass the webhooks query parameter on the run-creation URL: a base64-encoded JSON array of {eventTypes, requestUrl, payloadTemplate} objects.

Run timeout

The default run timeout is 3600 seconds, set under Run options in the Console or with ?timeout=7200 on the run-creation call. A large countries/keywords combination with includeJobDetails: true can legitimately need more. If the timeout is reached anyway, running boards are stopped gracefully with their rows kept (see Partial failure and statusMessage), boards not yet started are skipped, and the run still ends SUCCEEDED with what it collected.

API and Apify Console