Для разработчиков

Подключение входа по звонку к вашему сайту: сайт открывает окно авторизации, пользователь звонит на выданный номер, сервис подтверждает звонок.

Базовый адресhttps://входпономеру.рф
ФорматJSON, UTF-8
АвторизацияX-Api-Key
Версияv2 — окна, v1 — служебные

Как это работает

  1. Ваш сайт открывает окно авторизации для телефона пользователя и получает номер для звонка (номер выдаёт сервис из своего пула — своя телефония не нужна).
  2. Пользователь звонит на этот номер со своего телефона.
  3. Ваш сайт получает подтверждение: опросом статуса или вебхуком. Номер проверен — можно открывать сессию.

Что нужно перед началом

Сайт подключается в личном кабинете: Обзор → карточка сайта → «Настройки». Ключи показываются один раз, при активации (потеряли — выпустите новые, старые сразу погаснут):

КлючДля чего
api_keyВо всех запросах к API
webhook_secretПроверка подписи входящих вебхуков
webhook_urlАдрес приёмника на вашем сайте. Задаётся самим сайтом через POST /api/v1/webhook (см. ниже), в ЛК не вводится — там он показывается для проверки

Доставка вебхуков включается переключателем «Доставлять вебхуки» в ЛК → Вебхуки (при нескольких сайтах — для выбранного) либо флагом enabled в POST /api/v1/webhook. По умолчанию доставка выключена: события собираются в ЛК, но наружу не уходят. Включите её, когда обработчик на сайте готов принимать запросы.

Домен сайта задаётся при подключении сайта в ЛК и разрешает запросы к API со страниц сайта: поддомены подходят автоматически — для site.ru работают www.site.ru и shop.site.ru. Запросы с сервера без заголовков Origin/Referer проверку не проходят — плагин WordPress, ходящий через wp_remote_post, работает без изменений. ВАЖНО: для адреса вебхука правило строже — только сам домен и его www-форма, поддомены отклоняются (см. /api/v1/webhook).

Ключи храните только на сервере. Не вставляйте их в HTML или JS — их увидит любой пользователь. Браузер должен обращаться к вашему бэкенду, а тот уже к сервису.

Базовый адрес: https://входпономеру.рф (punycode: https://xn--b1aed1aeebbgp7ao.xn--p1ai). Если хостинг не поддерживает переписывание путей, используйте форму с параметром r — она равнозначна:

https://входпономеру.рф/api/v2/window
https://входпономеру.рф/index.php?r=/api/v2/window   ← то же самое

Так работает любой путь API: /api/v1/diagnostics, /api/v1/webhook, /api/v1/window/123 — все они доступны и как /index.php?r=/api/v1/….

Методы

POST /api/v2/window Открыть окно и получить номер для звонка

Открывает окно авторизации для конкретного телефона и выдаёт номер пула, на который нужно позвонить. Перед выдачей сервис проверяет баланс/пакет (логины в пакете бесплатны, сверх пакета — по ставке тарифа).

Параметры тело запроса, application/json

ПолеТипОписание
api_keystringОбязательно. Ваш ключ.
phonestringОбязательно. Телефон пользователя. Приводится к цифрам, 8… → 7….
windowintВремя окна: 1 = 2 мин, 2 = 3 мин, 3 = 5 мин.

Ответ 201

{
  "window_id": 1287,
  "dial_number": "79675550839",
  "expires_at": "2026-09-23 12:41:07",
  "status": "open",
  "window_url": "/api/v1/window/1287"
}

Покажите пользователю dial_number и запомните window_id.

Пример

curl -X POST 'https://входпономеру.рф/api/v2/window' \
  -H 'Content-Type: application/json' \
  -d '{"api_key":"<ваш api_key>","phone":"79094295569","window":1}'

Ошибки

