Blog / API guides

Craigslist API: Get Classified Postings as JSON

Craigslist's documented developer interface is for submitting ads, not reading them, but you can get its classified postings as JSON with four GET calls: sites, categories, search and posting. Each call costs 1 credit, and a failed call costs nothing. This guide walks through them in order, shows a real search response, and ends with a polling loop that watches a category for new postings and a worked credit budget for it.

Is there an official Craigslist API?

None that we know of for reading postings. The interface Craigslist documents is a bulk posting interface for submitting ads, which is a different job from searching them, and its terms of use are worth reading before you build anything on top of the site. If you need to read postings as structured data, the options are a scraper you maintain or an API that returns them already parsed.

The Perceptlayer Craigslist API is the second option. It returns JSON with named fields instead of HTML, and data is collected from publicly available sources and returned as found at the time of the call. If you're weighing the two approaches, what maintaining a scraper involves lays out the ongoing work.

What does each Craigslist endpoint return?

The four endpoints map onto how you'd browse the site by hand: pick a city, pick a section, search, open a posting. All four are GET requests under https://api.perceptlayer.com, all cost 1 credit per successful call, and none has required parameters except posting, which needs an id.

Endpoint Returns Credits Required Optional
/v1/craigslist/sites Craigslist sites (cities and regions) 1 none country, query
/v1/craigslist/categories Posting categories 1 none query
/v1/craigslist/search Search results 1 none site, subarea, category, query, min_price, max_price, sort, page
/v1/craigslist/posting One posting 1 id none

Each endpoint also has its own page with a playground: Sites, Categories, Search and Posting. The API belongs to the Marketplaces area of the catalogue.

How do you find the right site and category?

Call sites and categories once, filter them with query, and store the values you need. The site and category parameters on search take short codes, and looking them up beats guessing. The recorded search below uses site=portland and category=bia, and its results report portland as the site and mlt as the sub-area.

# Which sites match "portland"?
curl "https://api.perceptlayer.com/v1/craigslist/sites?query=portland" \
  -H "X-API-Key: $API_KEY"

# Which categories match "bike"?
curl "https://api.perceptlayer.com/v1/craigslist/categories?query=bike" \
  -H "X-API-Key: $API_KEY"

sites also takes a country filter, and query is capped at 100 characters on both endpoints. Store the values you get back rather than looking them up on every poll. Run the two calls once and the 2 credits are a setup cost, not a per-poll cost. If you're new to the key and headers, authentication covers them, and the quick start gets a first call running.

Which parameters does the search endpoint take?

GET /v1/craigslist/search takes eight optional parameters, so a bare call is valid, but you'll nearly always set site and category. Filters combine: a site, a category, a text query and a price range narrow the result set before it reaches you, which keeps pages short and credits low.

Parameter Type Notes
site string A site from the sites endpoint, such as portland
subarea string A sub-area within a site, such as mlt in the recorded response
category string A category from the categories endpoint, such as bia
query string Search text, up to 200 characters
min_price integer Lower price bound
max_price integer Upper price bound
sort string One of date, dateoldest, rel, priceasc, pricedsc
page integer Page number, starting from the page value in the response

The sort values are the lever for monitoring. For a poller, date is the one to start with. The reference doesn't spell out how each value orders results, so check the order on your first response before you rely on it.

What does a search response look like?

A search returns a data object with the match count and a results array of postings. Here's a real call for bikes between 50 and 300 dollars in Portland, followed by an excerpt of the recorded response, trimmed with ….

curl "https://api.perceptlayer.com/v1/craigslist/search?site=portland&category=bia&query=bike&min_price=50&max_price=300" \
  -H "X-API-Key: $API_KEY"
{
  "success": true,
  "data": {
    "total": 821,
    "page": 1,
    "per_page": 360,
    "has_more": true,
    "results": [
      {
        "position": 1,
        "id": "wnD16b4hEY7DaHprfGYGAg",
        "posting_id": 7988730852,
        "title": "21\" Specialized Sirrus A1 commuter",
        "url": "https://www.craigslist.org/view/wnD16b4hEY7DaHprfGYGAg",
        "price": 250,
        "currency": "USD",
        "images": [
          "https://images.craigslist.org/00s0s_f9B6cl8tXrr_0Cz0t2_600x450.jpg"
        ],
        "slug": "portland-21-specialized-sirrus-a1",
        "location": {
          "name": "multnomah county",
          "site": "portland",
          "subarea": "mlt",
          "latitude": 45.56,
          "longitude": -122.6001
        },
        "bumped_at": "2026-10-23T20:30:33Z"
      },
      …
    ]
  },
  "meta": {
    "endpoint_family": "search",
    "billable": true
  }
}

The fields that matter for most jobs are title, price with currency, images, location (with latitude and longitude) and bumped_at. There are two identifiers: id, a string, and posting_id, an integer. In the recorded response, total was 821 and has_more was true, so one call is a window onto a larger set, and page moves through it. For a safe page loop, see paginating a web data API in Python.

How do you fetch a single posting?

Call GET /v1/craigslist/posting with the posting's id, the one required parameter on any Craigslist endpoint. It costs 1 credit and returns that one posting, which is the call to make when you already have an id from a search and want that posting on its own.

curl "https://api.perceptlayer.com/v1/craigslist/posting?id=wnD16b4hEY7DaHprfGYGAg" \
  -H "X-API-Key: $API_KEY"

We don't have a recorded posting response to quote here, so run it in the playground on the Posting page to see its exact fields before you write a parser. Treat posting as the second step, not the first: search results already carry title, price, photos and location, so fetch a posting on its own only when you've decided you care about it. That alone cuts a monitoring job's credit use sharply.

