AI Data Gate

AI Data Gate API

Поисковый API для AI-агентов: принимает запрос, собирает выдачу из внешних источников, скачивает и очищает страницы, ранжирует фрагменты под запрос и возвращает готовый для LLM контекст. Интерфейс - обычный JSON по HTTP, плюс MCP-сервер для ИИ-ассистентов: подключение описано в разделе «Подключение через MCP».

  • Базовый адрес: https://aidatagate.ru
  • Авторизация: заголовок Authorization: Bearer <ключ>. Принимается также X-API-Key: <ключ> (голый ключ, без слова Bearer - так проще настраивать клиентов, которые заполняются формой) и, для совместимости, поле api_key в теле запроса. MCP-клиенты могут вместо ключа войти через OAuth, см. «Подключение через MCP»
  • Формат: JSON, UTF-8
  • Версия API отдаётся в заголовке ответа X-AIDataGate-Version
  • Тарификация видна в заголовках ответа: X-Credits-Charged (списано за этот запрос), X-Credits-Remaining (остаток на балансе) и X-AIDataGate-Cache (hit - запрос обслужен из кэша и потому дешевле, см. «Кэш и тарификация»)

POST /search

Запрос:

{
  "query": "льготная ипотека условия 2026",
  "search_depth": "basic",
  "topic": "general",
  "max_results": 5,
  "include_answer": false,
  "include_raw_content": false,
  "include_images": false,
  "include_domains": [],
  "exclude_domains": [],
  "days": 7,
  "time_range": null,
  "country": "ru"
}
Поле Тип По умолчанию Описание
query string обязательно Запрос, до 400 символов
search_depth basic / advanced basic advanced опрашивает два источника и склеивает топ-3 фрагмента документа
topic general / news general news - выдача, отсортированная по времени: сначала свежие материалы
max_results int 1-20 5 Сколько документов вернуть; больше 20 не ошибка, а те же 20. Тарифицируется за каждые 10: топ-20 стоит вдвое дороже топ-10
include_answer bool / basic / advanced false Короткий ответ по фрагментам; advanced - развёрнутый. См. answer_source ниже
include_raw_content bool / markdown / text false Полный очищенный текст документа, до 50 000 символов
include_domains / exclude_domains string[] [] Фильтр по доменам, совпадение по суффиксу
days int - Свежесть: не старше N дней
time_range day / week / month / year - Свежесть окном
start_date / end_date YYYY-MM-DD - Явное окно дат публикации, включительно
country string ru Страна: ISO-код (ru, kz) или полное название (Russia, Россия)
language string - Язык выдачи; по умолчанию определяется по самому запросу
filter_by_language bool false Отбросить документы на других языках (документы без определённого языка остаются)
include_domains_mode filter / boost filter boost не отсекает чужую выдачу, а поднимает свои домены в ранжировании
exact_match bool false Требует фразу в кавычках в query, иначе 400
chunks_per_source int 1-5 3 Сколько фрагментов документа склеивать в content при search_depth=advanced
include_images bool false Картинки найденных страниц - в поле images ответа
include_favicon bool false Значок сайта в поле favicon каждого результата
include_usage bool false Цена запроса в кредитах в поле usage ответа
auto_parameters bool false Вернуть в auto_parameters параметры, с которыми запрос выполнен

Неизвестные поля игнорируются, сервис на них не падает. null читается как «параметр не задан»: обёртки вокруг SDK шлют его вместо отсутствующего ключа.

Фильтры свежести. days, time_range, start_date и end_date не подсказка поисковику, а отсечение: документ с известной датой публикации вне окна в ответ не попадает. Заданные вместе, они сужают окно, а не расширяют. Документы, у которых дата не определилась, остаются в выдаче: дата читается меньше чем у половины страниц, и отсечение по её отсутствию выкосило бы выдачу целиком.

Фильтр по доменам. include_domains уходит прямо в запрос к поисковику оператором site: - иначе фильтровать было бы нечего: по общему запросу нужный домен в топ обычно не попадает. Список до 8 доменов; длиннее - оператор не добавляется, и работает только фильтрация уже полученной выдачи. Если запрос с оператором вернул пустоту, он автоматически повторяется без него. exclude_domains применяется к выдаче.

Ответ:

