Developers

Build on Crawlbench.

One API at https://api.crawlbench.com/v1 for team plans and data customers. Read and change your searches and listings from your own code, and get a signed webhook when something changes.

Create a key

Pro, Scale and data contracts

Book a demo

Free · 30 minutes · Video call

Overview

The same API serves the phone app, team plans that connect their searches to their own systems, and data customers. Each one uses its own kind of key.

Two kinds of keys
Key Starts with Who uses it Plan
Team key cb_live_ or cb_test_ Your own code: a CRM, a sheet or a script Pro and Scale
Data key cb_data_ Data customers, for one data contract A data contract

The phone app signs in with a session key (cb_sess_) bound to one device, on every plan. You never make one.

Base URL
https://api.crawlbench.com/v1
Keys
Send Authorization: Bearer <key> on every call. Make, name and revoke keys in Developers. A key is shown once. A test key (cb_test_) reads your team's real data and never writes: every call but a GET answers 403.
Format
JSON in and out. Times in UTC, ISO 8601 with Z. Money in major units, in the listing’s own currency, with its code.

Quickstart

Make a team key in Developers and keep it in CB_KEY. Then ask for the new listings on your searches.

1List new listings

RequestGET/v1/listings
import os, requestsKEY = os.environ["CB_KEY"] r = requests.get(    "https://api.crawlbench.com/v1/listings",    headers={"Authorization": f"Bearer {KEY}"},    params={"tab": "new", "sort": "best_deal", "limit": 20},)for x in r.json()["data"]:    print(x["title"], x["price_check"]["rating_label"])
Response200 OKapplication/json
{  "data": [    {      "id": "1129384756102938",      "title": "2019 Toyota Camry SE",      "price": 15400,      "currency": "USD",      "location": "Dallas, TX",      "first_seen_at": "2026-10-05T09:12:00Z",      "price_check": {        "rating": "super_price",        "rating_label": "Great price",        "expected": 17900,        "low": 15800,        "high": 19900      },      "searches": [{ "id": "Tc7vQ2mX", "name": "Toyota Camry" }],      "saved": false,      "handled": false,      "hidden": false    }  ],  "pagination": { "has_more": true, "after": "1129384756102938" }}

Each listing carries its price check in price_check: the verdict, the usual price and the range most similar listings ask. Pass pagination.after back to get the next page.

2Turn a search off

A search is on or off. Send { "on": false } to stop it, and { "on": true } to start it again.

RequestPATCH/v1/searches/{id}
r = requests.patch(    "https://api.crawlbench.com/v1/searches/Tc7vQ2mX",    headers={"Authorization": f"Bearer {KEY}"},    json={"on": False},)print(r.json()["on"])  # False
Response200 OKapplication/json
{  "id": "Tc7vQ2mX",  "name": "Toyota Camry",  "sentence": "2019 Toyota Camry under $16,000, anywhere in Texas",  "on": false,  "problem": null}

Reference

The main routes, by what they work on. The full reference, with every field, is built from https://api.crawlbench.com/v1/openapi.json.

Searches routes
Method and path What it does
GET /v1/searches Every search, newest first.
POST /v1/searches Make a search from a sentence, from a goal or from fields.
POST /v1/searches/preview What a search would find, before it is made.
GET /v1/searches/{id} One search and what it looks for.
PATCH /v1/searches/{id} Change what it looks for, or turn it on or off.
DELETE /v1/searches/{id} Delete a search.
POST /v1/searches/{id}/duplicate A copy of the search, off.

Webhooks

Add an endpoint in Developers, or with a team key at POST /v1/webhooks, and pick the events it gets. Each endpoint has its own signing secret and a Send a test button.

Check the signature

Every message carries x-crawlbench-signature: t=<unix>,v1=<hex>, an HMAC-SHA256 of "<t>.<raw body>" with your signing secret. Reject messages older than 5 minutes.

verify.pyPython
import hashlib, hmac, time def verify(raw_body: bytes, header: str, secret: str) -> bool:    parts = dict(p.split("=", 1) for p in header.split(","))    t, sig = parts["t"], parts["v1"]    if time.time() - int(t) > 300:        return False  # older than 5 minutes    signed = f"{t}.".encode() + raw_body    want = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()    return hmac.compare_digest(want, sig) ok = verify(request.get_data(), request.headers["x-crawlbench-signature"], SECRET)

The message

FieldTypeExampleWhat it means
api_versionalwaysstring2026-09-21The shape of this message, as a dated version.
eventalwaysstringmatch.createdWhat happened: one of the events below.
event_idalwaysstringevt_4f9Qx2Lr8Tz1Kc7Vb3NmOne ID for each event. Keep it to skip a message you already handled.
delivered_atalwaystimestamp2026-10-05T14:02:11ZWhen we sent this delivery, in UTC.
dataalwaysobject{ "listing": { "id": "1129…" }, "vs_market": { … } }The listing and its price check, the search that found it (in monitor) and the team. Its own shape, not the shape of GET /v1/listings.

