Confirmator REST API
Подтверждение номера через мессенджеры
Backend вашего приложения создаёт короткоживущий challenge. Пользователь открывает ссылку или вводит общий код в любом доступном мессенджере, а ваш backend получает проверяемый результат.
Машиночитаемый контракт: 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 · Быстрый старт
Три запроса до результата
- Создайте проектЗарегистрируйтесь, пополните баланс, включите Telegram, MAX и/или WhatsApp и выпустите API-ключ.
- Создайте challengeПередайте ожидаемый номер с backend вашего приложения и покажите пользователю
authorization_url. - Получите proofИспользуйте polling или подписанный webhook и создайте пользовательскую сессию только при
phoneVerified: true.
03 · Авторизация
Bearer API key
Каждый запрос к `/v1` требует ключ проекта в заголовке:
Authorization: Bearer confirmator_test_<prefix>.<secret>Полный ключ показывается один раз. Храните его в secret storage или переменной окружения.
04 · REST API
Жизненный цикл challenge
Если пользователь не завершил проверку за 30–180 секунд, API возвращает вычисляемый статус expired. consumed означает, что proof уже был получен server-to-server запросом.
/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 проекта./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 нужно остановить.
/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"
}
}07 · Безопасность
Что обязана делать интеграция
- Вызывать `/v1` только с доверенного backend.
- Создавать сессию приложения только при `phoneVerified: true`.
- Сопоставлять `externalUserRef` с текущей операцией пользователя.
- Не считать открытие messenger-ссылки подтверждением.
- Использовать HTTPS, таймауты и уникальный `Idempotency-Key`.
- Не хранить proof и полный номер дольше, чем требуется вашему продукту и законодательству.