ConfirmatorREST API docsВойти

Confirmator REST API

Подтверждение номера через мессенджеры

Backend вашего приложения создаёт короткоживущий challenge. Пользователь открывает ссылку или вводит общий код в любом доступном мессенджере, а ваш backend получает проверяемый результат.

Server-to-serverAPI-ключ нельзя передавать браузеру, мобильному приложению или пользователю.

Машиночитаемый контракт: OpenAPI 3.1 JSON.

01 · Готовые примеры

Скопируйте интеграцию для вашего языка

type Json = Record<string, unknown>;
type MessengerLink = { messenger: "telegram" | "max" | "whatsapp"; title: string; bot_id: string; link: string; image: string; color: string; qr_code: string };
type Challenge = { id: string; authorization_url: string; qr_code: string; fallbackCode: string; links: MessengerLink[] };
type Status = { status: "created" | "opened" | "confirmed" | "rejected" | "expired" | "consumed"; messenger: "telegram" | "max" | "whatsapp" | null; reason?: string; resolvedAt: string | null };
type Proof = { proof: { expectedPhone: string; reportedPhone: string | null; phonesMatch: boolean; phoneVerified: boolean } };

const apiUrl = "https://confirmator.ru";
const apiKey = process.env.CONFIRMATOR_API_KEY;
if (!apiKey) throw new Error("CONFIRMATOR_API_KEY is required");

async function call<T>(path: string, init: RequestInit = {}): Promise<T> {
  const response = await fetch(apiUrl + path, {
    ...init,
    headers: { authorization: "Bearer " + apiKey, "content-type": "application/json", ...init.headers },
  });
  const body = await response.json() as Json;
  if (!response.ok) throw new Error(String((body.error as Json | undefined)?.code ?? response.status));
  return body as unknown as T;
}

const challenge = await call<Challenge>("/v1/challenges", {
  method: "POST",
  body: JSON.stringify({ phone: "+79991234567", purpose: "login", deliveryMode: "polling" }),
});
console.log("Send the user to", challenge.authorization_url);

let status: Status;
do {
  await new Promise((resolve) => setTimeout(resolve, 2000));
  status = await call<Status>("/v1/challenges/" + challenge.id + "/status");
} while (status.status === "created" || status.status === "opened");

if (status.status !== "confirmed" && status.status !== "rejected") throw new Error("Challenge expired");

const result = await call<Proof>("/v1/challenges/" + challenge.id + "/consume", {
  method: "POST",
  headers: { "idempotency-key": crypto.randomUUID() },
});
if (result.proof.phoneVerified) console.log("Phone confirmed");

02 · Быстрый старт

Три запроса до результата

  1. Создайте проектЗарегистрируйтесь, пополните баланс, включите Telegram, MAX и/или WhatsApp и выпустите API-ключ.
  2. Создайте challengeПередайте ожидаемый номер с backend вашего приложения и покажите пользователю authorization_url.
  3. Получите proofИспользуйте polling или подписанный webhook и создайте пользовательскую сессию только при phoneVerified: true.

03 · Авторизация

Bearer API key

Каждый запрос к `/v1` требует ключ проекта в заголовке:

Authorization: Bearer confirmator_test_<prefix>.<secret>

Полный ключ показывается один раз. Храните его в secret storage или переменной окружения.

04 · REST API

Жизненный цикл challenge

createdopenedconfirmedилиrejectedconsumed

Если пользователь не завершил проверку за 30–180 секунд, API возвращает вычисляемый статус expired. consumed означает, что proof уже был получен server-to-server запросом.

POST/v1/challenges

Создать challenge

Канал не выбирается в запросе. Ответ содержит ссылки включённых и доступных в момент ответа мессенджеров и один общий fallback-код. Если все каналы недоступны, создание вернёт 409 channel_unavailable; при восстановлении канала его ссылка снова появится на странице авторизации.

Запрос

{
  "phone": "+79991234567",
  "externalUserRef": "user-4815",
  "purpose": "login",
  "expiresIn": 180,
  "deliveryMode": "polling"
}

201 Created