{
  "query": "льготная ипотека условия 2026",
  "answer": null,
  "follow_up_questions": null,
  "images": [],
  "results": [
    {
      "title": "Условия льготной ипотеки в 2026 году",
      "url": "https://example.ru/ipoteka",
      "content": "Программа льготной ипотеки в 2026 году...",
      "score": 0.93,
      "raw_content": null,
      "raw_content_truncated": false,
      "published_date": "Tue, 01 Sep 2026 10:00:00 GMT"
    }
  ],
  "response_time": 1.84,
  "request_id": "0f2a...",
  "answer_error": null,
  "answer_source": null
}

request_id, answer_error, answer_source и raw_content_truncated - наши расширения; сторонние клиенты их просто не читают.

answer_source говорит, чем отвечено:

  • extractive - выжимка из найденных фрагментов, без модели. Работает всегда, тарифицируется как обычный поиск, ссылки на источники в тексте - [1], [2] по номерам результатов;
  • llm - пересказ моделью, включается флагом FEATURE_ANSWER, стоит +2 кредита;
  • null - ответ не получился (например, по фрагментам нечего сказать), причина в answer_error.

Выбор источника выдачи не предусмотрен, и это решение, а не упущение. Параметра source нет, имена поисковых движков наружу не отдаются, и по ответу нельзя определить, кто именно его нашёл. Сервис отвечает за результат, а не за то, чьими руками он получен: набор источников меняется, часть из них работает по договорам, и обещать клиенту конкретный движок значит обещать то, чем мы не управляем. Маршрутизация описана ниже в разделе про источники - она зависит от языка и региона запроса, а не от параметров.

exact_match фильтрует по-настоящему: в выдаче остаются только страницы, где фраза из кавычек встречается дословно (ищется в заголовке, тексте страницы и сниппете выдачи). Без кавычек в запросе параметр даёт 400. Выдача при этом может оказаться пустой - это правильный ответ на запрос про фразу, которой нет.

Фильтры дат и документы без даты. time_range и days - пожелание свежести: страницы с неизвестной датой публикации из выдачи не выбрасываются, иначе она обмелела бы (дата известна меньше чем у половины страниц). start_date/end_date - точный запрос: там документ с неизвестной датой не отвечает условию и в выдачу не попадает.

Картинки. include_images отдаёт картинки найденных страниц: сначала объявленная страницей главная (og:image), затем иллюстрации по порядку, до пяти на ответ (PAGE_IMAGES_MAX). Счётчики посещений (1x1) и data:-картинки отбрасываются. Картинки собираются при разборе страницы и хранятся рядом с её текстом, поэтому приходят и с закэшированных страниц; у страниц, попавших в кэш до появления этой возможности, их не будет до обновления записи (TTL кэша - PAGE_CACHE_TTL_DAYS).

raw_content_truncated поднимается, когда текст документа обрезан: по лимиту RAW_CONTENT_MAX_CHARS (50 000 символов) или потому, что у PDF прочитаны только первые PDF_MAX_PAGES страниц. Обрезка идёт по границе слова.

POST /extract

{"urls": ["https://habr.com/ru/articles/1/"], "extract_depth": "basic", "format": "markdown"}

Дополнительно: include_images (картинки страницы), include_favicon (значок сайта), include_usage (цена запроса в кредитах в поле usage).

Ответ: {"results": [{"url", "title", "raw_content", "raw_content_truncated", "images", "favicon", "structured"}], "failed_results": [{"url", "error"}], "response_time", "request_id", "usage"}. До 20 URL за запрос. urls принимается и списком, и одной строкой. Ошибка одного URL не ломает весь запрос: он уходит в failed_results.

format=markdown (по умолчанию) отдаёт разметку, format=text - простой текст: если сама страница является markdown-документом, разметка снимается, а не просачивается в «текст». PDF извлекается наравне с HTML - первые PDF_MAX_PAGES страниц, с пометкой raw_content_truncated, если документ длиннее. У PDF без заголовка в метаданных в title попадает имя файла из адреса: пустая строка не говорит ни о чём.

structured заполняется ТОЛЬКО вместе с instructions: это JSON, который LLM собирает со страницы по вашей инструкции. Без instructions поле равно null, и это не признак поломки - просто инструкции не было. Пример:

