How to Scrape Yandex Search Results in 2026
Scrape Yandex search results reliably: lr region codes, Cyrillic queries, SmartCaptcha detection, the official XML API, and working SparkProxy code.

To scrape Yandex search results, request https://yandex.ru/search/?text=QUERY&lr=213&p=0&noreask=1 through a residential exit in the target country, parse the BEM serp-item markup, treat any response containing /showcaptcha as a block, and move organic-only workloads to the official Yandex Search API.
Most guides that claim to teach this are Google tutorials with the domain swapped, which earns you a captcha page in about forty requests. Yandex runs a different index per country, ranks by a region ID you pass in the URL, rewrites Cyrillic queries before searching, defends itself with its own SmartCaptcha, and publishes a first-party API with a quota model unlike anything Google offers. This guide is the delta, with runnable SparkProxy code and honest notes on where each approach breaks.
Why Yandex is not Google
Five differences change how you write the scraper.
One engine, several indexes. yandex.ru and yandex.com do not return the same ten results for one query: different ranking models, different language priors, different SERP furniture. Turkey has its own front door, yandex.com.tr.
Region is an explicit URL parameter. Google infers locality from your IP plus a gl hint. Yandex takes an integer, lr, from its own geo tree: two requests from one IP with lr=213 and lr=2 return visibly different SERPs.
Pagination is zero-indexed. p=0 is the first page where Google uses start=0. Everyone arriving from Google gets this wrong once, silently scraping page two as page one.
Query rewriting is aggressive. Yandex lemmatises Russian, repairs keyboard-layout mistakes, and transliterates Latin-typed Russian into Cyrillic, so your scraper can record a query you never sent.
The captcha is Yandex's own. SmartCaptcha is a Yandex Cloud product unrelated to reCAPTCHA or hCaptcha, and its challenge page returns HTTP 200, so a naive status check reports success.
If you have read our guide to scraping Google search results, treat this as the companion, not a reskin: the pacing instincts transfer, almost nothing else does.
Pick the domain and index
Decide the index first: it sets the language of the SERP furniture you parse, and how hard the target defends itself.
| Host | Index and audience | Interface | Notes |
|---|---|---|---|
| `yandex.ru` | Russian index, Russia and CIS | Russian | Deepest index, strictest anti-bot |
| `yandex.com` | International index | English | Own ranking model, thin Russian |
| `yandex.com.tr` | Turkish index | Turkish | Own ad inventory |
| `yandex.kz`, `yandex.by`, `yandex.uz` | Kazakhstan, Belarus, Uzbekistan | Local | Core index, preset region |
The path is identical everywhere, so a minimum viable SERP URL is https://yandex.com/search/?text=proxy%20server&lr=225&p=0. Country front doors are not separate indexes; they are the core index with a regional default, which is why setting lr explicitly beats picking a hostname and hoping.
Scraping at scale? Skip the blocks.
Fast, unblockable datacentre proxies with unlimited bandwidth.
The lr region code
lr is a numeric ID from Yandex's geo tree. It changes organic ranking, which local businesses appear, which ads are eligible, and the currency inside wizard blocks. No other parameter moves the results as much.
| `lr` | Region |
|---|---|
| 213 | Moscow |
| 2 | Saint Petersburg |
| 225 | Russia (country level) |
| 143 | Kyiv |
| 187 | Ukraine |
| 157 | Minsk |
| 149 | Belarus |
| 162 | Almaty |
| 159 | Kazakhstan |
| 11508 | Istanbul |
| 983 | Turkey |
Treat that table as a starting point, not gospel: codes for smaller cities move when Yandex reorganises the tree. Resolve them with the Direct API method Dictionaries.get and DictionaryNames: ["GeoRegions"], which returns GeoRegionId, GeoRegionName, GeoRegionType and ParentId for the same tree lr comes from. Cache it, refresh quarterly, key job configs on region names rather than raw integers.
Two behaviours catch people out. Region autodetection still runs: pass lr=213 from a German exit and Yandex sees a Moscow hint from Frankfurt. Ranking usually honours lr, but the mismatch is a bot signal and some local blocks fall back to the IP-derived region, so match exit country to region. And rstr is the negative form: &rstr=-213 returns everything except Moscow.
Geo accuracy is the whole game for rank tracking, and our explainer on geo-targeting in proxies covers the mechanics.
Cyrillic queries and Yandex operators
The query goes in text, percent-encoded as UTF-8, so ΠΊΡΠΏΠΈΡΡ ΠΏΡΠΎΠΊΡΠΈ becomes %D0%BA%D1%83%D0%BF%D0%B8%D1%82%D1%8C%20%D0%BF%D1%80%D0%BE%D0%BA%D1%81%D0%B8. Let the HTTP library encode it and never concatenate an already-encoded string, the usual source of double-encoded %25D0 garbage.
from urllib.parse import urlencode
def yandex_url(query, lr=213, page=0, host="yandex.ru", noreask=True):
params = {
"text": query,
"lr": lr,
"p": page, # zero-indexed: p=0 is the first page
"lang": "ru",
}
if noreask:
params["noreask"] = 1 # disable "did you mean" rewriting
return f"https://{host}/search/?{urlencode(params)}"
print(yandex_url("ΠΊΡΠΏΠΈΡΡ ΠΏΡΠΎΠΊΡΠΈ", lr=213, page=0))
# https://yandex.ru/search/?text=%D0%BA%D1%83%D0%BF%D0%B8%D1%82%D1%8C+...&lr=213&p=0&lang=ru&noreask=1
noreask=1 is the parameter most tutorials omit and the one that most affects data quality. Without it Yandex substitutes what it thinks you meant, and three rewrites fire regularly:
- Morphological expansion. Russian is heavily inflected and Yandex matches lemmas, so
ΠΊΡΠΏΠΈΡΡ ΠΏΡΠΎΠΊΡΠΈandΠΊΡΠΏΠ»Ρ ΠΏΡΠΎΠΊΡΠΈcollapse toward one result set. - Keyboard-layout repair. A query typed on the wrong layout,
ghjrcb, becomesΠΏΡΠΎΠΊΡΠΈ. Helpful for humans, poison for a keyword tracker. - Transliteration. Latin-typed Russian like
kupit proksimaps back to Cyrillic, so a transliterated keyword list measures a query you never stored.
With noreask=1 the suggestion banner still appears, but the results belong to the query you sent. Where you cannot use it, read the misspell indicator and flag those rows.
Yandex also ships a richer operator language than Google, working in HTML search and the XML API alike.
| Operator | Meaning |
|---|---|
| `"ΡΠΎΡΠ½Π°Ρ ΡΡΠ°Π·Π°"` | Exact phrase |
| `!ΡΠ»ΠΎΠ²ΠΎ` | Exact word form only, no morphology |
| `!!ΡΠ»ΠΎΠ²ΠΎ` | All morphological forms of this lemma |
| `+ΡΠ»ΠΎΠ²ΠΎ` | Word must be present |
| `-ΡΠ»ΠΎΠ²ΠΎ` | Exclude the word |
| `site:sparkproxy.io` | Restrict to a site including subdomains |
| `host:sparkproxy.io` | Restrict to one exact host |
| `url:sparkproxy.io/blog/*` | Restrict to a URL pattern |
| `mime:pdf`, `lang:ru` | Restrict by file type or language |
| `date:20260101..20260818` | Date range on the document |
| `ΠΏΡΠΎΠΊΡΠΈ /3 ΡΠ΅ΡΠ²Π΅Ρ` | Words within three positions |
The ! operator is the practical one: if a tracked brand name collides with a common Russian noun, !Π±ΡΠ΅Π½Π΄ stops Yandex expanding into every declension and gives you a stable time series.
Fetch the SERP with SparkProxy
The SparkProxy Scraping API handles the proxy pool, browser, and fingerprint, so your code makes one HTTP call. Base URL is https://scrape.sparkproxy.io/api/v1 with an X-API-Key header, and every parameter is in the Scraping API docs. Start with a residential fetch of a Moscow SERP:
curl -G "https://scrape.sparkproxy.io/api/v1" \
-H "X-API-Key: sk-your-key-here" \
--data-urlencode "url=https://yandex.ru/search/?text=ΠΊΡΠΏΠΈΡΡ+ΠΏΡΠΎΠΊΡΠΈ&lr=213&p=0&noreask=1" \
--data-urlencode "render_js=true" \
--data-urlencode "premium_proxy=true" \
--data-urlencode "country_code=RU" \
--data-urlencode "stealth=true"
--data-urlencode matters: it encodes the Cyrillic and the nested ? and & of the target URL so the API receives one intact parameter. Hand-building that string is the most common reason a first Yandex call returns the homepage instead of a SERP.
The production version is a POST with a wait condition, so you never capture the page before the organic list paints:
import requests
API = "https://scrape.sparkproxy.io/api/v1"
KEY = "sk-your-key-here"
def fetch_yandex(query, lr=213, page=0, host="yandex.ru", country="RU"):
target = yandex_url(query, lr=lr, page=page, host=host)
r = requests.post(
API,
headers={"X-API-Key": KEY, "Content-Type": "application/json"},
json={
"url": target,
"render_js": True,
"premium_proxy": True, # residential exit, not a datacenter ASN
"country_code": country, # keep the exit country aligned with lr
"stealth": True, # extra anti-bot layers, requires render_js
"wait_for": "#search-result, .serp-list",
"device": "desktop",
"format": "html",
"tag": f"yandex:{lr}:{page}",
},
timeout=120,
)
r.raise_for_status()
return r.text
Each parameter answers a specific Yandex defence, and each costs credits.
| Yandex signal | SparkProxy parameter | Why it helps |
|---|---|---|
| Cloud ASN reputation | `premium_proxy: true` | Residential exit, not a datacenter range |
| Region mismatch against `lr` | `country_code: "RU"` / `"TR"` | Exit country matches the requested region |
| Wizard blocks injected by JavaScript | `render_js: true` | Some blocks are absent from the raw HTML |
| Antirobot fingerprint checks | `stealth: true` | TLS and headers consistent with real Chrome |
| Mobile layout differs | `device: "mobile"` | Mobile returns a different block order |
| Late-painting result list | `wait_for: "#search-result"` | Avoids capturing an empty shell |
Credit costs stack: a residential request with JavaScript is 25 credits, with country_code and stealth adding 5 each. Where the SERP parses without a browser, drop render_js to false and pay 10. Test both on your own keywords; the answer differs by region and query type.
The Turkish index, from Node:
const axios = require('axios');
const target = 'https://yandex.com.tr/search/?text=' +
encodeURIComponent('vekil sunucu') + '&lr=11508&p=0';
axios.get('https://scrape.sparkproxy.io/api/v1', {
headers: { 'X-API-Key': 'sk-your-key-here' },
params: {
url: target,
render_js: 'true',
premium_proxy: 'true',
country_code: 'TR',
stealth: 'true'
}
})
.then(res => console.log(res.data.length, 'bytes'))
.catch(err => console.error(err.response?.status, err.response?.data));
Pagination is a loop over p, and this is where the zero-index bites:
import random, time
def collect(query, lr=213, pages=3, host="yandex.ru"):
rows = []
for p in range(pages): # p = 0, 1, 2 -> pages 1, 2, 3
html = fetch_yandex(query, lr=lr, page=p, host=host)
batch = parse_organic(html)
if not batch:
break # empty page means blocked or exhausted
for r in batch:
r["position"] += p * 10 # absolute position across pages
rows.extend(batch)
time.sleep(random.uniform(4, 12)) # jitter, never a constant sleep
return rows
Parse organic results and wizard blocks
Here is a genuine advantage over Google. Yandex invented BEM and its SERP markup still follows it, so class names read like serp-item, organic__url-text, OrganicTitle-LinkText. Semantic, and they rotate far less often than Google's obfuscated hashes. They do rotate, so anchor on structure and attributes first, classes second.
from bs4 import BeautifulSoup
def parse_organic(html):
soup = BeautifulSoup(html, "lxml")
rows = []
for i, item in enumerate(soup.select("li.serp-item"), start=1):
# Direct ads carry a "Π Π΅ΠΊΠ»Π°ΠΌΠ°" label; skip them for organic-only tracking
if "Π Π΅ΠΊΠ»Π°ΠΌΠ°" in item.get_text():
continue
link = item.select_one("a.OrganicTitle-Link, a.organic__url, a.Link")
title = item.select_one(".OrganicTitle-LinkText, .organic__url-text")
if not link or not link.get("href"):
continue
rows.append({
"position": i,
"url": link["href"],
"title": title.get_text(strip=True) if title else "",
"wizard": item.get("data-fast-name"), # present on wizard blocks
"snippet": (item.select_one(".OrganicTextContentSpan,"
" .organic__content-wrapper")
or item).get_text(" ", strip=True)[:300],
})
return rows
The data-fast-name attribute is the useful hook: set on a serp-item, it names the wizard type, classifying a block without pattern-matching visible text in three languages.
Yandex calls its feature blocks "ΠΊΠΎΠ»Π΄ΡΠ½ΡΠΈΠΊΠΈ", wizards. They are not the set Google ships, so a Google parser mislabels most of them.
| Block | Russian name | What it is | Parsing hook |
|---|---|---|---|
| Organic result | ΠΡΠ³Π°Π½ΠΈΡΠ΅ΡΠΊΠ°Ρ Π²ΡΠ΄Π°ΡΠ° | Standard blue link | `li.serp-item` with `.organic` |
| Direct ads | Π―Π½Π΄Π΅ΠΊΡ ΠΠΈΡΠ΅ΠΊΡ | Paid, top and bottom | "Π Π΅ΠΊΠ»Π°ΠΌΠ°" label inside the item |
| Quick answer | ΠΡΡΡΡΡΠΉ ΠΎΡΠ²Π΅Ρ | One-line factual answer | Above the first `serp-item` |
| Organization card | Π―Π½Π΄Π΅ΠΊΡ ΠΠΈΠ·Π½Π΅Ρ | Local pack from Yandex Maps | Links to `yandex.ru/maps` |
| Market carousel | Π―Π½Π΄Π΅ΠΊΡ ΠΠ°ΡΠΊΠ΅Ρ | Products with prices | Links to `market.yandex.ru` |
| Turbo page | Π’ΡΡΠ±ΠΎ-ΡΡΡΠ°Π½ΠΈΡΠ° | Yandex-hosted copy, the AMP equivalent | URL has `/turbo?text=` |
| Dzen articles | ΠΠ·Π΅Π½ | Promoted feed content | Links to `dzen.ru` |
| Video, Images, related | ΠΠΈΠ΄Π΅ΠΎ, ΠΠ°ΡΡΠΈΠ½ΠΊΠΈ | Carousels and suggestions | `data-fast-name` on the block |
Turbo pages deserve a warning. A Turbo result points at a Yandex-hosted copy rather than the publisher's URL, so a naive tracker records yandex.ru/turbo?text=... as the ranking domain and the client's real domain looks like it vanished. The destination sits in that URL's query string, so unwrap it before storing the row. Nobody porting a Google parser catches this first time.
To skip parsing, extract_rules returns structured JSON instead of HTML:
payload = {
"url": yandex_url("ΠΏΡΠΎΠΊΡΠΈ Π΄Π»Ρ ΠΏΠ°ΡΡΠΈΠ½Π³Π°", lr=213),
"render_js": True,
"premium_proxy": True,
"country_code": "RU",
"stealth": True,
"extract_rules": {
"titles": {"selector": "li.serp-item .OrganicTitle-LinkText", "type": "list"},
"links": {"selector": "li.serp-item a.OrganicTitle-Link", "type": "list",
"output": "@href"},
},
}
Keep selectors in config, not code. Yandex ships markup changes without notice, and editing a JSON file beats cutting a release.
SmartCaptcha: detect, do not fight
SmartCaptcha launched as a Yandex Cloud service in 2022 and also guards Yandex's own properties. It is a checkbox-first challenge that escalates to image and slider tasks on a behavioural score. Not reCAPTCHA, not hCaptcha, and the token flow differs, so solvers built for Google's challenge typically fail against it.
The detection rule that matters: the challenge returns HTTP 200, so check the body and the final URL, never the status code.
class BlockedError(Exception):
pass
CAPTCHA_MARKERS = (
"/showcaptcha", # the challenge path Yandex redirects to
"SmartCaptcha",
"captcha__image",
"ΠΠΎΠ΄ΡΠ²Π΅ΡΠ΄ΠΈΡΠ΅, ΡΡΠΎ Π·Π°ΠΏΡΠΎΡΡ ΠΎΡΠΏΡΠ°Π²Π»ΡΠ»ΠΈ Π²Ρ", # "confirm you sent these requests"
)
def is_blocked(html: str) -> bool:
return any(m in html for m in CAPTCHA_MARKERS)
def fetch_checked(query, **kw):
html = fetch_yandex(query, **kw)
if is_blocked(html):
raise BlockedError("SmartCaptcha served")
if "serp-item" not in html:
raise BlockedError("no results in response") # soft block or empty shell
return html
The /showcaptcha path is the strongest single signal. Yandex redirects to https://yandex.ru/showcaptcha?cc=1&retpath=..., and retpath holds your original query, so log which keyword tripped it.
When it fires:
- Stop that worker. Retrying the same query on the same session returns the challenge again and burns credits.
- Rotate the exit, then back off with jitter. Take a fresh residential IP and requeue the keyword at roughly 60 seconds, then 300, then 900, each delay multiplied by a random 0.5 to 1.5 so parallel workers do not retry in lockstep. Park it after the third failure instead of grinding. The general shape is in our post on retry and backoff strategies.
- Treat a captcha rate above roughly 5 percent as a configuration problem, not a traffic problem. Fingerprint, pacing, or exit geography is wrong, and concurrency makes it worse.
- Fall back to the XML API for keywords that keep failing. It is sanctioned and never serves a captcha.
Solving SmartCaptcha at scale is expensive and brittle. Better pacing plus the official API for the hard tail costs less.
The sanctioned route: Yandex XML API
Unlike Google, Yandex publishes a first-party interface returning real SERP documents, not a cut-down custom-search subset. If the quota fits your volume, reach for it before writing a selector.
Two generations exist. The classic XML endpoint is https://yandex.com/search/xml (or the .ru equivalent), authenticated with a Yandex Cloud folderid plus an apikey. Search API v2 lives at https://searchapi.api.cloud.yandex.net/v2/web/search, speaks REST and gRPC, and adds a deferred mode that buys a larger hourly allowance in exchange for waiting. Both share one documentation set.
curl -G "https://yandex.com/search/xml" \
--data-urlencode "folderid=b1gxxxxxxxxxxxxxxxxx" \
--data-urlencode "apikey=AQVNxxxxxxxxxxxxxxxxxxxx" \
--data-urlencode "query=ΠΊΡΠΏΠΈΡΡ ΠΏΡΠΎΠΊΡΠΈ" \
--data-urlencode "lr=213" \
--data-urlencode "l10n=ru" \
--data-urlencode "sortby=rlv" \
--data-urlencode "filter=none" \
--data-urlencode "groupby=attr=d.mode=deep.groups-on-page=10.docs-in-group=1" \
--data-urlencode "page=0"
In groupby, groups-on-page sets results per page and docs-in-group pages per domain. Parsing is plain XML, and the error element comes first.
import requests, xml.etree.ElementTree as ET
def yandex_xml(query, folder_id, api_key, lr=213, page=0):
r = requests.get("https://yandex.com/search/xml", params={
"folderid": folder_id, "apikey": api_key,
"query": query, "lr": lr, "l10n": "ru",
"sortby": "rlv", "filter": "none", "page": page,
"groupby": "attr=d.mode=deep.groups-on-page=10.docs-in-group=1",
}, timeout=60)
root = ET.fromstring(r.content)
err = root.find(".//error")
if err is not None:
raise RuntimeError(f"Yandex XML error {err.get('code')}: {err.text}")
out = []
for doc in root.findall(".//doc"):
title_el = doc.find("title")
passage = doc.find(".//passage")
out.append({
"url": doc.findtext("url"),
"domain": doc.findtext("domain"),
"title": "".join(title_el.itertext()) if title_el is not None else "",
"snippet": "".join(passage.itertext()) if passage is not None else "",
})
return out
Note the itertext() calls. Yandex wraps matched terms in tags inside title and passage, so .text truncates at the first highlighted word. That is behind most "why are my Yandex titles cut off" bug reports.
The documented error codes you will meet:
| Code | Meaning | What to do |
|---|---|---|
| 15 | Nothing found for this query | Record an empty SERP, do not retry |
| 32 | Request limit exceeded | Back off, you are over the hourly slice |
| 33 | Requesting IP is not allow-listed | Fix the allow list, stop proxying this call |
| 37 | Invalid API key | Credentials problem, fail loudly |
| 55 | Daily request limit exceeded | Stop until the quota window resets |
Two quota behaviours decide the fit.
The quota unit is the hour. Published quotas and limits run to 10,000 synchronous requests per hour at 10 per second, or 35,000 per hour deferred, where a request takes at least five minutes and the result is held for 12 hours. The legacy XML console said the same thing differently: a daily allowance spread across 24 hourly slots. Either way a burst at 09:00 exhausts that hour, and you get a hard error rather than gentle throttling. Size workers to the hourly slice, not the daily total.
Depth is capped too. The same table caps results returned at 250 per request, and HTML pagination dries up around 1,000 documents, so deep-tail rank research is unavailable by either route.
Error 33 exists because the interface is IP-bound, the constraint that surprises proxy users most. You register the addresses allowed to call it, so XML traffic through a rotating residential pool is rejected by design. Run XML calls from a few fixed egress addresses, keep the proxy pool for HTML, and never mix both in one worker. That mix is the most common architecture mistake in Yandex pipelines.
| HTML SERP scraping | Yandex XML / Search API v2 | |
|---|---|---|
| Sanctioned by Yandex | No | Yes |
| Ads and wizard blocks | Full, as rendered | Organic documents only |
| Captcha risk | Real | None |
| Cost driver | Credits per request | Quota per 1,000 requests |
| Rate model | Yours to control | Fixed hourly allocation |
| IP handling | Rotating pool required | Fixed allow-listed IPs |
| Depth | Limited by block rate | Up to 250 results per request |
The pragmatic architecture uses both: XML for organic ranking series, HTML for the ad blocks, Market carousels, and local packs the API omits. Our SERP scraping proxy explainer covers that split in the general case.
Datacenter IPs, pacing, block rates
Be honest about this part: Yandex is harder on datacenter ranges than Google. It runs its own cloud business, classifies ASNs in detail, and its Antirobot system treats cloud egress as suspicious. In practical testing, plain datacenter IPs hitting yandex.ru reach /showcaptcha within a few dozen queries, sometimes on the first request from a subnet with history, while residential exits inside the target country sustain roughly 100 to 200 queries per IP per day at human-like pacing. Those are practitioner ranges rather than a published study, and they move with subnet reputation.
The rules that follow:
- Residential exits in-country for
yandex.ru. A Russian query answered from a Moscow residential IP is unremarkable. From a Virginia cloud range it is not. The trade-offs sit in residential vs datacenter proxies. - Jitter, never a constant sleep. Uniform 5-second gaps are a stronger bot tell than a higher raw rate. Randomise between 4 and 12 seconds.
- Keep sessions short. Yandex sets a
yandexuididentifier and ayppreferences cookie carrying region state, so long-lived cookie jars accumulate a behavioural profile. Rotate identity with the IP. - Do not parallelise into a captcha. When block rate climbs, concurrency makes it climb faster. Cut workers first, then investigate.
- Cache hard. Most rank-tracking questions need daily granularity.
The .tr and .com properties are noticeably more tolerant than .ru, so if you only need the international index, target yandex.com and save residential budget for the queries that need a Russian exit. The wider detection surface is covered in how to avoid getting your proxy blocked.
Legal, ToS, and sanctions
Yandex's terms restrict automated access outside its own interfaces, which puts HTML SERP scraping in the same contractual grey zone as Google. Scraping public results is not a criminal act in most jurisdictions, but it can breach the contract, and a first-party API weakens any argument that no alternative existed. Use the Search API where it fits, keep volumes proportional, and honour back-off signals.
One factual note on the corporate situation, because it affects whether you can open an account at all. Yandex N.V. completed the sale of its Russian businesses on 15 July 2024 and renamed itself Nebius Group, so the Russian operations, Yandex Cloud in Russia included, sit under separate ownership. Several Russian entities and payment channels fall under EU, UK, and US sanctions programmes, so buying Yandex Cloud quota can be restricted depending on where you are incorporated and which entity you contract with. Confirm current designations with your compliance team.
Collect only public SERP data, never logged-in Yandex surfaces, personal accounts, or Yandex Mail. For your own site's rankings, Yandex Webmaster hands you the numbers with zero terms-of-service exposure.
Frequently asked questions
FAQ
Yes. Yandex XML and the newer Search API v2 return real SERP documents, authenticated with a Yandex Cloud folder ID and API key. It is the sanctioned route for organic results, but it excludes ads and wizard blocks, returns up to 250 results per request, and caps synchronous use at 10,000 requests per hour.
lr is a numeric region ID from Yandex's geo tree, such as 213 for Moscow or 11508 for Istanbul. It reorders organic results and selects which local blocks appear, so a scraping job that ignores lr collects an undefined region.
Usually not. SmartCaptcha is a separate Yandex Cloud product with its own challenge types and token flow, so services built around Google's reCAPTCHA generally cannot return a valid token. Rotating the exit IP, slowing down, or falling back to the XML API is cheaper than solving it.
Poorly on yandex.ru. Yandex classifies cloud ASNs aggressively and often serves /showcaptcha within a few dozen requests from a datacenter range. Residential exits in the target country hold up far better, and yandex.com is more tolerant than the Russian index.
Fewer than you would like. The Search API limits table caps one request at 250 results, and HTML pagination dries up near 1,000 documents. Pages are zero-indexed, so p=0 is the first page. Plan for shallow requests, not one deep pull.
No. yandex.com serves an international, English-first index, yandex.ru the Russian-language index, and yandex.com.tr Turkey. Rankings, ad inventory, and SERP blocks differ, so pick the host for the market you measure, then set lr on top.
Get 20% off your first month
Premium datacentre proxies with unlimited bandwidth. Use the code at checkout.
Save up to 15% more on quarterly, half-yearly and yearly plans
Related articles

How to Scrape Zomato and Swiggy Data (Menus and Prices)
Scrape Zomato and Swiggy data that means something: pin the delivery coordinates, key every row by restaurant, pin and timestamp, and split the fee stack.

How to Scrape Vinted Listings
Scrape Vinted listings across vinted.fr, .de and .co.uk: the internal JSON API, cookie bootstrapping, per-market catalogue IDs, and GDPR-safe resale analytics.

How to Scrape TikTok Public Data With Proxies
Scrape TikTok public data with proxies: read the hydration JSON blob, use the Research API and oEmbed, detect fake 200s, and cut credits per good page.