When a delivery fails, we retry with backoff for 24 hours. Then the endpoint shows as failing in Developers, and its owner gets one email.

Events

Webhook events
Event Sent to When
match.created Team keys One of your searches found a new listing.
listing.price_cut Team keys The asking price went down, on a listing your searches found or one you saved.
listing.status_change Team keys Pending, sold or no longer listed, on a listing your searches found or one you saved.

Data API and AI agents

Data customers use a data key on the same API, under /v1/data: listings, daily markets, segments, search, the price check, sellers, outcomes and demand. The same records arrive as daily files, and an MCP server lets AI agents search fresh listings and market numbers.

search_listingsget_listingprice_checkmarket_numbersoutcome_numbersdemand_numbers

Enterprise data

The data endpoints, the record and every field, daily files and the MCP server tools.

Rate limits and errors

These rules hold for every call, on every key.

Rate limit

600 calls a minute for each key. Every answer to a call with a key carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset. A 429 carries Retry-After.

Errors

Each error has a code, a stable word your code can check, and a message, a plain sentence you can show.

Pages

Lists return data and pagination. limit goes up to 100, or 500 on the data API. Pass after back until has_more is false.

Changes since

These lists take updated_since, so a sync asks only for what changed: GET /v1/searches, GET /v1/listings, GET /v1/saved, GET /v1/team/activity, GET /v1/webhooks, GET /v1/notifications, GET /v1/data/listings.

Writes

Send an Idempotency-Key header on every POST but the MCP server's, so a retry never does a thing twice.

Versions

v1 is in the path. A breaking change makes v2; additions never do. Webhooks carry a dated api_version.

POST/v1/searches
RequestPOST/v1/searches
curl https://api.crawlbench.com/v1/searches \  -H "Authorization: Bearer $CB_KEY" \  -H "Content-Type: application/json" \  -H "Idempotency-Key: 6b1f0d6e-3c2a-4e8b-9a51-2f7d9c0e4b11" \  -d '{ "sentence": "2019 Toyota Camry under $16,000 in Texas" }'
Errorapplication/json
{  "error": {    "code": "search_limit_reached",    "message": "All 20 searches are on. Turn one off or change plan."  }}

Changelog

  1. 17 Oct 2026

    openapi.json lists the routes your keys can call

    The spec at /v1/openapi.json and the reference here list the routes a team key or a data key can call. Sign in, the account, alerts, devices, bookings and Earn are for the Crawlbench apps and left the spec; they never answered a team key.

  2. 14 Oct 2026

    A listing's rating label matches the price check

    rating_label on a listing (price_check on GET /v1/listings, vs_market in webhook messages, the match feed and the explain brief) now uses the price check's words. Fair price, High price, Held back and Few similar listings. It used to say At market, Above market, Overpriced, Thin market and No comps yet. The rating value is unchanged.

  3. 14 Oct 2026

    A price check holds back finance words and parts

    POST /v1/data/price-check answers rating held_back when the item's own words are a down payment, a monthly payment or a part, even at a normal asking price. flags.implausible_ask stays false unless the ask is also far below the usual price. The usual price and the range are still sent.

  4. 14 Oct 2026

    A held back ask says held_back in listings and webhooks

    The price check of a listing (price_check on GET /v1/listings, vs_market in webhook messages) answers rating held_back, rating_label Held back, for an ask so far under the usual price that it is likely a deposit or placeholder, and for a listing we hold back as noise. It used to say verify. A listing your team marked not noise answers its price band instead. The usual price and the range are still sent. A client that maps every rating learns one value, as with the price check on 13 Oct 2026.

  5. 13 Oct 2026

    The price check can answer held_back

    POST /v1/data/price-check and the price_check tool answer rating held_back, rating_label Held back, for an ask so far under the usual price that it is likely a deposit or placeholder. It used to say super_price. flags.implausible_ask is still true for it, and the usual price and the range are still sent.

  6. 5 Oct 2026

    v1 published at api.crawlbench.com

    Searches, listings, saved, teams, alerts and webhooks, with the full reference in openapi.json.

Create a key

Make a team key in Developers, then run the quickstart with it. Each key is shown once.

Create a key

Pro, Scale and data contracts

CrawlbenchThe Crawlbench team

Book a demo

For data contracts and team plans: tell us what you want to build, and see the data and the API that fit it.

Book a demo

Free · 30 minutes · Video call