409 busyДля этого телефона уже открыто окно. В ответе есть retry_after_seconds — через сколько можно повторить
409 pool_exhaustedНет свободных номеров пула
400 bad_phoneТелефон не 10–16 цифр
400 bad_windowwindow не 1, 2 или 3
400 insufficient_fundsПакет входов израсходован и на балансе нет денег на вход: номер не выдаётся, вход не выполняется. В ответе — message (готовый текст для владельца сайта), price_kop (стоимость входа) и amount_kop (текущий баланс). Пополните баланс — входы заработают сразу
403 blockedАккаунт заблокирован; причина — в message. Обратитесь в поддержку
401 invalid_api_keyКлюч неизвестен или отключён
403 domain_not_allowedЗапрос пришёл с домена, которого нет в домене сайта ключа (см. выше)
429 rate_limitedПревышен лимит запросов
Вызовы из браузера (с Origin вашего сайта) поддерживаются: сервис отвечает CORS-заголовками для доменов из разрешённого списка. По умолчанию CORS закрыт — основной и рекомендуемый путь запросов со своего сервера.
GET /api/v1/window/{window_id} Статус окна — опрос до подтверждения

Опрашивайте раз в секунду, пока не получите matched или expired. Запрос шлите со своего сервера, чтобы ключ не попадал в браузер.

Параметры

ГдеЗначениеОписание
Путьwindow_idИз ответа первого метода
ЗаголовокX-Api-Key: <ваш api_key>Обязательно (или параметр api_key)

Ответ 200

{
  "window_id": 1287,
  "status": "matched",
  "expires_at": "2026-09-23 12:41:07",
  "matched_at": "2026-09-23 12:39:52",
  "delivery": {
    "enabled": true,
    "paused": false,
    "paused_until": null,
    "paused_forever": false,
    "paused_seconds_left": null
  }
}

Статусы

openЖдём звонок — продолжать опрос
matchedЗвонок подтверждён — пускать пользователя
expiredВремя вышло — предложить повторить или вход по паролю

delivery — состояние доставки вебхуков сайта: enabled (включена ли), paused/paused_until/paused_forever/paused_seconds_left (не приостановлена ли предохранителем). Если доставка приостановлена, вебхук не уйдёт — для входа остаётся только опрос; предупредите администратора сайта.

Пример

curl 'https://входпономеру.рф/api/v1/window/1287' \
  -H 'X-Api-Key: <ваш api_key>'
GET /api/v1/diagnostics Проверка связи и настроек без занятия номера пула

Служебный вызов для страницы настроек плагина: проверяет, жив ли сервис и всё ли настроено. Открытие окна для этого не подходит — оно занимает номер пула на 2+ минуты. Эндпоинт бесплатный: никаких списаний и записей в биллинге.

Параметры

ГдеЗначениеОписание
ЗаголовокX-Api-Key: <ваш api_key>Обязательно (или параметр api_key)

Ответ 200

{
  "ok": true,
  "service": true,
  "key_valid": true,
  "domain_ok": true,
  "allowed_domains": ["site.ru"],
  "allowed_domain_display": "site.ru",
  "balance_kop": 250000,
  "tariff": {
    "code": "medium",
    "name": "Средний",
    "monthly_price_kop": 1500000,
    "included_webhooks": 15360,
    "sites_bonus": 1024,
    "included_total": 16384,
    "webhooks_used": 120,
    "webhooks_left": 16264
  },
  "webhook": {
    "configured": true,
    "url_set": true,
    "enabled": true,
    "paused": false,
    "pause_level": 0,
    "pause_until": null,
    "forever": false
  },
  "server_time": "2026-09-23 12:00:00",
  "window_ttl_seconds": 120
}

domain_ok показывает, применилась ли привязка к домену к этому запросу: true, если запрос пришёл с разрешённого домена или без Origin/Referer вовсе (серверный вызов). allowed_domains — домен сайта, закреплённый за ключом (0–1 запись, в punycode); allowed_domain_display — тот же домен кириллицей для показа человеку; пусто = проверка отключена.

Поля тарифа: included_webhooks — пакет, включённый тарифом; sites_bonus — входы за дополнительные (сверх первого) АКТИВНЫЕ сайты по цене пакета; included_total = их сумма; webhooks_used — израсходовано в текущем расчётном периоде (30 дней); webhooks_left — остаток пакета. Сверх пакета каждый вход списывается по ставке тарифа (см. таблицу).

Тарифы (текущая сетка, этап 18)

