# Wellfound Jobs Scraper - Startup Roles, Salaries & Remote Jobs (`abotapi/wellfound-jobs-scraper`) Actor

Scrape startup jobs from Wellfound.com by keyword, location, role, or remote status. Extract job details, compensation, company badges, and other listing data, with MCP connector export support.

- **URL**: https://apify.com/abotapi/wellfound-jobs-scraper.md
- **Developed by:** [Abot API](https://apify.com/abotapi) (community)
- **Categories:** Jobs, Automation, Developer tools
- **Features:** Limited permissions
- **Stats:** 11 total users, 5 monthly users, 100.0% runs succeeded, 1 bookmarks
- **User rating**: 5.00 out of 5 stars

## Pricing

from $1.00 / 1,000 dataset items

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

## Wellfound Scraper: Startup Jobs, Compensation & Company Data

Wellfound Scraper turns Wellfound startup job listings into a clean dataset. Search one or more Wellfound locations, or paste Wellfound job-search links, and get job title, role, compensation, remote status, posting date, and company details (size, logo, high concept, hiring badges) for every matching listing, plus the full job description text whenever Wellfound's own page data carries it. Narrow results by keyword, role, remote status, compensation, and hiring badges in either mode, then export to JSON, CSV or Excel, or read results straight through the API.

### Why This Scraper?

- **Two clear modes.** Search mode builds Wellfound location pages for you from a list of location slugs; URL mode reads Wellfound links you paste in: `/location/...`, `/jobs`, `/role/...`, `/role/l/<role>/<city>`, `/role/r/<role>` and single job pages (`/jobs/<id>-<slug>`). No manual URL building for search mode.
- **Full job and company context in every row.** Title, role, compensation range, remote status, locations, posting date, plus the company's size, logo, one-line pitch and hiring badges.
- **Full job description text** included whenever Wellfound's own listing data carries it.
- **Five filters, both modes.** Keyword, role keyword, remote-only, require-compensation and actively-hiring-only all apply whether you searched by location or pasted URLs, so a bulk pull still comes back narrowed.
- **Resume and recurring monitoring.** Continue one interrupted run from its run ID, or turn on Incremental mode for a schedule that returns only the jobs that are new, changed, or newly back.
- **MCP connector export.** Send a condensed, human-readable job summary into Notion, Linear, Airtable or Apify connectors while the complete record always stays in the dataset.
- **Fails loudly when genuinely blocked.** A run where every request is refused stops with a clear message instead of quietly billing an empty result.

### Use Cases

- **Startup recruiters and sourcers:** build a running feed of new engineering, product and sales roles at companies in specific cities or remote-friendly searches.
- **Job boards and aggregators:** mirror Wellfound listings with company and compensation context to power your own search or alerting product.
- **Compensation research:** collect posted salary ranges and equity mentions across roles and locations for benchmarking.
- **Sales and BD prospecting:** use the "actively hiring" and investor/responder badges as signals for which startups are growing right now.
- **Job seekers and career sites:** track new remote-eligible roles matching specific keywords on a recurring schedule.

### Data You Get

> Sample shape: values are illustrative placeholders, not from a live listing.

| Field | Example |
| --- | --- |
| `jobId` | `"00000001"` |
| `jobUrl` | `"https://wellfound.com/jobs/00000001-sample-role"` |
| `jobTitle` | `"Sample Platform Engineer"` |
| `jobType` | `"full-time"` |
| `primaryRoleTitle` | `"Software Engineer"` (or a role slug like `"software-engineer"` when only the role code is available) |
| `descriptionText` | `"Full role description text appears here."` (markdown stripped, capped at 5000 characters) |
| `compensation` | `"$100k - $150k"` |
| `compensationMin` / `compensationMax` | `100000` / `150000` |
| `hasCompensation` / `hasEquity` | `true` / `false` |
| `locations` | `["San Francisco"]` |
| `acceptedRemoteLocations` | `["United States"]` |
| `remote` / `remoteKind` | `true` / `"remote"` |
| `yearsExperienceMin` / `yearsExperienceMax` | `3` / `7` |
| `postedAt` | `"2026-01-01T00:00:00Z"` |
| `companyName` | `"Sample Company"` |
| `companyUrl` / `companyLogoUrl` | `"https://wellfound.com/company/sample-company"` / logo image URL |
| `companySize` | `"SIZE_51_200"` |
| `companyHighConcept` | `"Sample company summary"` |
| `allBadgeLabels` | `["Actively Hiring", "Top Investors"]` |
| `activelyHiring` / `hasTopInvestors` | `true` / `true` |
| `quickResponder` / `topResponder` | `false` / `false` |
| `sourceUrl` / `sourceLabel` | `"https://wellfound.com/location/san-francisco"` / `"san-francisco"` |
| `scrapedAt` | `"2026-01-01T00:00:00Z"` |

A few secondary fields ride along too: `jobSlug` and `companySlug` (the URL slugs behind `jobUrl`/`companyUrl`), `descriptionLength` (character count of `descriptionText`), `companyId`, and `autoPosted`/`atsSource` (whether and through which applicant-tracking system a listing was auto-posted, when Wellfound tags one). Note that `compensationCurrency` currently recognizes only the U.S. dollar sign; postings priced in another currency leave it blank even though `compensation` still holds the original text. `remote` is true for any listing with a remote arrangement, including hybrid or flexible ones that merely carry remote configuration data; check `remoteKind` for the exact arrangement.

### How to Use

1. Pick a **mode**: `search` (build pages from Wellfound location slugs) or `url` (paste Wellfound links; see URL mode below).
2. Fill in locations or URLs for that mode, and switch on any keyword, role, remote, compensation or hiring-badge filters you need.
3. Set **Max items** (and optionally **Max pages**) to control run size and cost, then click **Start**.
4. Download the dataset as JSON, CSV or Excel, or read it through the API.

**Search one location:**

```json
{
  "mode": "search",
  "locations": ["san-francisco"],
  "maxItems": 20
}
```

**Search multiple locations with filters:**

```json
{
  "mode": "search",
  "locations": ["san-francisco", "new-york"],
  "keywords": ["python", "platform"],
  "roleKeywords": ["engineer"],
  "requireCompensation": true,
  "maxItems": 25
}
```

**Paste Wellfound URLs, remote jobs only:**

```json
{
  "mode": "url",
  "startUrls": [
    { "url": "https://wellfound.com/location/san-francisco" },
    { "url": "https://wellfound.com/jobs" }
  ],
  "remoteOnly": true,
  "maxItems": 10
}
```

**Daily monitoring with full expiry tracking:**

```json
{
  "mode": "search",
  "locations": ["san-francisco"],
  "incrementalMode": true,
  "emitExpired": true,
  "maxItems": 100000,
  "maxPages": 0
}
```

#### Run it from your code

Python:

```python
from apify_client import ApifyClient

client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("abotapi/wellfound-jobs-scraper").call(run_input={"mode": "search", "locations": ["san-francisco"]})
for job in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(job["jobTitle"], job["companyName"], job["compensation"])
```

JavaScript:

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

const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });
const run = await client.actor('abotapi/wellfound-jobs-scraper').call({ mode: 'search', locations: ['san-francisco'] });
const { items } = await client.dataset(run.defaultDatasetId).listItems();
```

Or connect it to Make, Zapier, n8n, Google Sheets or webhooks from the **Integrations** tab.

#### URL mode: supported URL shapes

| Shape | Example | Behaviour |
|---|---|---|
| Location | `https://wellfound.com/location/san-francisco` | Walks result pages |
| Jobs | `https://wellfound.com/jobs` | Walks result pages |
| Role | `https://wellfound.com/role/python-developer` | Walks result pages |
| Role and location | `https://wellfound.com/role/l/software-engineer/san-francisco` | Walks result pages |
| Remote role | `https://wellfound.com/role/r/software-engineer` | Walks result pages |
| Job page | `https://wellfound.com/jobs/4839998-senior-software-engineer-ruby-rails-mvr` | One row for that job, no pagination |