{
  "id": "ch_...",
  "challengeId": "ch_...",
  "projectId": "prj_...",
  "status": "created",
  "purpose": "login",
  "maskedPhone": "+799***4567",
  "fallbackCode": "482193",
  "authorization_url": "https://confirmator.ru/authorize/...",
  "qr_code": "https://confirmator.ru/public/qr/challenge/...",
  "links": [{
    "messenger": "telegram",
    "title": "Telegram",
    "bot_id": "@confirmator_bot",
    "link": "tg://resolve?domain=confirmator_bot&start=...",
    "image": "https://confirmator.ru/messengers/telegram.png",
    "color": "#229ED9",
    "qr_code": "https://confirmator.ru/public/qr/messengers/telegram/..."
  }, {
    "messenger": "max",
    "title": "MAX",
    "bot_id": "@confirmator_bot",
    "link": "https://max.ru/confirmator_bot?start=...",
    "image": "https://confirmator.ru/messengers/max.png",
    "color": "#7657FF",
    "qr_code": "https://confirmator.ru/public/qr/messengers/max/..."
  }, {
    "messenger": "whatsapp",
    "title": "WhatsApp",
    "bot_id": "@79991234567",
    "link": "https://wa.me/79991234567?text=confirm%20...",
    "image": "https://confirmator.ru/messengers/whatsapp.png",
    "color": "#25D366",
    "qr_code": "https://confirmator.ru/public/qr/messengers/whatsapp/..."
  }],
  "createdAt": "2026-07-15T12:00:00Z",
  "expiresAt": "2026-07-15T12:03:00Z",
  "expiresIn": 180,
  "deliveryMode": "polling"
}

`qr_code` ведёт на PNG с URL безопасной страницы Confirmator; `links[].qr_code` кодирует прямой messenger link. Логотип из `links[].image` также отдаётся нашим сервером. `links[].bot_id` — публичное имя бота с ведущим @ для ручного поиска, если ссылка не открылась.

Создание резервирует 0,80 ₽ на балансе. При успешном подтверждении сумма списывается, при отклонении или истечении срока — возвращается. При нехватке средств API отвечает 402 insufficient_balance.

phonestring · requiredНомер в международном или российском формате.
externalUserRefstring · optionalВаш идентификатор пользователя, до 128 символов.
purposestring · optionalНазначение проверки до 64 символов, например login.
expiresInnumber · optionalTTL от 30 до 180 секунд.
deliveryModepolling | webhookСпособ получения terminal result.
webhookUrlstring · conditionalCallback URL до 2048 символов из разрешённых origins проекта.
GET/v1/challenges/:id/status

Получить статус

Безопасный endpoint для polling. Он не возвращает полный номер или proof.

{
  "id": "ch_...",
  "challengeId": "ch_...",
  "status": "confirmed",
  "purpose": "login",
  "deliveryMode": "polling",
  "externalUserRef": "user-4815",
  "messenger": "telegram",
  "createdAt": "2026-07-15T12:00:00Z",
  "resolvedAt": "2026-07-15T12:01:17Z",
  "expiresAt": "2026-07-15T12:03:00Z"
}

Для rejected дополнительно приходит reason: phone_mismatch, foreign_contact или invalid_phone. Рекомендуемый интервал polling — 1 секунда; после terminal status polling нужно остановить.

POST/v1/challenges/:id/consume

Одноразово получить proof

Получает полный proof после confirmed или rejected. Заголовок Idempotency-Key обязателен, имеет длину до 128 символов и должен оставаться тем же при сетевом повторе. После первого успешного consume другой ключ получит 409 already_consumed.

Idempotency-Key: consume-user-4815

{
  "challengeId": "ch_...",
  "projectId": "prj_...",
  "externalUserRef": "user-4815",
  "purpose": "login",
  "proof": {
    "type": "phone",
    "messenger": "telegram",
    "method": "shared_contact",
    "expectedPhone": "+79991234567",
    "reportedPhone": "+79991234567",
    "phonesMatch": true,
    "phoneVerified": true
  },
  "resolvedAt": "2026-07-15T12:01:17Z"
}

05 · Webhook

Подписанная доставка

Для webhook-режима создайте secret в настройках проекта, добавьте callback origin и передайте deliveryMode: "webhook". Ответ 2xx подтверждает доставку. При ошибке выполняется до пяти попыток примерно на 0-й, 10-й, 40-й, 100-й и 160-й секундах; после трёх минут доставка прекращается.

x-confirmator-event-id: whd_...
x-confirmator-timestamp: 1784090000
x-confirmator-signature: v1=<hex-hmac-sha256>

Строка подписи: HMAC_SHA256(secret, timestamp + "." + rawBody). Проверяйте подпись по исходному телу до JSON parsing и отклоняйте слишком старый timestamp.

