WellfoundAnswer

Does Wellfound have an API? Startup jobs and salaries

Wellfound has no public jobs API, and the old AngelList API returns 404. Ours returns up to 500 jobs with salary and equity for 1 credit.

6 min read

Short answer

  • We found no public Wellfound API for job data. Its integrations import your own jobs from an ATS such as Greenhouse, and the old AngelList API returns 404.
  • ToolzerHub's /v1/wellfound/jobs/search returns up to 500 startup jobs for a role or location in one call, with salary, equity, remote flag and ATS source, for 1 credit.
  • The top 500 software engineer jobs and 500 product manager jobs listed a salary in 89% and 85% of cases, with US medians of $185,000 and $180,000.
On this page

Wellfound has no public API for job listings. Its integrations work the other way: you connect your applicant tracking system (ATS) and Wellfound imports your own jobs. The old AngelList API now answers 404. Our Wellfound API returns the public jobs, salaries and equity as JSON, and the script below saves a search to CSV.

What happened to the AngelList API

Wellfound used to be AngelList's jobs site, and the old API at api.angel.co now returns HTTP 404 on paths such as /1/jobs and /graphql.

Wellfound's help centre describes integrations for recruiters. To connect Greenhouse, you paste a Greenhouse Harvest API key into Wellfound and it pulls in your public job posts, with similar guides for Workable, Ashby, Lever and Jazz HR. Neither article describes reading other companies' jobs. If you hire on Wellfound, connect your ATS in its settings; that is what the integrations are for.

Wellfound jobs with ToolzerHub

We make ToolzerHub. Our Wellfound API has three routes for job data, each a GET with the x-api-key header at 1 credit a call:

  • /v1/wellfound/jobs/search?role=...&location=...&count=... returns up to 500 jobs for a role, a location or both. Roles and locations are the slugs in Wellfound's URLs, such as software-engineer and new-york; remote=true works on a role-only search.
  • /v1/wellfound/jobs/detail?job_id=...&slug=... returns one job in full, with the description and base salary. It needs the slug from the search result as well.
  • /v1/wellfound/startups?count=... returns Wellfound's startup directory, with the jobs it highlights for each startup.
bash
export TOOLZERHUB_API_KEY=your_key
curl -G -H "x-api-key: $TOOLZERHUB_API_KEY" \
  --data-urlencode "role=software-engineer" --data-urlencode "count=500" \
  "https://api.toolzerhub.com/v1/wellfound/jobs/search"

The first job in data.jobs, trimmed to the fields the script uses:

json
{
  "id": "4844846",
  "slug": "software-engineer",
  "title": "Software Engineer",
  "compensation": "$142k – $187k",
  "remote": false,
  "locationNames": ["New York"],
  "liveStartAt": 1792022526,
  "atsSource": "AtsIntegration::Ashby::Listing",
  "startup": {
    "name": "Check",
    "companySize": "SIZE_51_200",
    "badges": [
      { "name": "ACTIVELY_HIRING_BADGE", "label": "Actively Hiring" },
      { "name": "COMPANY_STAGE_BADGE", "label": "Growth Stage", "data": "growth_stage" }
    ]
  }
}

The field names are Wellfound's own. compensation is one string with the salary range and, when the startup lists it, the equity range ("$120k – $200k • 0.1% – 0.33%"), or "No equity". liveStartAt is a Unix timestamp, and atsSource names the ATS the job was imported from.

Save Wellfound jobs to CSV

The script runs one search and saves one row per job. It splits compensation into currency, salary and equity numbers, turns the badges into a stage column and the timestamp into a date. Save it as wellfound_jobs.py and install requests (pip install requests).

python
import csv
import os
import re
import sys
import time
from datetime import datetime, timezone

import requests

URL = "https://api.toolzerhub.com/v1/wellfound/jobs/search"
HEADERS = {"x-api-key": os.environ.get("TOOLZERHUB_API_KEY", "")}
FIELDS = ["job_id", "title", "company", "stage", "size", "remote", "locations", "currency",
          "salary_min", "salary_max", "equity_min", "equity_max", "posted", "ats", "url"]
SYMBOLS = {"$": "USD", "£": "GBP", "€": "EUR", "₹": "INR"}
UNITS = {"k": 1_000, "L": 100_000}  # Wellfound writes rupees in lakh


def pay(comp):
    # "$142k – $187k • 0.1% – 0.5%" -> USD, 142000, 187000, 0.1, 0.5
    comp = comp or ""
    amounts = re.findall(r"([$£€₹])([\d,.]+)([kL]?)", comp)
    code = re.search(r"\b([A-Z]{3})\b", comp)  # "$120k – $150k CAD"
    currency = code.group(1) if code else SYMBOLS.get(amounts[0][0], "") if amounts else ""
    values = [round(float(n.replace(",", "")) * UNITS.get(u, 1)) for _, n, u in amounts]
    if comp.startswith("Up to"):  # "Up to $170k" is a maximum
        values = [None] + values
    values += [None, None]
    equity = [float(e) for e in re.findall(r"([\d.]+)%", comp)] + [None, None]
    return currency, values[0], values[1], equity[0], equity[1]


