Career-Ops scans job sources through provider modules in providers/. Each
non-helper *.mjs file maps to one supported source. Files prefixed with _
are shared helpers and are not loaded as providers.
| Board | Type (API / Atom / RSS / parser) | Notes |
|---|---|---|
| 4 Day Week | API | Reads the public https://4dayweek.io/api/jobs JSON feed (4-day-week / reduced-hours roles). Configure with provider: 4dayweek; paginates ?page=N up to max_pages (default 3), drops expired postings, then scanner filters apply. |
| a16z speedrun talent network | API | Board-wide aggregator for the a16z speedrun + wider a16z portfolio (~200 startups). Auto-detects speedrun-talent-network.com URLs and reads the public zero-auth /api/v1/jobs JSON feed (OpenAPI at /api/v1/openapi.json); paginates ?page=N (0-indexed) up to max_pages (default 3). Optional q: (or keywords:) runs the feed's server-side full-text search with synonym expansion. Unannounced roles arrive pre-masked as "Stealth". Operated by the a16z speedrun program's talent team (provenance: the site links a16z.com's privacy/terms/disclosures; owner contact jmazer@a16z.com). Configure with provider: a16z-speedrun-talent. |
| ADP Workforce Now | API | Auto-detects workforcenow.adp.com/mascsr/default/mdf/recruitment/recruitment.html?cid=...&ccId=... recruitment pages and reads the public no-auth job-requisitions staffing API. Paginates a one-based $skip row offset (20/page), advancing by rows actually returned and stopping at a short or empty page, up to max_pages (default 100). Posting URLs use the API's ExternalJobID when present, falling back to the internal itemID. The list payload carries no full JD text; opt in per entry with adpWorkforcenow: { fetchDetails: true, detailLimit: N } to fetch each posting's detail JSON for a description (skipped during health probes). |
| Agentic Engineering Jobs | API | Reads the site's public zero-auth REST API (https://agentic-engineering-jobs.com/api/v1/jobs, OpenAPI at /api/v1/openapi.json). Configure with provider: agentic-jobs; paginates ?page=N (1-based, 50/page) up to 40 pages / 2000 postings, pacing requests under the documented 30 req/60s rate limit. Location uses the feed's human-readable string, falling back to decoded ISO country names; salary bounds attach when present. Job URLs point at /jobs/{slug} on the site. |
| Alibaba Group Careers | API | Auto-detects https://talent.alibaba.com URLs (host-matched, HTTPS-only) and posts to the public zero-auth position/search JSON API (zh-CN group-wide listings with title, category, city, experience range, full JD text, publish date). Each keywords: entry is queried server-side separately and results are deduped; omit keywords: to pull the whole board. Paginates up to max_pages per keyword (default 100, 100 posts/page). The endpoint's stateless double-submit CSRF check is satisfied by a self-minted per-run token pair — no login or session. |
| Amazon / AWS | API | Auto-detects amazon.jobs careers URLs and queries the public amazon.jobs search API. The board is one global endpoint, so narrow it with an amazon: config block (loc_query, base_query, category, …) whose keys pass through as query params. Configure with provider: amazon. |
| Apple | Parser | jobs.apple.com is a React Router SSR app with no usable public JSON API (the POST /api/v1/search endpoint needs an X-Apple-CSRF-Token), so this reads the server-rendered results HTML and the React Router hydration blob embedded in it. Auto-detects the jobs.apple.com host exactly. Two discovery axes, both deduped by URL, with no defaults (an entry needs at least one, or it throws): queries (text search, relevance-ranked) and teams (team-slug filter — how Apple's BPR org is reachable at all, since Apple has no "Corporate Strategy" title). Example: apple: { queries: ["business operations"], teams: ["business-process-management-OPMFG-BPM"] }. Paginates up to max_pages on the entry (default 2, hard cap 50), pacing pages and retrying through the shared helpers. |
| Arbeitnow | API | Reads the public https://www.arbeitnow.com/api/job-board-api JSON feed (EU/DACH-heavy, newest-first). Configure with provider: arbeitnow; paginates ?page=N up to max_pages (default 3), then scanner filters apply. |
| Arbeitsagentur | API | Uses the public Bundesagentur fuer Arbeit Jobsuche REST API. Configure with provider: arbeitsagentur; title, location, and dedup filters run after fetch. |
| Ashby | API | Auto-detects https://jobs.ashbyhq.com/<slug> boards and uses Ashby's public posting API. Opt-in ashby: { embed: true } reads the board from the embed page instead (jobs.ashbyhq.com/<slug>?embed=js, the page Ashby's own embed script loads on every careers site that embeds a board), for companies that turned the posting API off: it answers 404 while the board stays live. Opt-in because a 404 usually means a dead or mistyped slug, which should stay a loud error. The embed source takes its slug from careers_url, or from ashby: { board: <slug> } when the careers URL is not a jobs.ashbyhq.com one. Its payload carries no description or publish date, and compensation only as a display string (compensationTierSummary, e.g. £90K – £105K • Offers Equity), which is not parsed into a salary: a bare $ does not name a currency. |
| Atlassian | API | Atlassian's careers site is iCIMS-backed but fronted by a single public, auth-free JSON feed of every open posting (GET /endpoint/careers/listings), so one request covers the board. Auto-detects the www.atlassian.com host with a careers path. The feed carries only a last-edited date, so no postedAt is emitted (an absent date reads as "unknown", never "stale"). |
| Avature | Parser | Auto-detects <tenant>.avature.net career sites and parses the public server-rendered job list (/careers/SearchJobs?jobOffset=N, 6 results/page). A branded custom domain that proxies Avature needs provider: avature + api: pointing at the Avature origin. Paginates up to max_pages (default 50). |
| BambooHR | API | Auto-detects <tenant>.bamboohr.com careers pages, reads /careers/list, and builds job URLs matching BambooHR's own public share-link format (<origin>/careers/<id>). A redirect to BambooHR's marketing site means no such tenant; any other redirect (suspended account, login wall) probes the public embed-widget feed (/jobs/embed2.php) to tell a live-but-empty board from a suspended one (whose job pages answer with a login wall regardless) — only the latter, or a tenant unreachable on both endpoints, is reported dead. The list carries no publish date; scan:full's reverse sweep uses enrichDate() to fetch /careers/<id>/detail for postings that already passed title/location filters. |
| BeeSite | API | Auto-detects <tenant>.app.beesite.de boards — the milch & zucker search backend behind branded portals like jobs.mercedes-benz.com — and reads their public no-auth JSON search endpoint. Point careers_url/api at the beesite host (or set provider: beesite). |
| Breezy HR | API | Auto-detects <tenant>.breezy.hr boards and reads the public JSON position feed. |
| Built In | Parser | Board-wide aggregator across the many employers that post to one US tech board, so it has no detect() — reach it with an explicit provider: builtin entry. Parses two payloads from the same server-rendered /jobs page, joined on the numeric job id: the schema.org ItemList blob (title, url, ~400-char description) as the spine, and the rendered job card (company, location, workplace mode, salary band, posted date) as optional enrichment. host: selects one of the nine Built In market hosts (builtin.com default, plus the per-city boards) — the market host is the only location scoping the board offers, so it is what lets location_filter do real work. scope: remote restricts to /jobs/remote. Requires queries: and/or categories:; paginates ?page=N up to max_pages (default 3). Warns loudly if card parsing stops matching, since silent enrichment loss would empty every location and quietly widen the filter. |
| BWI GmbH | parser | Self-hosted careers site of Germany's armed-forces IT provider (~270 open roles, large SOC/cyber-defense operation). No third-party ATS, so no feed provider matches. Reads the listing page's application/ld+json CollectionPage, whose mainEntity ItemList carries every posting URL rather than only the ~15 cards rendered above the fold; titles are rebuilt from the slug, so a default scan is a single request. Detail enrichment is opt-in: with bwi: { fetchDetails: true } on the entry, security-adjacent postings are enriched from each detail page's JobPosting (real title, addressLocality), capped at bwi.detailLimit detail requests per scan (default 45, max 100) and skipped during health probes. Enrichment is best-effort: a failing detail page keeps the slug-derived title and an empty location. HTTPS + host allowlist on www.bwi.de, redirect: 'error' on every request. Configure with provider: bwi. |
| CareerViet | Parser | Reads Vietnam's broad-coverage board https://careerviet.vn/viec-lam — the project's second Vietnamese source. Configure with provider: careerviet; optional searchKeywords and searchLocation (Ho Chi Minh only — the only city code verified live) narrow the listing via the URL slugs the board's own search form and advanced filters generate. Paginates -trang-N- (10 pages by default, raise with max_pages up to 50), reads title, company, city and the "Cập nhật" (updated) date from each id="job-item-{ID}" card. The board serves its listing pages fully server-rendered over plain HTTPS, and robots.txt explicitly ALLOWS ClaudeBot/GPTBot by name (disallowing only /vi/jobs/*, /en/jobs/*, /en/tim-viec-lam/* and /api/*, none of which this parser requests) — a deliberate contrast with two sibling VN boards (TopDev, CareerLink) that are also server-rendered but individually name-block ClaudeBot/anthropic-ai in their own robots.txt and are skipped for it. If the FIRST listing page still contains job cards but parses to none, the provider throws, so a markup change surfaces as a broken board instead of an empty one; a later page that parses to nothing simply ends the pagination. |
| ClearCompany | API | Auto-detects https://careers-api.clearcompany.com/v1/<siteId> or the careers-page embed script https://careers-content.clearcompany.com/js/v1/career-site.js?siteId=<siteId> (copy the siteId GUID from the employer careers page source; a branded careers page is not detected). Reads the public, no-auth careers JSON API in pages of 100 (pageIndex, 0-based) until totalCount is served, up to max_pages (default 20). The posting URL is each row's applyLink (jobs.clearcompany.com/careers/jobs/<id>/apply); email-to-apply postings carry no link and are skipped. The list payload includes the job description, so description is filled at no extra request. |
| Collage HR | API | Uses Collage's public unauthenticated https://api.collage.co/v1/positions/{job-site-address} endpoint. Configure an explicit API URL or a Collage-hosted careers URL; the provider never guesses a company slug. |
| Comeet / Spark Hire Recruit | API | Uses Comeet's public careers API. Provide the full API URL with api: or careers_url; it cannot derive the endpoint from a branded careers page. Prefers a valid HTTPS active posting URL, falling back to a valid HTTPS Comeet-hosted URL when the active URL is unusable. |
| Computrabajo | Parser | Computrabajo, a general job board with one host per Latin American country (co.computrabajo.com, mx.computrabajo.com, ...; only the Colombian host was measured, 2026-10-23). Auto-detects an https <country>.computrabajo.com search URL (careers_url: https://co.computrabajo.com/trabajo-de-sistemas) and parses the server-rendered <article class="box_offer"> cards: title, employer, city. A hidden employer ("Importante empresa del sector") becomes ?, never the board's name. Deliberately tight crawl: at most 3 pages and 15 postings per entry (max_pages / max_jobs can only lower them), with a random 2-5 s pause between pages; ?p=N pagination is unverified, so the walk also stops on a page with no new ids. An anti-bot challenge page on page 1 throws instead of reading as an empty board. The default "Relevancia" order interleaves paid "Empleo destacado" cards (9 of 20 on the measured page), and with the 15-posting cap they take part of the budget. See the Source Indexing Log. |
| Cornerstone OnDemand | API | Reads hosted CSOD career sites (<tenant>.csod.com/ux/ats/careersite/...). The search API wants a bearer token, but the career-site home page embeds an anonymous JWT (no login), so each fetch bootstraps that token then pages the public search API. Point careers_url at the csod.com URL (or set provider: csod). |
| Consider | API | VC "talent network" portfolio job boards built on Consider (consider.com): Founderful, Creandum, Balderton, Lightspeed, Notion Capital, and other tenants on their own hosts, so not auto-detected. Required: provider: consider and careers_url (the board's /jobs page). The board id (opaque, e.g. Founderful's is wingman) is read from that page. Optional: consider_size (default 500), the most jobs the single request returns; consider_board sets the id by hand, needed only when the scan reports that it cannot be read from the page. |
| CryptocurrencyJobs | RSS | Curated Web3/crypto job board. Reads the public https://cryptocurrencyjobs.co/index.xml RSS feed (listings are 100% remote); the feed URL is hardcoded in the provider, so no careers_url or api field is needed. Configure with provider: cryptocurrencyjobs. |
| Dassault Systèmes | Parser | Single-company provider for www.3ds.com/careers/jobs via the public Exalead card-search API (returns Exalead XML, not JSON). Global to 3ds.com, so no per-tenant config. Select with provider: dassault. |
| Dayforce (Ceridian) | Parser | Playwright-driven scanner for Dayforce Recruiting career sites (jobs.dayforcehcm.com). Dayforce's job-search API sits behind Cloudflare (cf_clearance/__cf_bm) plus a NextAuth CSRF token — confirmed live that a bare fetch() gets a hard 403 even with spoofed browser headers, while a real Chromium session gets 200 — so scan-dayforce.mjs drives a real browser session instead of an HTTP client, same pattern as scan-interamt.mjs; run directly with npm run scan:dayforce. Bootstraps the Cloudflare/NextAuth session via page.goto() on the canonical board URL, reads the CSRF token from /api/auth/csrf, then pages /api/geo/{tenant}/jobposting/search (X-CSRF-TOKEN header) by the server's own returned offset/count/maxCount, never a hardcoded step. Applies title, location, content, country-eligibility, and visa filters before fetching per-posting detail JSON, to minimize load on the browser session. Reads dayforce_boards (tenant, board, culture, jobBoardId) from portals.yml; each run bootstraps its own short-lived session and never persists Cloudflare/session cookies across invocations. |
| Deutsche Bahn | Parser | Single-company provider for DB's db.jobs careers portal (the search front over DB's Avature tenant, jobs.deutschebahngroup.careers) via its server-rendered search endpoint over plain HTTP, newest postings first. Select with provider: deutschebahn. |
| Dover | API | Auto-detects Dover-hosted careers pages, https://app.dover.com/<Company>/careers/<board-uuid>, and reads the public zero-auth list endpoint the page itself calls (app.dover.com/api/v1/careers-page/<board-uuid>/jobs), paginating limit=300 / offset up to max_pages (default 5, max 20). Postings link to app.dover.com/apply/<Company>/<job-id>. Unpublished and sample jobs are dropped. Dover's worldwide-remote option (a REGION named "International" with country: US) is rendered as Remote (International), as on Dover's own page, never as a US location. The list payload carries no posting date or description. |
| EchoJobs | Retired (#2976, 2026-08-20) | The board-wide https://echojobs.io/api/jobs feed this provider read is gone — both the feed and its robots.txt now answer with a Vercel bot-protection checkpoint (HTTP 429), and the site's own robots.txt disallows /api. career-ops does not work around bot protection, so this is not a provider to repair. provider: echojobs still resolves in portals.yml for backward compatibility, but fetch() throws immediately with an explanation naming the cause — remove the entry from your portals.yml when you see it. |
| Eightfold AI | API | Auto-detects <tenant>.eightfold.ai career sites and reads the public zero-auth /api/apply/v2/jobs per-tenant endpoint. domain is optional (the server infers it from the tenant host) but is forwarded when the entry sets domain:, since multi-brand tenants scope their board by it. The server caps a page at 10 rows regardless of the requested num, so a large board costs count/10 requests; max_pages (default 200) bounds the loop. A branded host (careers.<company>.com) is not matched by pattern — point careers_url/api at the eightfold.ai host, or, for a tenant that serves the API only from its own domain, name that host exactly in eightfold_host: (one bare hostname; still https-only and checked by the private-address guard). |
| Eploy | XML | Reads the tenant's public, zero-auth /live-jobs.xml sitemap in one request. Eploy tenants normally use branded domains with no reliable hostname signature, so configure provider: eploy plus the public careers_url; the provider derives the fixed feed path from that origin. Sitemap rows expose canonical job URLs and modification dates only, so the default scan derives a readable title from each URL slug, leaves location blank, and does not mislabel <lastmod> as a publication date. Optional eploy: { fetchDetails: true, detailLimit: 25 } performs bounded, paced detail-page enrichment for exact schema.org title, location, description, and datePosted; probes skip it and individual detail failures fail open. Some branded feeds point job links at their canonical <tenant>.eploy.net host, which is accepted; arbitrary off-host links are dropped. Specsavers' join.specsavers.com jobs page is an Attrax frontend, not this standard Eploy surface, and is not claimed. |
| Feishu Jobs | API | operator: Beijing Feishu Technology Co., Ltd. and affiliates (declared in the official Feishu Hiring privacy statement); ByteDance operates its own tenant. Auto-detects ByteDance's jobs.bytedance.com and tenant sites on *.jobs.feishu.cn (HTTPS-only), then posts to the public zero-auth /api/v1/search/job/posts endpoint. Queries each keywords: value separately (or the whole board when omitted), paginates by offset up to max_pages (default 200, 100 posts/page), and warns rather than silently truncating when a known result count exceeds that bound. Uses the list payload's full JD text without detail requests; emitted links use ByteDance's /experienced/position/{id}/detail route or shared tenants' /index/position/{id}/detail route so they cold-load the selected posting. |
| Flowxtra | API | Reads the public, no-auth, cross-tenant https://app.flowxtra.com/api/central/jobs JSON feed (board-wide — every company hosted on Flowxtra, not one tenant). Configure with provider: flowxtra; paginates ?page=N up to max_pages (default 3), filtered to status=Live, then scanner filters apply. Job URLs use the API's ready-made urlJobApplay apply link on flowxtra.com. |
| Frontline AppliTrack | Parser | K-12 school districts hosted at https://www.applitrack.com/<district>/onlineapp/ (single-employer boards, one entry per district). Auto-detects an applitrack.com careers_url and fetches the board's jobpostings/Output.asp script, which returns the whole vacancy list as HTML in one request (no pagination, no browser). Optional default_location: "City, ST" is appended to a posting's site name when that names no place (AppliTrack sites read "District" or a school name), so location_filter has something to match. The script is served as Windows-1252, so a non-ASCII character in a title is dropped rather than shown as a replacement character. |
| Garena | API | Single-company provider for careers.garena.com. POSTs an empty JSON body to the public /api/job/list?office=<office> endpoint (no auth); office doesn't filter the result (every value returns the same board) but is still sent to match the real request, and is reused to build each job's page URL (/<office>/careers/<id>). Select with provider: garena; optional garena: { office: <code> } (default global). |
| Gem | API | Auto-detects https://jobs.gem.com/<boardId> SPA boards via Gem's public GraphQL endpoint. For the documented REST Job Board API, pin the exact discovered api: https://api.gem.com/job_board/v0/<vanity_path>/job_posts/; it is unauthenticated, read-only, and accepts no guessed company slug. |
| Generalist World | Parser | Curated board of generalist / operator roles at https://generalist.world/jobs/: a single server-rendered page (about 140 cards, no pagination) read in one plain GET. Auto-detects a careers_url on generalist.world, or configure with provider: generalist-world. Cards expose title, employer, location (data-region fallback: Remote / UK / EU / US) and a short teaser as description; the salary tag is free text and is not parsed; no posted date at list level. Job URLs are the board's own /jobs/{slug}/ pages (the employer apply link lives only on the detail page, which is not fetched). |
| Getro | API | VC "talent network" portfolio job boards built on Getro (getro.com): Atomico, Cherry Ventures, HV Capital, Point Nine, b2venture, and other tenants on their own hosts, so not auto-detected. Required: provider: getro and careers_url (the board's /jobs page). The board's collection id is read from that page. Optional: getro_max_pages (default 40) and getro_max_age_days (default 90), which stop the newest-first pagination; getro_collection sets the id by hand, needed only when the scan reports that it cannot be read from the page. |
| Get on Board | API | Reads the public https://www.getonbrd.com/api/v0/categories/<category>/jobs JSON:API feed (remote/LatAm-heavy tech roles). Configure with provider: getonbrd; paginates ?page=N up to max_pages (default 3) per category (expand[]=company), then scanner filters apply. Category defaults to programming; override with category: <slug> or scan several with categories: [<slug>, ...] (cap 12, deduped by URL across categories). The board files leadership and ML/data roles outside programming, so EM/Tech Lead searches want operations-management and machine-learning-ai too. Slugs come from https://www.getonbrd.com/api/v0/categories. |
| getManfred | API | Reads the board-wide https://www.getmanfred.com/api/v2/public/offers JSON feed (Spanish/EU tech roles, salary ranges published). Configure with provider: manfred and an optional lang: EN|ES (the API rejects a call without one); the whole catalogue arrives in a single request and only status: ACTIVE offers are kept. |
| Glints | API | Uses Glints' public GraphQL job search endpoint. Configure with provider: glints; query and filters can be set on the portal entry. |
| Parser | Google Careers is server-rendered, so the results page HTML carries one card per posting. Explicit-config via a google: block. Each card's title, employer and locations are read from the card: the same results list other Alphabet companies' postings (YouTube, DeepMind and others), so the company is the employer the card names, not the portals.yml entry name. First results page per query only, by design: google.com/robots.txt disallows every .../jobs/results?...page= URL for all crawlers, so this provider never sends page and max_pages does not apply to it. sort_by=date makes that single page the newest postings. |
|
| Greenhouse | API | Handles explicit api: URLs and auto-detects public Greenhouse board URLs for the boards API. |
| Gupy | API | Board-wide search across the Brazilian Gupy ATS, where thousands of employers host their career pages (<tenant>.gupy.io). Reads the public zero-auth portal.gupy.io/api/job-search/jobs JSON API that Gupy's own job search uses, keyed by keyword rather than company slug, so it surfaces employers absent from tracked_companies. Configure as a job_boards: entry with provider: gupy (or careers_url: https://portal.gupy.io, auto-detected; a single tenant's career page is not claimed). Search keys go in a gupy: block (the VDAB shape): keywords sweeps each term separately (the API's jobName is a narrow title match) and dedupes by URL; falls back to config/profile.yml's target roles (same as VDAB). Optional since_days is this entry's own recency window (a run's --since wins), plus workplace_types, job_types and state (the API has no country filter). max_pages stays on the entry. Paginates offset/limit (100/page) until a short page, up to max_pages per keyword (default 5, capped at 100, the deepest the API serves); pagination.total reports the page size, not the result count, and is ignored. The list payload carries description. Confidential postings, labelled "Confidencial" instead of an employer, are dropped. |
| Hacker News (Who is hiring?) | API | Finds the current monthly "Ask HN: Who is hiring?" thread via the Algolia HN search API and parses top-level comments as postings (the pipe-delimited "Company | Role | Location | URL" convention is extracted defensively; free-form comments keep the first line as title). Configure with provider: hackernews in a job_boards: entry. |
| Heckler & Koch | Parser | Single-company provider for heckler-koch.com/…/Stellenangebote — a Nuxt page that server-renders every posting in one bare-HTTP GET (small board); job links point at the karriere.heckler-koch.com apply backend. Select with provider: hecklerkoch. |
| HiringRoom | Parser | Per-company ATS common in Argentina/LatAm, one <company>.hiringroom.com subdomain per employer; auto-detected from that host (HTTPS-only, look-alike hosts rejected). No public API, so the tenant's microsite is parsed in-process. Two layouts exist: the legacy /jobs page (server-rendered vacancy cards: title and location) and the newer /portal/jobs page (schema.org JSON-LD JobPosting list, which also yields postedAt and a plain-text description). Tenants on the newer layout answer /jobs with a 302, which the redirect: 'error' guard refuses, so point careers_url at /portal/jobs for them (/portal alone lists only featured vacancies). Posting URLs are rebuilt on the tenant's https origin and kept only when they are /jobs/get_vacancy/<id> on that host. The company label is the entry's name. One request per entry, no pagination. |
| HigherEdJobs | RSS | Reads the public https://www.higheredjobs.com/rss/categoryFeed.cfm?catID={catID} feed and parses it in-process. Configure with provider: higheredjobs and optional cat_id (default 68 = Higher Education). Not auto-detected — requires explicit provider: config. Works where the feed is not walled: on some networks the endpoint answers a bot-challenge page, and the scan then reports a named error rather than an empty board (#3637). |
| Himalayas | API | Reads the board-wide https://himalayas.app/jobs/api?limit=20 JSON remote-jobs feed, following opaque nextCursor tokens up to max_pages (default 10, max 50); warns if the cap truncates the feed. Configure with provider: himalayas in a job_boards: entry. |
| Homerun | Atom | Auto-detects <slug>.homerun.co careers sites (any path) and https://feed.homerun.co/<slug>, and reads the tenant's public zero-auth Atom feed on feed.homerun.co in one request. For a branded careers domain, set api: to the https://feed.homerun.co/<slug> feed the careers page links. Carries the description, and an annual salary when the posting states one; location is passed through as the tenant wrote it. An unknown slug fails with a named "no such tenant" error. |
| IBM Careers | API | Posts to IBM's public careers search API and supports optional IBM facet filters in the portal entry. |
| iCIMS | Parser | Auto-detects any *.icims.com HTTPS host from careers_url/api (canonical form https://careers-<tenant>.icims.com/jobs/search?ss=1) and scrapes the public hosted-portal search pages. In the reverse sweep, a dataset entry that already starts with careers- is used as-is and carries no fallback; every other entry (a bare tenant like acme, or another portal subdomain like uscareers-acme) is tried on its likelier host first and carries the other host shape as a fallback, used only when the first host answers 404 on its first page. List pages carry no posted date; scan:full's reverse sweep uses enrichDate() to fetch the JD detail page's JSON-LD datePosted for jobs that already passed title/location filters. Paginates up to a fixed 30-page cap, warning if a tenant's postings exceed it. |
| InRecruiting (Zucchetti) | Parser | Auto-detects <tenant>.intervieweb.it careers URLs (the /it/career page, a /jobs/... posting link, or the bare host; /en/career etc. selects the UI language) and parses the tenant's public server-rendered career page, 15 postings per page. Further pages come from the same guest POST the page's own pager issues (/app.php?...module=newcareer, with the page's section token), up to max_pages (default 40, max 200). Widely used by Italian employers. Cards carry no publish date, so postedAt is omitted. |
| Interamt.de | Parser | Playwright-driven scanner for Germany's federal/state/municipal public-sector job portal. Interamt runs on Apache Wicket (stateful) with no REST API, so scan-interamt.mjs drives a real browser session instead of an HTTP client; run directly with npm run scan:interamt. Reads interamt_searches from portals.yml, falling back to a generic set of German IT keywords if that section is absent. |
| ITviec | Parser | Reads Vietnam's largest IT job board https://itviec.com/it-jobs — the project's first Vietnamese source. Configure with provider: itviec; optional searchKeywords and searchLocation (Ho Chi Minh / Hanoi / Da Nang) narrow the listing via the path segments the board's own search form generates. Paginates ?page=N (10 pages by default, raise with max_pages up to 50), reads title, company, city and the relative "Posted … ago" label from the per-card Stimulus attributes. The board serves its listing pages fully server-rendered over plain HTTPS; robots.txt disallows only /subscriptions/new, which this parser never requests. Pacing between pages is 750ms — throttling (HTTP 429) was observed under back-to-back sweeps, so this is measured politeness. If the FIRST listing page still contains job cards but parses to none, the provider throws, so a markup change surfaces as a broken board instead of an empty one; a later page that parses to nothing simply ends the pagination. |
| JazzHR | Parser | Auto-detects public *.applytojob.com career pages at either the bare tenant host or /apply, commonly branded with Powered by JazzHR. Reads server-rendered listing cards and optional JSON-LD detail pages (fetchDetails). |
| JibeApply | API | Auto-detects https://<slug>.jibeapply.com/jobs careers URLs (rewriting /jobs to the public /api/jobs endpoint); paginates ?page=N up to max_pages (default 50), warning if a tenant's postings exceed the cap. Also supports branded/iCIMS-hosted sites at their own /jobs path via an explicit provider: jibeapply + api: URL. |
| Job Bank Canada | Atom | Canada's federal job-search service (jobbank.gc.ca, run by Employment and Social Development Canada) — a national employment service, same class as Arbeitsagentur. Reads the public zero-auth Atom feed at /jobsearch/feed/jobSearchRSSfeed. Configure with provider: jobbankca; nested jobbankca.keywords is an explicit override, otherwise it falls back to config/profile.yml's target roles (same as VDAB). The feed's own locationstring filter is unreliable, so location filtering happens downstream. Paginates ?page=N (100 results/page) up to max_pages (default 5), honouring the site's robots.txt Crawl-delay: 5 between every request. |
| Jobicy | API | Reads the board-wide https://jobicy.com/api/v2/remote-jobs?count=50 JSON feed (remote-jobs aggregator). Configure with provider: jobicy in a job_boards: entry. |
| Jobs.cz | Parser | operator: Alma Career Czechia s.r.o. — declared per Source Indexing Policy rule 4. Configure with provider: jobscz; nested jobscz.keywords is an explicit override, otherwise it falls back to config/profile.yml's target roles. Reads the keyword search https://www.jobs.cz/prace/?q[]={keyword} (30 per page) up to max_pages (default 10). |
| Jobspresso | RSS | Reads the public WordPress https://jobspresso.co/?feed=job_feed XML job feed and parses it in-process. Configure with provider: jobspresso in a job_boards: entry. |
| Jobstreet / SEEK | API | Uses the public SEEK v5 JobSearch JSON API for Jobstreet and SEEK sites. Configure explicitly with provider: jobstreet. For SEEK Australia set api: https://www.seek.com.au/api/jobsearch/v5/search + siteKey: AU-Main (www.seek.co.nz + NZ-Main for New Zealand). Job links are built per market host (/id/job/<id> on the Indonesian sites, /job/<id> everywhere else). Optional appendWorkType: true suffixes titles with the listing's work type, e.g. [Part time]. At 30 jobs/page it walks to a short/empty final page (default max_pages: 100, hard cap 500), respects one-page probes, retries transient failures and warns/tags capped or failed partial scans; override the page cap when the source has more matches. An existing explicit maxPages: 3 still limits the scan to three pages; remove it or set a larger max_pages to use the new default. |
| Jobvite | API | Auto-detects https://jobs.jobvite.com/<slug> boards and reads the public XML feed at app.jobvite.com/CompanyJobs/Xml.aspx?c=<companyEId>. The feed keys on an opaque companyEId, not the vanity slug, so set company_eid: alongside careers_url: to skip a request per scan; without it the provider scrapes the eId off the board page first. A tenant with no usable careers_url can skip the slug entirely with provider: jobvite + api: pointing at the feed URL — the eId is read from its ?c= parameter. The feed inlines every full job description (1.9 MB for a large tenant) and app.jobvite.com answers 429 Retry-After: 30 from the second request onward, so fetches use an extended timeout and honour the retry header. A tenant with no open roles is reported as an empty board, not an error. |
| join.com | API | Auto-detects join.com/companies/<slug> careers pages and reads the public zero-auth GraphQL job list the page itself uses (join.com/candidate-api/graphql), 100 postings per request up to max_pages (default 50, max 200); a warning is logged if the cap truncates a board. Posting URLs carry join's numeric job id (join.com/companies/<slug>/<id>), which stays the same when a posting is republished. The list includes the full description and the creation date. A rate-limit refusal is retried with backoff, and a changed response shape is reported as an error rather than an empty board. |
| joinup.ch | Parser | Auto-detects joinup.ch career URLs (Swiss startup job platform, Typesense-backed Next.js). Reads the SSR'd __NEXT_DATA__ from https://joinup.ch/browse/jobs — the browse page renders the newest postings (created-DESC) in one bare-HTTP GET, no API key needed. Fails closed: a missing or unparseable __NEXT_DATA__ block is treated as a scraper break, not an empty board. Configure with provider: joinup or point careers_url at joinup.ch. |
| JustJoin.it | API | Auto-detects justjoin.it/job-offers/... URLs and reads the public justjoin.it/api/candidate-api/offers API (Polish/EU tech board); paginates up to max_pages (default 50). |
| Kalibrr | API | Reads the public, no-auth job-board search JSON (/kjs/job_board/search) that kalibrr.com's own board calls. South-East Asian board, Indonesia-heavy (99 of the first 100 live rows) with the Philippines and Thailand behind it. Configure with provider: kalibrr; optional searchKeywords maps onto the API's own text filter. Paginates by limit/offset, bounded by the response's count (the real total, so the whole board is traversed rather than a slice of a promoted view) up to max_pages (default 20, 100 rows per request). Posting URLs are the board's own canonical /c/{company code}/jobs/{id}/{slug} pages, the form the site's own <link rel="canonical"> emits. Two fields are deliberately unused: apply_redirect_url (~12% of rows) because it is an opaque shortener rather than the employer's own ATS link, and the salary figures because the board publishes a month/day amount while the Job contract's salary is annualized compensation. |
| Landing.jobs | API | Reads the board-wide https://landing.jobs/api/v1/jobs JSON feed (tech, Europe). Configure with provider: landingjobs; company is derived from the posting URL slug. |
| LaraJobs | RSS | Reads the board-wide https://larajobs.com/feed RSS feed (Laravel / PHP jobs) and parses it in-process. Configure with provider: larajobs; company and location come from the feed's job: namespace. |
| Lever | API | Auto-detects https://jobs.(eu.)?lever.co/<slug> boards and uses Lever's public postings endpoint. |
| Local parser | Parser | Runs an in-repo parser command from portals.yml. Use this for stable SSR or HTML pages that need a custom extractor. |
| Magneto365 | Parser | magneto365.com, a general job board for Colombia. Auto-detects an https www.magneto365.com search URL (careers_url: https://www.magneto365.com/co/trabajos/buscar/tecnologia) and parses the server-rendered job cards: title, employer, city. Only /<country>/empleos/<slug>-<id> detail links count as postings; the page's own category, search and pager links under /co/trabajos/ do not. "Confidencial" and the board's own name ("Magneto empleos") become ?. Paginates ?paginator[page]=N (the page's own pager links) up to max_pages (default 5, cap 20) and max_jobs (default 100, cap 500), with a random 1-2 s pause between pages. See the Source Indexing Log. |
| Meituan Careers | API | Auto-detects https://zhaopin.meituan.com URLs (host-matched, HTTPS-only) and posts to the public zero-auth getJobList JSON API (zh-CN social-hiring listings with title, department, city, JD text, refresh date). Each keywords: entry is queried server-side separately and results are deduped; omit keywords: to pull the whole board. Paginates up to max_pages per keyword (default 100, 100 posts/page), retrying empty mid-pagination pages (the board rate-limits sporadically). |
| MokaHR | API | operator: Beijing Sirius Technology Co., Ltd. (Moka) (declared in the official product privacy policy). Auto-detects HTTPS tenant URLs on app.mokahr.com or the international hire-r1.mokahr.com (which answers in plaintext instead of the encrypted envelope) under /social-recruitment/{org}/{site}, /campus-recruitment/{org}/{site}, or /apply/{org}/{site}. Posts to the public zero-auth encrypted jobs API, decrypts the observed AES-128-CBC envelope locally, queries each keywords: value separately, and paginates by offset up to max_pages (default 10, 50 posts/page). Emitted URLs use the tenant's #/job/{id} SPA route; scanner/tracker normalization preserves that fragment's job identity, and tenant paths explicitly excluded by Moka's robots.txt are rejected. |
| MyCareersFuture | API | Singapore's national job bank, run by Workforce Singapore (WSG) — a public employment service, same class as Arbeitsagentur/VDAB/Job Bank Canada. Reads the public zero-auth POST /v2/search JSON API. Configure with provider: mycareersfuture; nested mycareersfuture.keywords is an explicit override, otherwise it falls back to config/profile.yml's target roles (same as VDAB/Job Bank Canada). Pagination is driven by the URL's page query param (confirmed live — the JSON body's own page field is ignored server-side). Paginates ?page=N (up to 100 results/page) up to max_pages (default 5, capped at 20). |
| NEOGOV (schooljobs.com / governmentjobs.com) | Parser | Community colleges, school districts and public agencies on NEOGOV (single-employer boards, one entry per agency). Auto-detects https://www.schooljobs.com/careers/<agency>[/<folder>] and the governmentjobs.com equivalent, and reads the site's own HTML list endpoint /careers/home/index, 10 postings per page under a 30-page default ceiling (max_pages, capped at 200; a warning says when the ceiling truncated a board). A <folder> (a custom careers page such as facultypositions) is sent as departmentFolder, scoping the list to that page; without one, every posting for the agency is returned. The bare hosts redirect to www., which redirect: 'error' would refuse, so the provider normalises to www; an unknown agency slug redirects to the site's home page and fails loudly instead of reading as empty. |
| No Fluff Jobs | API | Auto-detects nofluffjobs.com and reads its public /api/search/posting API (Polish/EU tech board); paginates up to max_pages (default 5). |
| NoDesk | RSS | Reads the public https://nodesk.co/remote-jobs/index.xml feed and parses it in-process. Configure with provider: nodesk. |
| Notion | API | Auto-detects a public Notion careers page, https://<workspace>.notion.site/... with the page id in the URL (the slug, /p/<id> and bare-id forms Notion's share links use; a custom-slug URL without the id is not claimed), and reads the two zero-auth endpoints the published page calls itself: POST /api/v3/loadPageChunk, then POST /api/v3/queryCollection when the board is a database. These endpoints are undocumented (the official API needs a token and an owner's share), so an unexpected response shape throws rather than reading as an empty board. A database board yields one posting per row of the chosen view (the ?v= view in careers_url, else the first), sent with that view's own saved filter, with a Location column (select, multi-select or text) as the location; a plain page yields its child pages, inline ‣ page links and pages inside column/toggle layouts. Postings link to https://<workspace>.notion.site/<page-id>, which survives a title edit. postedAt is the page's created_time: Notion has no publish date, so a reused row reads as old. No description is set. |
| OCC Mundial | Parser | Mexico's dominant general job board (occ.com.mx) — the mid-size industrials and domestic groups that never appear on Greenhouse, Lever, Ashby, Workday or SuccessFactors post here, so without it the scanner's Mexico coverage has a structural hole no ATS probing can close. Board-wide aggregator on one host, so it has no detect() — reach it with an explicit provider: occ entry. /api/* returns 403, so the server-rendered search pages are parsed in-process (same tiny-tag-extractor approach as weworkremotely/successfactors, no HTML-parser dependency). Requires queries: — OCC is a keyword-search board with no browsable listing, and a built-in default would be one user's search profile applied to everyone's scan. Optional states: (omit for nationwide) and max_pages (default 3, hard cap 10). Pagination is a SLUG suffix on the keyword segment (de-{kw}-pagina-{N}): ?page=N, ?pagina=N, ?start=N and /N/ all silently re-serve page 1, so the loop also stops as soon as a page yields no new ids. Paces 750ms between pages. A total outage throws rather than returning an empty board: occ.com.mx's Cloudflare challenge is geo-scoped (measured 2026-09-04 — HTTP 403 cf-mitigated: challenge from two non-Mexican egresses, HTTP 200 with no challenge from a Mexican residential egress on the same four URLs), and a challenge page parses to zero cards, which would otherwise read as a live board with no jobs. Expect it to be unreachable from a foreign CI runner and reachable for its intended audience, candidates in Mexico. robots.txt (readable from Mexico) disallows account, help, saved-job, /rest and /empresas/ paths and name-blocks only LinkedInBot; the /empleos/ search paths this provider reads are not disallowed. |
| Oracle Recruiting Cloud (ORC) | API | Auto-detects <tenant>.fa[.<region>][.ocs].oraclecloud.com careers URLs (e.g. JPMorgan Chase, BNY Mellon, American Express) and reads the public zero-auth recruitingCEJobRequisitions REST API. Paginates by offset up to a safety cap (25 pages / ~5,000 jobs); pagination is driven by returned list length and TotalJobsCount, not the unreliable hasMore flag some tenants always report false. |
| PageUp | Parser | Auto-detects the vendor-hosted careers site https://careers.pageuppeople.com/<tenant>/<site>/<lang>/… from careers_url/api (numeric tenant id; any path under that prefix is claimed, so a pasted posting link resolves to the listing) and scrapes the public server-rendered listing table (verified live 2026-10 on eleven tenants: universities in the US, Australia and Hong Kong, e.g. Oklahoma State, Alabama, Canberra, HKU, and Bank of China (Hong Kong)). A tenant's branded domain (e.g. jobs.hku.hk) is not auto-detected — point the entry at the vendor-host URL. Listing columns are the tenant's own choice and none is a location or a posting date, so jobs carry an empty location and no posted date. Paginates ?page=N&page-items=100 up to max_pages (default 20, max 200), warning if a tenant's postings exceed the cap; a page with no results table (login, error or challenge page) throws instead of reading as an empty board, and a tenant that redirects the vendor-host URL to a newer front end on its own domain is reported as moved. |
| Paylocity | Parser | Auto-detects recruiting.paylocity.com/Recruiting/Jobs/All/<board-guid> boards and reads the job list embedded in the public board page. |
| PeopleSoft Fluid Candidate Gateway | Parser | Auto-detects the tenant-agnostic Fluid job-search path /psc/{site}/EMPLOYEE/HRMS/c/HRS_HRAM_FL.HRS_CG_SEARCH_FL.GBL on any branded institution domain (verified live: Western University recruit.uwo.ca, Toronto Metropolitan University careers.torontomu.ca, McMaster University hr.mcmaster.ca, WSIB wsib.ca) — there is no vendor-owned host to pattern-match, unlike Oracle/Workday, so the URL path is the signature instead. No JSON API exists; this is a stateful HTML scrape carrying a cookie session across a GET (search) → POST (the "load more" postback, replaying the FULL captured form state with one field overridden) → GET (detail, verifying the returned job id matches the one requested, retrying PostingSeq=2 when PostingSeq=1 is a dead deep-link) sequence. A response with no form[name="win0"] is treated as a login/session-expired/challenge page and throws, never as "zero postings". A reported-total/parsed-count mismatch after pagination marks the result explicitly incomplete rather than inferring postings closed. Detail-page description enrichment (peoplesoft: { fetchDetails: true }) is opt-in and bounded (sequential and paced, default 25/tenant), skipped during health probes. |
| Personio | Parser | Auto-detects <slug>.jobs.personio.de or .com hosts and parses the posting list embedded in the server-rendered careers page, which names each posting's languages; falls back to the public XML jobs feed, then to the careers page's job links for tenants that have disabled the feed, and warns when the page links jobs its payload doesn't list (a changed page build). Each posting comes back in its own default language, with ?language=<code> on its URL; a ?language=<code> on careers_url asks for that language instead wherever a posting has it. With top-level preferred_languages (and no ?language= on the entry), the version in each preferred language comes back next to the default one, at one extra request per preferred language that some posting has (up to 5 per tenant). With scan_history.dedup_include_language: true every language version comes back, at one extra request per further language (up to 5 per tenant, preferred languages first). When the careers page only embeds the Personio tenant as an iframe (common for companies using a branded careers URL), pin the tenant explicitly with personio: <slug> on the entry. |
| Phenom People | API | Not auto-detected — every tenant runs on its own branded domain (e.g. careers.allianz.com), so select with an explicit provider: phenom. Posts to the public, no-auth /widgets CareerConnect endpoint on that branded host; paginates via offset up to max_pages (default 20, 100 results/page, max 300), warning if a tenant's postings exceed the cap. |
| Pinpoint | API | Auto-detects <slug>.pinpointhq.com boards and reads the public zero-auth /postings.json per-tenant feed. |
| Platsbanken (Arbetsförmedlingen) | API | Sweden's national public employment service, read through the public zero-auth JobTech Dev JobSearch API (https://jobsearch.api.jobtechdev.se/search) that backs Platsbanken — a public employment service, same class as Arbeitsagentur/VDAB/Job Bank Canada/MyCareersFuture. Configure with provider: platsbanken; nested platsbanken.keywords is an explicit override, otherwise it falls back to config/profile.yml's target roles (same as the other four). Keywords scope the source rather than decorate it: measured 2026-10-12, the unfiltered board reports total.value 39,433 ads but the API stops serving at offset 2000 (offset=2000 still answers 100 rows, offset=2100 answers 0 with a plain 200), so no walk can pass 2,100 rows — each keyword gets its own window instead of a capped sweep being presented as the board. Paginates ?offset=N (100 rows/page) up to max_pages (default 5, capped at 21, the source's own last servable page), pacing 1s between pages past the first and warning when either ceiling — rather than the source's own end — stopped the walk. Job URLs are rebuilt as https://arbetsformedlingen.se/platsbanken/annonser/{id} from the ad id through _safe-url.mjs rather than taken from the payload's webpage_url, which was on that host in 100/100 sampled rows; employer.name and description.text ship in the list payload, so no detail request is made. |
| Prace.cz | Parser | operator: Alma Career Czechia s.r.o. — declared per Source Indexing Policy rule 4. Configure with provider: pracecz; nested pracecz.keywords is an explicit override, otherwise it falls back to config/profile.yml's target roles. Reads the keyword search https://www.prace.cz/nabidky/?q={keyword} (40 per page) up to max_pages (default 10). |
| PrevueAPS | API | Auto-detects <tenant>.prevueaps.ca careers pages (Canadian credit unions, colleges, and public-sector employers). Fetches the tenant's /jobs/ page once to resolve its numeric domainId, then reads the public zero-auth /core/jobs/{domainId} JSON endpoint. |
| Python.org Jobs | RSS | Auto-detects https://www.python.org/jobs/ or configure with provider: pythonorg. Reads the official Python Software Foundation public RSS feed (https://www.python.org/jobs/feed/rss/) and parses postings in-process with entity decoding and title/company extraction. |
| Radancy | Parser | Reads Radancy (TalentBrew) career sites (e.g. careers.munichre.com) via the server-rendered /{lang}/search-jobs?p={N} results page (1-based pagination). Select with provider: radancy. |
| Recruitee | API | Auto-detects <slug>.recruitee.com boards and uses the public per-tenant offers API. The offer list carries the posting body (description and requirements), the publish date and the salary, so no per-job request is made; a salary is annualized from its stated period and left out when the period is missing. Each offer comes back in its default language, read from the offer's translations, with ?lang=<code> on its URL, or from its top-level fields with no language when no translation matches them; a ?lang=<code> on careers_url asks for that language instead wherever an offer has it. With top-level preferred_languages (and no ?lang= on the entry) the translations in those languages come back as well (for an offer with no matching translation, the best-ranked one comes back instead of the top-level fields), and with scan_history.dedup_include_language: true every translation does, the default one first, all from the same response, except one with no title or with the same text as a version already returned. |
| Red Rover K12 | API | K-12 school districts on https://jobs.redroverk12.com/org/<orgId> (single-employer boards, one entry per district). Auto-detects that URL and POSTs the same public, unauthenticated GraphQL query the browse page sends to api.redroverk12.com/graphql. Returns public postings only: internal-only postings ("Internal applicants only", no public activation date) and closed ones are left out, since an outside applicant cannot apply to them. One response holds up to 500 postings and no page cursor is known, so a board past that fails loudly instead of returning a truncated list. |
| RemoteOK | API | Reads the board-wide https://remoteok.com/api JSON feed; scanner filters decide which rows are relevant. |
| Remotive | API | Reads the board-wide https://remotive.com/api/remote-jobs JSON feed, then applies local scanner filters. |
| Remotli | API | operator: eliador90 (declared per Source Indexing Policy rule 4 — the board is operated by the contributor who submitted this provider). Reads the board-wide https://remotli.ch/api/jobs JSON feed (remote roles at Swiss companies, paid in CHF). Select with provider: remotli; paginates ?page=N&limit=50&remote=all (the server caps limit at 50) up to max_pages (default 20). remote=all is required for rule 3: without it the API serves its remote-first default view, ~43% of the board. Rows are a nested { jobs, companies } join shape. Emits only status: active postings; the emitted URL is the employer's own applyUrl per rule 2, with the remotli page as fallback. Carries publishedAt, salary and the full description (the latter feeds the cross-listing fingerprint). |
| Rheinmetall | Parser | Single-company provider for www.rheinmetall.com/<lang>/career/vacancies — the Nuxt page server-renders its job cards, so plain ?page=N pagination works over bare HTTP (no XHR API is exposed). Select with provider: rheinmetall. |
| Rippling | API | Auto-detects https://ats.rippling.com/<slug>/jobs careers pages and reads the public zero-auth v2 board API (ats.rippling.com/api/v2/board/<slug>/jobs), paginating 0-indexed ?page=N requests at 1000 jobs/page up to max_pages (default 10, max 50); a warning is logged if the cap truncates a healthy board. First-page fetch failures propagate after retry; later-page failures keep collected jobs with a truncation warning. |
| SAP SuccessFactors | Parser | Reads SF Recruiting Marketing (RMK) career sites — branded boards like jobs.sap.com, jobs.zf.com, jobs.schaeffler.com — via the public no-auth /tile-search-results/?startrow=N HTML fragment. Branded hosts carry no "successfactors" string, so select with provider: successfactors + api: the board origin. |
| SchoolSpring | API | K-12 school districts hosted at https://<district>.schoolspring.com (single-employer boards, one entry per district). Auto-detects the district host (the platform's own www and api hosts are not claimed) and reads the public JSON API at api.schoolspring.com, 100 postings per page under a 20-page default ceiling (max_pages, capped at 100; a warning says when the ceiling truncated a board). A failure on any page fails the whole target rather than returning a partial board. The list carries no pay or description (those need a per-job request), so those fields are omitted. |
| Senjob | Parser | Reads the Senegalese board https://senjob.com/offres-d-emploi.php (Dakar and the regions) — the project's first African source. Configure with provider: senjob; paginates ?page=N (10 pages by default, raise with max_pages up to 50), merges the sticky rows the board repeats on every page, and reads title, place and the hidden ISO publication date. Employer names are absent from the listing rows, so company is left empty rather than guessed. robots.txt disallows candidate CVs and employer pages; this parser reads neither. If the FIRST listing page still contains postings but parses to none, the provider throws, so a markup change surfaces as a broken board instead of an empty one; a later page that parses to nothing simply ends the pagination. |
| SmartRecruiters | API | Auto-detects SmartRecruiters careers URLs or uses provider: smartrecruiters for branded custom domains. |
| softgarden | Parser | Reads the hosted softgarden job widgets at <tenant>.softgarden.io/<lang>/widgets/jobs — a server-rendered page listing every posting (no auth, no JS, no pagination). Select with provider: softgarden. |
| SolidJobs | API | Auto-detects https://solid.jobs/public-api/offers/<division> and reads the public offers API. |
| Startup Jobs | RSS | Reads the board-wide public https://startup.jobs/feeds/jobs RSS feed and parses it in-process. Configure with provider: startup-jobs under job_boards:. The feed returns only the ~50 most recent matching postings (no pagination), so add several entries, one per role slug (startup_jobs: { role: <slug> }, plus a workplace: remote variant — remote is the only accepted workplace value, anything else 404s), where <slug> is the last segment of a https://startup.jobs/roles/<slug> page — a single unscoped entry covers only a few hours of postings. Company and location are parsed from the title (… at {Company}) and the description's last line; a posting with no identifiable employer is skipped. |
| Taleo Enterprise Edition | API | Auto-detects *.taleo.net/careersection/<section>/jobsearch.ftl public career sections and replays the anonymous POST /careersection/rest/jobboard/searchjobs request. Dynamic columns, pagination, and canonical detail URLs are normalized without login, browser automation, or LLM calls. Public JobPosting descriptions are opt-in via fetchDetails: true and bounded by detailLimit; probes remain list-only. Taleo Business Edition (tre.taleo.net) uses a separate form flow and is not included here. |
| Teamtailor | RSS | Auto-detects <slug>.teamtailor.com career sites and reads the public zero-auth /jobs.rss per-tenant feed. For a branded careers domain, set provider: teamtailor and it reads /jobs.rss off that host. Job links may point at a branded custom domain; location comes from the tt: city/country tags, falling back to Remote when a posting carries no tt:city/tt:country but its remoteStatus is remote (fully or temporary). |
| Tech in Asia | API | operator: Tech in Asia (the board is run by the publisher itself; the legal entity should be confirmed out-of-band before listing, per the policy's operator-declaration step). Reads the public Algolia search index (job_postings) that the techinasia.com jobs UI itself queries — a SEA startup-ecosystem board, ~300 live postings when sampled on 2026-09-23, 273 of them Indonesian. Configure with provider: techinasia. The Algolia app id and client search key are public but rotate with the site's build, so they are discovered fresh on every run (search page → the current app.<hash>.js → the config inside it) instead of being hardcoded; an entry may override them with techinasia: { appId, apiKey }. The whole inventory arrives in one request, and the board's own default country_name[]=Indonesia view is deliberately not applied: the complete index is traversed and targeting is left to the scanner's own filters. Posting URLs use the employer's own application link when the row exposes a verifiable one, falling back to the Tech in Asia posting page for shortener links and for links that several postings share (one live example: five distinct roles all pointing at a single intake form — using it would have collapsed five real postings into one row). Salary is deliberately unattached, since the index's salary_min/salary_max/salary_avg fields are flags rather than money. The site's robots.txt explicitly allows ClaudeBot/GPTBot and other AI crawlers by name. |
| Telegram (public channels) | Parser | Reads a channel's public web preview (https://t.me/s/<channel>): 20 posts per page, paging back with ?before=<id> until since_days (default 30) or max_pages (cap 10, warns when it cuts the window short). A post becomes a posting only when it names an employer (a Компания: / Company: line, Title @ Employer, Title | Employer, a second line в Employer — / at Employer, or a hashtag-only first line followed by a bare employer name that does not read as a role such as Product Owner or QA) and links to a vacancy (a vacancy page on the employer's own site, or a known board's per-vacancy page; never a homepage, a page with no vacancy in its path or query, a form, shortener, social footer or LinkedIn post). Then company = that employer, url = that link (the employer's page wins over a board mirror), description = the post plus a Source: https://t.me/<channel>/<id> line. The title is derived from the post: normally its first non-empty line; for a hashtag-first post, the first subsequent line that is neither hashtag-only metadata, the matched employer name, recognized employer metadata nor a bare link. A hashtag-first post without such a line is dropped. Posts without both, hidden-employer posts ("название скрыто", "our client") and digests of several vacancies are dropped, so a community channel of contact-only posts yields little by design; HR-curated channels pass 32-77% of their posts (measured 2026-09-03). Matching runs on this derived title via title_filter; content_filter only narrows it. Configure provider: telegram-channel and channel: <handle> under job_boards:. Public channels only: a private channel, one with the preview switched off, or a page whose markup no longer parses is reported as an error, never as an empty board. |
| Tencent Careers | API | Auto-detects https://careers.tencent.com URLs (host-matched, HTTPS-only) and reads the public zero-auth Query JSON API (zh-CN listings with title, BG, category, JD text, last-update date). Each keywords: entry is queried server-side separately and results are deduped; omit keywords: to pull the whole board. Paginates up to max_pages per keyword (default 100, 100 posts/page). |
| The Hub | API | Reads the board-wide https://thehub.io/api/v2/jobsandfeatured JSON feed (Nordic/EU startups). Configure with provider: thehub; paginates ?page=N up to max_pages (default 3), then scanner filters apply. Every request also sends countryCode (default EU, override via a thehub: { countryCode: ... } config block) since the API scopes results to the caller's geo-IP when it's omitted. Set thehub: { includeRemote: true } to also run a separate isRemote=true pass and merge it in — the site's own "remote" filter is a distinct query mode, not combinable with countryCode. The v2 payload carries no publication date, so Hub postings have no postedAt: date filters (--since, max_age_days) never exclude them, and they sort last among dated results. |
| The Muse | API | Reads the public https://www.themuse.com/api/public/jobs?page={n} JSON feed, fetching all pages sequentially before normalizing. Configure with provider: themuse in a job_boards: entry. |
| TKMS (thyssenkrupp Marine Systems) | API | Single-employer provider for jobs.tkmsgroup.com via the public POST /api/filter/query endpoint; the subclient and locale come from a config block. Select with provider: tkms. |
| Torre | Retired (#4859, 2026-10-08) | The public POST https://search.torre.co/opportunities/_search opportunity search this provider read now answers HTTP 400 {"meta":{"message":"Invalid request"}} to every body (including {}), and torre.ai's own job search is now an AI chat that makes no request to that host, so the API looks changed or retired. provider: torre still resolves in portals.yml for backward compatibility, but fetch() throws immediately with a "source unavailable" explanation instead of one bare HTTP 400 Bad Request per entry — remove the entry from your portals.yml when you see it. |
| UKG Pro / UltiPro Recruiting | API | Auto-detects {host}/{tenant}/JobBoard/{boardId}/ public board URLs across UKG's several recruiting hosts (recruiting.ultipro.ca, recruiting.ultipro.com, recruiting2/recruiting3.ultipro.com, matched by pattern, not a fixed host — preserves whichever host the tenant's board actually uses) and posts to the public no-auth JobBoardView/LoadSearchResults API. Paginates a Top/Skip row offset (50/page) up to max_pages (default 100), stopping at the response's own totalCount, a short page, or an empty page; the safety cap is independent of totalCount and a tenant truncated by it is flagged, not reported as complete. The list payload carries only a brief description — full JD text lives on the OpportunityDetail page's embedded JSON (extracted with a string-aware brace-balancing parser, not a regex) and is opt-in per entry via ultipro: { fetchDetails: true, detailLimit: N } (skipped during health probes). |
| VDAB | API | Reads VDAB's public vindeenjob search API for Flanders/Belgium roles. Configure with provider: vdab; nested vdab.keywords is an explicit override, otherwise falls back to config/profile.yml's target roles. Optional fetchDetails: true follows bounded public detail JSON calls for descriptions (detailLimit, default 25). |
| We Work Remotely | RSS | Reads the public https://weworkremotely.com/remote-jobs.rss feed and parses it in-process. |
| Welcome to the Jungle | API | Queries WTTJ's public Algolia search index (wttj_jobs_production_en) — the same one the welcometothejungle.com jobs UI calls. The public app id + referer-locked client search key are fetched fresh from https://www.welcometothejungle.com/api/env on every run (they rotate), and every Algolia request sends a welcometothejungle.com Referer. The board is global, so an explicit wttj: { queries: ["…"] } config block is required (the provider throws without one); optional max_hits per query (default 100, cap 200). Salary attaches from salary_yearly_minimum/salary_maximum; ", Remote" is appended for fulltime-remote postings. Configure with provider: wttj. |
| Workable | API | Auto-detects https://apply.workable.com/<slug> and reads the public no-auth widget API (/api/v1/widget/accounts/<slug>?details=true), which returns the account's full posting list in one request; falls back to the older, 30-row-capped markdown feed (/<slug>/jobs.md) only if that request fails. Requests carry browser-like headers, retry transient failures with backoff (giving up early on a long Retry-After rather than stall the scan), and are serialized process-wide against the host. |
| Workday | API | Auto-detects <tenant>.<instance>.myworkdayjobs.com[/<locale>]/<site> careers URLs and posts to the public CXS jobs endpoint; paginates via offset up to max_pages (default 100), warning if a tenant's postings exceed the cap. |
| Working Nomads | API | Reads the board-wide https://www.workingnomads.com/api/exposed_jobs/ JSON feed, then applies scanner filters. |
| WP Job Manager | RSS | Generic provider for any job board running the WP Job Manager WordPress plugin. Not auto-detected (a WordPress site has no host pattern), so configure provider: wp-job-manager plus the board's careers_url in a job_boards: entry; only its host is used. Reads the plugin's public /?feed=job_feed RSS feed, 100 items per request (posts_per_page=100, the feed's default is 10), walking paged=N until a short page, up to max_pages (default 20, max 100) with a warning when the cap cuts a full board, and stops with a warning if a full page adds no new postings (a board or cache that ignores paged). Maps title, company, location and publication date. The feed carries no employer application link, so the job URL is the board's own posting page; item links off the board's host are dropped. A response that is not an RSS feed, or a feed whose items all fail to parse, throws instead of reading as an empty board. robots.txt is the board's own: check it before adding an entry (the feed URL carries a query string, so a Disallow: /*? rules the board out). A subdirectory WordPress install is not supported. |
| Yourator | API | operator: 友睿資訊股份有限公司 (Yourator) — declared per Source Indexing Policy rule 4; the submitting contributor is not affiliated with the board (provenance: the operating entity is named on the site's own privacy page). Taiwanese job board for startup and digital roles (1,760 listings across 151 employers when sampled on 2026-08-18). Reads the board-wide zero-auth https://www.yourator.co/api/v4/jobs?page=N JSON feed (20/page; no key, cookie or Referer required). Select with provider: yourator. The API exposes no free-text search parameter — q/keyword/search/term/query/title are accepted and silently ignored — and no filter parameter is needed for full coverage, so the provider walks every page until payload.hasMore turns false and lets title_filter gate the results (rule 5); max_pages (default 120) is a safety bound above the observed 88 pages, not a coverage setting. Per rule 2 the emitted URL is each row's thirdPartyUrl — the employer's own ATS (teamdoor.io, Greenhouse, Lever, BambooHR, Breezy, self-hosted) — with the board's utm_* ad parameters stripped, falling back to the Yourator posting page on the 63.9% of rows that carry none. No absolute timestamp is published (only a relative lastActiveAt string), so postedAt is always omitted. |
| Zoho Recruit | Parser | Auto-detects any <tenant>.zohorecruit.<tld> URL (com, ca, eu, in, com.au, jp, sa, com.cn) and reads the tenant's whole board from its server-rendered /jobs/Careers page in one request, whatever path the URL carries. Postings the site shows as filled are skipped. Optional zohoRecruit: { fetchDetails: true, detailLimit: 25 } fetches each posting's page for the full description, the publish date and the salary. A tenant with only the legacy /recruit/Portal.na career site is not supported and fails with a named error. |
These boards were evaluated alongside providers/careerviet.mjs and found NOT
eligible for the zero-auth/no-browser provider contract. Recorded here so a
future evaluation doesn't repeat the same investigation from scratch.
| Board | Reason | Evaluated |
|---|---|---|
| Lagou (拉勾网) | Its server-rendered search payload is technically parseable, but every usable /wn/zhaopin request needs query parameters and the site's robots.txt disallows /*?* for User-agent: *. The project does not route around that stated restriction. |
2026-08-26 |
| TopDev (topdev.vn) | Search results ARE server-rendered plain HTTPS (confirmed live), but robots.txt disallows ClaudeBot via Cloudflare's default managed AI-bot block (alongside GPTBot, Google-Extended, Bytespider, etc. — generic boilerplate, not a TopDev-specific choice, but still a named block). |
2026-08-26 |
| CareerLink (careerlink.vn) | Search results ARE server-rendered plain HTTPS (confirmed live: 47 job links found on a location listing page), but robots.txt individually lists ClaudeBot, Claude-Web and anthropic-ai as disallowed — separate from its generic bad-bot list, reading as a deliberate choice rather than boilerplate. |
2026-08-26 |
| JobsGo (jobsgo.vn) | Listing pages return HTTP 403 behind a Cloudflare-managed JavaScript challenge, confirmed under both a declared-bot User-Agent and a browser-impersonating one — no zero-auth path exists without solving the challenge. | 2026-08-27 |
| ViecOi (viecoi.vn) | Same failure mode as JobsGo: HTTP 403 behind a Cloudflare-managed JavaScript challenge, consistent across User-Agents — even /robots.txt itself returns the challenge page instead of its contents. |
2026-08-27 |
| Glassdoor (global, not VN-specific) | A reference scraper implementation for this board (goodjobs, a sibling project) requires Playwright plus playwright-stealth (active bot-detection evasion) to get any content — out of scope on both technical and ethical grounds, independent of robots.txt. |
2026-08-26 |
| Dice (global, not VN-specific) | Same failure mode as Glassdoor: the same reference implementation needs stealth-mode browser automation despite Dice's own docs describing it as static HTML. | 2026-08-26 |
TopDev and CareerLink are the interesting case: both are technically
scrapable, but this project treats a robots.txt entry naming Claude or
Anthropic's crawler as the site owner's stated intent, not a literal
User-Agent string to route around under a different name — even though
providers/*.mjs all identify as career-ops, not ClaudeBot.
When adding a new provider, add a new non-helper module under providers/ and
update this table in the same PR — see
Adding a provider for the full
requirements.