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 assoftware-engineerandnew-york;remote=trueworks 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 theslugfrom the search result as well./v1/wellfound/startups?count=...returns Wellfound's startup directory, with the jobs it highlights for each startup.
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:
{
"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).
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.