Company Domain to Job Postings: Greenhouse, Lever, Ashby
Pricing
Pay per event
Company Domain to Job Postings: Greenhouse, Lever, Ashby
Paste company domains, get their live job postings. Finds each company's ATS board across Greenhouse, Lever, Ashby, Recruitee, SmartRecruiters and Personio, verifies it really belongs to them, and returns every open role in one schema with salary, seniority and remote type. No slug hunting.
Pricing
Pay per event
Rating
0.0
(0)
Developer
youssef farhan
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
2 days ago
Last modified
Categories
Share
Paste company domains, get their live job postings. Finds each company's ATS board across Greenhouse, Lever, Ashby, Recruitee, SmartRecruiters and Personio, verifies it really belongs to them, and returns every open role in one schema with salary, seniority and remote type. No slug hunting.
Every other multi-ATS job scraper makes you supply a board slug first: greenhouse:stripe, lever:spotify. Your CRM export does not contain those. It contains stripe.com.
This Actor closes that gap. Give it a domain and it finds the company's careers page, works out which applicant tracking system they use, verifies the board actually belongs to that company, and returns every open role in one unified schema.
stripe.com -> greenhouse:stripe -> 578 jobschannable.com -> recruitee:channable -> 13 jobspersonio.com -> personio:personio -> 1 jobmonzo.com -> greenhouse:monzo -> 110 jobsexample.com -> no board found -> reported, not guessed
No login. No API key. No proxy. No browser.
Why verification matters
Guessing a board slug from a domain is easy and wrong often enough to poison a dataset. During development, a plain guess resolved:
personio.comto an unrelated Recruitee demo board named "FD Sandbox"example.com, IANA's reserved domain, to a Greenhouse board belonging to "Democorp"neon.techto a different company also called Neon, on Lever
All three looked like clean successes. All three would have been billed and shipped into your dataset.
So this Actor accepts a board only when it can prove ownership, by either of two independent routes:
- The board mentions the company's own domain, in apply URLs, company fields or description text. Measured across the known cases, correct boards mentioned the domain between 32 and 840 times, and wrong boards mentioned it zero times.
- The board's company name contains the domain label. This catches boards with no description text at all, where route 1 has nothing to read.
A board the company links to from its own careers page is accepted as a second pass even without either signal, and flagged in resolved_via so you can tell the difference.
A domain that cannot be resolved is reported, never guessed. Unresolved domains appear in the COMPANY_REPORT record with their status, and they are not charged.
Supported ATS platforms
| ATS | Source | Descriptions | Structured salary |
|---|---|---|---|
| Greenhouse | boards-api.greenhouse.io | Yes | Parsed from description |
| Lever | api.lever.co | Yes | Parsed from description |
| Ashby | api.ashbyhq.com | Yes | Yes, native compensation tiers |
| Recruitee | {company}.recruitee.com | Yes | Yes, native salary object |
| SmartRecruiters | api.smartrecruiters.com | Yes, one extra request per job | Parsed from description |
| Personio | {company}.jobs.personio.de | Yes | Parsed from description |
Adding a seventh platform means adding one file. If one you need is missing, suggest it here.
Quick start
Example input
{"companies": ["stripe.com","monzo.com","channable.com","https://www.airtable.com"],"keywords": ["engineer", "data"],"remoteOnly": true,"includeDescription": true,"descriptionFormat": "markdown","incremental": false,"maxItems": 0}
You can mix in boards you already know, and they skip the lookup entirely:
| Format | Example | Behaviour |
|---|---|---|
| Company domain | stripe.com | Resolved and verified |
| Company website URL | https://www.airtable.com | Resolved and verified |
| Board slug | stripe | Probed across all six platforms |
| Qualified slug | lever:spotify | Used directly, no lookup |
| ATS board URL | https://jobs.ashbyhq.com/ramp | Used directly, no lookup |
How resolution works
Candidates are collected cheaply, then verified against the real board APIs. The resolved_via field on every row records which route won.
resolved_via | Meaning |
|---|---|
homepage | An ATS link was in the site's own homepage markup |
careers-page | Found on a careers page the homepage linked to |
common-path | Found at /careers or /jobs |
careers-subdomain | Implied by a careers subdomain the company publishes, e.g. jobs.channable.com |
slug-guess | Guessed from the domain label, then corroborated |
slug-probe | You supplied a bare slug and it was probed across platforms |
given | You named the board directly |
...-redirect | Found in a redirect target rather than page body |
...-unconfirmed | The company linked to this board, but the board never mentions the company |
Unified output schema
Every record carries every field, on every platform. A field the source does not publish comes back null rather than missing, so downstream code never branches on which ATS a row came from.
| Field | Type | Description |
|---|---|---|
job_id | string | Stable id, ats:slug:id. Unique across platforms, steady between runs |
ats | string | greenhouse, lever, ashby, recruitee, smartrecruiters or personio |
company | string | Company name as published |
company_slug | string | The board slug it was scraped from |
source_domain | string | The domain this row was resolved from, for joining back onto your input list |
title | string | Job title as posted |
department | string | Department or job family |
team | string | Sub-team or job function |
location | string | Primary location string |
locations | array | Every location on the posting |
country | string | From the platform field, or inferred from the location text |
is_remote | boolean | True for remote roles. Hybrid is false |
remote_type | string | remote, hybrid or onsite |
employment_type | string | full_time, part_time, contract, temporary, internship, volunteer |
seniority | string | internship, entry, mid, senior, staff, principal, lead, manager, director, executive |
salary_min | number | Lower bound of the pay range |
salary_max | number | Upper bound of the pay range |
salary_currency | string | ISO 4217 code, e.g. USD, EUR, GBP |
salary_period | string | hour, day, week, month or year |
salary_raw | string | The pay string as published, so a parsed range can be checked |
description_html | string | Full body as HTML |
description_markdown | string | Full body converted to Markdown |
description_text | string | Full body as plain text |
apply_url | string | Direct link to the application form |
job_url | string | Public link to the posting |
posted_at | string | ISO 8601 first publication |
updated_at | string | ISO 8601 last edit reported by the platform |
scraped_at | string | ISO 8601 timestamp of this run |
change_status | string | new or updated in incremental mode |
resolved_via | string | How the board was found, per the table above |
Only the description field you asked for is filled. The other two stay null, so a record never carries the same body three times.
Example output
{"job_id": "recruitee:channable:2697907","ats": "recruitee","company": "Channable","company_slug": "channable","source_domain": "channable.com","title": "Technical Customer Support DACH - German speaking","department": "Support","team": "customer_service","location": "Utrecht, Utrecht, Netherlands","locations": ["Utrecht, Utrecht, Netherlands"],"country": "Netherlands","is_remote": false,"remote_type": "hybrid","employment_type": "full_time","seniority": "entry","salary_min": 2850.0,"salary_max": 2950.0,"salary_currency": "EUR","salary_period": "month","salary_raw": null,"description_html": null,"description_markdown": "Are you fluent in German and passionate about solving complex problems...","description_text": null,"apply_url": "https://jobs.channable.com/o/technical-customer-support-dach-german-speaking-2/c/new","job_url": "https://jobs.channable.com/o/technical-customer-support-dach-german-speaking-2","posted_at": "2026-08-03T15:40:43+00:00","updated_at": "2026-08-04T07:14:55+00:00","scraped_at": "2026-08-16T14:06:44.989721+00:00","change_status": null,"resolved_via": "slug-guess"}
The company report
Alongside the dataset, every run writes a COMPANY_REPORT record with one row per input, so you always know what happened to each domain:
[{"input": "channable.com", "domain": "channable.com", "ats": "recruitee","slug": "channable", "resolvedVia": "slug-guess", "jobsFound": 13,"jobsKept": 13, "status": "resolved"},{"input": "example.com", "domain": "example.com", "ats": null, "slug": null,"resolvedVia": null, "jobsFound": 0, "jobsKept": 0, "status": "unresolved"}]
Use cases
Enrich a CRM or prospect list. Your sheet has domains. Feed them in, get back every open role per company, joined on source_domain. No manual slug hunting, no dead ends left unexplained.
Score hiring intent for sales. A company that just opened five RevOps roles has budget. Filter by keywords, run on a schedule, and watch change_status: new.
Track a VC portfolio or competitor set. You know the domains, not the ATS vendors. Resolution handles the difference, and re-checks it on every run in case a company migrates platform.
Build a niche job board. Stable job_id makes upserts trivial. Salary and remote type are already normalized across all six platforms.
Run compensation benchmarking. salary_min, salary_max, salary_currency and salary_period are normalized everywhere, so you can aggregate pay bands by title, seniority and country with no cleaning step.
Feed an AI recruiting agent. Markdown descriptions and a flat schema drop straight into a RAG pipeline with no HTML stripping.
Filters and modes
| Input | What it does |
|---|---|
keywords | Keep only titles containing one of these words. Case-insensitive |
locationFilter | Keep only jobs whose location mentions one of these |
remoteOnly | Keep only roles classified as remote. Hybrid does not count |
includeDescription | Turn off to skip description fetching. Makes SmartRecruiters boards dramatically faster |
descriptionFormat | markdown, html or text |
incremental | Emit only new or changed jobs since the last run |
maxItems | Stop after N jobs. 0 means no limit |
preset | Add a curated company list: yc_companies, ai_labs or unicorns |
How incremental mode works
Each job is fingerprinted over the fields that matter (title, location, department, employment type, salary, apply URL, updated date and description). Fingerprints live in a named key-value store that survives between runs.
- First run: everything is
new, because nothing has been seen yet. - Later runs: only jobs whose fingerprint changed come back, tagged
updated, plus genuinely new postings taggednew.
scraped_at is excluded from the fingerprint, since it changes on every run by definition.
Frequently asked questions
What if a company does not use one of the six platforms?
It is reported as unresolved in COMPANY_REPORT and costs you nothing. It is never guessed at and never silently dropped.
How accurate is the resolution?
Across a 13-domain test set spanning all six platforms, 10 resolved and every one was correct. The three that did not resolve genuinely had no supported board: IANA's reserved example.com, a company acquired and folded into another's careers site, and one using an unsupported ATS. Zero false positives is the design goal, and it is why boards must prove ownership before they are accepted.
Why did my domain not resolve when the company clearly has a Greenhouse board?
The most common cause is a fully JavaScript-rendered careers page that never puts the board URL in the HTML. This Actor is HTTP-only by design, which is what makes it fast and cheap. If you know the slug, pass greenhouse:theirslug directly and it will be used with no lookup.
Do I need an API key for any of these platforms? No. All six endpoints are the public job-board APIs those platforms serve to render company career pages.
Does this need a proxy? No. These are public APIs with no anti-bot layer, so there is no proxy configuration at all.
What happens if an input is wrong? It is logged and skipped, and the run continues. A typo can never end a run or empty a dataset.
Why is salary_min empty on some jobs?
Most companies do not publish pay. Ashby and Recruitee expose structured compensation, read directly. For the other four the Actor parses ranges out of the description, accepting a match only when it sits next to a pay-related word and falls in a plausible band, so unrelated figures in the prose are not misread.
Why is country sometimes empty?
Because guessing wrong is worse than leaving it blank. Two-letter codes that are both an ISO country and a US state (CA, DE, IL, IN, AR, CO, ID) are never resolved from the code alone. Chicago, IL resolves through the city; Boise, ID stays empty.
Can I run this on a schedule?
Yes. Pair a schedule with incremental: true and each run returns only that period's movement.
Pricing
$1.00 per 1,000 jobs. $6.00 per 1,000 companies discovered, once each.
Pay per event, and platform usage is free: no compute units on top, no monthly fee beyond your Apify plan.
| Event | What triggers it | Free | Starter | Scale | Business |
|---|---|---|---|---|---|
| New company board discovered | A domain is matched to a verified board | $6.00 / 1,000 | $6.00 / 1,000 | $5.50 / 1,000 | $5.00 / 1,000 |
| Company board read | A board is read from a slug you gave or already discovered | $2.00 / 1,000 | $2.00 / 1,000 | $1.90 / 1,000 | $1.80 / 1,000 |
| Job posting | A job is written to the dataset | $1.00 / 1,000 | $1.00 / 1,000 | $0.95 / 1,000 | $0.90 / 1,000 |
| Actor start | A run starts | $0.00005 | $0.00005 | $0.00005 | $0.00005 |
Higher plan tiers carry better rates: Starter gets the Bronze discount, Scale the Silver, Business the Gold. Starting a run is effectively free at $0.00005, so scheduling this every hour costs nothing in fixed fees.
Three things make it cheaper than it looks:
- Discovery is charged once per company, not once per run. Boards that are found get remembered between runs, so the second run over the same domain list bills the $2.00 / 1,000 read rate instead of the $6.00 / 1,000 discovery rate. A remembered board is still fetched live every run, so a company that switches ATS is re-discovered automatically.
- The two company events never both fire. Discovery already covers reading the board it found.
- Unresolved domains cost nothing. Discovery bills only on success, after the board has been verified against the live API.
| Scenario | Cost |
|---|---|
| Trial: 3 new domains, 150 jobs | $0.17 |
| CRM enrichment, first run: 50 domains (40 resolve), 4,000 jobs | $4.24 |
| Same list, every run after: 40 remembered boards, 4,000 jobs | $4.08 |
| Known slugs, no discovery: 20 boards, 2,000 jobs | $2.04 |
| Hourly incremental watch: 40 remembered boards, 40 changed jobs | $0.12 per run, about $86 a month |
Free-plan runs are limited to 3 companies and 50 jobs. Subscribe to an Apify plan to remove the caps.
Notes and limits
- Resolution is HTTP-only. A careers page that renders its board purely in JavaScript will not resolve; pass the slug directly in that case.
- SmartRecruiters answers an unknown company with an empty list rather than a 404, so a bare slug is never auto-detected as SmartRecruiters. Use
smartrecruiters:Slugfor those boards. The slug is case-sensitive there. - Personio boards are served in the language the company publishes. The Actor does not force a language, because requesting one the board does not publish returns postings with empty descriptions.
- Runs are migration-safe. Progress is checkpointed after every company, so a run moved to another host resumes instead of restarting.
๐ผ Need a custom solution? Reach out at youssefarhan24@gmail.com ๐ Got a site in mind? Suggest it here