codeТарифАбонплатаВходов в пакетеСверх пакетаВходов за доп. сайт
testТест (без тарифа)0 ₽/мес05,00 ₽/шт200
promoПРОМО (первый месяц)1 399 ₽/мес1 0242,50 ₽/шт732
miniМини1 999 ₽/мес1 0242,50 ₽/шт512
smallМалый4 999 ₽/мес3 0722,00 ₽/шт614
mediumСредний15 000 ₽/мес15 3601,50 ₽/шт1 024
businessБизнес69 900 ₽/мес102 4001,00 ₽/шт1 464
corporateКорпоративный349 000 ₽/мес1 024 0000,50 ₽/шт2 934

Ставка сверх пакета плоская — второй ступени «свыше 5000» с отдельной ценой больше нет. Тарифы mobile, tollfree, bonus удалены миграцией 017, коды standart, optimum, maximum переименованы миграцией 018 в mini, small, medium: если в ответе встретился иной code, обновите интеграцию. Неудачные звонки не тарифицируются.

Пример

curl 'https://входпономеру.рф/api/v1/diagnostics' \
  -H 'X-Api-Key: <ваш api_key>'

Ошибки

401 api_key_requiredКлюч не передан
401 invalid_api_keyКлюч неизвестен или отключён
Проверяйте webhook.url_set (адрес задан), webhook.enabled (доставка включена) и webhook.paused (не приостановлена ли): если доставка выключена или приостановлена, вход по звонку у посетителей не завершается — предупредите администратора сайта.
GET POST /api/v1/webhook Ссылка вебхука: сайт сам задаёт адрес приёмника

Новый способ задать адрес приёмника вебхуков: сам сайт сообщает его со своей стороны, а не через ЛК. Ключ определяет сайт (один ключ — один сайт), поэтому «чужая» ссылка не пройдёт: хост обязан совпасть с доменом сайта.

Аутентификация

ГдеЗначениеОписание
ЗаголовокX-Api-Key: <ваш api_key>Обязательно (или параметр api_key)

GET — текущее состояние

Возвращает домен сайта и текущую ссылку вебхука. В БД и в поле webhook_url ссылка лежит в punycode (его требует curl при доставке), а webhook_url_display отдаёт её кириллицей — как её писал клиент. enabled — включена ли доставка.

Ответ 200

{
  "ok": true,
  "domain": "xn--80aairftm.xn--p1ai",
  "domain_display": "сайт.рф",
  "webhook_url": "https://xn--80aairftm.xn--p1ai/wc/call-login-hook",
  "webhook_url_display": "https://сайт.рф/wc/call-login-hook",
  "url_set": true,
  "enabled": true
}

Пример

curl 'https://входпономеру.рф/api/v1/webhook' \
  -H 'X-Api-Key: <ваш api_key>'

POST — задать ссылку тело запроса, application/json

ПолеТипОписание
webhook_urlstringОбязательно. Адрес приёмника на вашем сайте: https://сайт.рф/wc/call-login-hook. Домен можно без схемы — https:// допишется сам. Разрешены путь, порт и query.
enabledint/boolНеобязательно. 1/true — включить доставку, 0/false — выключить. Не передано — текущее значение не меняется.

Правило домена

  • Хост ссылки обязан совпасть с доменом сайта. Допускается сам домен и его www-форма с любой стороны: site.ru ≈ www.site.ru.
  • Поддомены отклоняются: shop.site.ru не пройдёт, даже если shop — настоящий поддомен.
  • Кириллица, латиница и смесь принимаются одинаково — сравнение идёт после приведения к punycode: сайт.рф, xn--80aairftm.xn--p1ai и сайт.xn--p1ai — одно и то же.
  • У ключа без сайта (легаси-ключ) доменом считается привязка allowed_domain; если и она пуста — проверка отключена и принимается любой http(s) адрес.

Пример

curl -X POST 'https://входпономеру.рф/api/v1/webhook' \
  -H 'X-Api-Key: <ваш api_key>' \
  -H 'Content-Type: application/json' \
  -d '{"webhook_url":"https://сайт.рф/wc/call-login-hook","enabled":1}'

Ответ 200

{
  "ok": true,
  "domain": "xn--80aairftm.xn--p1ai",
  "domain_display": "сайт.рф",
  "webhook_url": "https://xn--80aairftm.xn--p1ai/wc/call-login-hook",
  "webhook_url_display": "https://сайт.рф/wc/call-login-hook",
  "url_set": true,
  "enabled": true,
  "delivery": {
    "enabled": true,
    "paused": false,
    "paused_until": null,
    "paused_forever": false,
    "paused_seconds_left": null
  }
}

