KEH API & Scraper
The KEH API returns keh.com, the United States' largest managed used photo and video gear store, as clean JSON in five actions: search, product, facets, categories and suggest.
🤖 Using an AI assistant? Copy this link into ChatGPT / Claude / Cursor — it reads every endpoint and parameter instantly and tells you if this API fits your use case.
KEH is not a classifieds board. It buys gear in, inspects and grades it in its own facility and resells it with a warranty, so a listing is one model held in KEH's own cosmetic grades — New, Like New, Like New Minus, Excellent Plus, Excellent, Bargain, Ugly and As Is — and the grade is the product. The catalogue is 54,499 products deep, of which 7,553 are in stock today, and the API defaults to the in-stock ones on purpose: on a sold-out record KEH's index keeps a stale price and drops the grade entirely, so every row carries price_is_live_offer and units_in_stock and you can ask for the sold-out records deliberately with in_stock=any when you want them as price history. What makes this source unusually good to query is the depth of its own taxonomy, and all of it is a filter here: brand (577 values), KEH's grade (8), category (135 nodes), system (139 — Leica M, Nikon Manual Focus, Canon EOS, Large Format), gear type (91), coverage or film format (75), lens mount (257, from Canon EF down to Agfa Agfaflex), megapixels, film type, filter thread and filter type, prime versus zoom, focus type including the camera-motor versus lens-motor split that decides whether an old autofocus lens will focus on a modern body, maximum aperture (258 values including cine T-stops), focal-length range, memory-card type, special optical design (fisheye, macro, tilt-shift, mirror, pinhole), TTL flash system, intended use, KEH's own merchandising shelves (Overstock, New Lower Price, Newly Arrived, While Supplies Last) and price. Every one of those 25 filters was re-queried against the count KEH's own facet block claimed for it and 25 of 25 matched exactly, so a filter that bites and a filter that is ignored are never confused. The facets action returns that whole taxonomy live with KEH's counts, which is how you find the exact value instead of guessing it; categories walks the 135-node tree with ids, parents, breadcrumbs and counts; product opens one item with its full spec sheet, which grades KEH currently holds of it, its category breadcrumb, its unit count and its description; and suggest is KEH's own autocomplete, which matters here because KEH's search never answers empty and will relax a keyword that matches nothing into a catalogue slice instead. One honest limit, stated up front rather than discovered later: KEH's catalogue index carries ONE price per product, and measured against KEH's own product pages that price is the CHEAPEST grade's — a body indexed at 1,352.00 is offered at 1,352, 1,469 and 1,527 across its three grades — so the API publishes it as both price and price_from, publishes grades_available so you can see the rungs of the ladder, and sets per_grade_prices_available to false rather than pretending. No KEH account and no API key on their side: one ReefAPI key and the standard { ok, data, meta, error } envelope.
KEH's eight condition grades, and how much stock each one had on 2026-10-08
The grade names are KEH's own, carried verbatim; the value is what you pass to the grade filter, and the rank is a normalised best-to-worst number you can sort and threshold on. A grade exists only on gear KEH actually has in stock — on a sold-out record KEH drops it — so these counts are the in-stock catalogue. A product can hold several grades at once, which is why the grades add up past the 7,553 in-stock products.
| grade (KEH's own name) | grade value | rank | products in stock |
|---|---|---|---|
| New | new | 0 | 567 |
| Like New | like_new | 1 | 194 |
| Like New Minus | like_new_minus | 2 | 1,740 |
| Excellent Plus | excellent_plus | 3 | 2,199 |
| Excellent | excellent | 4 | 4,218 |
| Bargain | bargain | 5 | 1,892 |
| Ugly | ugly | 6 | 505 |
| As Is | as_is | 7 | 18 |
KEH's short forms are accepted too: LN, LN-, EX+, EX, BGN, UG, As-Is. Several grades can be OR-ed in one call. The product action returns grades_available for one model, ranked best to worst, so you can see which rungs of the ladder KEH is holding right now.
Real request and response JSON
Captured from the indexed primary action, search, on .
{
"method": "POST",
"url": "https://api.reefapi.com/keh/v1/search",
"headers": {
"x-api-key": "$REEF_KEY",
"content-type": "application/json"
},
"body": {
"query": "canon eos r6",
"sort": "price_asc"
}
}{
"ok": true,
"meta": {
"api": "keh",
"endpoint": "search",
"mode": "live",
"latency_ms": 637.3,
"record_count": 24,
"bytes": 141247,
"cache_hit": false,
"completeness_pct": 32,
"currency": "USD",
"page": 1,
"page_size": 24,
"sort": "price_asc",
"pagination": {
"page": 1,
"page_size": 24,
"has_more": true,
"total_results": 75,
"depth_limit": 10200,
"reachable_results": 75,
"depth_limit_reached": false
},
"filters_applied": {
"in_stock": "true"
},
"search_page_url": "https://www.keh.com/shop/search?q=canon+eos+r6",
"notes": "`price` is the cheapest grade's price — KEH's catalogue index carries no per-grade price; call `product` for the grades it holds. A keyword that matches nothing is relaxed by KEH's search instead of answering empty, so judge a long-tail query by the row titles.",
"upstream_requests": 1,
"total_results": 75,
"charged_credits": 2,
"version": "1.0.0",
"request_id": "3e5ad0c3912f428e",
"queue_ms": 1.1,
"fetched_at": "2026-10-08T11:33:18.654Z"
},
"data": {
"products": [
{
"pid": "390229",
"title": "Canon ER-EOSR6 MarkII Neck Strap, Black/Red Edge with EOS R6 Mark II Stitched in Silver, 1.25 in.",
"title_tokens": "[redacted-secret]",
"brand": "Canon",
"url": "https://www.keh.com/shop/28263457.html",
"image": "https://cgi.keh.com/media/catalog/product/3/9/390229-4352156_01_r370x.jpg",
"price": 2,
"price_from": 2,
"currency": "USD",
"units_in_stock": 36,
"in_stock": true,
"price_is_live_offer": true,
"description": null,
"relevance_score": 507.5373
},
{
"pid": "383969",
"title": "Canon EOS R Instructions",
"title_tokens": "[redacted-secret]",
"brand": "Canon",
"url": "https://www.keh.com/shop/canon-eos-r-instructions.html",
"image": "https://cgi.keh.com/media/catalog/product/3/8/383969-3362366_01_r370x.jpg",
"price": 2,
"price_from": 2,
"currency": "USD",
"units_in_stock": 23,
"in_stock": true,
"price_is_live_offer": true,
"description": null,
"relevance_score": 504.4806
},
{
"pid": "396176",
"title": "Canon ER-EOSR6 MarkIII Neck Strap, Black/Red with EOS R6 Mark III Stitched in Silver, 1.25 in.",
"title_tokens": "[redacted-secret]",
"brand": "Canon",
"url": "https://www.keh.com/shop/canon-er-eosr6-markiii-neck-strap-black-red-with-eos-r6-mark-iii-stitched-in-silver-1-25-in.html",
"image": "https://cgi.keh.com/media/catalog/product/3/9/396176-6239549_01_r370x.jpg",
"price": 19,
"price_from": 19,
"currency": "USD",
"units_in_stock": 12,
"in_stock": true,
"price_is_live_offer": true,
"description": null,
"relevance_score": 506.7355
}
]
}
}What the KEH API does
| Action | Description | Concrete use case | Key params |
|---|---|---|---|
| search | Search or browse KEH's catalogue. Pass `query`, or any combination of the twenty-six filters, or nothing at all to walk the whole shop. Every row carries KEH's own product id, title (with the brace shorthand KEH prints in it pulled out as `title_tokens`), brand, price, how many units are in stock, the product URL, image and description. `meta.total_results` is KEH's own exact match count for the request, so a filter that bites and a filter that is ignored are told apart without guessing, and `meta.search_page_url` is the public KEH page that shows the same slice. IN-STOCK ONLY BY DEFAULT: 46,946 of the 54,499 indexed products are sold out and their indexed price is stale, so pass `in_stock=any` if you want those records too. NOTE `price` is the CHEAPEST grade's price — the catalogue index carries no per-grade price (measured against the product page: one body indexed at $1,352 is offered at $1,352 / $1,469 / $1,527 across its three grades) — so call `product` to see which grades exist. | Pricing teams call search to search or browse KEH's catalogue. | query, brand, category, grade, system, ... |
| product | The complete record of ONE product by its `pid` (or by its keh.com `url`). Adds everything a search row cannot carry: WHICH of KEH's grades it currently holds of this model, ranked best to worst; its full spec sheet as KEH's own catalogue describes it (system, gear type, coverage format, lens mount, megapixels, film type, filter thread and type, prime/zoom, focus type, maximum aperture, focal length range, memory-card types, special optical design, TTL flash system, intended uses); its category breadcrumb; KEH's merchandising tags; the unit count; and the cleaned description. Honest about its one real gap: KEH's per-grade prices live only on the product page, which answers a challenge to everything, so `per_grade_prices_available` is false and the index's own `price_range` is republished as `price_range_index` beside `price_range_is_grade_blind: true` rather than passed off as a grade ladder. A pid KEH does not index answers NOT_FOUND. | Marketplace operators call product to get the complete record of ONE product by its `pid` (or by its keh.com `url`). | pid, url, include_pii |
| facets | The live filter taxonomy of KEH's catalogue: every brand, grade, category, system, gear type, lens mount, format, aperture, filter size and type, card type, flash system and intended use that actually has stock right now, each with KEH's own count — plus the price and focal-length ranges as real numeric spans. This is how you discover the exact value to pass to `search` instead of guessing it, and the counts alone answer questions a listing cannot ('how many Canon EF lenses does KEH hold today', 'how much of the shop is Bargain grade'). Takes every filter `search` takes, so you can ask for the taxonomy INSIDE a scope — the lens mounts available under Fujifilm, say. | Catalog enrichment teams call facets to get the live filter taxonomy of KEH's catalogue. | facets, query, brand, category, grade, ... |
| categories | KEH's own category tree — 135 nodes with their id, name, parent, readable breadcrumb and live product count, deepest counts first. Pass it any `search` filter to get the tree WITHIN a scope (which categories hold Leica gear, which hold something in Bargain grade). The ids it returns are exactly what the `category` filter takes. | Retail analysts call categories to get kEH's own category tree. | query, brand, category, grade, system, ... |
| suggest | KEH's own search autocomplete: turn a half-typed camera or lens name into the queries KEH actually resolves. One small request (under 5 KB measured), meant to be called before `search` so a keyword lands on real gear — which matters here, because KEH's search never answers empty and will relax a miss into a catalogue slice instead. | Pricing teams call suggest to get kEH's own search autocomplete. | query, count, include_pii |
Call search from your stack
curl -X POST https://api.reefapi.com/keh/v1/search \
-H "x-api-key: $REEF_KEY" \
-H "content-type: application/json" \
-d '{"query":"canon eos r6","sort":"price_asc"}'import requests
r = requests.post(
"https://api.reefapi.com/keh/v1/search",
headers={"x-api-key": REEF_KEY},
json={
"query": "canon eos r6",
"sort": "price_asc"
},
)
print(r.json()["data"])const res = await fetch("https://api.reefapi.com/keh/v1/search", {
method: "POST",
headers: {
"x-api-key": process.env.REEF_KEY,
"content-type": "application/json",
},
body: JSON.stringify({
"query": "canon eos r6",
"sort": "price_asc"
}),
});
const { ok, data, meta, error } = await res.json();Ask your MCP-connected assistant: call reefapi.keh.search with {"query":"canon eos r6","sort":"price_asc"}.Who uses this API and why
- Price the US used-camera market by condition: pull every in-stock body of one brand with KEH's grade mix and price from, and see what Bargain costs against Excellent Plus on the same gear.
- Build a lens finder that actually filters the way photographers think: mount, prime or zoom, focal-length range, maximum aperture, filter thread and whether the autofocus needs a motor in the lens or in the body.
- Track supply and condition mix in used photo gear with KEH's own counts: 7,553 in stock of 54,499 indexed, 4,218 Excellent, 1,892 Bargain, 505 Ugly on 2026-10-08.
- Source film gear by format and film type: 4,948 used film cameras, 2,915 large-format items, 2,633 that take 35mm roll, 278 that take 120 — filterable, with prices.
- Watch KEH's own markdown shelves as a feed: Overstock 1,538, New Lower Price 735, Newly Arrived 392, While Supplies Last 4,728, each a single filter with a live count.
- Benchmark your trade-in or resale pricing against a graded reference market where the grade vocabulary is published and every comparable carries a unit count.
Questions developers ask before integrating
Is the price the one on the page?
It is KEH's own catalogue price, and we measured exactly what it means rather than assuming. KEH's catalogue index carries ONE price per product while a product page offers several condition grades at several prices, and checked against KEH's own pages the index price is the CHEAPEST grade's: the Canon EOS R6 body indexed at 1,352.00 is offered at 1,352 Excellent, 1,469 Excellent Plus and 1,527 Like New Minus. So the API publishes that figure twice — price, which is what KEH's listing shows, and price_from, which says what it means — and publishes grades_available so you can see which grades exist. The per-grade prices are not on this surface and per_grade_prices_available says so. The index also publishes a price_range field, and that field is a fake: it was [price, price] on all 1,600 rows we sampled, so it is returned only as price_range_index beside price_range_is_grade_blind rather than passed off as a grade ladder. All prices are plain US dollars; 1352.0 is 1,352.00 dollars, not cents.
Why does it only return in-stock gear by default?
Because 46,946 of KEH's 54,499 indexed products are sold out, and on a sold-out record the price left in the index is stale and the grade is gone. One measured example: a Leica M6 indexed at 1,266.00 with no grade, while KEH's own page still showed a Bargain copy at 3,200.00. Returning those by default would make 86 percent of every result set unbuyable and some of it wrong. So in_stock defaults to true, every row carries units_in_stock and price_is_live_offer, and you can pass in_stock=any or in_stock=false to get the sold-out records deliberately — they are genuinely useful as a what-KEH-handles list and as price history, as long as you know which they are.
How many units does KEH have, really?
The API returns units_in_stock, KEH's own unit count, and we checked that it is a real number rather than a figure that saturates at a round cap. Over 800 rows: with the in-stock filter the minimum is 1 and 0 never appears once, without it 0 is the single most common value, and there are 83 distinct counts with a maximum of 264 and no clustering at any ceiling. 0 and 'in stock' never occur together. The product page that would corroborate the number is behind KEH's own bot protection, so this is KEH's figure as KEH publishes it, and that is said rather than glossed over.
How do I know a filter actually did something?
Every response carries meta.total_results, KEH's own match count for your exact request, plus meta.filters_applied. More than that: all 25 filters were verified by asking KEH's facet block what a value should count and then re-querying that value as a filter — 25 of 25 matched exactly, including the long tail, where a lens mount with exactly one product returned exactly one. Against the 7,553 in-stock products in one run: Canon 783, Canon plus Nikon 1,688 (the exact sum, because values on one field are OR-ed), Bargain grade 1,892, Full Frame 35mm 2,034, Canon EF mount 437, prime lenses 1,897, autofocus with a lens motor 1,457, telephoto 593, f/2.8 589, camera bodies 656, Overstock 1,538, over 2,000 dollars 299, under 50 dollars 2,762, reaching past 400 mm 68. Different fields are AND-ed: Canon plus Excellent grade is 562 against 2,983 and 4,218. Two things the source does silently and the API refuses to: an unknown filter value returns zero rows rather than an error, and an unknown sort is ignored and the unsorted order handed back — so the API validates both itself and tells you.
How do I find the right filter value instead of guessing?
Call facets. It returns KEH's own live taxonomy — 25 of them, every brand, grade, category, system, gear type, lens mount, format, aperture, filter size and type, card type, flash system, optical design and intended use that has stock right now, each with KEH's own count, plus the price and focal-length ranges as real numeric spans. Every value it returns is a value search accepts, and it takes the same filters as search, so you can ask for the taxonomy inside a scope: the lens mounts available under Fujifilm came back as 9 values with counts. It also marks five values filterable: false — those are values KEH's own taxonomy advertises but cannot filter on, because their labels carry a stray trailing space in KEH's data and match zero products whichever way they are spelled. The API refuses those with that explanation instead of handing you an empty success.
Can I browse by category rather than search?
Yes, and the categories action gives you the map first: 135 nodes with their id, name, parent, readable breadcrumb and live product count, deepest counts first — Used Camera Lenses 15,795, Accessories 18,390, Used SLR and DSLR Lenses 9,939, Used Cameras 9,248, Used Film Cameras 4,948, Tripods and Supports 4,692. The ids it returns are exactly what the category filter takes, and a parent id includes its children. Pass it any search filter and you get the tree within that scope, which answers questions a flat listing cannot: which categories hold Leica gear came back as 58 of the 135.
Does it cover film gear, or only digital?
Film is a first-class part of this catalogue and of this API. KEH holds 4,948 used film cameras and 2,915 large-format items, and film-specific attributes are their own filters: film_type covers 35mm roll (2,633), 120 roll (278), 220 roll, 4x5 through 11x14 sheet, Instax, APS, 110 and 126 cartridges, 8mm, 16mm and Super 8. Coverage format runs from Full Frame 35mm down to 8x10 inch and up through medium format. Flash system covers the film-era TTL protocols by name — Canon A-TTL, Minolta TTL, Hasselblad TTL pre-flash and 18 more — which is exactly what you need to match an old flash to an old body.
What does the product action add over a search row?
Which of KEH's grades it currently holds of that model, ranked best to worst; the full spec sheet as KEH's own catalogue describes it — system, gear type, coverage format, lens mount, megapixels, film type, filter thread and type, prime or zoom, focus type, maximum aperture, focal-length range, memory-card types, special optical design, TTL flash system and intended uses; the category breadcrumb; KEH's merchandising tags; the unit count; and the cleaned description. You can call it with KEH's product id or by pasting a keh.com product URL. A product id KEH does not index answers NOT_FOUND, not an empty success.
Is there a seller to read?
No, and that is the nature of this source rather than something removed. KEH buys the gear, inspects and grades it and sells it itself, so there is no third-party merchant, seller profile or seller rating anywhere on the page — the warranty and the grade are KEH's own. Nothing is masked here; there is simply no seller entity to publish.
How deep does paging go?
To 10,200 rows per query, and the API tells you rather than letting you find out. That is KEH's search service's own hard cap — at most 200 rows per call and a start offset of at most 10,000, which it states in its own refusal — so meta.pagination carries depth_limit, reachable_results and depth_limit_reached, and asking past the cap is refused as an invalid parameter before a request is spent, with the advice to split the query by category, brand or price band. Paging itself is honest: pages one, two and three at 50 rows had zero overlapping products in either direction, so there is no silent repeat of the last page.
What happens with a keyword that matches nothing?
You get rows, and you should know that before you trust a long-tail query. KEH's search relaxes instead of answering empty: zzqqxxnotathingqq returned 1,217 products, qwertyasdfzxcv 870, a three-word nonsense phrase 4. The relevance score does not separate that fallback from a real hit (520.9 against 505.3) and KEH's own precision metadata reports the identical value in both cases, so there is no honest flag for us to compute and we do not invent one — it is stated on the query parameter and repeated in meta.notes, and the fix is to call suggest first so a keyword lands on gear KEH actually carries. A precise model name is exact: canon eos r6 returned 157, canon eos r6 mark ii black body 13, hasselblad 503cw 293.
What is the KEH API?
KEH API is a ReefAPI endpoint group for used cameras, lenses and film gear from keh.com as json: 54,000 products, 7,500 in stock, with keh's own condition grades, 25 measured filters and the live us price. It returns live JSON through POST requests under /keh/v1.
Is the KEH API free to try?
Yes. ReefAPI starts with 1,000 free credits, no card required. KEH calls use the same shared credit balance as every other ReefAPI engine.
Do I need a KEH login or account?
No login to KEH is needed for the API response. You call ReefAPI with your x-api-key header, and the playground can run live examples before you create a production key.