# Jobs API: Search 168k Company Jobs, No Board URLs Needed (`dododata/job-search-api`) Actor

Search 168,000 jobs from company career pages by words, location and remote. No board URLs, no company list, no key. 31 fields. $1 per 1,000.

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

## Pricing

from $0.70 / 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

## Jobs API: Search 168k Company Jobs, No Board URLs Needed

**Search jobs by words, location and remote, with no board URLs and no company list.** Type what you are looking for and get clean JSON: **31 fields per job, 0 failures**, including a pay range when the employer states one. Measured on Apify on 2026-09-22, a 120-job search took 9 seconds on one run and 57 on another: the endpoint's own latency is what varies, not the paging.

Every other job Actor we publish asks you for career-page URLs. This one does not: it searches the public index of **168,000 open jobs** from every company hiring through Workable, one of the applicant-tracking systems those career pages run on.

### What you get

```json
{
  "id": "workable:dd9ebded-b634-4c95-b483-99c2ef13751c",
  "ats": "workable",
  "company": "Terabase Energy",
  "company_name": "Terabase Energy",
  "company_website": "https://www.terabase.energy/",
  "title": "Manager of Project Scheduling, Project Controls",
  "department": null,
  "team": null,
  "location": "Woodland, California, United States",
  "locations": [
    "Woodland, California, United States"
  ],
  "country": "United States",
  "workplace_type": "remote",
  "remote": true,
  "employment_type": null,
  "experience_level": null,
  "salary_min": 120000,
  "salary_max": 150000,
  "salary_currency": "USD",
  "salary_period": "year",
  "salary_text": "mined by role, level, and location. This role offers a base salary of $120,000 – $150,000",
  "salary_source": "description",
  "posted_at": "2026-09-21T21:12:35.639Z",
  "updated_at": "2026-09-21T21:12:35.639Z",
  "requisition_id": "dd9ebded-b634-4c95-b483-99c2ef13751c",
  "description": "What we do At Terabase Energy, we believe that digitalization and automation will drive the next wave of innovation and cost reduction in large scale solar. To fully unlock the potential of this oppor ...",
  "apply_url": "https://jobs.workable.com/view/tngdG3viSCtrbpzqFC9XBj/remote-manager-of-project-scheduling%2C-project-controls-in-woodland-at-terabase-energy",
  "url": "https://jobs.workable.com/view/tngdG3viSCtrbpzqFC9XBj/remote-manager-of-project-scheduling%2C-project-controls-in-woodland-at-terabase-energy",
  "change_status": null,
  "first_seen_at": null,
  "last_seen_at": null,
  "scraped_at": "2026-09-22T07:29:31.495Z"
}
```

### What you can search

| Filter | Example | Size today |
|---|---|---|
| words | `data engineer` | 492 remote in the last 30 days |
| location | `London` | 3,317 |
| workplace | `remote` | **48,183** |
| posted within | `7` days | 18,048 |
| remote **and** last 7 days | | 6,035 |
| everything | | **168,324** |

Then narrow further, before a row is saved so you never pay for the rest: **Title must contain**, **Title must not contain** (drop "senior", "unpaid"; whole words, so list both "intern" and "internship"), **Countries**, and **Only jobs that state pay**.

### A daily feed of only the jobs you have not seen

Give the run a **Monitor name** and schedule it. Every job says whether it is `new`, `updated` or already delivered, and carries `first_seen_at`. With **Only changes** on, jobs you have already received are left out and **not billed**, so a daily run returns the day's new matches instead of the same list again.

Measured: two runs of a "remote product manager, last 7 days" search returned 40 jobs, then 40 **different** ones, because the second run skipped everything it had already delivered and paged deeper.

A search is a window onto a moving index, never a complete listing, so this Actor **never claims a job was removed**: absence from a search result proves nothing.

### Pay, where the employer states it

Workable publishes no salary field. When a job states a range next to a pay word (Salary, Compensation, Base pay, Pay range, Hourly rate), this Actor reads it out into `salary_min`, `salary_max`, `salary_currency` and `salary_period`, and sets `salary_source` to `description` so you always know where a number came from. Measured on 480 real jobs on 24 September: **63 ranges read, every one checked by hand, none wrong.** The currency comes from the text when it says (CAD, £, €, AUD), otherwise from the job's country for a bare `$`, so a Canadian salary is CAD, not USD. A range with no pay word in front of it is not read, on purpose: a wrong salary is worse than none.