How do you poll for new postings?

Run a search with sort=date on a timer, remember the id values you've already seen, and treat anything new as a fresh posting. The loop below uses the Python requests library, honours Retry-After on a 429 or 503, and prints new postings; swap the print for a database write or a notification.

import os
import time

import requests

BASE = "https://api.perceptlayer.com"
HEADERS = {"X-API-Key": os.environ["API_KEY"]}
PARAMS = {
    "site": "portland",
    "category": "bia",
    "query": "bike",
    "min_price": 50,
    "max_price": 300,
    "sort": "date",
}
INTERVAL = 15 * 60  # seconds between polls

seen = set()
first_run = True

while True:
    resp = requests.get(
        f"{BASE}/v1/craigslist/search", headers=HEADERS, params=PARAMS, timeout=30
    )
    if resp.status_code in (429, 503):
        time.sleep(int(resp.headers.get("Retry-After", 30)))
        continue
    resp.raise_for_status()

    for item in resp.json()["data"]["results"]:
        if item["id"] in seen:
            continue
        seen.add(item["id"])
        if not first_run:
            print(item["price"], item["title"], item["url"])

    first_run = False
    print("credits left:", resp.headers.get("X-Credits-Remaining"))
    time.sleep(INTERVAL)

The first pass only fills seen, so you don't get the whole current page as "new". If sort=date returns the newest first, as you should confirm on your first response, page 1 is enough as long as fewer new postings arrive between polls than one page holds. If your category is busier than that, shorten the interval or narrow it with subarea or a tighter price range. A timeout on the client side may mean the call succeeded and was billed, since requests have no idempotency key; the next poll simply picks up whatever you missed. Retrying without paying twice covers the edge cases, and errors lists every code.

What does polling cost in credits?

Every poll is one search call at 1 credit, so the cost is the number of polls plus any posting lookups. You choose the interval, and the arithmetic is simple enough to do before you start. The table assumes one search per poll and no detail lookups.

Interval Calls per day Credits over 30 days
Every 60 minutes 24 720
Every 15 minutes 96 2,880
Every 5 minutes 288 8,640

The free tier gives 1,000 credits a month after you verify an email address, so an hourly poll of one search (720 credits) fits inside it with 280 credits to spare. Polling every 15 minutes for 30 days is 96 × 30 = 2,880 credits, which is within the smallest pack of 10,000 credits. Purchased credits last 12 months, there's no subscription, and pricing has what credits cost.

Add detail lookups on top. If 150 new postings turn up in a month and you fetch each one with posting, that's 150 × 1 credit = 150 more credits. Every response carries X-Credits-Used and X-Credits-Remaining, so the loop above can stop itself when the balance gets low. See credits for how billing works.

A job like this is also a request-rate question. One search every few minutes is far below the 1 request a second a free account gets, but if you run many sites and categories at once, stagger them. The rate limits page has the headers to watch.

When should you not use this?

If you only need a handful of postings once, the free tier and the playground are enough, and you don't need a loop. If you're building something that reaches out to the people who posted, stop and read the site's terms and the law that applies to you first: the data here is for analysis such as price tracking and market research, not for contacting individuals.

For a different marketplace shape, the same pattern applies to eBay search results, and deduplicating listings across sources helps when the same item appears in more than one place. Next step: get a free API key, then run the sites call above in the playground on the Craigslist API page.

APIs in this post

Frequently asked questions

Is there a Craigslist API for searching postings?

Craigslist documents a bulk posting interface for submitting ads, but we know of no documented endpoint for searching them. Perceptlayer's Craigslist API covers that with four endpoints (sites, categories, search and posting) that return postings as JSON, each costing 1 credit per successful call.

How much does a Craigslist API call cost?

Every Craigslist endpoint costs 1 credit per successful call, and failed calls cost nothing and return X-Credits-Used: 0. The free tier includes 1,000 credits every month after email verification, with no credit card. A 15-minute polling loop uses 2,880 credits over 30 days.

Can I filter Craigslist results by price and location?

Yes. search accepts min_price and max_price as integers, plus site and subarea for location, category for the section and query for text. Each result also carries a location object with a name, site, sub-area, latitude and longitude.

How do I get more than one page of results?

Pass page as an integer on search. The response includes total, page, per_page and has_more, so loop while has_more is true. Each page is a separate call at 1 credit, so budget pages the same way you budget polls.

What happens when I hit the rate limit?

You get a 429 with a Retry-After header giving the seconds to wait, and the rejected call isn't billed. Free accounts get 1 request a second, and paying accounts run at least 5 a second. Limits are per account, shared by all its keys.

Should I build a Craigslist scraper instead?

You can, but a Craigslist scraper is yours to keep working: page markup changes, request limits apply, and Craigslist's terms govern how you collect postings. The search endpoint returns postings as named JSON fields for 1 credit per successful call. A scraper makes sense only for data the endpoint doesn't return; run the endpoint in the playground first to compare.

  1. Kleinanzeigen API: Search German Classifieds as JSON

    Search Kleinanzeigen ads by query, city, category and price range with one GET call. Parameters, a real response and the cost: 1 credit per call.

  2. eBay API: Search Results, Items and Autocomplete as JSON

    Use the eBay API to get search results, items and autocomplete as JSON with one key. Real parameters, a recorded response and the cost: 1 credit per call.

  3. Redfin API: Search Homes, Then Property and Valuation

    Four Redfin endpoints at 1 credit each: find a region, search homes with a real JSON response, then call property and valuation by property_id.

Start with 1,000 free credits

No password to set. We email you a sign-in link, and your key works on every API.

No credit card. 1,000 free credits a month.