ATS Jobs by Company Domain avatar

ATS Jobs by Company Domain

Pricing

from $2.00 / 1,000 jobs

Go to Apify Store
ATS Jobs by Company Domain

ATS Jobs by Company Domain

Resolve which applicant tracking system a company uses from its domain (Greenhouse, Lever, Ashby, Workable, SmartRecruiters, Recruitee) through the vendors' own public JSON endpoints, then return every live opening as one flat record with a stable id, location, remote flag and apply URL.

Pricing

from $2.00 / 1,000 jobs

Rating

0.0

(0)

Developer

Omar Nagy

Omar Nagy

Maintained by Community

Actor stats

0

Bookmarked

1

Total users

0

Monthly active users

7 days ago

Last modified

Share

Give it a company domain. Get back every live opening that company is currently advertising, already normalised.

This Actor works out which applicant tracking system a company publishes on, then reads that vendor's own public JSON job board endpoint. It covers Greenhouse, Lever, Ashby, Workable, SmartRecruiters and Recruitee. There is no HTML scraping of job pages, no headless browser, no proxy and nothing to log in to.

Up to 500 companies per run, processed in internal batches of 20 with 4 companies resolved at a time. Every input entry gets exactly one resolution record saying what happened to it. The default cap is the first 100 entries, which is a cost guard on a pay-per-event Actor: set maxCompanies to 500 when you want the whole list.

Who it is for

AI agents and job pipelines that start from a company, not from a job board.

The usual flow is backwards: you search a job aggregator, get stale reposts, and never know which company is really hiring. Here you start from a list of companies you already care about, a target account list, a portfolio, a set of competitors, and the Actor tells you where each one posts and what is open right now, straight from the source the company itself publishes.

Concretely:

  • An agent asked "is Stripe hiring backend engineers in Dublin" can answer from the company's own board in one call.
  • A recruiting or sales tool enriches a CRM account list with live headcount signals.
  • A job board or newsletter pulls fresh listings for a curated set of companies without scraping anyone.
  • A founder tracking competitors sees which teams a rival is staffing up.

What it does

  1. Turns each input into board slug candidates. https://www.stripe.com/jobs becomes stripe.
  2. Probes the six ATS endpoints in a fixed order until it finds a board with live openings.
  3. If nothing is found, reads the company's own /careers page and homepage and follows the job board link it finds there. This is what resolves companies whose board slug is not their domain: oysterhr.com publishes on Ashby as oyster, a.team as a-team.
  4. Pages through the winning board deterministically and returns one flat record per job.
  5. Writes one resolution record per input entry so nothing disappears silently.

Companies it cannot place come back as records with a status, a reason and the list of boards that were tried, not as a failed run.

Input

{
"mode": "jobs",
"companies": ["stripe.com", "spotify.com", "posthog.com"],
"maxJobsPerCompany": 25,
"includeDescription": true,
"descriptionMaxChars": 600,
"includeResolutionRecords": true
}
FieldTypeDefaultNotes
modejobs or resolvejobsRequired. resolve only reports which ATS each company uses and how many jobs it has. Any other value is rejected.
companiesarray of stringsRequired in both modes. Domain, full URL or plain company name. Up to 500 per run, the first 100 by default.
maxCompaniesinteger 1 to 500100Safety cap on how many entries are processed. The default is a cost guard: a large pasted list gets a bounded first run rather than a surprise bill. Raise it to 500 for the whole list; it can lower the ceiling but never raise it past 500.
maxJobsPerCompanyinteger 1 to 1000200Newest jobs first, counted after any role filter. Ignored in resolve mode.
includeDescriptionbooleantrueAdds plain text descriptions.
descriptionMaxCharsinteger 0 to 200001200Only used when includeDescription is on. 0 keeps the full text.
includeResolutionRecordsbooleantrueWrites the type: "resolution" rows.
employmentTypeFilterarray of stringsnoneKeep only these employment types, for example ["Contract", "Freelance", "Part-time"].
titleRegexstringnoneKeep only titles matching this case-insensitive regular expression. Max 200 characters.
resolveViaCareersPagebooleantrueRead the careers page when slug guessing finds nothing. Turn it off for the lowest latency.

