Find Craigslist Jobs by City and Category (2026 Python)
Scrape Craigslist job listings across multiple US metros. Filter by category, keyword, and city. Get title, location, description, posting date. Python.

Thirdwatch's Craigslist Scraper extracts job listings from any Craigslist city and job category — title, compensation, location, neighborhood, full description, and posting date. Pull software jobs from San Francisco, healthcare roles from Chicago, skilled trades from Houston, or all jobs from any metro. Built for job market analysts, workforce development boards, recruiters targeting local markets, and researchers studying employment patterns across US cities. Returns structured JSON with up to 500 listings per run, filtered by city subdomain, category code, and optional keyword search.
Why scrape Craigslist for job data
Craigslist job boards serve a segment of the labor market that major platforms undercount. While Indeed and LinkedIn dominate professional and corporate hiring, Craigslist is the primary channel for small businesses, independent contractors, gig work, skilled trades, and local service roles. According to the Bureau of Labor Statistics, firms with fewer than 50 employees account for over 30 percent of US private-sector employment — and a disproportionate share of these firms recruit through Craigslist rather than paid job boards.
For a labor market researcher, Craigslist data fills the gap between BLS survey data (lagged by months) and Indeed/LinkedIn data (biased toward professional roles). A workforce development board tracking construction hiring across Gulf Coast metros needs Craigslist trade postings. A recruiter specializing in restaurant management wants to monitor new postings across 5 cities daily. An economist studying gig-economy growth needs Craigslist's service and contract listings. Research from the Federal Reserve Bank of St. Louis on job openings shows that official statistics lag real-time posting activity by months, making Craigslist data valuable as a leading indicator. The common requirement: structured job data from multiple Craigslist cities and categories, pulled on a schedule and queryable.
The scale of Craigslist's job board makes manual monitoring impractical. Each metro has its own subdomain with separate search results. A researcher covering 10 cities across 3 job categories faces 30 separate searches per day. With the actor, all 30 searches become a single batch job that runs in under 10 minutes and exports to a unified dataset.
How does this compare to the alternatives?
Three approaches to systematic Craigslist job data collection:
| Approach | Reliability | Setup time | Maintenance |
|---|---|---|---|
| DIY Python scraper | Breaks on layout changes | 2-4 days | Parser updates on every breakage |
| Craigslist RSS feeds | 100-item cap, no description | 20 minutes | Minimal but structurally limited |
| Thirdwatch Craigslist Scraper | Maintained; full description returned | 5 minutes | Thirdwatch handles site changes |
Indeed and LinkedIn scrapers capture different employer populations. The Craigslist Scraper is complementary — it covers the local, small-business hiring that those platforms miss. For a complete jobs picture, pair it with Indeed Scraper and LinkedIn Jobs Scraper. Craigslist RSS feeds are structurally limited to 100 items per feed and exclude the full job description, making them unsuitable for research-grade data collection. The actor returns the complete post text, which contains requirements, compensation details, and application instructions that RSS omits.
How to find Craigslist jobs across cities in 4 steps
Step 1: How do I authenticate with the Apify API?
Create a free account at apify.com and copy your API token from Settings, then Integrations:
export APIFY_TOKEN="apify_api_xxxxxxxxxxxxxxxx"Step 2: How do I pull job listings by city and category?
Set city to a Craigslist subdomain and category to a job code. Use query to filter by keywords within that category.
import os, requests, pandas as pd
ACTOR = "thirdwatch~craigslist-scraper"
TOKEN = os.environ["APIFY_TOKEN"]
resp = requests.post(
f"https://api.apify.com/v2/acts/{ACTOR}/run-sync-get-dataset-items",
params={"token": TOKEN},
json={
"city": "sfbay",
"category": "sof",
"query": "python developer",
"maxResults": 100,
},
timeout=300,
)
df = pd.DataFrame(resp.json())
print(f"{len(df)} software jobs in SF Bay matching 'python developer'")Key job category codes: jjj (all jobs), sof (software), acc (accounting/finance), eng (engineering), hea (healthcare), edu (education), lab (general labor), ofc (office/admin), ret (retail/wholesale), tra (skilled trades/artisan).
Step 3: How do I sweep multiple metros and compare job volumes?
Loop over cities, run the actor for each, and merge results. This example compares software job volume across five tech metros:
METROS = [
{"city": "sfbay", "label": "SF Bay Area"},
{"city": "newyork", "label": "New York"},
{"city": "seattle", "label": "Seattle"},
{"city": "austin", "label": "Austin"},
{"city": "chicago", "label": "Chicago"},
]
all_jobs = []
for metro in METROS:
resp = requests.post(
f"https://api.apify.com/v2/acts/{ACTOR}/run-sync-get-dataset-items",
params={"token": TOKEN},
json={
"city": metro["city"],
"category": "sof",
"maxResults": 100,
},
timeout=300,
)
jobs = resp.json()
for j in jobs:
j["metro"] = metro["label"]
all_jobs.extend(jobs)
df = pd.DataFrame(all_jobs)
print(df.groupby("metro").size().sort_values(ascending=False))Step 4: How do I track new postings daily with scheduled runs?
Set up an Apify schedule per city. Add a webhook to push results into a database or Google Sheet for time-series analysis.
curl -X POST "https://api.apify.com/v2/schedules?token=$APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "craigslist-sfbay-software-jobs-daily",
"cronExpression": "0 7 * * *",
"timezone": "America/Los_Angeles",
"isEnabled": true,
"actions": [{
"type": "RUN_ACTOR",
"actorId": "thirdwatch~craigslist-scraper",
"runInput": {
"city": "sfbay",
"category": "sof",
"maxResults": 200
}
}]
}'To detect new postings, compare today's url values against yesterday's. New URLs are fresh listings. Missing URLs are expired or filled positions. Track both to measure posting velocity and listing half-life by metro.
yesterday_urls = set(pd.read_csv("yesterday.csv")["url"])
today_urls = set(df["url"])
new_postings = today_urls - yesterday_urls
expired = yesterday_urls - today_urls
print(f"New: {len(new_postings)}, Expired: {len(expired)}")Sample output
A single record from the dataset for an Austin software job:
{
"title": "Python Backend Developer - Startup (Remote OK)",
"price": "$120,000 - $150,000/year",
"location": "Austin",
"neighborhood": "East Austin",
"description": "Fast-growing Series A startup looking for a Python backend developer. 3+ years experience with Django or FastAPI. PostgreSQL required. Remote-friendly, Austin preferred. Benefits include health, dental, unlimited PTO, equity.",
"posted_date": "2026-05-24",
"url": "https://austin.craigslist.org/sof/d/austin-python-backend-developer/7834567890.html"
}price contains compensation when the poster includes it — format varies between hourly, monthly, and annual. neighborhood is self-reported and optional; not all job posts include it. description is the full post text, typically 100-400 words, containing requirements, benefits, and application instructions. The url encodes the category and city in the path, so you can derive the Craigslist section from it programmatically.
Common pitfalls
Three issues arise in multi-city Craigslist job pipelines. Category overlap — a poster may list a job under both sof (software) and jjj (all jobs). If you pull both categories for the same city, deduplicate on url to avoid double-counting. Compensation parsing — price is a raw string that mixes hourly ($25/hr), monthly ($5,000/mo), and annual ($120K/year) formats. Normalize to a common period before comparing across listings. Some postings omit pay entirely. City boundary mismatch — Craigslist metro areas are broader than city limits. sfbay includes San Jose, Oakland, and the Peninsula; newyork covers all five boroughs plus northern New Jersey. If your research requires city-level precision, filter by neighborhood or parse the location field.
The actor handles Craigslist's HTML parsing, pagination, and rate limiting. For broad sweeps across 10+ metros, stagger your Apify schedules by 5-10 minutes to avoid concurrent-run limits on the free tier.
A fourth consideration: Craigslist's posting velocity varies wildly by metro. San Francisco and New York generate hundreds of new job listings daily, while smaller cities like Boise or Raleigh may see 20-30. Calibrate your maxResults and scheduling frequency per city to match actual posting volume. Over-polling a slow metro wastes runs; under-polling a fast metro misses listings before they expire. Start with daily runs and adjust after reviewing the new-vs-expired ratio from your first week of data.
Related use cases
Frequently asked questions
What Craigslist job category codes are available?
Craigslist uses three-letter codes for job subcategories. Common ones: jjj for all jobs, sof for software, acc for accounting, eng for engineering, hea for healthcare, edu for education, lab for general labor, ofc for office admin, ret for retail, tra for skilled trades. The code is visible in the URL when browsing Craigslist.
Do Craigslist job listings include salary information?
Some do. When a poster includes compensation, it appears in the price field as a formatted string. Roughly 20-30 percent of Craigslist job listings include pay. The format varies widely — hourly, weekly, monthly, annual, or a range — so parsing is needed before aggregation.
How many job listings can I pull per run?
Up to 500 listings per run via the maxResults parameter. For larger volumes, run multiple queries with different keywords or across multiple cities and merge the datasets downstream.
Can I use this for labor market research?
Yes. Craigslist job postings reflect demand from small and mid-size employers who do not post on Indeed or LinkedIn. Academic researchers, workforce boards, and policy analysts use Craigslist data to study the gig economy, blue-collar labor demand, and local employment conditions.
How do I deduplicate jobs posted in multiple categories?
Craigslist assigns a unique URL per listing. Deduplicate on the url field after merging datasets from multiple category pulls. The same listing posted in sof and jjj will have the same URL.
Related
100 free credits, no credit card.
About 30 real searches. Add the MCP to Claude or Cursor in two minutes.