Add `?page=2` to any listing URL to start from that page. `www.wellfound.com` and legacy `angel.co` links are accepted and read as `wellfound.com`. Company pages (`/company/...`) are not supported: Wellfound shows them behind a security challenge, so they are skipped with a clear warning in the log.

**Default behaviour:** if you select URL mode without editing the Wellfound URLs field (or leave it empty), the run now scrapes one example of each shape above instead of reading nothing. Replace the list with your own links to scrape only those. A job page returns the fields the page itself exposes (company slug, badges and role details are empty there).

#### How Max items is split across locations and URLs

**Max items** is a cap, not a guaranteed count: a run can return fewer rows when the selected locations, URLs, keywords and filters match fewer current jobs. When a run has more than one location or URL source, the remaining budget is divided roughly evenly across the sources still to be read, in order, so an early source cannot use up the whole cap before later ones are tried. **Max pages** limits how many result pages are read per source; leave it empty or `0` to walk every page a source has, since Max items is the primary control on run size.

#### Resume and recurring updates

- **Resume** (`resumeFromRunId`) continues one interrupted run: paste its run or dataset ID and the actor skips every job it already collected there (matched by `jobId`), so you don't pay twice. A bad or unreadable ID fails the run immediately, before any new scraping starts.
- **Incremental mode** (`incrementalMode`) is for a schedule over the same search. The first run returns everything as `NEW`. Later runs classify each job as `NEW`, `UPDATED` (with `changedFields`), `UNCHANGED` (suppressed and not billed unless `emitUnchanged` is on), `REAPPEARED`, or leave the previous state untouched. `REAPPEARED` is not something that "happens by default": a job can only be classified `REAPPEARED` once a *prior* run marked it absent, which only happens when that prior run had `emitExpired` on and completed a full, uncapped, non-resumed scan of the tracked search (that run normally also emits an `EXPIRED` row for the job, though a Resume-seeded placeholder can be marked absent without one). Until that has happened once, no job in that campaign can ever come back as `REAPPEARED`. `stateKey` names or shares the stored state; leaving it empty derives one automatically from mode, locations, URLs, keywords and filters, so different searches never mix state. With Incremental mode off, output is exactly as before, with no extra fields.
- `resumeFromRunId` and `incrementalMode` are different tools: use Resume after one run was cut off, and Incremental mode for a schedule that runs the same search again and again. Combining them only works to bootstrap a brand-new monitoring campaign from a prior plain run; if that campaign already has saved state, the run fails rather than silently mixing the two.