Both filters are off unless you set them, so an unfiltered run returns everything.

Filtering for contract, fractional and part-time roles

{
"mode": "jobs",
"companies": ["gitlab.com", "zapier.com", "n8n.io"],
"employmentTypeFilter": ["Contract", "Freelance", "Part-time"],
"titleRegex": "(contract|fractional|freelance|part[- ]time|interim)",
"maxJobsPerCompany": 50
}

Two rules worth knowing:

  • When both filters are set, a job is kept if it matches either one. That is deliberate. Greenhouse's public board endpoint publishes no employment type at all, so requiring both would silently discard every Greenhouse result from a contract search.
  • Filtering happens before the per-company cut, so maxJobsPerCompany counts matching jobs. jobs_found in the resolution record stays the board's unfiltered total, and the reason string says so when a filter is active.

When the slug does not match the domain

Careers-page discovery handles most of these automatically. When you already know the answer, skip the guessing with the ats:slug prefix form. Ubisoft is on SmartRecruiters as Ubisoft2, which no amount of guessing from ubisoft.com will find:

{ "mode": "jobs", "companies": ["smartrecruiters:Ubisoft2", "workable:exotec", "recruitee:bunq"] }

Valid prefixes: greenhouse, lever, ashby, workable, smartrecruiters, recruitee. An explicit override is taken as final, so careers-page discovery does not run for that entry.

Output

Two record types share one dataset, split by the type field, with a saved view for each in Apify Console.

A job record

Real output, run on 5 September 2026.

{
"type": "job",
"company_input": "spotify.com",
"company": "spotify",
"ats": "lever",
"ats_slug": "spotify",
"record_key": "lever:spotify:9e8b3af7-d0f7-4aa9-ab8d-89b5bd238ccf",
"job_id": "9e8b3af7-d0f7-4aa9-ab8d-89b5bd238ccf",
"title": "Talent Acquisition Lead",
"department": "People",
"location_raw": "Singapore",
"city": "Singapore",
"country": "Singapore",
"country_code": "SG",
"is_remote": false,
"workplace_type": "onsite",
"employment_type": null,
"posted_at": "2026-09-04T18:33:00.688Z",
"updated_at": null,
"job_url": "https://jobs.lever.co/spotify/9e8b3af7-d0f7-4aa9-ab8d-89b5bd238ccf",
"apply_url": "https://jobs.lever.co/spotify/9e8b3af7-d0f7-4aa9-ab8d-89b5bd238ccf/apply",
"description": "Join Spotify's Talent Acquisition team and help us find and engage the people who will shape what comes next...",
"source_url": "https://api.lever.co/v0/postings/spotify?mode=json",
"scraped_at": "2026-09-04T22:38:01.118Z"
}

record_key is ats:slug:job_id and is unique across the run, so you can upsert on it. workplace_type is one of remote, hybrid, onsite, unknown, and is_remote is always a boolean, never null.

A resolution record

Real output, run on 6 September 2026. This is the careers-page path: the slug oysterhr has an abandoned Workable board, and the real one is Ashby under oyster.

{
"type": "resolution",
"company_input": "oysterhr.com",
"company": "oyster",
"status": "resolved",
"ats": "ashby",
"ats_slug": "oyster",
"jobs_found": 25,
"jobs_returned": 25,
"reason": "Resolved to Ashby with 25 open job(s), 25 returned.",
"probes_attempted": 7,
"candidate_slugs": ["oysterhr", "oyster"],
"candidates_tried": [
"greenhouse:oysterhr", "lever:oysterhr", "ashby:oysterhr",
"workable:oysterhr", "smartrecruiters:oysterhr", "recruitee:oysterhr",
"ashby:oyster"
],
"discovered_via": "https://www.oysterhr.com/careers",
"source_url": "https://api.ashbyhq.com/posting-api/job-board/oyster",
"scraped_at": "2026-09-06T06:01:35.631Z"
}