{
  "id": "whd_...",
  "type": "challenge.completed",
  "createdAt": "2026-07-15T12:01:17Z",
  "data": {
    "challengeId": "ch_...",
    "projectId": "prj_...",
    "status": "confirmed",
    "externalUserRef": "user-4815",
    "purpose": "login",
    "proof": {
      "type": "phone",
      "messenger": "telegram",
      "method": "shared_contact",
      "expectedPhone": "+79991234567",
      "reportedPhone": "+79991234567",
      "phonesMatch": true,
      "phoneVerified": true
    },
    "resolvedAt": "2026-07-15T12:01:17Z"
  }
}

Каждый пример ниже показывает полный цикл на backend заказчика: endpoint POST /login отправляет запрос создания challenge в Confirmator, а POST /webhooks/confirmator принимает и проверяет результат. Задайте CONFIRMATOR_API_KEY, CONFIRMATOR_WEBHOOK_SECRET и публичный HTTPS URL callback в CONFIRMATOR_WEBHOOK_URL.

import express from "express";
import { createHmac, timingSafeEqual } from "node:crypto";

const app = express();
const secret = process.env.CONFIRMATOR_WEBHOOK_SECRET;
const apiKey = process.env.CONFIRMATOR_API_KEY;
const webhookUrl = process.env.CONFIRMATOR_WEBHOOK_URL;
if (!secret || !apiKey || !webhookUrl) throw new Error("Confirmator environment variables are required");

app.post("/login", express.json({ limit: "4kb" }), async (request, response) => {
  const apiResponse = await fetch("https://confirmator.ru/v1/challenges", {
    method: "POST",
    headers: { Authorization: "Bearer " + apiKey, "Content-Type": "application/json" },
    body: JSON.stringify({
      phone: request.body.phone,
      purpose: "login",
      deliveryMode: "webhook",
      webhookUrl,
    }),
  });
  const result = await apiResponse.json();
  return response.status(apiResponse.status).json(result);
});

app.post("/webhooks/confirmator", express.raw({ type: "application/json", limit: "32kb" }), (request, response) => {
  const timestamp = request.header("x-confirmator-timestamp") ?? "";
  const signature = request.header("x-confirmator-signature") ?? "";
  const eventId = request.header("x-confirmator-event-id") ?? "";
  const rawBody = request.body as Buffer;

  const timestampMs = Number(timestamp) * 1000;
  if (!Number.isFinite(timestampMs) || Math.abs(Date.now() - timestampMs) > 5 * 60_000) {
    return response.status(401).end();
  }

  const expected = "v1=" + createHmac("sha256", secret)
    .update(timestamp + ".")
    .update(rawBody)
    .digest("hex");
  const actualBuffer = Buffer.from(signature);
  const expectedBuffer = Buffer.from(expected);
  if (actualBuffer.length !== expectedBuffer.length || !timingSafeEqual(actualBuffer, expectedBuffer)) {
    return response.status(401).end();
  }

  const event = JSON.parse(rawBody.toString("utf8"));
  // Сначала атомарно сохраните eventId в БД с уникальным индексом.
  // Повтор уже обработанного eventId должен также получить 204.
  if (event.data?.proof?.phoneVerified === true) {
    // Создайте сессию именно вашего приложения.
  }
  return response.status(204).end();
});

app.listen(8080);

Все webhook-заголовки используют единый префикс x-confirmator-*. Сначала атомарно дедуплицируйте событие по x-confirmator-event-id, затем обрабатывайте data.proof. Повтор уже принятого события также должен получить 2xx.

06 · Ошибки

Единый формат

{
  "error": {
    "code": "invalid_phone",
    "message": "Invalid phone",
    "requestId": "550e8400-e29b-41d4-a716-446655440000"
  }
}
400 invalid_json, invalid_request, invalid_phone, invalid_expiry, invalid_external_user_ref, invalid_idempotency_key, webhook_url_required, invalid_webhook_url401 invalid_api_key402 insufficient_balance403 project_inactive, webhook_origin_not_allowed404 challenge_not_found409 not_ready, already_consumed, channel_unavailable, webhook_not_configured413 payload_too_large500 internal_error

07 · Безопасность

Что обязана делать интеграция

  • Вызывать `/v1` только с доверенного backend.
  • Создавать сессию приложения только при `phoneVerified: true`.
  • Сопоставлять `externalUserRef` с текущей операцией пользователя.
  • Не считать открытие messenger-ссылки подтверждением.
  • Использовать HTTPS, таймауты и уникальный `Idempotency-Key`.
  • Не хранить proof и полный номер дольше, чем требуется вашему продукту и законодательству.