В ответе — состояние после записи, включая delivery (как в статусе окна). Запись успешной ссылки снимает паузу предохранителя, если доставка была приостановлена из-за «мёртвого» прежнего адреса (кроме явного enabled: 0).

Ошибки

400 webhook_url_requiredПоле webhook_url не передано или пустое
400 bad_urlНе похоже на http(s)-адрес: пробелы, фрагмент #, user@host, схема не http(s), длиннее 500 символов
400 domain_mismatchХост ссылки не совпадает с доменом сайта. В ответе поля expected_domain (ожидаемый домен) и got_host (что пришло)
401 api_key_requiredКлюч не передан
401 invalid_api_keyКлюч неизвестен или отключён
Проверка домена идёт в момент записи, а не при первом звонке: неверную ссылку сервис не примет сразу, и вебхуки никогда не уйдут на чужой хост.
POST ваш webhook_url Уведомление об успешном входе — вместо опроса

Сервис сам вызовет ваш адрес, когда звонок подтверждён. Адрес задаёт сам сайт через POST /api/v1/webhook (либо при подключении в ЛК); проверить его можно на карточке сайта в ЛК. Доставка включается переключателем «Доставлять вебхуки» в ЛК → Вебхуки или флагом enabled того же запроса.

Как происходит доставка

  • Событие отправляется сразу после подтверждения звонка.
  • Доставка идёт синхронно внутри того же HTTP-запроса телефонии: ответ Novofon уже отдан (запрос закрыт через fastcgi_finish_request), и PHP-воркер сразу делает 3 попытки доставки — пауза между попытками 3 секунды, таймаут каждой 2 секунды. Крон в доставке не участвует.
  • Отвечайте быстро: достаточно 200 без тела. Тяжёлую работу делайте после ответа.
  • Все попытки укладываются в ~12 секунд. Если сайт не ответил за это время, событие остаётся failed — повторов позже нет, оно видно в ЛК → Вебхуки.

Заголовок подписи

X-CallLogin-Signature: <hex>

Тело

{
  "event": "auth.success",
  "window_id": 1287,
  "phone": "79094295569",
  "call_session_id": "1718...",
  "timestamp": 1789999999
}

Проверка подписи

Подпись — HMAC-SHA256 от сырого тела запроса на ключе webhook_secret. Сравнивайте безопасно:

$raw    = file_get_contents('php://input');
$expect = hash_hmac('sha256', $raw, '<ваш webhook_secret>');
$got    = $_SERVER['HTTP_X_CALLLOGIN_SIGNATURE'] ?? '';

if (!hash_equals($expect, $got)) {
    http_response_code(403);
    exit;                       // запрос не от сервиса
}

$e = json_decode($raw, true);
if (($e['event'] ?? '') === 'auth.success') {
    // номер подтверждён — открывайте сессию по $e['phone']
}
http_response_code(200);        // отвечайте быстро, 2xx
Доставка повторяется, поэтому обработчик должен быть идемпотентным: одно window_id — одна выдача сессии.

Если сайт не отвечает

Сервис защищает себя от «мёртвых» адресов: после 5 неудачных циклов подряд доставка ставится на паузу, которая растёт при повторе:

ПаузаКогда наступает
15 минутпосле 5 неудачных циклов
1 часесли после возобновления снова 5 неудач
6 часовдальше
24 часадальше
бессрочнодальше — снимается только вручную в ЛК

Состояние доставки видно в ответе статуса окна (delivery) и в ответе POST /api/v1/webhook — проверяйте его, чтобы не пропустить паузу:

{
  "window_id": 1287,
  "status": "matched",
  "delivery": {
    "enabled": true,
    "paused": false,
    "paused_until": null,
    "paused_forever": false,
    "paused_seconds_left": null
  }
}

История отправок

Все события с HTTP-кодом и ответом вашего сайта видны в ЛК → Вебхуки. Там же кнопка принудительного возобновления доставки после паузы (при нескольких сайтах — для выбранного).

POST /api/v1/window Легаси: окно на вашем собственном номере

Старый вариант: окно привязывается к номеру, который подключён у вас, а не к номеру из пула сервиса. Для новых интеграций используйте /api/v2/window.

