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.
Pro, Scale and data contracts
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.
| 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
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"])curl -G https://api.crawlbench.com/v1/listings \ -H "Authorization: Bearer $CB_KEY" \ -d tab=new \ -d sort=best_deal \ -d limit=20const KEY = process.env.CB_KEY;const res = await fetch( "https://api.crawlbench.com/v1/listings?tab=new&sort=best_deal&limit=20", { headers: { Authorization: `Bearer ${KEY}` } },);const { data, pagination } = await res.json();for (const x of data) { console.log(x.title, x.price_check.rating_label);}{ "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.
r = requests.patch( "https://api.crawlbench.com/v1/searches/Tc7vQ2mX", headers={"Authorization": f"Bearer {KEY}"}, json={"on": False},)print(r.json()["on"]) # Falsecurl -X PATCH https://api.crawlbench.com/v1/searches/Tc7vQ2mX \ -H "Authorization: Bearer $CB_KEY" \ -H "Content-Type: application/json" \ -d '{ "on": false }'const off = await fetch("https://api.crawlbench.com/v1/searches/Tc7vQ2mX", { method: "PATCH", headers: { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ on: false }),});console.log((await off.json()).on); // false{ "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.
| 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. |
| Method and path | What it does |
|---|---|
| GET /v1/listings | Your feed, one row for each listing. |
| POST /v1/listings/bulk | Many listings at once. |
| GET /v1/listings/{id} | One listing, with the team's notes. |
| PUT /v1/listings/{id}/state | Save, handle or hide a listing, or undo it. |
| POST /v1/listings/{id}/notes | Add a note on a listing. |
| PATCH /v1/listings/{id}/notes/{note} | Edit your own note. |
| DELETE /v1/listings/{id}/notes/{note} | Delete your own note. |
| POST /v1/listings/{id}/draft | The assistant's opening message to the seller. |
| Method and path | What it does |
|---|---|
| GET /v1/saved | Saved listings, newest saved first. |
| POST /v1/saved | Save a listing by its Marketplace link. |
| PATCH /v1/saved/{id} | Set or clear the alert price. |
| DELETE /v1/saved/{id} | Stop tracking a saved listing. |
| Method and path | What it does |
|---|---|
| GET /v1/team | The team's people, roles and open invites. |
| PATCH /v1/team | Rename the team, or leave its results out of group numbers. |
| GET /v1/team/activity | Who did what on the team desk. |
| POST /v1/team/invites | Invite someone by email. |
| PATCH /v1/team/members/{id} | Change a role. |
| DELETE /v1/team/members/{id} | Remove someone from the team. |
| GET /v1/webhooks | The team's webhook endpoints. |
| POST /v1/webhooks | Add a webhook endpoint. |
| DELETE /v1/webhooks/{id} | Remove a webhook endpoint. |
| Method and path | What it does |
|---|---|
| GET /v1/categories | The pickers. |
| GET /v1/counts | Tab and badge counts for the current team. |
| GET /v1/notifications | The bell, newest first. |
| POST /v1/notifications/read | Mark items read. |
| GET /v1/results | Your results, the Proof of value for one month. |
| POST /v1/marketplace-requests | Ask for a marketplace that is not live. |
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.
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
| Field | Type | Example | What it means |
|---|---|---|---|
| api_versionalways | string | 2026-09-21 | The shape of this message, as a dated version. |
| eventalways | string | match.created | What happened: one of the events below. |
| event_idalways | string | evt_4f9Qx2Lr8Tz1Kc7Vb3Nm | One ID for each event. Keep it to skip a message you already handled. |
| delivered_atalways | timestamp | 2026-10-05T14:02:11Z | When we sent this delivery, in UTC. |
| dataalways | object | { "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
| 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_numbersEnterprise 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.
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" }'{ "error": { "code": "search_limit_reached", "message": "All 20 searches are on. Turn one off or change plan." }}Changelog
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.
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.
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.
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.
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.
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.
Pro, Scale and data contracts
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.
Free · 30 minutes · Video call