Документация Kontur

Инженерная документация

Антибот-проверка форм без внешних сервисов. Подключается тегом script, отвечает как reCAPTCHA — чужой код часто уже умеет разбирать такой ответ. Что это и зачем — на главной.

01

Быстрый старт

Два шага. Скрипт сам находит блоки с классом kontur, решает задачу в фоне и подставляет в форму скрытое поле kontur-response. Ваш сервер отдаёт это поле нам вместе с секретом и получает «да» или «нет».

<script src="https://mallaev-dev.ru/kontur.js?k=knt_site_ваш_ключ" async defer></script>

<form method="post" action="/submit">
  <input name="email" type="email" required>
  <div class="kontur" data-sitekey="knt_site_ваш_ключ"></div>
  <button>Отправить</button>
</form>
const res = await fetch('https://mallaev-dev.ru/api/kontur/v1/verify', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    secret: process.env.KONTUR_SECRET,
    response: formData.get('kontur-response'),
  }),
});

const { success } = await res.json();
if (!success) return new Response('Проверка не пройдена', { status: 400 });
02

Ключи и домены

Kontur пока в начале пути, и каждое подключение я разбираю лично: смотрю, как проверка ведёт себя на конкретной форме, какие признаки поведения там вообще собираются и где она ошибается на живых людях. На чужой форме всё это выглядит иначе, чем на моей, а вслепую такие вещи не настраиваются. Поэтому ключи завожу я, и Kontur идёт вместе с сайтом, который я делаю.

Ключ сайтаknt_site_…

Публичный. Стоит в адресе скрипта и в разметке, его видно всем — так же устроены reCAPTCHA и Turnstile. Без него /kontur.js отдаёт заглушку, а не проверку.

Секретknt_secret_…

Только для вашего сервера. Показывается один раз при выдаче, потом — в профиле по вводу текущего пароля. У нас он не хранится в открытом виде: в базе лежат хеш для поиска и шифртекст, ключ от которого не покидает Yandex KMS.

Доменыобязательно

Пока домены не указаны, проверка отклоняет все запросы. Впишите домен сайта, где стоит форма. Поддомены разрешённого адреса проходят, похожие имена — нет: evil-example.com не считается поддоменом example.com.

Выданное подключение лежит в разделе Kontur вашего профиля: там же домены, статистика по рубежам и перевыпуск секрета. Прежний секрет перестаёт работать сразу же, поэтому перевыпускать стоит только когда есть куда вписать новый.

03

Разметка и атрибуты

Блок должен стоять внутри формы — иначе скрытому полю некуда попасть, виджет выглядит рабочим, а проверка до сервера не доезжает. Скрипт скажет об этом в консоли один раз; значение тогда придётся забирать через getResponse().

data-sitekey

Ключ сайта. Можно не писать: скрипт берёт ключ из своего же адреса. Нужен, когда на странице несколько форм с разными ключами.

data-skin

Оформление: neutral (по умолчанию) подстраивается под светлую и тёмную тему вашего сайта, stand — фирменный тёмный вид Kontur.

data-theme

dark или light — закрепляет схему нейтрального оформления. Нужен сайтам, которые всегда тёмные или всегда светлые, независимо от темы системы посетителя.

data-callback

Имя глобальной функции. Вызывается со значением поля, когда пропуск готов. Нужно, если форму отправляет ваш код.

data-expired-callback

Имя глобальной функции. Вызывается, когда задача истекает и берётся новая — это происходит само, каждые 2 мин.

04

Проверка на сервере

Секрет никогда не должен попадать в браузер. У адреса проверки намеренно нет заголовков CORS: вызвать его со страницы не получится, и это сразу подскажет, что что-то не так.

Ответ
{ "success": true, "hostname": "example.com", "challenge_ts": "2026-08-25T12:00:00Z" }

{ "success": false, "error-codes": ["invalid-response"] }
Когда просят картинку

При отказе с кодом challenge-required в ответе приходит поле challenge с готовой картинкой. Верните его на страницу и передайте в виджет — иначе посетитель, ошибшийся один раз, упрётся в тупик до перезагрузки.

