Yandex reverse image search is four different datasets behind one URL, switched by a single query parameter. This is a yandex reverse image search api built on ScrapingBee's web scraping API, documented tab by tab, with the parameter that selects each one.
Yandex calls the feature CBIR, content based image retrieval, and that acronym is all over the response payload, so it is worth knowing before you read the JSON.
Verified live on 2026-09-10 against two real images. Where a configuration returned nothing, the reason is recorded.
The first thing to know is that the cheap request does not work.
# 1 credit. Returns a CAPTCHA, not results.
curl -G "https://app.scrapingbee.com/api/v1/" \
-H "Authorization: Bearer $SCRAPINGBEE_API_KEY" \
--data-urlencode "url=https://yandex.com/images/search?rpt=imageview&url=$IMG" \
-d mode=autoThat returned HTTP 200, 14,885 bytes, and a page titled Are you not a robot? carrying Yandex SmartCaptcha. The spb-initial-status-code header read 302. A 200 from the API with a challenge page inside it is the failure mode to watch for here, because nothing about the status code tells you the scrape failed.
Stealth is what clears it:
# 75 credits. Returns results.
curl -G "https://app.scrapingbee.com/api/v1/" \
-H "Authorization: Bearer $SCRAPINGBEE_API_KEY" \
--data-urlencode "url=https://yandex.com/images/search?rpt=imageview&cbir_page=sites&url=$IMG" \
-d stealth_proxy=true -d wait=8000221 KB, title Yandex Images: search for websites with images, and a populated result set. Reverse image search on Yandex costs 75 credits per call. Budget for that rather than discovering it in production.
Every tab is the same base URL with a different cbir_page value. These forms were read out of the live page's own cbirNavigation.menuItems, not guessed:
| Tab | URL parameter | Answers |
|---|---|---|
| Search by image | rpt=imageview only |
Everything Yandex knows, in one page |
| Similar | cbir_page=similar |
Visually similar images |
| Sites | cbir_page=sites |
Every page where the image appears |
| Products | cbir_page=products |
Shopping matches for the object |
The image goes in the url parameter, percent encoded, nested inside the ScrapingBee url parameter. Two layers of encoding, which is the most common place this breaks.
Not in the DOM. Yandex renders its result grid client side and ships the data in data-state attributes as HTML escaped JSON. There are no result links in the markup to select, so CSS selectors and extract_rules both come back empty on this target.
The parse is: find every data-state attribute, unescape the HTML entities, load the JSON, then read the slice you want off initialState.
import html, json, os, re
import requests
img = "https://upload.wikimedia.org/wikipedia/commons/3/3c/Shaki_waterfall.jpg"
target = (
"https://yandex.com/images/search?rpt=imageview&cbir_page=sites"
f"&url={requests.utils.quote(img, safe='')}"
)
page = requests.get(
"https://app.scrapingbee.com/api/v1/",
headers={"Authorization": f"Bearer {os.environ['SCRAPINGBEE_API_KEY']}"},
params={"url": target, "stealth_proxy": "true", "wait": 8000},
timeout=180,
).text
for attr in re.findall(r'data-state="([^"]{200,})"', page):
state = json.loads(html.unescape(attr)).get("initialState")
if isinstance(state, dict) and state.get("cbirSites", {}).get("sites"):
for site in state["cbirSites"]["sites"]:
print(site["domain"], site["title"])
print(" ", site["url"])
print(" ", site["originalImage"]["url"])That returned 37 matches for the test image, with pageSize: 10.
Every field below came back populated on the live call.
initialState.cbirSites.sites is the main event. One entry per page carrying the image:
{
"title": "Shaki Waterfall - Wikipedia",
"description": "Shaki Waterfall. ",
"url": "https://en.wikipedia.org/wiki/Shaki_Waterfall?utm_medium=organic&utm_source=yandexsmartcamera",
"domain": "en.wikipedia.org",
"thumb": { "url": "//avatars.mds.yandex.net/i?id=eb74556e...", "height": 90, "width": 148 },
"originalImage": { "url": "https://upload.wikimedia.org/.../960px-Shaki_waterfall.jpg", "height": 719, "width": 960 }
}Two things to handle in your own code. Yandex appends ?utm_medium=organic&utm_source=yandexsmartcamera to every result URL, so strip the query string before you deduplicate. And thumb.url is protocol relative, starting with //, so prefix https: before you fetch it.
The other slices, all confirmed present on the live page:
| Slice | Holds |
|---|---|
cbirSites.sites |
37 source pages on the test image, pageSize 10 |
cbirSitesList.sites |
4 entries, same object shape, the abbreviated render |
cbirSimilar.thumbs |
Visually similar images, plus similarPageUrl |
cbirMarketProducts.products |
Shopping matches, plus relatedProducts and moreButtonUrl |
cbirOcr |
plainText, hasText, blocks, entities. Text recognised inside the image |
cbirBarcode.barcodeDataList |
Barcodes and QR codes decoded from the image |
cbirTags.tags |
Yandex's own category labels for the image |
cbirOtherSizes.items |
small_dups, medium_dups, large_dups. The same image at other resolutions |
cbirPreview |
cropRect, imageWidth, imageHeight, imageExpired |
cbirOcr deserves a mention on its own. It means one 75 credit call gives you reverse image matches and the text inside the image, with no separate OCR step.
This is the failure that looks like a scraping problem and is not one. Three images, same configuration, same stealth tier:
| Image | cbirPreview |
Result |
|---|---|---|
| Wikimedia Commons full size JPEG | imageExpired: false, 1024x767 |
37 site matches, 40 similar, 5 tags |
A Wikimedia thumb/ derivative URL |
imageExpired: true |
0 matches |
A nasa.gov PNG |
imageExpired: true, width 0, height 0 |
0 matches, pageSize: 0 |
Yandex fetches the image from the URL you hand it before it searches. If its crawler cannot reach the file, or the URL is a short lived derivative, you get a valid page with an empty result set and no error message. So before you conclude an image has no matches:
- Check
cbirPreview.imageExpired. If it istrue, the URL was not usable, and both failing images above reported exactly that. - Check
cbirPreview.imageWidth. A zero there means Yandex never loaded the file. The working image reported 1024 by 767 instead. - Retry with a stable, directly addressable, full size URL.
An empty sites array plus imageWidth: 0 is an input problem. An empty sites array with real dimensions is a genuine no match.
pip install yandex-reverse-image-api
npm install yandex-reverse-image-apifrom yandex_reverse_image_api import YandexReverseImage
bee = YandexReverseImage("YOUR_API_KEY")
img = "https://upload.wikimedia.org/wikipedia/commons/3/3c/Shaki_waterfall.jpg"
bee.check_image(img) # {'expired': False, 'width': 1024, 'height': 767, 'usable': True}
bee.sites(img) # 37 matches, utm stripped, thumbnails made absolute
bee.domains(img) # match count per domain, deduplicated
bee.similar(img) # 40 visually similar images
bee.tags(img) # 5 Yandex category labels
bee.other_sizes(img) # {'small_dups': 6, 'medium_dups': 6, 'large_dups': 6}
bee.ocr(img) # {'hasText': False, 'plainText': '', ...} on a photo with no text
bee.products(img) # empty on a landscape photo, populated on an objectThose are the real return values from the live sweep, not illustrative ones. products and ocr came back empty because a waterfall photograph has neither a shopping match nor text in it, which is the correct answer rather than a failure.
Measured on live calls:
| Configuration | Credits | Outcome |
|---|---|---|
mode=auto |
1 | SmartCaptcha page |
stealth_proxy=true |
75 | Real results |
| Validation error | 0 | Nothing billed |
There is no middle rung that works here, so the practical cost of Yandex reverse image search is 75 credits per lookup. At the entry paid tier of 250,000 credits that is roughly 3,300 lookups a month. Cache aggressively, because ScrapingBee does not cache for you and reverse image results for a fixed image change slowly. Plan tiers are on the pricing page.
Also worth knowing: stealth_proxy forces JavaScript rendering, and mode=auto is incompatible with it. Sending both returns HTTP 400 and bills nothing.
Public Yandex Images results only. There is no signed in surface involved here, and scraping under login credentials is prohibited by ScrapingBee's terms of service in any case. Reverse image results routinely point at pages carrying photographs of identifiable people, so if you are running this for brand protection or counterfeit detection, handle the matches as personal data where that applies rather than as a plain URL list.
Yandex text search and Yandex Images keyword search are different endpoints with different economics. ScrapingBee covers those through the Yandex images API page, and the walkthrough for the text side is in how to scrape Yandex search results. For interacting with the upload widget rather than passing a URL, the JavaScript scenario parameter drives clicks and waits inside the rendered page.
Yandex's own Terms of Use of Yandex Search govern the service, and Yandex publishes a paid Search API for the text corpus, which is the right tool when the text index is what you need.
Why do I get a CAPTCHA on Yandex reverse image search? Because the request came from a datacenter IP. Yandex serves SmartCaptcha to anything below the stealth tier on this surface. Verified across both cheap configurations tested.
How do I find every website using my image?
The sites tab, cbir_page=sites, then read initialState.cbirSites.sites. Each entry gives the page URL, its domain, and the resolution of the copy hosted there.
Can I use extract_rules for this?
No. There are no result nodes in the delivered markup, so selectors match nothing. The data is JSON inside data-state attributes and has to be parsed after the fetch.
Does Yandex reverse image search read text in images?
Yes, and it comes back in the same response, under cbirOcr.plainText. No separate call and no extra credits.
Is it better than Google reverse image search? For some categories, notably faces and Eastern European sources, Yandex returns matches Google does not. Run both if coverage matters.
MIT. See LICENSE.