Equity grants and on-target earnings are deliberately not matched: an equity table looks exactly like a salary range, and OTE is not base pay.

### The same row shape as the rest of the catalogue

The 31 fields here are the ones our [Career Page Jobs Scraper](https://apify.com/dododata/career-site-jobs-scraper) produces, so a pipeline that already reads one needs no branch for the other. Use this Actor to **find** jobs across companies, and that one to **watch** a specific company's board in full.

### What it reads

The public job index at `jobs.workable.com`, through the same endpoint the site's own search uses. No key, no login, no browser. `jobs.workable.com/robots.txt` allows it and disallows only the `/search` HTML pages, which this Actor does not touch. That file also carries a `Content-Signal` of `search=yes, ai-input=yes, ai-train=no`, and we respect it: this Actor delivers job data to the person who asked for it and nothing here is used to train a model.

**What that means in practice:** the index covers companies hiring through Workable. It is a large slice of the market, not all of it. If you need a specific employer who uses Greenhouse, Lever, Ashby or Workday, use the [Career Page Jobs Scraper](https://apify.com/dododata/career-site-jobs-scraper) and paste their board.

### What we deliberately do not collect

- No e-mail addresses or phone numbers. Contact details written inside a job description are replaced before the row is saved. A first name in a sentence such as "call Maurice" is not detected and stays.
- Public endpoints only, read at a polite rate (5 requests a second at most).

### Why not build it yourself

The endpoint is undocumented. Its paging token comes back as `nextPageToken` but is only accepted as `pageToken`: send it back under the name it was given and you get page one again, for ever, which is the kind of bug that quietly halves a dataset. `limit` refuses anything above 20. Locations arrive lowercased with a `TELECOMMUTE` marker mixed into the list. Descriptions are three separate HTML fragments. **$1.00 per 1,000 jobs**, and when the source changes we fix it within 48 hours. Every Apify account includes $5 of free usage a month, so your first 5,000 jobs cost nothing.

**Founding price:** $1.00 per 1,000 jobs for the first 100 users. It then rises to $1.50. Users who joined at the founding price keep it.

### How to run it

1. Click **Run**. The prefilled input returns remote jobs posted in the last week.
2. Put your own words in **Search words**, and set **Location** and **Workplace**.
3. Add **Posted within (days)** and schedule it for a feed of new jobs.
4. Narrow with **Title must contain / must not contain**, **Countries** and **Only jobs that state pay**.
5. Set **Max rows** to cap the run. That is also your spend cap.
6. Add a **Monitor name** with **Only changes** so each run returns only what is new.

### Use it from an AI agent (MCP)

```text
https://mcp.apify.com/?tools=actors,docs,dododata/job-search-api
```

A search in, typed job rows out, with every field documented in the dataset schema, so an agent can call it without reading this page. The Actor is enabled for Apify's agentic payments ([x402](https://docs.apify.com/platform/integrations/x402), experimental on Apify's side).

### Related Actors

- [Career Page Jobs Scraper, API & Monitor](https://apify.com/dododata/career-site-jobs-scraper): every open job from a specific company's board, across twelve applicant-tracking systems
- [France Travail Jobs Scraper, API & Monitor](https://apify.com/dododata/francetravail-jobs-scraper): French public-service job offers with salary as numbers

### Input reference

| Field | Default | Meaning |
|---|---|---|
| `query` | none | Words to search for |
| `location` | none | City, region or country |
| `workplace` | any | any, remote, hybrid, on\_site |
| `dayRange` | none | Only jobs posted in the last N days |
| `titleKeywords` | none | Title must contain one of these |
| `excludeKeywords` | none | Title must not contain any of these |
| `countries` | none | Keep only these countries, by whole name (US, UK, USA understood) |
| `onlyWithSalary` | false | Keep only jobs that state pay |
| `includeDescription` | true | Turn off for lighter rows |
| `monitorName` | none | Turns on change tracking |
| `onlyChanges` | false | With a monitor name: skip and do not bill jobs you already have |
| `maxItems` | 500 | Stop after this many jobs |
| `maxConcurrency` | 5 | Parallel requests |
| `useProxy` | false | Route requests through the Apify proxy. Off by default: the public endpoint answered more than twice as fast without it. Turn it on if a run reports being blocked |

### FAQ

**Do I need to know which companies to search?**
No, and that is the point. Every other job Actor we publish needs a board URL. This one takes a search.

**Which companies are covered?**
Every company hiring through Workable that lists on its public index: about 168,000 open jobs as we write this. It is a large slice of the market, not all of it.

**How do I get only today's new jobs?**
Set **Posted within (days)** to 1, add a **Monitor name** with **Only changes**, and schedule it daily. You then pay only for jobs you have not already received.

**Why do so few jobs have a salary?**
Because the source has no salary field, so a pay range only appears when the employer wrote one into the job text. We read those and mark them `salary_source: description`, and we never invent the rest.

**How fresh is the data?**
Every run is live. There is no database in between.

### Changelog

- 2026-09-22, v0.1: first release. Search by words, location, workplace and recency; title and country filters; pay parsed from the job text; monitoring with `change_status` and `first_seen_at`. Source verified live on 2026-09-22.

# Actor input Schema

## `query` (type: `string`):

What to search for in the job, for example "data engineer", "product manager", "nurse". Leave empty to take everything and narrow with the filters below.

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

A city, region or country as you would type it into a job site, for example London, Berlin, Texas. Leave empty for everywhere. For remote jobs use Workplace, not this field: the word Remote here is read as a place, and there is a Remote in Oregon.

## `workplace` (type: `string`):

Filter on how the job is worked. There are about 48,000 remote jobs in the index at the moment.

## `dayRange` (type: `integer`):

Keep only jobs posted in the last N days. Use 1 with a daily schedule for a feed of brand-new jobs.

## `titleKeywords` (type: `array`):

Narrow further: keep only jobs whose TITLE contains one of these words or phrases, as whole words (accents and case ignored), so art matches Art Director and not Part-Time. Applied before a row is saved, so you do not pay for the rest.

## `excludeKeywords` (type: `array`):

Drop jobs whose title contains one of these words, as whole words, for example senior, intern, internship, unpaid. Whole words means intern does not drop Internal Communications Manager, so list internship too if you want it gone.

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

Keep only jobs in these countries, for example United States, UK, Germany. Whole country names, matched against the job's country, so UK never matches Ukraine and Georgia the US state is not Georgia the country. US, USA, UK, GB and UAE are understood; England, Scotland, Wales and Northern Ireland match their own jobs.

## `onlyWithSalary` (type: `boolean`):

Keep only jobs where a pay range could be read out of the job text. Workable publishes no salary field, so this depends on the employer writing one in the description.

## `includeDescription` (type: `boolean`):

Turn off for lighter rows when you only need titles, companies, locations and links.

## `monitorName` (type: `string`):

Turns on change tracking. Pick any name, for example remote-pm-watch, and use the same name on every run. Each job then says whether it is new, updated or already delivered, with the date it was first seen. The first run marks everything new. State is kept in a key-value store on your own Apify account, and each Actor keeps its own.

## `onlyChanges` (type: `boolean`):

Needs a monitor name. Jobs you already received are left out of the results and not billed. A daily run on a busy search usually returns a handful of rows instead of hundreds.

## `maxItems` (type: `integer`):

Stop after this many jobs. You pay per row, so this is also your budget cap.

## `maxConcurrency` (type: `integer`):

Kept for compatibility. The search pages one after another, because each page's token comes from the last, so this does not change speed.

## `useProxy` (type: `boolean`):

Off by default: Workable's public search endpoint answered 2.4 times cheaper without it (5 s against 11 s for 120 jobs, 25 Sept 2026). Turn it on only if a run reports being rate-limited or blocked.

## Actor input object example

```json
{
  "workplace": "remote",
  "dayRange": 7,
  "titleKeywords": [],
  "excludeKeywords": [],
  "countries": [],
  "onlyWithSalary": false,
  "includeDescription": true,
  "onlyChanges": false,
  "maxItems": 500,
  "maxConcurrency": 5,
  "useProxy": false
}
```

# Actor output Schema

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

No description

# 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 = {
    "workplace": "remote",
    "dayRange": 7,
    "titleKeywords": [],
    "excludeKeywords": [],
    "countries": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("dododata/job-search-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 = {
    "workplace": "remote",
    "dayRange": 7,
    "titleKeywords": [],
    "excludeKeywords": [],
    "countries": [],
}

# Run the Actor and wait for it to finish
run = client.actor("dododata/job-search-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 '{
  "workplace": "remote",
  "dayRange": 7,
  "titleKeywords": [],
  "excludeKeywords": [],
  "countries": []
}' |
apify call dododata/job-search-api --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,dododata/job-search-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/Im035jCs5mjcUfvIH/builds/OJ0SbI98PazhemNDU/openapi.json