### Send results into your apps (MCP connectors)

Optionally pipe the scraped jobs into the apps you already use, via Model Context Protocol (MCP) connectors. This is an extra delivery step **after** the scrape: the Apify dataset is never changed.

**What gets written to the connector:** a condensed, human-readable **summary** of each job, not the full JSON. Each job becomes one entry with a **title** and its key fields flattened to plain text. The **complete record always stays in the Apify dataset**, and a connector write error never fails the run.

1. Authorize a connector once under **Apify → Settings → Integrations** (Notion, Linear, Airtable, or Apify).
2. Select it in the **"Pipe results into your apps"** input field. (If the picker is empty, you haven't authorized a connector yet.)
3. For **Notion**, also set `notionParentPageUrl` to the page where job entries should be created.
4. Use **Max jobs to export per connector** to cap how many of this run's jobs are sent, separately from the dataset itself.

The connection is mediated by Apify's MCP proxy, so this actor never sees your third-party credentials. Leave the field empty to skip.

### Input Parameters

| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `mode` | string | `search` | `search` for Wellfound location pages, or `url` for pasted Wellfound links. |
| `locations` | array | no default (prefill: `["san-francisco"]`) | Wellfound location slugs or names for search mode. |
| `startUrls` | array | one example of each supported shape (6 URLs) | Wellfound URLs for URL mode: `/location/...`, `/jobs`, `/role/...`, `/role/l/...`, `/role/r/...`, `/jobs/<id>-<slug>`. |
| `keywords` | array | no default (prefill: `["python"]`) | Keeps rows where at least one term appears in the job, company, role, location, badge, compensation or description text. |
| `roleKeywords` | array | no default | Keeps rows where at least one term appears in the job title or primary role. |
| `remoteOnly` | boolean | `false` | Keeps only jobs marked remote or carrying remote configuration data. |
| `requireCompensation` | boolean | `false` | Keeps only jobs with a compensation value. |
| `activelyHiringOnly` | boolean | `false` | Keeps only jobs whose company carries an actively-hiring badge. |
| `jobTypes` | array | empty | Keeps only these job types (full-time, part-time, contract, internship). |
| `experienceLevel` | string | `any` | `entry` (0-1 years), `mid` (2-5), `senior` (6+), from the years of experience the listing states. `includeNoExperience` (default on) keeps jobs that state none. |
| `includeNoExperience` | boolean | `true` | Keeps jobs that state no years of experience when an experience level is set. |
| `minSalary` | integer | `0` | Keeps jobs whose listed pay range reaches at least this value, compared as listed (job's own currency and period). |
| `maxSalary` | integer | `0` | Keeps jobs whose listed pay range starts at or below this value, compared as listed. |
| `includeNoSalary` | boolean | `true` | Keeps jobs that list no salary when a salary bound is set. |
| `postedWithinDays` | integer | `0` | Keeps only jobs posted in the last N days. |
| `includeCompanies` | array | empty | Keeps only jobs from these Wellfound company slugs or exact names. |
| `excludeCompanies` | array | empty | Drops jobs from these Wellfound company slugs or exact names. |
| `companySize` | array | empty | Keeps companies in these size bands, e.g. `11-50`, `201-500`. |
| `sortBy` | string | `default` | `newest` or `oldest` sorts by posted date. Wellfound has no sort parameter, so the sort is applied client-side to the matched jobs collected up to Max items (in incremental mode only rows that will be returned count toward the cap), not to the whole site. Matched jobs are held and delivered together at the end of the scan, or just before the run time limit if the scan has not finished by then. Held jobs are saved with the run's resume checkpoint, so a platform migration or Resurrect during the scan or the delivery continues without returning a job twice; for very large sorted runs (tens of thousands of jobs) that checkpoint can exceed the storage record limit and the run then restarts from the beginning after an interruption. |
| `includeJobDetails` | boolean | `false` | Opens each saved job's page and adds `benefits`, `visaSponsorship`, `relocation`, `directApply`, `applicationUrl`, `validThrough`, `salaryUnit`, `equityMin`, `equityMax`, `experienceLevel`, `remotePolicy`, `remoteWorkPolicy`, `preferredTimezones`, `collaborationHours`, `skills`, `companyWebsite`, `companyIndustries` (empty when the page does not state them). One extra request per saved row; billing is unchanged, each saved row is still one dataset item. Failed detail fetches leave those fields empty. |
| `maxItems` | integer | `20` | Maximum matching rows to save (1 to 100000). Runs can return fewer rows when filters match fewer current jobs. |
| `maxPages` | integer | `0` | Maximum pages to read per location or URL source. `0` walks every result page; `maxItems` is the primary output cap. |
| `resumeFromRunId` | string | no default (empty) | A previous run ID (or dataset ID) from this actor. Jobs already collected there (matched by `jobId`) are skipped. |
| `incrementalMode` | boolean | `false` | Daily/recurring monitoring of the same search. See "Resume and recurring updates" above. |
| `stateKey` | string | no default (empty) | Optional name for a monitoring campaign; auto-derived from mode/locations/URLs/keywords/filters when left empty. |
| `emitUnchanged` | boolean | `false` | Incremental mode only. Also return (and bill) jobs unchanged since the last run, marked `UNCHANGED`. |
| `emitExpired` | boolean | `false` | Incremental mode only. Also return (and bill) jobs no longer found, marked `EXPIRED`, once a run has fully scanned the search. |
| `ignoreFieldsForChanges` | array | `[]` | Incremental mode only. Field names (or `parent.child` / `parent.*` paths) left out of change detection; they are still returned in every row. Scraped time and source page URL are always left out. |
| `proxy` | object | Apify Proxy Residential | Connection settings. The default is recommended for Wellfound. |
| `mcpConnectors` | array | no default (empty) | Optional connector IDs for sending a job summary into your apps. |
| `notionParentPageUrl` | string | no default (empty) | Required only to enable the Notion export. |
| `maxNotifyListings` | integer | `50` | Maximum jobs sent to each selected connector per run. Does not affect the dataset. |

### Output Example

> Sample shape: values are illustrative placeholders, not from a live listing.

```json
{
  "jobId": "00000001",
  "jobUrl": "https://wellfound.com/jobs/00000001-sample-platform-engineer",
  "jobSlug": "sample-platform-engineer",
  "jobTitle": "Sample Platform Engineer",
  "descriptionText": "Full role description text appears here.",
  "descriptionLength": 40,
  "jobType": "full-time",
  "primaryRoleTitle": "Software Engineer",
  "compensation": "$100k - $150k",
  "compensationMin": 100000,
  "compensationMax": 150000,
  "compensationCurrency": "$",
  "hasCompensation": true,
  "hasEquity": false,
  "locations": ["San Francisco"],
  "acceptedRemoteLocations": ["United States"],
  "remote": true,
  "remoteKind": "remote",
  "yearsExperienceMin": 3,
  "yearsExperienceMax": 7,
  "postedAtUnix": 1767225600,
  "postedAt": "2026-01-01T00:00:00Z",
  "companyId": "00000001",
  "companyName": "Sample Company",
  "companySlug": "sample-company",
  "companyUrl": "https://wellfound.com/company/sample-company",
  "companyLogoUrl": "https://photos.wellfound.com/startups/i/00000001-medium_jpg.jpg",
  "companySize": "SIZE_51_200",
  "companyHighConcept": "Sample company summary",
  "allBadgeLabels": ["Actively Hiring", "Top Investors"],
  "activelyHiring": true,
  "hasTopInvestors": true,
  "quickResponder": false,
  "topResponder": false,
  "sourceUrl": "https://wellfound.com/location/san-francisco",
  "sourceLabel": "san-francisco",
  "scrapedAt": "2026-01-01T00:00:00Z"
}
```

### Plan Requirement

Keep the default Apify Residential proxy setting for the most reliable results on Wellfound. **Max items** is a maximum cap, not a guaranteed count: a run can save fewer rows when the selected location, URL, keyword and filter combination matches fewer active jobs.

Wellfound answers some connections with a security check page, and which connection it accepts changes from one exit to the next, so the actor reads each page on a fresh connection and moves to another one when a page is refused. Keep the default **Proxy** setting (Apify Residential) for the most complete results; a paid Apify plan with residential proxy access, or your own proxy URLs, is recommended.

**Free plan:** Apify free plans do not include residential proxies. The run then logs "The selected connection is unavailable to this run. Apify free plans do not include residential proxies. Continuing on a backup connection; some pages may still be unavailable. For residential access, upgrade at https://apify.com/pricing or supply your own proxy URLs." once, and carries on with a backup connection. When that backup connection serves pages, the run logs "Served over the backup connection (the default connection was unavailable)." once. If the backup cannot load the pages either, the run ends as failed with: "No results: residential proxies are unavailable on this run (Apify free plans do not include them), and the backup connection could not load the requested Wellfound pages. Upgrade at https://apify.com/pricing or supply your own proxy URLs, then try again." If no connection of any kind is allowed for the account, the run fails with: "The proxy connection was refused for this account, so no page could be read. Your Apify plan does not include proxy access; a paid plan is required: https://apify.com/pricing. If you entered your own proxy URLs, check that their credentials are valid." Any other run where no page loads fails with: "Could not load any results page (the requests failed), so nothing was collected. This is a connection problem, not an empty search. Please try again later." Free-plan results are therefore not guaranteed. Own proxy URLs and runs without a proxy never use a backup connection.

### FAQ

#### How much does it cost?

You pay a small one-time run start fee plus a per-row price for every job saved to the dataset. The **Pricing** tab shows the current rates. Use **Max items** to cap the cost of any run, and leave `emitUnchanged`/`emitExpired` off unless you specifically want those extra rows returned and billed.

#### Is it legal to scrape Wellfound?

This actor collects only publicly available job listing data. You are responsible for how you use it: follow Wellfound's terms and the laws that apply to you, and get legal advice if you plan commercial redistribution.

#### Can I get only new or changed jobs on a schedule?

Yes. Schedule the actor from the **Schedules** tab and turn on **Incremental mode**. Each run then returns only new, updated and (once eligible) reappeared jobs, and unchanged ones are not billed unless you turn `emitUnchanged` on.

#### What's the difference between Resume and Incremental mode?

**Resume** (`resumeFromRunId`) continues one specific interrupted run from a pasted ID; it is a one-time delta against that one dataset. **Incremental mode** remembers state itself and is for running the exact same search again and again on a schedule. Use Resume after a run was cut off; use Incremental mode for recurring monitoring. Mixing the two only works to bootstrap a brand-new monitoring campaign; if that state key already has saved history, the run fails rather than guessing which one you meant.

#### Why did my run fail instead of returning an empty dataset?

If no page could be loaded (see Plan Requirement for the exact messages), the run stops with a clear message so "no jobs matched" is never confused with "nothing could be read." Retry in a few minutes, ideally with the default Residential proxy. A genuinely empty search, where Wellfound answered normally but nothing matched your location, URL or filters, still succeeds and reports the reason through the run status, without billing a placeholder row.

#### Can I use it with AI agents or MCP?

Yes. Call it from any Apify integration or MCP client, and use the connector field to push job summaries into Notion, Linear or Airtable.

### 🔗 Want more jobs data?

Pair this actor with these related scrapers from the same team:

<table>
<tr><td>💼 <a href="https://apify.com/abotapi/careerbuilder-scraper"><b>CareerBuilder Scraper</b></a><br>Scrape CareerBuilder.com job listings by search or URL. Returns title, company profile...</td><td>💼 <a href="https://apify.com/abotapi/remoteok-com-scraper"><b>RemoteOK Jobs Scraper</b></a><br>Scrape RemoteOK jobs by keyword or job URL. Extract title, company, location, remote...</td></tr>
<tr><td>💼 <a href="https://apify.com/abotapi/jora-jobs-scraper"><b>Jora Jobs Scraper</b></a><br>Scrape Jora.com job listings across supported countries by keyword, filters, or URL...</td><td>💼 <a href="https://apify.com/abotapi/monster-com"><b>Monster Jobs Scraper</b></a><br>Scrape Monster.com job listings from search and direct job URLs. Extract full...</td></tr>
<tr><td>🏨 <a href="https://apify.com/abotapi/caterer-com-scraper"><b>Caterer.com Scraper</b></a><br>Scrape UK hospitality jobs from Caterer.com, including chef, hotel, restaurant, bar, and...</td><td>💼 <a href="https://apify.com/abotapi/we-work-remotely-jobs-scraper"><b>We Work Remotely Scraper</b></a><br>Extract current remote job listings from weworkremotely.com. Use keyword search or WWR...</td></tr>
</table>

👉 [Browse all abotapi scrapers](https://apify.com/abotapi)

### 💬 Support

- 🐞 **Found a bug or a missing field?** Open a ticket on the [Issues tab](https://apify.com/abotapi/wellfound-jobs-scraper/issues/open). We usually reply within hours.
- 🛠️ **Need another site, extra fields or a private build?** Email <contact@abotapi.com>.
- ⭐ **Enjoying it?** A quick review on the actor page helps other users find it.

# Actor input Schema

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

Search mode builds Wellfound location pages from the locations field. URL mode reads the pasted Wellfound URLs.

## `locations` (type: `array`):

Wellfound location slugs or names. Examples: san-francisco, new-york, london. Each location is searched separately.

## `startUrls` (type: `array`):

Paste Wellfound URLs: /location/<city>, /jobs, /role/<role>, /role/l/<role>/<city>, /role/r/<role>, or a single job page /jobs/<id>-<slug>. www.wellfound.com and angel.co links are accepted. Company pages (/company/...) are not supported. Listing pagination starts at the page in the URL; a job page returns that one job.

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

Only keep jobs where at least one keyword appears in the title, company, role, location, badges, compensation, or description text.

## `roleKeywords` (type: `array`):

Only keep jobs whose title or primary role contains at least one of these values. Examples: engineer, product, sales.

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

Only keep jobs marked as remote or with remote configuration data.

## `requireCompensation` (type: `boolean`):

Only keep jobs with a compensation value.

## `activelyHiringOnly` (type: `boolean`):

Only keep jobs attached to companies marked as actively hiring in the listing data.

## `jobTypes` (type: `array`):

Only keep jobs of these types: full-time, part-time, contract, internship. Leave empty for all.

## `experienceLevel` (type: `string`):

Entry = up to 1 year required, mid = 2 to 5 years, senior = 6+ years (from the years of experience Wellfound lists).

## `includeNoExperience` (type: `boolean`):

Only used with an experience level. When on, jobs that list no years of experience are kept.

## `minSalary` (type: `integer`):

Keep jobs whose listed pay reaches at least this amount (as listed, in the job's own currency and period). 0 = off.

## `maxSalary` (type: `integer`):

Keep jobs whose listed pay starts at or below this amount. 0 = off.

## `includeNoSalary` (type: `boolean`):

Only used with a minimum or maximum salary. When on, jobs without a listed salary are kept.

## `postedWithinDays` (type: `integer`):

Only keep jobs posted in the last N days. 0 = off.

## `includeCompanies` (type: `array`):

Only keep jobs from these companies (Wellfound company slug or exact name).

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

Drop jobs from these companies (Wellfound company slug or exact name).

## `companySize` (type: `array`):

Only keep jobs from companies in these size bands, e.g. 1-10, 11-50, 51-200, 201-500, 501-1000, 1001-5000, 5001+.

## `sortBy` (type: `string`):

Wellfound has no sort parameter, so newest/oldest sorts client-side by posted date over the matched jobs collected up to Max items (not the whole site). Default keeps the site order.

## `includeJobDetails` (type: `boolean`):

Open each saved job's page for extra fields: benefits, visa sponsorship, relocation, application link, salary period, equity range, experience level, remote policy, company website and industries. Adds one request per saved row; rows still bill once.

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

Maximum number of matching job rows to save. The run can return fewer rows when the selected location, URL, keyword, and filters match fewer current jobs.

## `maxPages` (type: `integer`):

Maximum Wellfound pages to read per location or pasted URL. Leave empty to walk every result page; max items is the primary output cap.

## `proxy` (type: `object`):

Uses Apify Proxy residential by default. Leave as-is unless your account or run environment requires another route.

## `resumeFromRunId` (type: `string`):

Paste a previous run ID (or dataset ID) from this actor to continue a large job pull without returning or charging for jobs already collected there (matched by jobId). Use this after an interrupted run. For recurring daily monitoring of the same search, use Incremental mode below instead.

## `incrementalMode` (type: `boolean`):

Turn this on for daily or recurring monitoring. The first run returns all matching jobs as NEW. Later runs normally return only NEW, UPDATED, and REAPPEARED jobs. Turn on "Emit unchanged" or "Emit expired" only when you also want those jobs returned (and billed). State is kept separately for each search/URL and filter setup; use State key when you want to name or deliberately share a monitoring campaign. To continue one specific interrupted run instead, use Resume from a previous run above.

## `stateKey` (type: `string`):

Optional. Name this monitoring campaign to keep its state stable, or to deliberately share state across differently-configured runs. Leave empty to let the actor derive a key automatically from the mode/locations/URLs/keywords/filters: different searches then never mix state with each other.

## `emitUnchanged` (type: `boolean`):

Off by default. Turn on to also return jobs that have not changed since the last run, marked UNCHANGED. This returns (and bills) extra rows you already have, so leave it off unless you specifically want the full snapshot every run.

## `emitExpired` (type: `boolean`):

Off by default. Turn on to also return jobs that were present in a previous run but are no longer found, marked EXPIRED. Only produced once a run has fully scanned the tracked search, not when Max items capped it or when Resume was used. This returns (and bills) extra synthetic rows, so leave it off unless you need expiry tracking.

## `ignoreFieldsForChanges` (type: `array`):

Field names that must NOT make a job count as UPDATED, for example noisy counters. Top-level names (e.g. applicantCount) or nested paths (parent.child, parent.\*). The fields are still returned in every row; they are only left out of the comparison. Scraped time and source page URL are always ignored. Changing this list does not re-flag your saved jobs.

## `mcpConnectors` (type: `array`):

Optionally send the scraped jobs into the apps you already use, via Model Context Protocol (MCP) connectors. Authorize a connector once under Apify > Settings > API & Integrations, then select it here. Notion gets a page-per-job export; other connectors receive a best-effort write or digest. Leave empty to skip. Supported: Notion (https://mcp.notion.com/mcp), Linear (https://mcp.linear.app/sse), Airtable (https://mcp.airtable.com/mcp), Apify (https://mcp.apify.com).

## `notionParentPageUrl` (type: `string`):

URL or id of the Notion page under which job pages are created. Required to enable the Notion export; ignored by other connectors.

## `maxNotifyListings` (type: `integer`):

Cap on how many jobs are written to each selected connector per run. Does not affect the dataset.

## Actor input object example

```json
{
  "mode": "search",
  "locations": [
    "san-francisco"
  ],
  "startUrls": [
    {
      "url": "https://wellfound.com/location/san-francisco"
    },
    {
      "url": "https://wellfound.com/location/san-francisco?page=2"
    },
    {
      "url": "https://wellfound.com/jobs"
    },
    {
      "url": "https://wellfound.com/jobs?page=2"
    },
    {
      "url": "https://wellfound.com/role/python-developer"
    },
    {
      "url": "https://wellfound.com/role/python-developer?page=2"
    },
    {
      "url": "https://wellfound.com/role/l/software-engineer/san-francisco"
    },
    {
      "url": "https://wellfound.com/role/r/software-engineer"
    },
    {
      "url": "https://wellfound.com/jobs/4839998-senior-software-engineer-ruby-rails-mvr"
    },
    {
      "url": "https://www.wellfound.com/location/london"
    },
    {
      "url": "https://angel.co/location/london"
    }
  ],
  "keywords": [
    "python"
  ],
  "remoteOnly": false,
  "requireCompensation": false,
  "activelyHiringOnly": false,
  "experienceLevel": "any",
  "includeNoExperience": true,
  "minSalary": 0,
  "maxSalary": 0,
  "includeNoSalary": true,
  "postedWithinDays": 0,
  "sortBy": "default",
  "includeJobDetails": false,
  "maxItems": 20,
  "maxPages": 0,
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "ignoreFieldsForChanges": [],
  "maxNotifyListings": 50
}
```

# Actor output Schema

## `overview` (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 = {
    "mode": "search",
    "locations": [
        "san-francisco"
    ],
    "startUrls": [
        {
            "url": "https://wellfound.com/location/san-francisco"
        },
        {
            "url": "https://wellfound.com/location/san-francisco?page=2"
        },
        {
            "url": "https://wellfound.com/jobs"
        },
        {
            "url": "https://wellfound.com/jobs?page=2"
        },
        {
            "url": "https://wellfound.com/role/python-developer"
        },
        {
            "url": "https://wellfound.com/role/python-developer?page=2"
        },
        {
            "url": "https://wellfound.com/role/l/software-engineer/san-francisco"
        },
        {
            "url": "https://wellfound.com/role/r/software-engineer"
        },
        {
            "url": "https://wellfound.com/jobs/4839998-senior-software-engineer-ruby-rails-mvr"
        },
        {
            "url": "https://www.wellfound.com/location/london"
        },
        {
            "url": "https://angel.co/location/london"
        }
    ],
    "keywords": [
        "python"
    ],
    "proxy": {
        "useApifyProxy": true,
        "apifyProxyGroups": [
            "RESIDENTIAL"
        ]
    },
    "incrementalMode": false,
    "emitUnchanged": false,
    "emitExpired": false,
    "ignoreFieldsForChanges": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("abotapi/wellfound-jobs-scraper").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 = {
    "mode": "search",
    "locations": ["san-francisco"],
    "startUrls": [
        { "url": "https://wellfound.com/location/san-francisco" },
        { "url": "https://wellfound.com/location/san-francisco?page=2" },
        { "url": "https://wellfound.com/jobs" },
        { "url": "https://wellfound.com/jobs?page=2" },
        { "url": "https://wellfound.com/role/python-developer" },
        { "url": "https://wellfound.com/role/python-developer?page=2" },
        { "url": "https://wellfound.com/role/l/software-engineer/san-francisco" },
        { "url": "https://wellfound.com/role/r/software-engineer" },
        { "url": "https://wellfound.com/jobs/4839998-senior-software-engineer-ruby-rails-mvr" },
        { "url": "https://www.wellfound.com/location/london" },
        { "url": "https://angel.co/location/london" },
    ],
    "keywords": ["python"],
    "proxy": {
        "useApifyProxy": True,
        "apifyProxyGroups": ["RESIDENTIAL"],
    },
    "incrementalMode": False,
    "emitUnchanged": False,
    "emitExpired": False,
    "ignoreFieldsForChanges": [],
}

# Run the Actor and wait for it to finish
run = client.actor("abotapi/wellfound-jobs-scraper").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 '{
  "mode": "search",
  "locations": [
    "san-francisco"
  ],
  "startUrls": [
    {
      "url": "https://wellfound.com/location/san-francisco"
    },
    {
      "url": "https://wellfound.com/location/san-francisco?page=2"
    },
    {
      "url": "https://wellfound.com/jobs"
    },
    {
      "url": "https://wellfound.com/jobs?page=2"
    },
    {
      "url": "https://wellfound.com/role/python-developer"
    },
    {
      "url": "https://wellfound.com/role/python-developer?page=2"
    },
    {
      "url": "https://wellfound.com/role/l/software-engineer/san-francisco"
    },
    {
      "url": "https://wellfound.com/role/r/software-engineer"
    },
    {
      "url": "https://wellfound.com/jobs/4839998-senior-software-engineer-ruby-rails-mvr"
    },
    {
      "url": "https://www.wellfound.com/location/london"
    },
    {
      "url": "https://angel.co/location/london"
    }
  ],
  "keywords": [
    "python"
  ],
  "proxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ]
  },
  "incrementalMode": false,
  "emitUnchanged": false,
  "emitExpired": false,
  "ignoreFieldsForChanges": []
}' |
apify call abotapi/wellfound-jobs-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,abotapi/wellfound-jobs-scraper"
        }
    }
}
```

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/0DWixlPP0dvqA3rRx/builds/3MuO5pz4YkZqlPBtx/openapi.json
