Чтение страницы
Открывает страницу по ссылке и возвращает её текст в чистом markdown: без бокового меню, рекламных врезок, иконок и HTML. Нужен, когда агенту надо прочитать конкретную статью, страницу документации или карточку товара - а не искать информацию по теме.
https://srezai.ru/api/v1/readКогда использовать
- Прочитать статью или страницу документации, ссылку на которую вы уже знаете (в том числе полученную из поиска).
- Собрать контекст для RAG: чистый markdown ложится в индекс без собственного парсинга HTML и вычистки бойлерплейта.
- Проверить факт на первоисточнике, когда сниппета поисковой выдачи недостаточно.
Если нужно найти информацию по теме, а не прочитать известную страницу - это веб-поиск: с excerpts: true он сразу вернёт текст топ-страниц под запрос одним вызовом вместо десяти отдельных чтений.
Пример запроса
curl -X POST https://srezai.ru/api/v1/read \
-H "Content-Type: application/json" \
-H "Authorization: Bearer srz_live_ВАШ_КЛЮЧ" \
-d '{ "url": "https://exa.ai/blog", "maxChars": 12000 }'Параметры тела
Тело запроса - JSON. Обязателен адрес страницы: либо url для одной, либо urls для батча.
urlstringобязательныйПолный адрес страницы, схема http или https, порт 80 или 443. Адреса, ведущие во внутреннюю сеть (приватные IP, локальные имена), отклоняются с ошибкой 400 - см. ниже. Не нужен, если передан urls.
urlsstring[]необязательныйДо 5 адресов вместо url: страницы читаются параллельно, ответ приходит массивом pages. См. Несколько страниц за раз.
maxCharsnumberнеобязательныйСколько символов текста вернуть: от 200 до 50000 (4000 по умолчанию). Для длинной документации и RAG ставьте с запасом. Значения вне диапазона подгоняются к границе.
offsetnumberнеобязательныйС какого символа текста начать - для чтения длинной страницы окнами. Значение берите из meta.nextOffset предыдущего ответа. См. Длинные страницы по частям.
enginestringнеобязательныйКак забирать страницу: auto (по умолчанию), fast, dynamic, stealth - см. раздел ниже. Неизвестное значение молча трактуется как auto.
freshbooleanнеобязательныйtrue - прочитать страницу заново, минуя кеш. По умолчанию свежий ответ из кеша переиспользуется; на цену это не влияет ни в ту, ни в другую сторону. См. Кеш страниц.
Движки и авто-эскалация
Страницы устроены по-разному: статике и документации хватает обычного запроса, а SPA на React или Vue отдаёт пустой каркас, пока не выполнится JavaScript. Поэтому по умолчанию (engine: "auto") мы начинаем с самого быстрого способа и поднимаемся на ступень выше только если текста не оказалось.
auto-fast→dynamic→stealth, до первого осмысленного результата. Обычной странице хватает одной попытки, сложной - двух-трёх. Цена от числа попыток не зависит.fast(~1-3 с) - без ожидания JavaScript: статические сайты, документация, блоги, новости.dynamic(~3-8 с) - ждём затишья сети и отрисовки: для SPA и ленивой подгрузки контента.stealth(~8-20 с) - максимально браузерное поведение: полная прокрутка страницы (чтобы сработала ленивая подгрузка) и закрытие куки-баннеров и модальных окон, перекрывающих текст. На страницах, которые и так отдают заглушку,autoделает последнюю попытку через второй, независимый браузерный движок.
В ответе приходит engine - ступень, на которой контент удалось взять, и meta.attempts - сколько ступеней было пройдено. Если у конкретного сайта attempts стабильно равен 2 или 3, передавайте нужный движок явно: запрос перестанет тратить время на заведомо неудачные попытки.
Длинные страницы по частям
Документация на 60 000 символов не влезет ни в один разумный maxChars, а модели с окном 32k - и вовсе никуда. Поэтому страницу можно читать окнами: offset задаёт, с какого символа начать, а ответ сам говорит, откуда продолжать.
// Первая страница
{ "url": "https://exa.ai/blog", "maxChars": 12000 }
// → meta.nextOffset: 11980, meta.hasMore: true
// Продолжение - с того места, где текст оборвался
{ "url": "https://exa.ai/blog", "maxChars": 12000, "offset": 11980 }
// → meta.nextOffset: 23960, meta.hasMore: true
// Последнее окно
{ "url": "https://exa.ai/blog", "maxChars": 12000, "offset": 23960 }
// → meta.nextOffset: null, meta.hasMore: falseСмещение считается по чистому тексту, а не по сырому HTML: сырой объём вы не видите, и от прогона к прогону он меняется (сайт отдал баннер или не отдал). Чистый текст при одном снимке страницы детерминирован, поэтому nextOffset указывает ровно туда, где текст оборвался.
- Границы окна двигаются по пробелам, чтобы слово не разрезалось.
nextOffsetпри этом считается по фактически отданному тексту - между окнами ничего не теряется и не дублируется. hasMore- тот же признак, чтоnextOffset !== null, булевым: моделям проще ветвиться по флагу, чем сравнивать сnull.offsetза пределами текста вернёт пустое окно иnextOffset: null, а не ошибку. Отрицательный трактуется как0.- Второе и последующие окна почти всегда попадают в кеш: страница читается один раз, окна нарезаются из одного и того же текста, поэтому смещения между ответами согласованы.
- Каждое окно - отдельный вызов и отдельный кредит. Читать страницу целиком одним запросом с большим
maxCharsдешевле, если она влезает в контекст.
Кеш страниц
Прочитанная страница держится в кеше 15 минут. Повторный запрос той же ссылки за это время возвращается за десятки миллисекунд вместо секунд - краулер не поднимается вообще. В ответе это видно по meta.cached: true и meta.cacheAgeMs - насколько давно страницу читали на самом деле.
Кеш ускоряет, но не влияет на цену: 1 кредит списывается и за попадание в кеш тоже. Так проще считать бюджет - стоимость чтения не зависит от того, читал ли кто-то эту ссылку минуту назад.
- Кеш ключуется по ссылке и движку:
engine: "fast"иengine: "dynamic"- разные записи, потому что и текст у них разный. - Хранится полный чистый текст страницы, поэтому окна с разными
offsetнарезаются из одного снимка - см. выше. - Нужен именно свежий снимок (лента новостей, курс, статус заказа) -
fresh: true. Ответ придёт сcached: false, а кеш обновится. - Проверка адреса на принадлежность внутренней сети идёт до обращения к кешу: запрещённая ссылка получит
ssrf_blockedи в том случае, когда её текст уже лежит в кеше.
Несколько страниц за раз
Вместо url можно передать urls - до 5 ссылок на бесплатном тарифе и до 10 на подписке PRO. Страницы читаются параллельно, поэтому пять адресов занимают примерно столько же, сколько самый медленный из них, а не сумму. Пачка больше лимита не отклоняется целиком: лишние адреса отбрасываются, а их число приходит в meta.skipped. Остальные параметры (maxChars, engine) применяются ко всем ссылкам батча.
{
"urls": [
"https://exa.ai/blog",
"https://habr.com/ru/articles/1/",
"https://example.com/404",
"https://exa.ai/blog"
],
"maxChars": 6000
}Ответ - массив pages в порядке переданных ссылок. Успешная страница содержит те же поля, что и одиночный ответ; неудачная - только url, error и code. Одна упавшая ссылка не отменяет остальные: HTTP-статус остаётся 200.
{
"pages": [
{
"url": "https://exa.ai/blog",
"title": "Exa | Blog",
"markdown": "# Exa Blog\n\nThe latest from the team…",
"truncated": false,
"engine": "fast",
"meta": {
"tookMs": 1840, "totalChars": 5980,
"offset": 0, "nextOffset": null, "hasMore": false,
"tokensApprox": 1495, "cached": false, "attempts": 1
}
},
{
"url": "https://habr.com/ru/articles/1/",
"title": "…",
"markdown": "…",
"truncated": true,
"engine": "dynamic",
"meta": {
"tookMs": 4210, "totalChars": 19740,
"offset": 0, "nextOffset": 5990, "hasMore": true,
"tokensApprox": 1498, "cached": false, "attempts": 2
}
},
{
"url": "https://example.com/404",
"error": "Не удалось прочитать страницу. / Failed to read the page.",
"code": "read_failed"
}
],
"meta": { "tookMs": 4380, "requested": 3, "succeeded": 2, "skipped": 1 }
}Общий meta батча описывает, что вообще произошло со списком: requested - сколько ссылок пошло в работу, succeeded - сколько прочитано, skipped - сколько отброшено ещё до чтения как дубликаты. В примере выше skipped: 1 - второй раз переданный exa.ai/blog. Так по ответу видно судьбу каждой ссылки, без сверки своего списка с полученным.
- Дубликаты в списке отбрасываются до запроса - за одну и ту же ссылку дважды платить не нужно; они попадают в
meta.skipped. - Каждая страница тарифицируется отдельно (1 кредит) и отдельной строкой попадает в историю запросов.
maxCharsздесь - лимит на каждую страницу. Пять страниц по 12000 символов - это 60000 символов в ответе; для батча обычно разумнее оставить значение по умолчанию.offsetиfreshв батче тоже общие на все ссылки. Общийoffsetосмыслен редко, поэтому длинную страницу из батча дочитывайте отдельным вызовом сurl.- Больше 5 ссылок за запрос вернут
400. Нужен объём - шлите несколько батчей с оглядкой на burst-лимит.
Ответ
{
"url": "https://exa.ai/blog",
"title": "Exa | Blog",
"markdown": "# Exa Blog\n\nThe latest from the team…",
"truncated": true,
"engine": "fast",
"meta": {
"tookMs": 1840,
"totalChars": 28407,
"offset": 0,
"nextOffset": 11980,
"hasMore": true,
"tokensApprox": 2995,
"cached": false,
"cacheAgeMs": 0,
"attempts": 1
}
}Поля ответа
urlstringнеобязательныйНормализованный адрес, который был прочитан.
titlestringнеобязательныйЗаголовок страницы (из <title>/метаданных), может быть пустым.
markdownstringнеобязательныйТекст страницы, очищенный от навигации, иконок и служебной разметки, и обрезанный до maxChars.
truncatedbooleanнеобязательныйtrue, если текст не поместился в maxChars - у страницы есть продолжение. Сколько всего дала страница, видно в meta.totalChars; откуда продолжать - в meta.nextOffset.
enginestringнеобязательныйСтупень движка, которая дала результат: fast, dynamic или stealth.
meta.tookMsnumberнеобязательныйОбщее время запроса, мс.
meta.totalCharsnumberнеобязательныйОбъём чистого текста страницы целиком, до нарезки окна. По нему видно, с каким maxChars имеет смысл повторить запрос.
meta.offset / nextOffset / hasMorenumber | null | booleanнеобязательныйКакое окно текста вы получили и откуда продолжать. nextOffset: null и hasMore: false - страница дочитана. См. Длинные страницы по частям.
meta.tokensApproxnumberнеобязательныйГрубая оценка объёма ответа в токенах (≈4 символа на токен). Нужна, чтобы агент решал, влезет ли текст в остаток контекста, не угадывая по числу символов.
meta.cached / cacheAgeMsboolean | numberнеобязательныйОтвет пришёл из кеша и насколько давно страницу читали. На цену не влияет - см. Кеш страниц.
meta.attemptsnumberнеобязательныйСколько ступеней движка было пройдено.
Остаток лимита приходит в заголовках X-RateLimit-* - см. Лимиты и квоты. Обращение к дневной квоте - одно, наравне с поиском, даже если читается батч: в журнале и в списании каждая ссылка батча считается отдельно.
Через MCP: read_url и read_urls
Тот же функционал доступен ИИ-агенту как встроенные инструменты MCP-сервера: read_url для одной страницы и read_urls для батча - с теми же параметрами maxChars, offset, engine, fresh. Ответ - текстовый блок: заголовок страницы и её markdown; при обрезке добавляется явная пометка с полным объёмом текста и готовое значение offset для продолжения - арифметику модели не поручаем, ошибка на пару символов режет слово или дублирует абзац. В батче первая строка - итог («Обработано 2 из 3»), с отдельными строками про неудачи и про отброшенные дубликаты; дальше каждая страница идёт своим блоком с номером и адресом, а непрочитанные ссылки - блоком с кодом ошибки вида [read_failed]. У read_urls параметра offset нет: он был бы общим на весь батч, а дочитать нужно обычно одну ссылку из пяти - поэтому продолжение адресуется на read_url.