← Практика · 8 сентября 2026
Получаем токен, ходим в API Вебмастера из терминала, вешаем на крон переобход и наблюдение, а разбор жалоб отдаём Claude Code: спрашиваете словами «что там с сайтом?» и получаете диагноз с исправлением.
10 минут чтенияинструменты

У меня двадцать два сайта в Яндекс.Вебмастере. Заходить в интерфейс каждого раз в неделю — это два часа кликов, и я их не делаю. А значит, и не вижу вовремя ни выпавших из поиска страниц, ни жалоб диагностики: узнаю о них, когда уже надо чинить.
Вебмастер при этом отдаёт всё то же самое через API за секунду. Если рядом сидит нейросеть, которая умеет вызывать curl, интерфейс становится не нужен вовсе: «что там с eden96?» — и через минуту в терминале диагноз, а не отчёт.
Ниже ровно то, что стоит у меня и работает каждое утро.
# я пишу в Claude Code:
# «Вебмастер жалуется на eden96, разберись»
#
# сессия сама:
# 1. снимает summary и diagnostics по API
# 2. сверяет страницы в поиске с картой сайта
# 3. курлит подозрительные адреса и находит петли редиректов
# 4. чинит .htaccess в репозитории, пересобирает, выкладывает
# 5. отправляет исправленные адреса на переобход
Плюс два скрипта на кроне: один тратит суточную квоту переобхода, другой раз в день записывает показатели и кричит, если что-то просело.
API Вебмастера ходит по OAuth-токену. Токен получают одной ссылкой:
https://oauth.yandex.ru/authorize?response_type=token&client_id=ba16a05cf7134a29aadd139e8c12c891
Это идентификатор стандартного приложения «Яндекс.Вебмастер API» — у него нет секрета, свой регистрировать не нужно. Открываете ссылку, входите в аккаунт, которому принадлежат сайты, и Яндекс показывает токен прямо на странице. Копируете в файл:
umask 077
echo 'ВСТАВИТЬ_ТОКЕН' > ~/.yandex_webmaster_token
Три вещи, которые стоит знать заранее.
Токен привязан к аккаунту, а не к сайту. Один файл открывает все подтверждённые хосты сразу: у меня — двадцать два, любому проекту и любой сессии.
Живёт шесть месяцев. Выдан пятого сентября — протухнет в начале марта. Признак — на любой запрос приходит HTTP 401. Совсем чужой или битый токен, кстати, отдаёт 403, так что любой отказ с кодом 4xx на вчера работавший запрос — это про токен, а не про сайт. Продлить токен может только человек: та же ссылка, тот же вход. Нейросеть тут бессильна, и это правильно.
Значение токена в терминал не печатать. Все примеры ниже читают его из файла. Если сессия нейросети всё же выведет его в лог — заводите новый.
user_id и host_id сайтаВсе адреса API начинаются с числового идентификатора пользователя:
T=$(cat ~/.yandex_webmaster_token)
curl -s -H "Authorization: OAuth $T" https://api.webmaster.yandex.net/v4/user/
# {"user_id":79452740}
Дальше — список хостов. Здесь появляются первые грабли:
U=79452740
curl -s -H "Authorization: OAuth $T" "https://api.webmaster.yandex.net/v4/user/$U/hosts/"
{"host_id": "https:eden96.ru:443", "verified": true, "ascii_host_url": "https://eden96.ru/"}
{"host_id": "https:xn--80aahsbi1ccix.xn--p1ai:443", "unicode_host_url": "https://играйсразу.рф/"}
host_id — это не URL, а строка вида схема:домен:порт. Она подставляется в адрес как есть, без urlencode. Если закодировать двоеточия, получите 404 на все запросы и полчаса потерянного времени. Кириллический домен уже в punycode — так и передаём.
Заводим переменную, все команды ниже от неё:
H="https:eden96.ru:443"
B="https://api.webmaster.yandex.net/v4/user/$U/hosts/$H"
Четыре запроса закрывают девяносто процентов вопросов.
Сводка — сколько в поиске, сколько исключено, индекс качества:
curl -s -H "Authorization: OAuth $T" "$B/summary/"
# {"sqi":30,"excluded_pages_count":1,"searchable_pages_count":96,"site_problems":{"POSSIBLE_PROBLEM":1}}
Диагностика — те самые «жалобы» из интерфейса:
curl -s -H "Authorization: OAuth $T" "$B/diagnostics/" | python3 -m json.tool
Тут важно, как это устроено. problems приходит не списком, а словарём, где ключ — код проблемы, и в нём перечислены все известные Вебмастеру проблемы, включая те, которых у сайта никогда не было:
"DOCUMENTS_MISSING_DESCRIPTION": {
"severity": "POSSIBLE_PROBLEM",
"state": "PRESENT",
"last_state_update": "2026-08-23T10:24:03.804+03:00"
},
"SLOW_AVG_RESPONSE_TIME": {
"severity": "CRITICAL",
"state": "ABSENT",
"last_state_update": null
}
Живая проблема — только state: PRESENT. ABSENT — снятая или никогда не возникавшая. И обязательно смотрите last_state_update: Вебмастер обновляет диагностику не каждый день, и отчёт часто описывает состояние, которого уже нет. Если дата последнего обновления раньше даты вашей выкладки — проблема, скорее всего, уже вылечена, просто робот не перепроверил.
Страницы в поиске — с заголовком и датой последнего обхода:
curl -s -H "Authorization: OAuth $T" "$B/search-urls/in-search/samples/?limit=100"
События — что появилось и что выпало, и главное — почему:
curl -s -H "Authorization: OAuth $T" "$B/search-urls/events/samples/?limit=100"
{"url": "https://eden96.ru/category/esteticheska-stomatologiya/restavraciya-zubov/",
"event": "REMOVED_FROM_SEARCH",
"excluded_url_status": "REDIRECT_NOTSEARCHABLE",
"target_url": "http://eden96.ru/"}
Вот эта запись — готовый диагноз. Страница старого движка выпала, потому что редиректит, а редиректит она на http://, хотя сайт давно на https://. Про это ниже.
Самый быстрый способ найти причину жалобы — не читать отчёты, а вычесть одно множество из другого. Берём адреса из поиска и адреса из sitemap.xml:
curl -s -H "Authorization: OAuth $T" "$B/search-urls/in-search/samples/?limit=500" \
| python3 -c "import json,sys; print('\n'.join(s['url'] for s in json.load(sys.stdin)['samples']))" \
| sort > /tmp/in-search.txt
grep -o '<loc>[^<]*' dist/sitemap.xml | sed 's/<loc>//' | sort > /tmp/sitemap.txt
comm -23 /tmp/in-search.txt /tmp/sitemap.txt # в поиске, но не в карте: мусор старого движка
comm -13 /tmp/in-search.txt /tmp/sitemap.txt # в карте, но не в поиске: робот их не видит
На eden96 первая разница показала хвост от WordPress: /category/..., /tag/..., /page/2/. Сайт два раза сменил движок, а поисковик всё это помнил и держал в индексе, потому что адреса не отдавали ничего внятного.
Здесь вторые грабли, и они дорогие. У сайтов на Beget первый запрос без cookie получает не страницу, а JavaScript-заглушку на 274 байта с кодом 200. Браузер её проходит незаметно, а curl — нет:
curl -s -o /dev/null -w '%{http_code} %{size_download}\n' https://eden96.ru/
# 200 274 ← это не сайт, это заглушка
curl -s -o /dev/null -w '%{http_code} %{size_download}\n' -b "beget=begetok" https://eden96.ru/
# 200 48013 ← а вот это сайт
Без cookie любой диагноз будет ложным: все страницы «отдают 200», редиректов «нет», а Вебмастер при этом жалуется. Поэтому у меня правило, записанное в инструкцию для нейросети: курл к сайту на Beget всегда с -b "beget=begetok".
Вторая проверка — петли и лишние прыжки:
curl -s -o /dev/null -L -b "beget=begetok" \
-w '%{num_redirects} %{url_effective}\n' https://eden96.ru/category/novosti/
Если прыжков два, а не один, — смотрите на .htaccess. За прокси Beget Apache видит запрос как http, и относительная цель редиректа достраивается до http://, после чего срабатывает правило http→https, и получается лишний хоп. Лечится двумя вещами: цели редиректов пишутся абсолютными, с https://, а правило http→https ставится в файле последним.
У сайтов на Astro .htaccess лежит в public/ и уезжает на прод сборкой. Править его на проде по ssh — потерять правку при следующем деплое. Всё, что ниже, — в репозитории.
С мусорными адресами старого движка соблазн один: перекинуть всё на главную через 301. Не надо. Поисковик считает 301 на нерелевантную главную мягкой 404 и держит адрес в индексе месяцами, а страницы-переезды, наоборот, продолжают висеть как «редирект». Правильно так: у чего есть новый адрес — 301 ровно на него; чего больше нет — 410 Gone:
# --- Страницы, удалённые вместе с WordPress: 410 Gone ---
# Яндекс и Google выкидывают 410 из индекса быстрее, чем 301 на главную
RewriteRule ^author(/|$) - [G,L]
RewriteRule ^bez-rubriki(/|$) - [G,L]
# --- Переезды: только абсолютные https-цели ---
RewriteRule ^novosti/?$ https://eden96.ru/o-klinike/novosti/ [R=301,L]
RewriteRule ^protezirovanie/?$ https://eden96.ru/protezirovanie-zubov/ [R=301,L]
# --- Рубрики WordPress: /category/<раздел>/ -> одноимённый раздел, если он есть ---
RewriteCond %{DOCUMENT_ROOT}/$1/index.html -f
RewriteRule ^category/([^/]+)/?$ https://eden96.ru/$1/ [R=301,L]
# --- http -> https: строго последним ---
После выкладки — обход всех адресов из карты сайта на 200 и проверка, что нет петель. Только после этого «готово».
Исправить мало — надо, чтобы робот пришёл. Иначе диагностика будет висеть до следующего планового обхода, а это недели. Заявка на переобход:
curl -s -X POST -H "Authorization: OAuth $T" -H "Content-Type: application/json" \
-d '{"url":"https://eden96.ru/o-klinike/novosti/"}' "$B/recrawl/queue/"
Квота на заявки:
curl -s -H "Authorization: OAuth $T" "$B/recrawl/quota/"
# {"daily_quota":560,"quota_remainder":560}
Две особенности. Квота своя у каждого сайта: у eden96 — 560 адресов в сутки, у играйсразу.рф — 150, у соседнего проекта — 850; Яндекс считает её сам, повлиять нельзя. И она не копится: непотраченное за сутки сгорает. Из второго прямо следует крон.
У играйсразу.рф три с половиной тысячи страниц и 150 заявок в день. Скрипт recrawl_daily.py каждое утро спрашивает остаток квоты и отправляет ровно столько адресов, сколько можно, двигаясь по циклической очереди:
TOKEN = open(os.path.expanduser('~/.yandex_webmaster_token')).read().strip()
BASE = f'https://api.webmaster.yandex.net/v4/user/{USER}/hosts/{HOST_ID}'
def api(method, path, data=None):
req = urllib.request.Request(BASE + path, data=data, method=method,
headers={'Authorization': f'OAuth {TOKEN}', 'Content-Type': 'application/json'})
with urllib.request.urlopen(req, timeout=60) as r:
return json.loads(r.read().decode('utf-8') or '{}')
q = api('GET', '/recrawl/quota/')
left = q.get('quota_remainder', 0)
if left <= 0:
print('Квота на сегодня исчерпана.'); return
queue = build_queue() # листинги вперёд, потом 3000 самых читаемых страниц
st = load_state() # {'pos': 450, 'circles': 0} в маленьком json
pos = st['pos'] % len(queue)
batch = [queue[(pos + i) % len(queue)] for i in range(left)]
ok = 0
for u in batch:
try:
api('POST', '/recrawl/queue/', json.dumps({'url': u}).encode('utf-8'))
ok += 1
except Exception:
break # квота кончилась или отказ — дальше долбить бессмысленно
st['pos'] = (pos + ok) % len(queue)
save_state(st)
Очередь строится из исходников: сначала листинги, через которые робот находит остальное, потом страницы по убыванию просмотров. Позиция хранится в файле, и когда список кончается, всё начинается заново — повторная заявка не спам, это штатный механизм, а страницы к тому времени успевают измениться. Полный круг по голове очереди занимает двадцать дней.
В кроне одна строка:
40 6 * * * cd ~/accords && timeout 900 python3 tools/recrawl_daily.py >> tools/_recrawl/cron.log 2>&1
Лог за сегодня:
2026-09-07 квота: 150 в сутки, доступно 150
Очередь: 3005 адресов, позиция 300. Отправляю 150.
Отправлено на переобход: 150. Следующая позиция: 450.
Второй скрипт, webmaster_watch.py, как раз и закрывает эту дыру: смотреть самому раз в неделю я не буду, а он смотрит каждый день. Раз в сутки он снимает сводку, пишет строку в историю и сравнивает с прошлой неделей:
s = api('/summary/')
searchable, excluded, sqi = s['searchable_pages_count'], s['excluded_pages_count'], s['sqi']
alerts = []
# 1) живые проблемы диагностики — только PRESENT
for name, p in api('/diagnostics/').get('problems', {}).items():
if p.get('state') == 'PRESENT':
mark = '🔴' if p.get('severity') in ('FATAL', 'CRITICAL') else '🟡'
alerts.append(f'{mark} диагностика: {name}, с {(p.get("last_state_update") or "?")[:10]}')
# 2) обвал страниц в поиске относительно среднего за неделю
prev = [r[1] for r in history()[-7:]]
if prev and searchable < sum(prev) / len(prev) * 0.7:
alerts.append(f'🔴 в поиске {searchable} — падение со среднего {sum(prev)/len(prev):.0f}')
# 3) ошибки в картах сайта
for sm in api('/sitemaps/').get('sitemaps', []):
if sm.get('errors_count'):
alerts.append(f'🔴 sitemap: {sm["errors_count"]} ошибок')
# 4) квота не тратится — тихая потеря
q = api('/recrawl/quota/')
if q.get('quota_remainder') == q.get('daily_quota'):
alerts.append(f'🟡 квота переобхода не тратится: {q["daily_quota"]} в сутки')
Итог — два файла. history.tsv: по строке в сутки, дата, в поиске, исключено, индекс качества. alerts.log: только отклонения. Пустой файл отклонений означает «всё в порядке», и читать его достаточно раз в неделю — или попросить нейросеть заглянуть.
2026-09-05 501 27 10
2026-09-06 501 27 10
2026-09-07 688 21 10
Порог падения — тридцать процентов, не меньше: молодой сайт Яндекс регулярно переоценивает, и на десять–пятнадцать процентов показатель гуляет сам по себе. Кричать на каждое колебание — значит через неделю перестать читать лог.
Честно, чтобы вы не искали.
Удаления страниц. Раздел «Удалить URL» в интерфейсе есть, в API v4 — нет. Если нужно срочно выкинуть адрес из поиска, а не ждать, пока робот увидит 410, — только руками.
Регионов. Привязка сайта к региону — тоже только в интерфейсе.
Полного списка страниц. Методы samples отдают выборку, максимум по несколько сотен за запрос, а не все страницы. Для сверки с картой сайта на сотне страниц этого хватает, на десяти тысячах — уже сравниваете выборку.
Остальное — сводка, диагностика, события, карты сайта, переобход, история индексирования — есть, и работает без сюрпризов.
Все команды выше человек может выполнить сам. Разница в том, что делать это раз в неделю по двадцати двум сайтам он не будет. Я не буду точно.
Поэтому вся эта инструкция в сжатом виде лежит у меня в ~/.claude/CLAUDE.md — файле, который Claude Code читает в начале каждой сессии. Там путь к файлу с токеном, user_id, шаблоны запросов, правило про cookie Beget, правило про 410 и алгоритм разбора жалобы из восьми пунктов. После этого диалог выглядит так:
— Что там с eden96?
— В поиске 96 страниц, исключена 1, живая проблема одна: DOCUMENTS_MISSING_DESCRIPTION, отмечена 23 августа. Сверил с картой сайта: все 88 адресов из карты в поиске есть. Восемь адресов в поиске лишние — хвост от WordPress, все отдают 410 после правок 5 сентября, робот их ещё не перепроверил. Отклонений нет, делать ничего не нужно.
Минута вместо получаса. А при реальной жалобе сессия проходит весь алгоритм сама: снимает диагностику, сверяет с картой сайта, курлит подозрительные адреса с правильной cookie, находит петлю в редиректах, правит .htaccess в репозитории, пересобирает, выкладывает, проверяет все адреса на 200 и отправляет исправленное на переобход. На eden96 это заняло один вечер вместе с выкладкой; руками я бы потратил его только на чтение отчётов.
Где помощник экономит часы: обход всех сайтов сразу, сверка множеств, чтение словаря проблем с датами, проверка сотни адресов курлом, написание правил редиректов по списку мусорных адресов, отправка на переобход пачкой.
Где решает человек, и это в инструкции сказано прямо:
И одно правило, которое я вывел из первых попыток: нейросети нужно давать не доступ, а алгоритм. Токен без инструкции про cookie Beget привёл к уверенному «все страницы отдают 200, проблем нет» — при живой жалобе Вебмастера. Восемь пунктов в файле инструкций стоят дороже любого доступа.
host_id не кодировать. https:eden96.ru:443 подставляется в адрес как есть.problems — словарь, живое только PRESENT. И сверяйте last_state_update с датой своих правок.-b "beget=begetok", иначе диагноз ложный.https://, правило http→https последним. Иначе за прокси лишний прыжок.