{"urls": ["https://example.com/prices"], "instructions": "верни массив тарифов: name, price"}

Если пересказ на сервере выключен (FEATURE_ANSWER=false), instructions игнорируется и structured остаётся null.

POST /crawl

BFS-обход сайта от стартового URL по ссылкам того же корневого домена (включая поддомены).

{
  "url": "https://docs.example.ru/",
  "max_depth": 1,
  "max_breadth": 20,
  "limit": 50,
  "select_paths": ["^/ru/"],
  "exclude_paths": ["/archive/"]
}
Поле Тип По умолчанию Описание
url string обязательно Стартовый URL, абсолютный http(s)
max_depth int 1-3 1 Глубина обхода от стартовой страницы
max_breadth int 1-100 20 Сколько новых ссылок брать с каждой страницы
limit int 1-50 50 Максимум страниц за обход (MVP: не больше 50)
select_paths string[] [] Regex по пути URL: обходить только совпавшие
exclude_paths string[] [] Regex по пути URL: совпавшие не обходить

Числа выше потолка (max_depth, max_breadth, limit) прижимаются к потолку, а не дают 400: отказ ломал бы работающий код там, где достаточно вернуть максимум. | instructions | string | - | Принимается для совместимости со сторонними клиентами и игнорируется: в ответе появляется warning |

Битый regex в select_paths / exclude_paths - ошибка 400 bad_request, limit больше 50 - тоже 400.

Ответ:

{
  "base_url": "https://docs.example.ru/",
  "results": [
    {"url": "https://docs.example.ru/ru/start", "raw_content": "# Начало работы\n..."}
  ],
  "response_time": 3.12,
  "request_id": "0f2a...",
  "warning": null
}

raw_content - markdown страницы. request_id и warning - наши расширения. Каждая страница проходит те же механизмы, что и /search: SSRF-защита, robots.txt, лимиты одновременных запросов на хост. Бинарные файлы (pdf, картинки, архивы) не обходятся. Редирект, уводящий с корневого домена стартового URL, отбрасывает страницу целиком. Бюджет всего обхода - 60 секунд (настройка CRAWL_BUDGET); по истечении возвращается собранное к этому моменту. Если не открылся сам стартовый URL, ответ - 400 с объяснением, а не пустой список: пустой ответ читается как «сайт обойдён, брать нечего», и это худший вид ошибки для агента. Такой запрос не тарифицируется. Списание: 1 кредит за каждые 5 успешно обработанных страниц, округление вверх.

POST /map

Тот же обход, что и /crawl, но без извлечения контента - только карта ссылок. Параметры те же (url, max_depth, max_breadth, limit, select_paths, exclude_paths).

{"url": "https://docs.example.ru/", "max_depth": 2}

Ответ:

{
  "base_url": "https://docs.example.ru/",
  "results": ["https://docs.example.ru/", "https://docs.example.ru/ru/start"],
  "response_time": 1.20,
  "request_id": "0f2a...",
  "warning": null
}

В карту попадают и ссылки на бинарные файлы (pdf и т.п.), хотя сам обход по ним не идёт. Недоступный стартовый URL - 400, как и в /crawl. Списание: 1 кредит за каждые 10 URL в ответе, округление вверх.

GET /usage

Баланс кредитов, расход за месяц, лимит запросов на ключ, расход по дням за 30 дней.

GET /healthz

Состояние приложения: 200 - всё живо, 503 - деградация.

Ошибки

Единый формат: {"detail": {"error": "..."}, "code": "..."}. Текст ошибки всегда лежит в detail.error - именно оттуда его читают совместимые SDK. code - наше расширение, сторонние клиенты его просто не читают.

Ошибки валидации (400) добавляют разбор по полям:

{
  "detail": {"error": "query: String should have at most 400 characters"},
  "code": "bad_request",
  "errors": [{"field": "query", "message": "String should have at most 400 characters"}]
}
Код HTTP Когда
invalid_api_key 401 Ключ неверный или отсутствует
insufficient_credits 402 Не хватает кредитов (фаза 2)
rate_limited 429 Превышен лимит запросов в минуту
provider_unavailable 503 Все источники выдачи недоступны
bad_request 400 Некорректные параметры
internal 500 Внутренняя ошибка