const data = await res.json();
if (!data.success) {
  if (data.challenge) window.kontur.challenge(data.challenge);
  return;
}
05

JavaScript API

Аргумент target есть у всех методов, работающих с одной проверкой: индекс, id блока или сам элемент. Он нужен, когда форм на странице больше одной — без него берётся первая, и скрипт предупредит об этом в консоли.

kontur.render(scope?)

Ищет непроинициализированные блоки .kontur и поднимает их. Нужен, когда форма появилась после загрузки страницы — в модальном окне или после перерисовки.

kontur.getResponse(target?)

Значение скрытого поля. Нужен, если форму отправляет ваш код, а не браузер.

kontur.reset(target?)

Берёт новую задачу. Без аргумента сбрасывает все проверки на странице.

kontur.challenge(payload, target?)

Показывает картинку, которую вернул ваш /verify в поле challenge. Без этого посетитель, ошибшийся один раз, упирался в тупик до перезагрузки.

<div class="kontur" id="signup-check" data-callback="onKonturReady"></div>

<script>
  function onKonturReady(response) {
    // пропуск готов — можно отправлять форму своим кодом
  }
  // на странице с несколькими формами называйте нужную явно
  const value = window.kontur.getResponse('signup-check');
</script>
06

Коды ошибок

Ответ /verify — их видит ваш сервер
missing-input-secretВ запросе нет поля secret
invalid-input-secretСекрет не найден или подключение выключено
missing-input-responseВ запросе нет поля response
invalid-responseОтвет повреждён, просрочен или не проходит проверку
challenge-requiredНужна картинка: показать её человеку и повторить отправку
Выдача задачи — их пишет скрипт в консоль браузера
invalid-site-keyКлюч сайта не найден или подключение выключено. Проверьте на /kontur/check.
domain-not-allowedДомен страницы не вписан в список разрешённых у этого ключа.
exchange-unavailableОбмен картинки на задачу не состоялся: токен уже погашен или просрочен.
07

Проверка подключения

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

Проверить подключение
08

Живая проверка

Кнопка показывает второй режим — так проверка выглядит, когда отправка кажется подозрительной.

Живая проверка

Это не картинка и не запись — рабочий виджет, ровно тот же, что встаёт на форму подключённого сайта. Он уже проверил вас, пока вы читали эту строку.

Kontur
Идёт проверка…
~0,2 сна проверку
0внешних запросов
2 минжизнь задачи
09

Как считается риск

Четыре отправки одной и той же формы, посчитанные настоящим движком. Признаки браузера умеют только снижать подозрительность, и их суммы хватает ровно до обычного режима; повышать без ограничения умеют лишь серверные факты — время, происхождение адреса, форма заголовков, номер попытки.

Заполнил форму мышью и клавиатурой, с домашнего адреса.

ИНДЕКС ПОДОЗРИТЕЛЬНОСТИ24
полоса обычная
0 (чистый клиент)порог проверки — 40100 (бот)
  • -8Страницу трогали
  • -7Неровный ритм набора
  • -6Живая траектория курсора
  • -5Пауза перед первым полем
Проходит незаметносложность PoW: 16 бит

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

10

Сравнение

Строки выбраны по тому, что решает при выборе, а не по тому, где Kontur выигрывает. Последняя строка — там, где он проигрывает, и её стоит прочитать раньше остальных.

ПараметрKonturreCAPTCHATurnstile
Кто видит посетителяТолько ваш сервер и mallaev-dev.ruGoogle — и связывает визит с аккаунтомCloudflare
Что делает человекОбычно ничего: задача решается в фонеОбычно ничего, иногда — светофорыОбычно ничего
Если картинку не видноЗадача вместо неё, по одному вычислениюАудио — и оно распознаётся ботомКартинки нет вовсе
Согласие на обработкуНе нужно: данные не уходят третьим лицамНужно упомянуть в политикеНужно упомянуть в политике
Кто чинит, если сломалосьЯ, и вы знаете, кому писатьНикто персональноНикто персонально
Устоит против выделенной атакиНет — задача решается на видеокартеТоже нет, но моделей у них большеТоже нет
11

Что нового в 2.0

