# Job Board Scraper & API — LinkedIn, Indeed, Glassdoor + 29 More (`spool/job-board-api`) Actor

Full job descriptions and USD salaries on every row, from LinkedIn, Indeed, Glassdoor and 29 more sources, including 2,335 employers' own career sites on 17 ATS platforms, in one deduplicated dataset. Monitor mode for daily feeds of new postings, MCP-ready for AI agents.

- **URL**: https://apify.com/spool/job-board-api.md
- **Developed by:** [Spool](https://apify.com/spool) (community)
- **Categories:** Jobs, Lead generation
- **Features:** Limited permissions
- **Stats:** 32 total users, 8 monthly users, 98.3% runs succeeded, 1 bookmarks
- **User rating**: 5.00 out of 5 stars

## Pricing

from $0.35 / 1,000 jobs

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/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Job Board Scraper & API — LinkedIn, Indeed, Glassdoor and 29 more sources in one run

**One search. Every major job board, ten free job feeds and 2,335 employers' own career sites. One clean dataset.**

- 📝 **Full descriptions on every row**, including the LinkedIn and Glassdoor rows other scrapers leave empty, read from the employer's own posting.
- 💵 **Salaries you can compare**: as stated, annualised, and in USD, with pay ranges pulled from the posting text when the board has no field.
- 🔁 **One row per posting**: the same job on three boards is merged, the richest copy kept, every board it appeared on listed.
- 🏢 **The employer attached**: direct apply link, real posting date, department, and an optional hiring profile per company.
- ⚡ **Rows in seconds**: the first rows land while the slower boards are still answering; a default run finishes in about a minute.

```
Software Development Engineer II   Amazon       $143,700–$213,800   Engineering · mid     indeed  +linkedin
Embedded Software Engineer         General Dyn. $115,252–$121,554   Engineering · mid     indeed
Staff Platform Engineer            Stripe       $190,000–$260,000   Engineering · lead    linkedin → greenhouse
```

You pay per job row delivered, at the price on the Pricing tab. Duplicates, rows a filter removed, empty boards and companies without a public board cost nothing.

***

### 🚀 Try it in one minute

Press **Try for free**, keep the prefilled input or paste one of these, and press **Start**. Rows appear in the Output tab within seconds.

**A title and a place**

```json
{ "searchTerms": ["software engineer"], "location": "New York, NY" }
```

**Remote, posted this week, with a salary**

```json
{ "searchTerms": ["data engineer"], "location": "Remote", "remoteOnly": true, "postedWithinHours": 168, "requireSalary": true }
```

**A company's whole board, straight from the employer**

```json
{ "companies": ["stripe.com", "Datadog"] }
```

**A daily feed of only new postings, delivered to Slack**

```json
{ "searchTerms": ["product manager"], "location": "Austin, TX", "onlyNew": true, "webhookUrl": "https://hooks.slack.com/services/…" }
```

A default run returns 120 jobs, about 25 each from Indeed, LinkedIn, Glassdoor and The Muse plus the employers' own boards, in under a minute. Set `results` to 500 or 1,000 for the whole market of a search; a 1,000-row run takes two to four minutes. Ten ready-made searches, from Workday career sites to Naukri, are in the **Examples** tab at the top of this page.

***

### 👥 Who uses it

- **Recruiters and sourcers** who want one live feed of open roles across every board, deduplicated, with the employer's apply link.
- **Sales and lead-generation teams** who read a new posting as a buying signal: the company rows give open roles, department mix, tech stack and pay band per company.
- **Job seekers** running a daily search across every board, with a keyword-fit score against their own skills and only new postings each morning.
- **Salary and market researchers** comparing pay, remote share and demand for skills across cities and countries on one USD scale.
- **Job aggregators and niche boards** that need a steady supply of fresh, merged listings without maintaining thirty scrapers.
- **AI agents and LLM pipelines** that need clean job data on demand through MCP or the API.

***

### 📦 What every row carries

**The posting** — `title`, `company`, `location`, `jobUrl`, `jobUrlDirect`, `applyUrl` (the employer's own page when found), `description` with `descriptionSource` (board, employer's board, job page), `datePosted` and `postedDaysAgo`, `jobType`, `isRemote`.

**The pay** — `salaryMin` and `salaryMax` as stated with `salaryCurrency` and `salaryInterval`; `salaryMinAnnual` and `salaryMaxAnnual`; `salaryMinUsd` and `salaryMaxUsd`; `salarySource` (the board, the employer's board, the job page, or the posting text).

**The company** — `companyIndustry`, `companyNumEmployees`, `companyRevenue`, `companyRating`, `companyReviewsCount`, `companyLogo`, `companyUrl`, and when its own board was found `atsProvider`, `atsBoardUrl`, `atsDepartment`, `atsPostedAt`.

**Where it is** — `city`, `region`, `country` (ISO code), `locationType` (onsite, hybrid, remote), `inSearchedCity` (true for the city you searched, false for its metro area), `otherLocations` when one posting covers several cities.

**What we read from the text** — `department`, `seniority`, `jobCategory` (30 categories), `employmentType`, `yearsExperienceMin` and `yearsExperienceMax`, `visaSponsorship`, `educationLevel`, `tech` (190 technologies) and `techCategories`, `skills`, `emails`.

**Bookkeeping** — `site` and `alsoOn` (every board the posting appeared on), `searchTerm`, `matchesTitle` (the title carries the term that found it, or it is one of the boards' looser matches), `matchedKeywords` and `keywordMatchPercent`, `dedupeKey`, `firstSeenAt` in monitor mode. LinkedIn rows add `applicants`, `isClosed`, `jobPosterName` and `jobPosterTitle`.

Views on the Output tab: **Jobs**, **Salaries**, **Technologies**, **Companies**, **Employer postings**, **Experience, visa, education**, **Leads** and **Notices**. Export JSON, CSV or Excel, or read the dataset from the API.

***

### 🌍 Where it reads: LinkedIn, Indeed, Glassdoor and 29 more

**Job boards** — Indeed (salary and description on every row), LinkedIn (global), Glassdoor (salaries, company ratings), The Muse (named employers, full descriptions), Naukri (India), Bayt (Middle East), BDJobs (Bangladesh).

**Free job feeds** — Remotive, RemoteOK, Arbeitnow (Europe), Jobicy, Himalayas, Working Nomads, We Work Remotely, the monthly *Ask HN: Who is hiring?* thread, Welcome to the Jungle (Europe, with salary fields), and the daily list of software-engineering internships and new-grad roles. The remote feeds join a remote search on their own.

**Employers' own boards, 17 platforms** — Greenhouse, Ashby, Lever, SmartRecruiters, Rippling, Breezy, Personio, Teamtailor and Pinpoint by company name; Workday, Oracle Cloud HCM, SAP SuccessFactors, Phenom, Eightfold, BambooHR and Recruitee from a careers URL; Amazon's careers site by name.

**A verified employer list** — 2,335 well-known employers' boards, from Greenhouse and Ashby start-ups to the Workday, Oracle, SuccessFactors, Phenom and Eightfold sites of Lockheed Martin, Capital One, JPMorgan Chase and Nvidia, with some 350,000 open roles. Every search asks the 600 largest for your terms directly; `searchEmployerBoards` goes up to 2,000.

***

### ✨ Why this one

**The employer's posting on the board's row.** LinkedIn and Glassdoor return a title, a company and a location, and no description. For every company a search turns up, this Actor finds the company's own board and attaches the employer's posting: full description, direct apply URL, the department as the employer labels it, the real posting date. Rows still without text get the board's own job detail. `descriptionSource` tells you which case you have.

**Salaries on one scale.** Boards report "$45/hour", "₹12L/year" and "€4,200/month" as bare numbers. Every salary is reported as stated, annualised in its currency, and annualised in USD, so a London role and an Austin role are judged together. Pay ranges printed in the description are read back out conservatively: a figure counts only when it carries a currency, sits in a plausible band and forms a range or stands next to a pay word. Every run writes `SALARY_TEXT_SAMPLE` so you can check the extractor's work.

**One row per posting.** Rows are matched on normalised title, company and city; title variants, requisition IDs, company suffixes and city spellings collapse to one key; the richest copy is kept and missing fields are filled from the others. `MERGE_SAMPLE` in the run's store shows the first sixty merges.

**A hiring profile per company.** A company posting five SDR roles needs sales tooling. With `companyRollup` on, each company gets one row: open roles found, boards posted on, the ATS and its whole open count, department and seniority mix, top technologies, USD pay range, remote share.

```json
{
  "recordType": "company", "company": "Stripe", "openRolesFound": 7,
  "atsProvider": "greenhouse", "atsOpenRoles": 716, "boards": ["linkedin", "indeed", "greenhouse"],
  "departments": { "Engineering": 5, "Product": 2 }, "seniorityMix": { "senior": 4, "lead": 2, "mid": 1 },
  "topTech": ["Kubernetes", "Go", "Kafka", "Snowflake"], "salaryMinUsd": 165000, "salaryMaxUsd": 290000, "remoteShare": 0.43
}
```

**Only what is new.** Turn on `onlyNew` and the search remembers every posting it has delivered, in a key-value store in your own account. On a schedule, each run returns only postings that were not there before: nothing delivered twice, nothing charged twice. Add `webhookUrl` and each run that finds something posts a digest to Slack, Discord or any URL.

**Filters that never bill.** Salary floor, technologies, title words, companies, agencies, posting age, experience level, job type and remote all run after scraping and before delivery, so a removed row is never charged; `RUN_SUMMARY.filterRemoved` counts each one.

***

### 🧪 Recipes

**Remote senior data roles posted this week, paying over $150k**

```json
{ "searchTerms": ["senior data engineer", "staff data engineer"], "location": "Remote", "remoteOnly": true, "postedWithinHours": 168, "requireSalary": true, "minSalaryUsd": 150000 }
```

**Who is hiring for Snowflake in London**

```json
{ "searchTerms": ["data engineer", "analytics engineer"], "location": "London", "country": "uk", "requireTech": ["Snowflake"] }
```

**A named company's whole board, straight from the employer**

```json
{ "companies": ["stripe.com", "https://jobs.ashbyhq.com/ramp", "Datadog"] }
```

**An enterprise on Workday**, about 250 Nvidia roles with full text in under a minute

```json
{ "companies": ["https://nvidia.wd5.myworkdayjobs.com/NVIDIAExternalCareerSite"], "searchTerms": ["software engineer"], "boards": [] }
```

**Three cities in one run**, deduplicated together

```json
{ "searchTerms": ["software engineer"], "location": "San Francisco, CA\nAustin, TX\nSeattle, WA", "results": 500 }
```

**A daily feed of new product manager jobs in Austin**

```json
{ "searchTerms": ["product manager"], "location": "Austin, TX", "postedWithinHours": 24, "onlyNew": true, "webhookUrl": "https://hooks.slack.com/services/…" }
```

**A search you already have open**: paste the LinkedIn, Indeed or Glassdoor URL and its filters come with it

```json
{ "searchUrls": ["https://www.indeed.com/jobs?q=python+developer&l=Austin%2C+TX&fromage=14"] }
```

**The whole verified list, no aggregator in between**

```json
{ "searchTerms": ["machine learning engineer"], "boards": [], "searchEmployerBoards": 2000 }
```

***

### ⚙️ Input

The form shows 20 fields in four sections.

| Option | Default | What it does |
|---|---|---|
| *Search* | | |
| **`searchTerms`** | — | Up to 5 titles or keywords, merged; each row says which one found it. Related titles are added only when your own words leave the run short |
| **`location`** | — | City, region, country or `Remote`; several places one per line, each searched and merged |
| `remoteOnly` | `false` | The boards' own remote filter, then every row judged again on its location and text |
| `country` | `usa` | National Indeed and Glassdoor site: usa, uk, canada, australia, germany, france, india, singapore, uae and most others |
| `results` | `120` | Jobs the run delivers, 20 to 10,000; the boards are asked for enough to reach it after merging. 120 is about 20 per source; 500 or 1,000 for the whole market |
| *Employers' own boards* | | |
| **`companies`** | — | Employers to read directly from their own board: names, domains or board URLs |
| `searchUrls` | — | LinkedIn, Indeed or Glassdoor search pages; each replaces the form for its board, filters included |
| `searchEmployerBoards` | `600` | Ask the N largest boards in the verified list of 2,335 employers for your terms (0 to 2,000; 0 turns it off) |
| *Filters* | | |
| `boards` | indeed, linkedin, glassdoor, muse | Also naukri, bayt, bdjobs, wttj, the free remote feeds, hackernews and collegejobs |
| `postedWithinHours` | — | 24 = last day, 168 = last week |
| `jobType` | — | fulltime, parttime, contract, internship, temporary |
| `experienceLevel` | — | junior, mid, senior, manager, director, executive |
| `requireSalary` | `false` | Only rows with a stated salary, from the board, the employer or the text |
| `minSalaryUsd` | — | Annualised USD floor |
| `requireTech` | — | Only rows naming any of these technologies |
| `excludeTitleKeywords` | — | Drop titles containing these words |
| `excludeCompanies` | — | Drop these employers; `Wipro` also covers `Wipro Limited` |
| `excludeAgencies` | `false` | Drop staffing and recruiting agencies by name |
| *Monitor and alerts* | | |
| `onlyNew` | `false` | Deliver and charge only postings this search has not delivered before |
| `webhookUrl` | — | Slack, Discord or any URL: a digest of each run that delivered rows |

The API accepts more: `companyRollup` for the hiring profiles, `resumeKeywords` for a fit score against your own skills, `targetCompanies`, `requireTitleKeywords`, `excludeCities`, `excludeClosed`, `maxAgeDays`, `strictKeywordMatch`, `sortBy`, `preset`, `monitorName`, `resultsPerBoard`, `maxDescriptionsPerBoard`, `linkedinCompanyIds`, `linkedinEasyApply`, `descriptionFormat`, `radiusMiles`, `offset` and `proxyConfiguration`. Every one keeps a sensible default when left out.

***

### 🤖 AI agents, API and integrations

**MCP.** Add the Apify MCP server to Claude, Cursor, ChatGPT or any MCP client and the agent calls this Actor as a tool:

```
https://mcp.apify.com/?tools=spool/job-board-api
```

Prompts that work: *"Find remote staff engineer roles posted this week that mention Kubernetes and tell me which companies are hiring the most."* *"Which companies in Austin are hiring data engineers, with their salary ranges?"* *"Read Stripe's job board and list the senior roles."*

**API.** One call returns the rows:

```
POST https://api.apify.com/v2/acts/spool~job-board-api/run-sync-get-dataset-items?token=YOUR_TOKEN
```

**Python**: `pip install apify-client`, then `client.actor("spool/job-board-api").call(run_input={...})` and `client.dataset(run["defaultDatasetId"]).list_items()`. **JavaScript**: `npm i apify-client`, then `await client.actor("spool/job-board-api").call(input)`.

**Zapier, Make, n8n, Google Sheets, Airtable, Slack, HubSpot.** Run it on a schedule with `onlyNew` from the Integrations tab and route new rows wherever they belong; filter on `recordType` to send jobs and company profiles separately.

***

### 📋 What it covers, honestly

- **Boards fail, the run does not.** Each source runs on its own timeout; one that errors or returns nothing is reported in `RUN_SUMMARY.boardHealth` while the others deliver. A board that answers with far fewer rows than asked is tried again through another proxy exit.
- **Nothing you did not get is charged.** A run that hits its time limit stops itself, delivers what it has and ends as a normal run. An unexpected error does the same, with a notice row that says what happened. Filters remove rows before delivery.
- **Filtered searches take longer.** A filter on the posting text means a candidate's text is read before the filter can judge it. Expect one to three minutes for a heavily filtered run, about a minute without.
- **Board quirks.** LinkedIn cannot combine `postedWithinHours` with Easy Apply; Indeed cannot combine it with `jobType` or `remoteOnly`. The board silently drops one; the run tells you in the log.
- **Descriptions depend on the source.** Indeed and the employer boards carry them; LinkedIn and Glassdoor rows get the employer's posting or the board's own job detail; Bayt, Naukri and BDJobs publish none.
- **Salary coverage is the board's.** Indeed and Glassdoor state salaries on most US rows; LinkedIn rarely; employer boards when the law makes them.
- **The free feeds are remote-first and English-first.** Excellent for remote and tech roles, thin for on-site roles in Lyon.
- **Indeed, LinkedIn and Glassdoor need Apify's residential proxy**, which the Actor uses on its own; without it those three return nothing, and the log says so.

***

### ⚖️ Is this legal?

The job boards collect public job postings only: the same search results any visitor sees, no login, no candidate or recruiter profiles, no paywall bypass. The employer boards publish public JSON meant for exactly this use. Job listings are business information published to be found; handle any personal data a description happens to contain according to the law that applies to you.

***

### ❓ FAQ

**Is there a LinkedIn jobs API, an Indeed API or a Glassdoor API?** Not a public one. This is a job board API over their public listings plus the employers' own public ATS APIs.

**Why do I see titles I did not type?** The boards match loosely, and related titles are searched when your words leave the run short. `matchesTitle` is true on rows whose title carries your term; keep those for the role itself.

**Why fewer rows than I asked for?** Fewer matches exist, duplicates were merged, or a board rejected a filter combination. The log shows per-board counts and `RUN_SUMMARY.duplicatesRemoved` the merges.

**How fast is it?** The first rows land in seconds; a default run finishes in about a minute; 1,000 rows in two to four minutes.

**How many jobs can one run return?** Up to 10,000, across up to ten places, plus whole employer boards.

**Which countries?** LinkedIn is global. Indeed and Glassdoor follow `country`. Naukri covers India, Bayt the Middle East, BDJobs Bangladesh, Welcome to the Jungle Europe. The employer boards are wherever the employer is.

**Can I use it for lead generation?** Yes. Turn on `companyRollup` for one row per company with its open roles, tech stack and pay band, and `onlyNew` on a schedule for a daily feed of companies that just started hiring.

**Can it alert me?** Set `webhookUrl` to a Slack or Discord incoming webhook, or any URL. With `onlyNew` on a schedule, you hear only about new postings.

**What happens if my run times out?** It stops itself before the limit, delivers the rows it had, writes the summary and ends as succeeded.

***

### 💬 Support

Open an issue on the **Issues** tab and you will get a reply, usually the same day. If a board changes and breaks, say so there; the per-board health in your run's `RUN_SUMMARY` is the fastest way to show it. If the Actor saved you time, a review on this page helps other people find it.

### More from spool

- **[Job Search Scraper — Live Jobs Direct from Company Career Pages](https://apify.com/spool/job-search)** — the verified employer list searched on its own, no proxy, no aggregator
- **[Career Page Scraper & ATS Jobs API — 17 ATS Platforms](https://apify.com/spool/career-page-jobs-api)** — every open role straight from employers' own boards, by company name, domain or careers URL
- **[Indeed Jobs Scraper — Salaries, Benefits & Company Data](https://apify.com/spool/indeed-scraper)** — Indeed alone, in depth: benefits, schedule, skills and the company dossier on every row
- **[Wellfound Jobs Scraper (AngelList) — Salary & Equity Data](https://apify.com/spool/angellist-jobs-scraper)** — startup roles with salary and equity
- **[Company Tech Stack API — Detected from Job Postings](https://apify.com/spool/company-tech-stack-api)** — a company's technologies read from its own job board

# Actor input Schema

## `searchTerms` (type: `array`):

Up to five, one per line, searched together and merged — `software engineer`, `data analyst`, `account executive`. Related titles are added only when your own words leave the run short of the result count, and a row found that way must carry the related title itself.

Leave empty when you name companies below: then their whole boards are read.

## `location` (type: `string`):

City, region or country — `New York, NY`, `London`, `Germany`, or `Remote`. Several places, one per line, are each searched and merged. Boards answer a city with its 25-mile area; every row says `inSearchedCity` so you can keep the city proper. LinkedIn is global; the country below picks the Indeed and Glassdoor site.

## `remoteOnly` (type: `boolean`):

Passed to each board's own remote filter. Every row also carries `isRemote` and `locationType`, judged from the title, location and text, which catches postings the boards mislabel.

## `country` (type: `string`):

Which national Indeed and Glassdoor site to search: `usa`, `uk`, `canada`, `australia`, `germany`, `france`, `india`, `singapore`, `uae`, `netherlands`, `spain`, `brazil`, `mexico`, `japan` and 50 more, as Indeed names them.

## `results` (type: `integer`):

How many jobs the run delivers: the boards are asked for enough to reach this number after duplicates are merged, and delivery stops at it. 20 to 10,000. The default 120 is about 20 rows from each source and costs about $0.06; set 500 or 1,000 for the whole market of a search. You are charged per row delivered; duplicates and filtered rows cost nothing. Memory: 2 GB covers about 6,000 rows; above that, start the run with 4 GB.

## `companies` (type: `array`):

Employers to read directly from their own board: a name (`Stripe`), a domain (`ramp.com`), or a board URL. Greenhouse, Ashby, Lever, SmartRecruiters, Rippling, Workday, Oracle, SuccessFactors, Phenom, Eightfold and nine more platforms are read; a name that is not matched directly is found through the company's careers page. With job titles set, only matching roles are kept; without them, the whole board.

## `searchUrls` (type: `array`):

LinkedIn, Indeed or Glassdoor search pages, one per line. Each URL replaces the search term, location and board for the board it came from: keywords, place, posted-within, remote, job type, seniority, radius and page offset are read from it. A URL that cannot be read is named in RUN_SUMMARY.searchUrls and skipped, never guessed.

## `searchEmployerBoards` (type: `integer`):

Ask the N largest boards in the bundled list of 2,335 verified employers (Greenhouse, Ashby, Lever, Workday, Oracle, SuccessFactors, Phenom and more; 353,000 open roles) for your terms, straight from the employer: postings no job board carries, with the employer's own salary and text. Default 600, up to 2,000; 0 turns it off.

## `boards` (type: `array`):

Indeed (salary and description on every row), LinkedIn (global), Glassdoor (salaries, company ratings) and The Muse are on by default. Add Naukri (India), Bayt (Middle East), BDJobs (Bangladesh), Welcome to the Jungle (Europe), the free remote feeds (Remotive, RemoteOK, Arbeitnow, Jobicy, Himalayas, Working Nomads, We Work Remotely), Hacker News "Who is hiring?" and the college jobs list. A remote search adds the remote feeds on its own. Rows from every source share one shape; the same posting on two boards is one row with `alsoOn`.

## `postedWithinHours` (type: `integer`):

24 = last day, 168 = last week. **Board quirks:** LinkedIn cannot combine this with Easy Apply; Indeed cannot combine it with job type, remote or Easy Apply — the board silently ignores one of them.

## `jobType` (type: `string`):

Employment type filter.

## `experienceLevel` (type: `array`):

Keep only these levels, read from the board's own label where it has one, else from the title (Senior, Principal, Lead; Junior, Graduate, Entry level; Intern; Director; VP, Chief, Head of). A row with no level cue is kept, never dropped. Removed rows are never charged.

## `requireSalary` (type: `boolean`):

Keep rows with a salary from the board or the employer's ATS. Applied after deduplication and enrichment, so a LinkedIn row whose salary came from Greenhouse is kept.

## `minSalaryUsd` (type: `integer`):

Compared against the annualised USD figure, so an hourly US wage and a monthly EUR salary are judged on the same scale.

## `requireTech` (type: `array`):

Keep rows whose text names any of these — `Kubernetes`, `Snowflake`, `Salesforce`, `React`… Spelled as in the output's `tech` field. 190 technologies are recognised.

## `excludeTitleKeywords` (type: `array`):

Drop jobs whose title contains any of these words or phrases (case-insensitive). Removed rows are never charged.

## `excludeCompanies` (type: `array`):

Drop these employers. Names are normalised first, so `Wipro` also drops `Wipro Limited`. Removed rows are never charged.

## `excludeAgencies` (type: `boolean`):

Drop rows whose company name reads like a staffing or recruiting agency (staffing, recruit…, headhunt…, talent…, manpower, personnel, placement…, consultanc…, resourcing, workforce…). A heuristic on the name only. Removed rows are never charged.

## `onlyNew` (type: `boolean`):

Remember every posting this search has delivered and return only the ones not seen before. Put the run on a schedule — daily, hourly — and you get a feed of new postings; nothing delivered earlier is charged again. The memory is a record per search in a key-value store named `job-board-api-monitor` in **your** account (see *Monitor name*). Postings not seen for 60 days are forgotten, so a role closed and reposted later shows up again.

## `webhookUrl` (type: `string`):

When rows are delivered, POST a digest here: a Slack or Discord incoming webhook gets a short message with the first rows and a link to all of them; any other URL gets the full JSON digest (run and dataset ids and links, counts, the first ten rows). A run that delivers nothing sends nothing, so a scheduled monitor only speaks when there is something new.

## Actor input object example

```json
{
  "searchTerms": [
    "data engineer",
    "analytics engineer"
  ],
  "location": "New York, NY",
  "remoteOnly": false,
  "country": "usa",
  "results": 120,
  "companies": [
    "stripe.com",
    "https://boards.greenhouse.io/airbnb"
  ],
  "searchUrls": [
    "https://www.indeed.com/jobs?q=python+developer&l=Austin%2C+TX&fromage=14",
    "https://www.linkedin.com/jobs/search?keywords=python%20developer&location=Austin%2C%20Texas&f_TPR=r604800"
  ],
  "searchEmployerBoards": 600,
  "boards": [
    "indeed",
    "linkedin",
    "glassdoor",
    "muse"
  ],
  "jobType": "",
  "requireSalary": false,
  "excludeTitleKeywords": [
    "senior",
    "intern"
  ],
  "excludeAgencies": false,
  "onlyNew": false,
  "webhookUrl": "https://hooks.slack.com/services/…"
}
```

# Actor output Schema

## `jobs` (type: `string`):

Job rows with enrichment (the `jobs` view: title, company, location, seniority, department, salary in USD, boards, apply link).

## `companies` (type: `string`):

One row per company: open roles, boards, ATS, department and seniority mix, top technologies, salary range.

## `salaries` (type: `string`):

Jobs with a stated salary, annualised and converted.

## `runSummary` (type: `string`):

Per-board rows returned, time and errors; ATS lookups; duplicates removed.

# 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 = {
    "searchTerms": [
        "software engineer"
    ],
    "location": "New York, NY"
};

// Run the Actor and wait for it to finish
const run = await client.actor("spool/job-board-api").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 = {
    "searchTerms": ["software engineer"],
    "location": "New York, NY",
}

# Run the Actor and wait for it to finish
run = client.actor("spool/job-board-api").call(run_input=run_input)

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

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

```

## CLI example

```bash
echo '{
  "searchTerms": [
    "software engineer"
  ],
  "location": "New York, NY"
}' |
apify call spool/job-board-api --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,spool/job-board-api"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/4bmbVU1p27yl1okXO/builds/glzHC0WAJtxd4BJCQ/openapi.json