status is one of:

StatusMeaning
resolvedA live board with at least one open job was found.
board_found_emptyA board exists under that slug but currently lists no jobs. This is a weak signal, not proof the company uses that ATS. candidates_tried shows everything that was ruled out first.
not_foundNo public board answered on any of the six systems, for any candidate slug, and the careers page carried no board link.
input_errorThe entry or the run input was not usable. The reason says why. A valid domain never lands here.

A sample of both record types is in examples/sample-output.json in the source. Note that description text comes verbatim from the company, so it can contain any punctuation the company used.

Use it from an agent (Standby)

Standby keeps the Actor warm so an agent can call it mid task with no container start. The Actor's Standby base URL is shown on its Apify page; the published example task exposes one too.

GET {standbyUrl}/ service description and limits
GET {standbyUrl}/resolve?companies=stripe.com,spotify.com
GET {standbyUrl}/jobs?companies=stripe.com,posthog.com&maxJobsPerCompany=25&descriptionMaxChars=600
GET {standbyUrl}/jobs?companies=gitlab.com,zapier.com&employmentTypeFilter=Contract,Freelance&titleRegex=(contract|fractional)

Every input field works as a query parameter, and list fields accept a comma-separated string. Calls are authenticated the way every Apify Standby Actor is, with your token:

curl -H "Authorization: Bearer $APIFY_TOKEN" \
"https://omargnagy--ats-jobs-by-domain.apify.actor/jobs?companies=stripe.com,posthog.com&maxJobsPerCompany=5"

A POST to the same paths is accepted and behaves identically; the parameters are read from the query string either way.

Response shape:

{
"ok": true,
"mode": "jobs",
"resolutions": [ { "type": "resolution", "...": "..." } ],
"jobs": [ { "type": "job", "...": "..." } ],
"count": 59,
"tookMs": 851
}

Route discipline is strict, because an agent needs a wrong call to fail loudly rather than return plausible nonsense:

CodeWhen
200A valid call. ok is true.
400A caller mistake, such as a missing companies or an invalid titleRegex. The reason is in error.
404An unknown path such as /postings. Never data.
413More than 50 companies in one HTTP call. Use a batch run for up to 500.
500A genuine internal fault.

Limits on the HTTP path: 50 companies per call, because the caller is waiting on the answer. Board responses are cached in memory for 10 minutes, so a burst of calls about the same company costs the vendor one request rather than one per call. Batch runs do not cache, so a scheduled run always reads fresh.

In batch mode a bad input is written as an input_error record and the run still finishes successfully, so the caller gets an explanation instead of a red run with nothing in it. A value the input schema itself rejects, such as a mode outside the enum, is refused by the platform before the Actor starts, so that one never reaches the dataset.

Pricing

Pay per event, no start fee, no subscription.

EventPriceCharged when
resolution$0.01Once per distinct ATS board that resolved to live openings.
job$0.002Once per normalised job record returned, in batch mode and through Standby.

What is free: a domain that resolves to nothing, a board that exists but is empty, an input_error row, and every duplicate suppressed inside a run. You pay for boards that answered and for records you received.

Worked example. 20 companies of which 12 resolve, returning 418 job records: 12 resolutions at $0.01 plus 418 jobs at $0.002 is $0.956.

Charging happens after each internal batch of 20 companies lands in the dataset, not at the end of the run, and the checkpoint is written before the charge rather than after it. A run that is aborted or migrated half way is therefore charged for at most what it delivered, and a resumed run does not charge for the same board twice. The one window that remains is a migration between the checkpoint and the charge, which loses that batch's charge: the error is deliberately in your favour, never against you.

job is charged before resolution. If you set maxTotalChargeUsd on the run, the cap is spent on the records you came for first. A charge the platform rejects is logged and the run carries on, so you still get the whole dataset.