Обновление про одно: раньше браузер рассказывал о себе, а сервер верил. Теперь браузер показывает, а сервер проверяет — и повышать подозрительность стали факты, которые с клиента подделать нельзя. Для живого посетителя не изменилось ничего.

  1. 01

    Поведение проверяется, а не принимается на слово

    Раньше браузер присылал готовые выводы о себе: «страницу трогали», «ритм набора такой-то». Сервер их принимал. Бот, написавший эти шесть чисел от руки, проходил незаметно — и это был самый дешёвый обход, какой в Kontur был. Теперь браузер присылает саму последовательность событий, а выводы делает сервер. Их можно сверить: поведение не может длиться дольше, чем прошло от выдачи задачи, а это время сервер отмерил сам.

  2. 02

    Отсекаются арендованные адреса

    Около полутора тысяч диапазонов AWS, Google Cloud, Oracle, DigitalOcean, Linode и Vultr. Оттуда приходит почти вся дешёвая автоматика, и оттуда отправка теперь получает картинку. Сети доставки содержимого в список не входят: через них выходят живые люди — например, все, у кого включено «Частное реле iCloud».

  3. 03

    Заметен клиент, который только притворяется браузером

    Браузер отправляет набор заголовков целиком и всегда одинаково. Подделать одно поле легко, собрать весь набор — заметно труднее, а забыть половину очень просто, и именно это делают простые скрипты.

  4. 04

    Картинка появляется там, где раньше не могла

    В подписке на рассылку и восстановлении пароля её показать было негде — не по существу, а по устройству виджета. Теперь есть где, и подозрительная отправка там встречает визуальный рубеж, а не просто задачу подороже.

  5. 05

    Постоянный посетитель платит меньше

    Браузер, прошедший несколько проверок за сутки, получает задачу вчетверо легче. Память привязана к браузеру, а не к адресу, поэтому переход с домашнего Wi-Fi на мобильный интернет её не сбрасывает. На чужих сайтах это не работает: там это кука третьей стороны, и браузеры её режут.

  6. 06

    Вам делать не нужно ничего

    Скрипт обновляется сам, ключ и секрет прежние, ответ /verify не менялся ни одним полем. Единственное, что стоит знать: первые минут десять после обновления у части посетителей в кеше ещё лежит старый скрипт, и они увидят картинку. Проходит само.

12

Чего Kontur не делает

Ключ сайта публичен по определению — он лежит в разметке, его видно всем. От чужого использования его защищает список разрешённых доменов, и защищает лишь в браузере: заголовок с адресом страницы браузер проставляет сам и не даёт его подделать, а запрос не из браузера может прислать любой. Так же устроены reCAPTCHA и Turnstile — настоящий барьер даёт не привязка к домену, а вычислительная задача: она стоит одинаково при любом заголовке.

Выдача скрипта по ключу — не защита от копирования. Она закрывает копипасту тега с чужой страницы и вызов без ключа, но браузер вашего посетителя всё равно получает исходный текст скрипта — иначе он не смог бы его исполнить. Ценность не в нём: движок риска, оценка поведения, отрисовка картинки и проверка вычисления живут на сервере и наружу не уезжают.

Поведенческие признаки подделываются целенаправленным ботом. Они отсекают дешёвую автоматизацию, не более.

Объёмный DDoS гасит только хостинг. Наплыв запросов по HTTP на mallaev-dev.ru сдерживает режим «под атакой»: проверка стоит в nginx, до приложения, и бот без пропуска до сервера сайта не доходит. Но атака, забивающая сам канал, до nginx не доберётся вовсе — её гасит инфраструктура хостинга. Подключаемым сайтам режим пока не предлагается: там Kontur защищает формы — от рассылки, перебора паролей и слива бюджета на языковые модели, — а не входящий трафик.

Внешний сервис в схеме всё-таки есть — Yandex KMS, но не на пути проверки. Им зашифрован ваш секрет, чтобы его можно было показать вам второй раз, не храня открытым. Проверка отправки формы ищет сайт по хешу и в KMS не заходит никогда, поэтому недоступность облака её не ломает.

Ключа ещё нет? Расскажите о задаче — подключение завожу я, и оно появляется в вашем профиле.