Повтор запроса: что повторять и как

Сервис живёт за обратным прокси, и в редких окнах перегрузки хозяина клиент может получить 502/503 или обрыв соединения вместо ответа. Это сетевой сбой, а не отказ API: повтор через паузу проходит. Агентные клиенты сами обычно не повторяют, поэтому ретрай стоит заложить в код.

Повторять имеет смысл 429, 502, 503, 504 и обрывы соединения - с удвоением паузы. Повторять 400, 401 и 402 бессмысленно: ответ не изменится.

import time

import httpx
from httpx import HTTPTransport

# 3 повтора с нарастающей паузой - на соединение и на сетевые сбои
client = httpx.Client(transport=HTTPTransport(retries=3), timeout=30.0)

RETRY_ON = {429, 502, 503, 504}

def search(query: str, key: str, attempts: int = 3) -> dict:
    for attempt in range(attempts):
        r = client.post(
            "https://aidatagate.ru/search",
            headers={"Authorization": f"Bearer {key}"},
            json={"query": query},
        )
        if r.status_code not in RETRY_ON or attempt == attempts - 1:
            r.raise_for_status()
            return r.json()
        # 1 с, 2 с, 4 с; Retry-After у 429 важнее нашей паузы
        time.sleep(float(r.headers.get("Retry-After") or 2 ** attempt))
    raise RuntimeError("unreachable")

В curl то же самое одним флагом:

curl --retry 3 --retry-delay 1 --retry-all-errors -s https://aidatagate.ru/search \
  -H "Authorization: Bearer $AIDATAGATE_KEY" -H "Content-Type: application/json" \
  -d '{"query": "ключевая ставка ЦБ"}'

Примеры

curl

curl -s https://aidatagate.ru/search \
  -H "Authorization: Bearer rs-dev-0000000000000000" \
  -H "Content-Type: application/json" \
  -d '{"query": "льготная ипотека условия 2026", "max_results": 5}'

Python, httpx

import httpx

resp = httpx.post(
    "https://aidatagate.ru/search",
    headers={"Authorization": "Bearer rs-dev-0000000000000000"},
    json={"query": "postgres vacuum full блокировки", "max_results": 5},
    timeout=30,
)
for item in resp.json()["results"]:
    print(item["score"], item["title"], item["url"])

LangChain

Инструмент для агента - тридцать строк на httpx, отдельный пакет не нужен.

import httpx
from langchain_core.tools import tool

BASE = "https://aidatagate.ru"
KEY = "rs-dev-0000000000000000"


@tool
def aidatagate(query: str, max_results: int = 5) -> str:
    """Поиск в вебе: возвращает фрагменты страниц по запросу."""
    resp = httpx.post(
        f"{BASE}/search",
        headers={"Authorization": f"Bearer {KEY}"},
        json={"query": query, "max_results": max_results},
        timeout=30,
    )
    resp.raise_for_status()
    return "\n\n".join(
        f"[{i}] {r['title']} ({r['url']})\n{r['content']}"
        for i, r in enumerate(resp.json()["results"], start=1)
    )

n8n (HTTP Request node)

  • Method: POST
  • URL: https://aidatagate.ru/search
  • Authentication: Header Auth, имя Authorization, значение Bearer rs-dev-0000000000000000
  • Body (JSON): {"query": "{{ $json.query }}", "max_results": 5}

Подключение через MCP

MCP-сервер работает в том же процессе по адресу https://aidatagate.ru/mcp (транспорт streamable HTTP) и даёт агентам четыре инструмента - search, extract, crawl и map с теми же параметрами, что и у HTTP-эндпоинтов (кроме instructions у crawl/map: в MVP параметр всё равно игнорируется, поэтому в MCP-схеме его нет). Авторизация - тот же API-ключ в заголовке Authorization: Bearer, списание кредитов как у обычных запросов (в журнале такие вызовы помечены mcp:search / mcp:extract / mcp:crawl / mcp:map). Неверный ключ даёт ошибку авторизации (HTTP 401), при нехватке кредитов инструмент возвращает понятное сообщение с кодом insufficient_credits.

Четыре поля для ручного добавления

Клиенты, которые добавляют сервер формой, а не файлом конфигурации, спрашивают поля по отдельности:

Поле Значение
Адрес сервера (URL) https://aidatagate.ru/mcp
Транспорт HTTP, streamable (не SSE и не stdio)
Имя заголовка Authorization либо X-API-Key
Значение заголовка Bearer rs-ВАШ_КЛЮЧ либо просто rs-ВАШ_КЛЮЧ

Больше ничего вписывать не нужно: сервер авторизует по этому единственному заголовку. В Authorization значение вписывается целиком, вместе со словом Bearer и пробелом; в X-API-Key - голый ключ, и ошибиться там негде.

У Claude Desktop поле для заголовка живёт в разделе Request headers диалога добавления коннектора, и вписывать туда надо значение целиком, вместе со словом Bearer, см. раздел "Claude Desktop" ниже.

Готовый фрагмент со своим ключом можно скопировать в кабинете на странице "API-ключи". Ниже те же фрагменты с плейсхолдером rs-ВАШ_КЛЮЧ.

Claude Code

Одной командой:

claude mcp add --transport http aidatagate https://aidatagate.ru/mcp \
  --header "Authorization: Bearer rs-ВАШ_КЛЮЧ"

Либо файлом ~/.claude.json или .mcp.json в корне проекта:

{
  "mcpServers": {
    "aidatagate": {
      "type": "http",
      "url": "https://aidatagate.ru/mcp",
      "headers": {
        "Authorization": "Bearer rs-ВАШ_КЛЮЧ"
      }
    }
  }
}

Проверка: claude mcp list должен показать aidatagate со статусом connected, после чего агенту доступны инструменты search, extract, crawl и map.

Claude Desktop

Два способа, оба без правки файлов конфигурации.

Способ 1: вход через OAuth (работает у всех)

Customize -> Connectors -> Add custom connector:

Поле диалога Что вписать
Name AI Data Gate
MCP server URL https://aidatagate.ru/mcp
Authentication Sign in now (или оставьте определённое автоматически)

Ключ вписывать никуда не нужно: Claude сам найдёт наш сервер авторизации, откроет страницу входа, вы войдёте своей почтой и нажмёте "Разрешить". Доступ после этого виден в кабинете на странице "API-ключи" под именем OAuth: <название клиента> и отзывается там же одной кнопкой.

Так подключаться и удобнее, и безопаснее: ключ не проходит через буфер обмена и не оседает в скриншотах.

Способ 2: ключом в заголовке

Если в диалоге есть раздел Request headers (он в бете и открыт не всем):

Поле диалога Что вписать
Authentication No sign-in
Request headers имя x-api-key, значение rs-ВАШ_КЛЮЧ

Заголовок x-api-key принимает голый ключ. Если выбрать authorization, то значение надо вписывать целиком - Bearer rs-ВАШ_КЛЮЧ, вместе со словом и пробелом: Claude отправляет значение дословно и схему сам не подставляет. Из-за этого x-api-key надёжнее.

Настройки авторизации после добавления не редактируются. Чтобы сменить ключ, коннектор удаляют и добавляют заново.

Если не подошло ни то, ни другое

Мост mcp-remote (нужен Node.js). В claude_desktop_config.json:

{
  "mcpServers": {
    "aidatagate": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-remote@latest",
        "https://aidatagate.ru/mcp",
        "--header",
        "Authorization:${AIDATAGATE_AUTH}"
      ],
      "env": {
        "AIDATAGATE_AUTH": "Bearer rs-ВАШ_КЛЮЧ"
      }
    }
  }
}

Значение заголовка вынесено в env не для красоты: в args строка с пробелом после двоеточия разбирается неправильно, поэтому подставляется переменной.

Свой stdio-сервер без Node.js - обёртка sdk/mcp/aidatagate_mcp.py, см. раздел "Клиенты без HTTP-транспорта" ниже. Ключ там передаётся переменной окружения.

После правки файла Claude Desktop нужно перезапустить.

Вход через OAuth вместо ключа

Клиенту, который умеет OAuth, ключ не нужен: он сам найдёт сервер авторизации и проведёт человека через вход. Реализовано по спецификации авторизации MCP - OAuth 2.1, authorization code с PKCE (S256).