Параметры

ПолеТипОписание
api_keystringОбязательно
phonestringОбязательно

Ответ 201 — окно 2 минуты; статус читается тем же /api/v1/window/{id}

{ "window_id": 123, "expires_at": "…", "status": "open", "window_url": "/api/v1/window/123" }

Общие ошибки

HTTPerrorКогдаЧто делать
400bad_jsonТело не разобралось как JSONПроверить запрос
400api_key_and_phone_requiredВ POST /window нет api_key или phoneПередать оба поля
400insufficient_fundsПакет входов израсходован, и на балансе нет денег на вход — номер не выдаётсяПополнить баланс в ЛК
403blockedАккаунт заблокирован (message — причина)Обратиться в поддержку
401api_key_requiredНет ключа в запросеДобавить X-Api-Key
403domain_not_allowedЗапрос пришёл с домена вне домена сайта ключаПроверить домен сайта в ЛК
404not_foundОкно не существует или чужоеПроверить window_id
409busyДля этого телефона уже открыто окноПовторить после retry_after_seconds
409pool_exhaustedНет свободных номеров пулаПовторить позже
429rate_limitedБольше 30 запросов в минуту на ключСнизить частоту
503service_unavailableСервис недоступенПовторить позже, показать вход по паролю

Ошибки POST /api/v1/webhook (webhook_url_required, bad_url, domain_mismatch) описаны в разделе метода.

Всегда давайте пользователю запасной вход по логину и паролю — на случай таймаута окна или недоступности сервиса.

Пример: PHP + JS

// 1) открыть окно
function startLogin(string $phone): array {
    $ch = curl_init('https://входпономеру.рф/api/v2/window');
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST           => true,
        CURLOPT_TIMEOUT        => 10,
        CURLOPT_HTTPHEADER     => ['Content-Type: application/json'],
        CURLOPT_POSTFIELDS     => json_encode([
            'api_key' => '<ваш api_key>',
            'phone'   => preg_replace('/\D+/', '', $phone),
            'window'  => 1,
        ]),
    ]);
    $body = curl_exec($ch);
    curl_close($ch);
    return json_decode((string)$body, true) ?: ['error' => 'service_unavailable'];
}

// 2) статус окна — вызывается вашим AJAX-обработчиком
function loginStatus(int $windowId): array {
    $ch = curl_init('https://входпономеру.рф/api/v1/window/' . $windowId);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 10,
        CURLOPT_HTTPHEADER     => ['X-Api-Key: <ваш api_key>'],
    ]);
    $body = curl_exec($ch);
    curl_close($ch);
    return json_decode((string)$body, true) ?: ['status' => 'unknown'];
}

// 2.5) задать адрес вебхука — один раз, при настройке сайта
function setWebhookUrl(string $url): array {
    $ch = curl_init('https://входпономеру.рф/api/v1/webhook');
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST           => true,
        CURLOPT_TIMEOUT        => 10,
        CURLOPT_HTTPHEADER     => [
            'Content-Type: application/json',
            'X-Api-Key: <ваш api_key>',
        ],
        CURLOPT_POSTFIELDS     => json_encode([
            'webhook_url' => $url,
            'enabled'     => 1,
        ]),
    ]);
    $body = curl_exec($ch);
    curl_close($ch);
    return json_decode((string)$body, true) ?: ['error' => 'service_unavailable'];
}

$start = startLogin('+7 909 429-55-69');
echo isset($start['dial_number'])
    ? 'Позвоните на ' . $start['dial_number']
    : 'Ошибка: ' . ($start['error'] ?? 'unknown');
// 3) опрос статуса в браузере (ключ остаётся на сервере)
async function waitForCall(windowId, onMatched, onExpired) {
  const deadline = Date.now() + 120000;          // окно window:1

  while (Date.now() < deadline) {
    const r = await fetch('/api/my-login-status?window_id=' + windowId,
                          { credentials: 'same-origin' });
    const s = await r.json();

    if (s.status === 'matched') return onMatched(s);
    if (s.status === 'expired') return onExpired(s);

    await new Promise(res => setTimeout(res, 1000));
  }
  onExpired({ status: 'timeout' });
}

Чек-лист

Нужна помощь? Напишите через форму обратной связи — поможем подключить.