Limits and honest caveats

  • 500 companies per batch run, 50 per Standby HTTP call. Entries past the cap are written as input_error rows with the reason, never dropped in silence.
  • Six systems only. Greenhouse, Lever, Ashby, Workable, SmartRecruiters, Recruitee. A company on Workday, Taleo, SuccessFactors, BambooHR, Personio or a hand built careers page comes back as not_found.
  • Careers-page discovery only reads the served HTML of /careers and the homepage, and only when slug guessing has already failed. If the board link is injected by client-side JavaScript, discovery will not see it. It reads at most 400 KB per page with an 8 second timeout, and it never runs when you give an explicit ats:slug override.
  • Discovery honours robots.txt. It is the only path here that fetches a company's own website rather than a vendor's public job-board endpoint, so before each page it reads that origin's robots.txt once, with a 3 second timeout, and matches the group named ats-jobs-by-domain or the * group. A page that is disallowed is skipped and the resolution reason says so. A robots.txt that is missing, empty, unreachable or slow is treated as no restrictions, which is what the exclusion standard asks for and means a flaky host is not mistaken for a refusal.
  • An empty Ashby answer is ambiguous. Ashby's public job board API answers {"jobs":[]} both for a board with no openings and for a company that has not switched that API on. Measured on deel.com on 6 September 2026: its own careers page carried live Ashby postings while the public endpoint returned none. Those come back as board_found_empty with that caveat in the reason, never as a confident zero.
  • SmartRecruiters answers 200 for every string, including nonsense, so it is only ever claimed when it actually returns postings. It is never used as the "empty board" fallback.
  • employment_type is always null for Greenhouse. Greenhouse's public job board endpoint does not publish it. Lever, Ashby, Workable, SmartRecruiters and Recruitee do. This is why employmentTypeFilter and titleRegex combine with OR rather than AND.
  • company falls back to the board slug for Lever and Ashby, because those two endpoints carry no company display name.
  • updated_at is only available from Greenhouse and Recruitee. The others publish a created or published date only.
  • Location parsing is conservative. Greenhouse publishes one free text location string, so a multi location posting such as SF, NYC, SEA, CHI keeps location_raw and leaves city null rather than guessing. Two letter tokens are never read as countries in free text, because San Francisco, CA means California.
  • resolve mode ignores the role filters. It stops before job mapping, so jobs_found there is always the board's full total and employmentTypeFilter and titleRegex have no effect on it. Use mode: "jobs" when you want a filtered count.
  • Descriptions are plain text and truncated at descriptionMaxChars. Set it to 0 for the full text.
  • Job postings are public data the companies publish themselves. Respect each vendor's terms and any applicable law in how you store and use it.

What it does not do

It does not scrape LinkedIn, Indeed, Glassdoor or any job aggregator, it does not render careers pages in a browser or bypass any bot protection, it does not touch salary estimates, applicant data or anything behind a login, and it does not apply to jobs on your behalf.

FAQ

Does it need a proxy? No. Every job endpoint it calls is a public, documented, no auth JSON endpoint served by the ATS vendor.

How do I find out which ATS a company uses without pulling all its jobs? Run mode: "resolve". It reports the ATS, the slug and the open job count, and fetches no job records.

Why did my company come back as board_found_empty? A board exists under that slug but has no live openings, or the vendor's public API is not switched on for it. Check candidates_tried to see everything that was ruled out, and use the ats:slug override if you know the real board.

What happens to a list of 400 companies? It runs. The list is processed in batches of 20 with 4 companies resolved at a time, results are written after each batch, and every entry gets its own resolution record.

Can I schedule it? Yes. Use Apify's scheduler for batch runs, or call the Standby endpoint from your own job.

Is the output stable enough to upsert into a database? Yes. record_key is stable for as long as the posting exists on the board, and job records are deduplicated within a run.


Built by Omar Nagy. Part of an agent-native data-tool series on Apify.