def search(role, location, count, tries=3):
    # 429, 5xx and dropped connections are retried; failed calls cost no credits.
    params = {"count": count, **({"role": role} if role else {})}
    params.update({"location": location} if location else {})
    for attempt in range(tries):
        last = attempt == tries - 1
        try:
            r = requests.get(URL, headers=HEADERS, params=params, timeout=300)
        except requests.RequestException:
            if last:
                raise
            time.sleep(10 * (attempt + 1))
            continue
        if r.status_code in (429, 500, 502, 503, 504) and not last:
            time.sleep(10 * (attempt + 1))
            continue
        if r.status_code == 402:
            sys.exit("out of credits")
        r.raise_for_status()
        return r.json().get("data") or {}


def main(role, location="", count="500", out=None):
    data = search(role, location, int(count))
    jobs = data.get("jobs") or []
    name = re.sub(r"\W+", "_", f"{role}_{location}".strip("_"))
    out = out or f"wellfound_{name}.csv"
    with open(out, "w", newline="", encoding="utf-8-sig") as f:  # -sig: Excel reads UTF-8
        writer = csv.DictWriter(f, fieldnames=FIELDS)
        writer.writeheader()
        for job in jobs:
            if not isinstance(job, dict):
                continue
            startup = job.get("startup") or {}
            badges = {b.get("name"): b.get("label") for b in startup.get("badges") or []
                      if isinstance(b, dict)}
            currency, low, high, eq_low, eq_high = pay(job.get("compensation"))
            posted = job.get("liveStartAt")
            ats = (job.get("atsSource") or "").split("::")  # "AtsIntegration::Ashby::Listing"
            writer.writerow({
                "job_id": job.get("id"), "title": job.get("title"),
                "company": startup.get("name"), "stage": badges.get("COMPANY_STAGE_BADGE"),
                "size": (startup.get("companySize") or "").replace("SIZE_", "").replace("_", "-"),
                "remote": job.get("remote"),
                "locations": "; ".join(p for p in job.get("locationNames") or [] if p),
                "currency": currency, "salary_min": low, "salary_max": high,
                "equity_min": eq_low, "equity_max": eq_high,
                "posted": datetime.fromtimestamp(posted, timezone.utc).date() if posted else "",
                "ats": ats[1] if len(ats) > 1 else "",
                "url": f"https://wellfound.com/jobs/{job.get('id')}-{job.get('slug')}",
            })
    more = " (more available)" if data.get("has_more") else ""
    print(f"{len(jobs)} jobs written to {out}{more}")


if __name__ == "__main__":
    if len(sys.argv) < 2:
        sys.exit('usage: python wellfound_jobs.py ROLE [LOCATION] [COUNT] [out.csv]; "" skips one')
    if not HEADERS["x-api-key"]:
        sys.exit("set TOOLZERHUB_API_KEY first")
    main(*sys.argv[1:])

The three searches (software-engineer, product-manager and "" new-york 100) took 11, 10 and 3 seconds and wrote 500, 500 and 100 jobs. A search returns at most 500 jobs and has no offset, so split bigger pulls by location.

What the jobs show

These are the top 500 jobs Wellfound returned for each role, so read the shares as a sample of the site.

  • 89% of the software engineer jobs and 85% of the product manager jobs list a salary, and 99 of the 100 New York jobs do. For engineers, 415 were in US dollars, with a median midpoint of $185,000 and half between $152,500 and $217,000. The US product manager median is $180,000.
  • 15% of the engineer jobs list equity, and 11 of those ranges start at 0%. The median top of the range is 0.5%.
  • 356 of the 500 engineer jobs were imported from an ATS: Ashby 196, Greenhouse 152, Workable 6 and Lever 2. The other 144 were posted on Wellfound directly.
  • 50 engineer jobs and 26 product manager jobs had been live for more than a year. 197 engineer jobs were older than 90 days, against 1 of the 100 in the New York search.
  • 175 engineer jobs and 163 product manager jobs are remote.

What it costs

A search costs 1 credit whether it returns 40 jobs or 500. The 3 searches returned 1,100 jobs for 3 credits, so 1,000 jobs take 2 calls. A job detail call is 1 credit per job.

You get 100 free credits a month; $19 buys 10,000 credits that never expire, and plans for higher volume cover more searches. Set your key and the first search is a CSV in about 10 seconds.

For other job boards, see Indeed job listings as JSON and Seek job ads as JSON.

We are not affiliated with Wellfound or AngelList.

Get this data from your own code

The Wellfound API returns it as JSON with one x-api-key header. 100 free credits monthly.