# Jobs Search API: 236,000 Roles, No Cookies, No Company List (`glitchbound/ats-jobs-search`) Actor

Search every open role across 7,368 Greenhouse, Ashby and Workable boards at once, without knowing a company name. The data comes from each employer's own board, so there is no login and no cookies. Or switch to Companies mode for who is hiring hardest and the departments they just entered.

- **URL**: https://apify.com/glitchbound/ats-jobs-search.md
- **Developed by:** [Daniel Meshulam](https://apify.com/glitchbound) (community)
- **Categories:** Jobs, Lead generation, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.10 / 1,000 job postings

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Jobs Search API: 236,000 Roles, No Cookies, No Company List

Search **230,508 open roles** across **7,131 company job boards** on Greenhouse,
Ashby and Workable at once. You do not need to know a single company name.

Every row comes from the employer's own careers API, the one they publish so
job aggregators can syndicate it. That is the whole difference from a scraper
that reads a job site: **there is no login, no cookies to hand over, and no
account of yours in the loop.**

Two modes, because two very different people need this data.

**Jobs** returns one row per open role. **Companies** returns one row per company
ranked by how hard they are hiring right now, with the departments and locations
they have just entered called out:

```
company                open  30d   7d   pace      just started hiring for
Bjak                   1378 1174  361   surging   A1 CEO Office, A1 Engineering, A1 HR
Renuity                 437  364  152   surging   Corporate, Field
Mindrift                275  262  246   surging   Creator (Writer)
```

A company opening its first security, sales or finance roles has just changed
strategy, and that is the strongest buying signal in the dataset. Staffing
agencies are excluded from Companies mode by default, because they post on
behalf of others and otherwise dominate every ranking.

Most job scrapers ask you for a list of companies. That only works if you
already have the list, which is the hard part. This one holds the index, so you
ask a question instead:

> senior backend engineers in Berlin, remote, posted since July, not internships

No API key. No cookies. No proxy. No company list. No rate limits to work
around.

**Duplicates are already merged.** A role posted to several offices, or listed
twice by the same board, arrives once with its locations kept together: 29,653
rows, 11.1% of the raw corpus, collapsed at build time rather than left for you
to deduplicate.

### What you get back

One row per job, as clean JSON:

| field | notes |
|---|---|
| `title` | the role, as the employer wrote it |
| `company` | the employer's real name, on 99% of rows; the board token where the ATS publishes no name |
| `location` | as published; `locations` too when a job is open in several places |
| `country` | resolved from that string, on 87.8% of roles; `countries` when a role spans several |
| `region`, `city` | the state or province and the city, where the string gave one |
| `url` | the real application link, on the employer's own site |
| `department` | present on 44% of jobs |
| `postedAt` | ISO-8601, present on 99.8% |
| `seniority` | intern, junior, senior, staff, principal, lead, director, executive |
| `employmentType` | full-time, part-time, contract, temporary, internship |
| `workArrangement` | remote, hybrid, onsite; 42,894 roles are remote |
| `salary` | the band the employer published, on 7.7% of jobs, Ashby only |
| `platform`, `boardToken`, `jobId` | where it came from, so you can go back to the source |

### Filters

Text filters accept several lines, OR-ed together, and a trailing `*` for
prefix matching (`engineer*` also matches engineering). A multi-word line is a
phrase, so `product manager` will not match "Product Marketing Manager".

- **`title`**, **`company`**, **`location`**, **`department`** — match a field
- **`countries`** — match a resolved country, so German roles arrive whether the
  board wrote `Berlin, Germany`, `Berlin`, `Munich, DE` or `DE - Berlin`
- **`keywords`** — match any of those fields at once
- **`titleExclude`**, **`companyExclude`**, **`locationExclude`** — drop matches
- **`workArrangements`** — remote / hybrid / onsite
- **`seniority`** — intern through executive
- **`employmentTypes`** — full-time through internship
- **`platforms`** — greenhouse / ashby / workable
- **`postedAfter`** — an ISO date
- **`maxResults`** — ceiling on rows returned and charged (1,000 default, 10,000 max)

**Companies mode** adds:

- **`hiringLabels`** — surging (top 1%), growing (top 10%), steady, slow, quiet
- **`minOpenedLast30`**, **`minOpenedLast7`**, **`minOpenRoles`**
- **`enteringNewDepartments`** — only companies moving into a new function
- **`includeStaffingAgencies`** — off by default

Excluded and filtered jobs are never returned and never charged.

### Honest limits

These matter more than the feature list, so they are here rather than buried.

**How the labels are made.** `workArrangement` and `employmentType` come from
what the board itself publishes wherever it publishes one: Ashby states a
workplace type, Workable states a remote flag and an employment type. Where the
board says nothing, the label is read from the title and location text. A job
that could not be classified is left **unlabelled** rather than guessed into a
bucket, so filtering on a label narrows to jobs that actually carry it:

```
workArrangement   100% labelled
employmentType     42% labelled
seniority          33% labelled
```

`seniority` is derived from the title on every platform, because no ATS
publishes a seniority field. Two thirds of titles say nothing recognisable.

**How "recently opened" is known.** Two sources, and every company row says
which one answered in `signalSource`. `postedAt` is the employer's own stamp,
on 99.8% of jobs. `firstSeen` is the day this index first saw the posting,
which is better because it moves when a company quietly reposts a role, and it
is the only thing that can ever tell you a job **closed**. A job that was
already open when this index started watching has no meaningful `firstSeen`, so
those fall back to `postedAt` rather than pretending we saw them appear.
`closed30` and `net30` are absent, not zero, until enough history exists to
know: "closed nothing" and "we do not know yet" must never look the same.

**Freshness, with the actual distribution.** The index is rebuilt nightly from
the live boards, not scraped per run, which is what makes a query cost a
fraction of a cent instead of eight thousand HTTP requests. Every role here is
one the employer still has up, but "still up" is not the same as "just posted",
so here is the spread by the employer's own `postedAt`:

```
under 1 week      9%
under 1 month    33%
under 3 months   63%
over a year      11%
```

The long tail is real: some boards keep evergreen pipeline reqs open for years.
Filter with `postedAfter` if you only want fresh ones, and follow `url` to the
employer's own page, which is always authoritative.

**Country is resolved, not searched as text.** Employers publish a location
however they like, and 288,814 place strings across this corpus come in three
shapes at roughly a third each: no comma (`Remote`, `London`), one (`San
Francisco, CA`), two (`Bengaluru, Karnataka, India`). `country` resolves 87.8%
of roles to a single name so you can ask for Germany once instead of guessing
four spellings.

**What it refuses to answer is the point.** `EMEA`, `Europe` and `Worldwide` are
not countries and get none. A bare `Cambridge` gets none either: on the rows
that do carry a disambiguator, this corpus resolves Cambridge to the United
States 439 times and the United Kingdom 143, so a bare one is a coin flip and is
left empty rather than filled in confidently. The same applies to `Birmingham`,
`Vienna`, `Valencia` and a bare `Georgia`, which is a US state and a country. A
name is only answered where one country takes at least 80% of the disambiguated
evidence, which is why `London` (99%) and `San Jose` (96%) still resolve.

**Coverage.** Greenhouse, Ashby and Workable. **Lever is not included**: their
robots.txt blocks the crawler this index is seeded from, so there is no
complete list of Lever boards to build from and a partial one would be worse
than none. If you need one specific Lever company, use our Lever Jobs Scraper.

**No descriptions.** A search returns the job; fetching its full description is
one more call against that one board. Carrying descriptions would make this
index eighteen times larger to serve a query almost nobody makes.

**Salary** is present on 7.7% of jobs, 17,767 of them, from Ashby only, as the
band the employer published rather than a number parsed out of prose.
Greenhouse and Workable expose no salary field at all, so the honest options
were to ship what exists or to guess, and guessing a salary is worse than
omitting one.

Every one of those bands also ships **as numbers**, so you can ask for roles
over $150k instead of reading strings:

| field | what it is |
|---|---|
| `salary` | exactly what the employer wrote, e.g. `$250K - $300K` |
| `salaryMin` / `salaryMax` | the band as numbers, in its own currency and period |
| `salaryCurrency` | `USD`, `EUR`, `GBP`, `CAD`, … |
| `salaryPeriod` | `year`, `month`, `week`, `day`, `hour` |
| `salaryAnnualMin` / `salaryAnnualMax` | normalised to a year: hourly × 2,080, monthly × 12 |

Filter with `minAnnualSalary`, `maxAnnualSalary` and `salaryCurrency`.

99.9% of published bands parse into numbers and **98.9% carry an annual
figure**. **Where it is not sure, it says nothing rather than guessing.** Nine in ten bands state no period at all, so a figure under
2,000 with nothing said about it is left un-annualised instead of being called
either a salary or an hourly rate; a band written `$0 - $100K` is treated as an
unfilled field; and an hourly figure that would annualise past $10M is refused
outright. You get fewer rows than the maximum possible, and no wrong ones.

Nothing is converted between currencies. 84% of published bands are US dollars,
and baking today's exchange rate into an index that rebuilds every night would
be stale by morning, so `salaryCurrency` ships beside the numbers and the
conversion is yours to make if you need it.

### Pricing

Pay per job returned. Nothing else is charged: not the run, not filtered rows,
not a search that matches nothing.

### Related Actors

If you already know the company, the per-board scrapers are cheaper and always
current, because they read that board live at the moment you ask:

- Greenhouse Jobs Scraper
- Ashby Jobs Scraper
- Lever Jobs Scraper
- Workday Jobs Scraper

# Actor input Schema

## `mode` (type: `string`):

Jobs returns one row per open role. Companies returns one row per COMPANY ranked by how many roles they opened recently, with new departments and locations called out. Use Companies for sales and recruiting prospecting; use Jobs to find the roles themselves.

## `title` (type: `array`):

Words that must appear in the job title, one per line. Several lines are OR-ed. End a word with \* to match its prefix: engineer\* also matches engineering. Quotes are not needed and a multi-word line is matched as a phrase, so product manager finds "Product Manager" but not "Product Marketing Manager".

## `location` (type: `array`):

Places to match, one per line, e.g. berlin or united kingdom. Several lines are OR-ed. Matched against the location the employer published, so a country works when a city is not given. For remote work use Work arrangement instead, which uses what the board states rather than the word in the text.

## `countries` (type: `array`):

Only roles in these countries, matched on a country resolved from the employer's location string rather than on the text itself. Use it when you want German roles without caring whether the board wrote "Berlin, Germany", "Berlin", "Munich, DE" or "DE - Berlin". Resolved on 87.8% of roles; the rest publish something no rule can read, like "EMEA" or a bare "Cambridge", and are excluded rather than guessed at.

## `company` (type: `array`):

Company or board names to match, one per line. Several lines are OR-ed. Use \* for a prefix.

## `department` (type: `array`):

Departments or teams to match, one per line, e.g. engineering or design. Present on 44% of jobs, so this narrows results to those that publish it.

## `keywords` (type: `array`):

Search title, company, location and department together, one term per line. Use this when you do not care which field matched.

## `titleExclude` (type: `array`):

Drop roles whose title contains any of these, one per line, e.g. intern or sales. Excluded jobs are never returned and never charged.

## `companyExclude` (type: `array`):

Drop jobs from these companies, one per line. Useful for staffing agencies or companies you have already applied to.

## `locationExclude` (type: `array`):

Drop jobs in these places, one per line.

## `workArrangements` (type: `array`):

Remote, hybrid or on-site. Taken from what the board itself states wherever it does: Ashby publishes a workplace type and Workable publishes a remote flag. Where neither says, it is read from the location text, and a job we could not classify is left out of all three rather than guessed into one.

## `seniority` (type: `array`):

Derived from the job title, because no ATS publishes a seniority field. About a third of jobs carry one; the rest state nothing recognisable in the title and are left unlabelled rather than guessed. Filtering here therefore narrows to jobs that say so in the title.

## `employmentTypes` (type: `array`):

Full-time, contract, part-time, temporary or internship. Taken from the board's own field on 42% of jobs; the rest do not publish one and are left unlabelled.

## `platforms` (type: `array`):

Limit to jobs from one system. Leave empty to search all three.

## `postedAfter` (type: `string`):

An ISO date, e.g. 2026-07-01. Jobs whose board publishes no posting date are excluded when this is set, because no date is not evidence of a recent one.

## `withSalaryOnly` (type: `boolean`):

Keep only roles where the employer published a pay band. Present on 7.7% of the index, from Ashby boards, as the band the employer wrote rather than a number parsed out of prose.

## `minAnnualSalary` (type: `integer`):

Only roles whose published lower bound is at least this, per year. Hourly and monthly bands are converted (2,080 hours or 12 months). Compared in the role's own currency, so pair it with Salary currency. Roles that publish no band are excluded, because an absent number is not evidence of a high one.

## `maxAnnualSalary` (type: `integer`):

Only roles whose published lower bound is at most this, per year. Same conversion and same currency caveat as the minimum.

## `salaryCurrency` (type: `array`):

Restrict to bands published in these currencies. Nothing is converted between them: 88% of published bands are US dollars, and baking today's exchange rate into an index rebuilt every night would go stale by morning.

## `maxResults` (type: `integer`):

A hard ceiling on jobs returned and charged. Defaults to 1,000 if you leave it out, and is capped at 10,000 for one search, so a broad query can never produce a surprise bill. Set it to 0 for a dry run that returns and charges nothing.

## `hiringLabels` (type: `array`):

Surging is the top 1% of companies by roles opened in 30 days, growing the top 10%, steady the top half. The cut points are percentiles of the live data, not fixed numbers, so they keep their meaning as the index grows.

## `minOpenedLast30` (type: `integer`):

Only companies that opened at least this many roles in the last 30 days.

## `minOpenedLast7` (type: `integer`):

Only companies that opened at least this many roles in the last 7 days. The sharpest signal that something changed at that company this week.

## `minOpenRoles` (type: `integer`):

Filters out companies with only a handful of postings.

## `enteringNewDepartments` (type: `boolean`):

Only companies whose recent postings are in a department they were not hiring for before. A company opening its first sales or security roles is a company that just changed strategy, which is the strongest buying signal in the dataset.

## `shrinkingOnly` (type: `boolean`):

Only companies that CLOSED more roles in the last 30 days than they opened, sorted by how fast they are shrinking. Needs closure history, so companies with no closure data yet are excluded rather than counted. A churn signal for sales prospecting and a warning sign for job seekers.

## `withContactOnly` (type: `boolean`):

Only companies whose own website publishes a way to reach them: a role address like info@ or sales@, a phone number from a tel: link, or the company's own social profiles. Nothing is guessed and no personal address of a named individual is ever collected. Companies not yet crawled are excluded rather than counted as having none.

## `includeStaffingAgencies` (type: `boolean`):

Staffing agencies and recruiters post enormous volumes on behalf of other companies and dominate any hiring ranking, so they are excluded by default in Companies mode. 126 companies (2.7% of jobs) are flagged, conservatively and by name, and every row carries looksLikeAgency so you can judge for yourself. Jobs mode never hides them.

## Actor input object example

```json
{
  "mode": "jobs",
  "withSalaryOnly": false,
  "maxResults": 200,
  "enteringNewDepartments": false,
  "shrinkingOnly": false,
  "withContactOnly": false,
  "includeStaffingAgencies": false
}
```

# Actor output Schema

## `results` (type: `string`):

Every row this search returned, as JSON.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "maxResults": 200
};

// Run the Actor and wait for it to finish
const run = await client.actor("glitchbound/ats-jobs-search").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "maxResults": 200 }

# Run the Actor and wait for it to finish
run = client.actor("glitchbound/ats-jobs-search").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "maxResults": 200
}' |
apify call glitchbound/ats-jobs-search --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=glitchbound/ats-jobs-search",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/hG7U434ezxtGSvZzn/builds/2FTdH2fjlbZZI8gkE/openapi.json