Документ или точка Адрес
Метаданные ресурса (RFC 9728) /.well-known/oauth-protected-resource
Метаданные сервера авторизации (RFC 8414) /.well-known/oauth-authorization-server
Регистрация клиента (RFC 7591) POST /oauth/register
Страница согласия GET /oauth/authorize
Обмен кода на токен POST /oauth/token

Что стоит знать:

  • Секрет клиенту не выдаётся: MCP-клиенты публичные, подлинность доказывает PKCE. Метод plain не поддерживается, только S256.
  • Выданный токен доступа - это обычный API-ключ сервиса. Он виден в кабинете на странице "API-ключи" под именем OAuth: <название клиента>, тарифицируется как любой другой и отзывается одной кнопкой. Отзыв ключа обрывает доступ приложения немедленно.
  • Обращение к /mcp без токена отвечает 401 с заголовком WWW-Authenticate, в котором указан адрес метаданных - по нему клиент и находит, где входить.

Cursor

Файл ~/.cursor/mcp.json (или .cursor/mcp.json в проекте):

{
  "mcpServers": {
    "aidatagate": {
      "url": "https://aidatagate.ru/mcp",
      "headers": {
        "Authorization": "Bearer rs-ВАШ_КЛЮЧ"
      }
    }
  }
}

Continue

В config.json (раздел experimental):

{
  "experimental": {
    "modelContextProtocolServers": [
      {
        "transport": {
          "type": "streamable-http",
          "url": "https://aidatagate.ru/mcp",
          "headers": {
            "Authorization": "Bearer rs-ВАШ_КЛЮЧ"
          }
        }
      }
    ]
  }
}

Клиенты без HTTP-транспорта (stdio)

Для клиентов, которые запускают MCP-серверы только локальным процессом, есть stdio-обёртка sdk/mcp/aidatagate_mcp.py: она проксирует те же четыре инструмента в HTTP API. Установка и примеры конфигурации - в sdk/mcp/README.md; ключ и адрес задаются переменными окружения AIDATAGATE_API_KEY и AIDATAGATE_BASE_URL.

Сколько стоит запрос

Цена поиска берётся за каждые 10 результатов: топ-10 - один раз цену глубины, топ-20 - два раза. search_depth=advanced дороже basic, поэтому топ-20 в режиме advanced стоит вчетверо дороже топ-10 в basic. Сколько ссылок поисковик отдаёт за один заход, на цену не влияет: платить за то, как нам удобнее забрать выдачу, клиент не должен. Наценка за include_answer берётся один раз на запрос - ответ в выдаче один.

Актуальные цифры - на странице /pricing, их правит администратор.

Кэш и тарификация

Выдача и страницы кэшируются: повторный запрос приходит за доли секунды вместо секунд и стоит дешевле - за то, что не пришлось запрашивать заново, денег не берут.

  • /search тарифицируется по цене search_cached (по умолчанию 0), если выдача взята из кэша целиком, то есть к поисковику не было ни одного обращения. Частичное попадание считается по обычной цене.
  • /extract тарифицируется по цене extract_cached_per5 (по умолчанию 0), если все URL запроса отданы кэшем страниц. Скидка «всё или ничего»: считать группы отдельно нельзя, округление вверх в каждой сделало бы запрос с частичным попаданием дороже, чем он же без кэша.
  • Наценка за include_answer прибавляется и к кэшированному запросу, если ответ написала модель: кэшируется выдача, а не пересказ.

Что именно случилось, видно в ответе: заголовок X-AIDataGate-Cache равен hit или miss, а X-Credits-Charged показывает фактическое списание. Резерв на время обработки берётся по полной цене - заранее неизвестно, попадёт ли запрос в кэш, поэтому баланса должно хватать на обычный запрос даже перед бесплатным. Актуальные цены - на странице /pricing, их правит администратор.

Ограничения

  • Лимит запросов: 60 в минуту на ключ (заголовок ответа при превышении - HTTP 429).
  • Тело запроса - до 1 МБ (MAX_REQUEST_BODY_BYTES), иначе HTTP 413.
  • Максимум 20 URL в /extract, тело страницы до 2 МБ, таймаут скачивания 4 с.
  • /crawl и /map: не больше 50 страниц за обход (MVP), глубина до 3, бюджет обхода 60 с.
  • Робот представляется как AIDataGateBot/0.1 и уважает robots.txt (страница /bot).