Confirmator REST API
Проверка телефона и email единым API
Backend создаёт короткоживущий challenge с одним назначением: phone подтверждается через Telegram, MAX или WhatsApp, а email — шестизначным одноразовым кодом из письма. Status, consume и webhook общие для обоих вариантов.
Машиночитаемый контракт: OpenAPI 3.1 JSON.
01 · Готовые примеры
Скопируйте интеграцию для вашего языка
Переключаемые примеры ниже показывают телефонный flow через мессенджеры. Email использует тот же create/status/consume lifecycle и добавляет один запрос проверки OTP:
# Создать email challenge
curl -sS -X POST https://confirmator.ru/v1/challenges \
-H "Authorization: Bearer $CONFIRMATOR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"email":"user@example.com","purpose":"registration","deliveryMode":"polling"}'
# Проверить код, введённый пользователем
curl -sS -X POST https://confirmator.ru/v1/challenges/ch_.../verify \
-H "Authorization: Bearer $CONFIRMATOR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"code":"482193"}'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 · Быстрый старт
Три запроса до результата
- Создайте проектЗарегистрируйтесь, пополните баланс, настройте нужные каналы и выпустите API-ключ.
- Создайте challengeПередайте ровно одно поле:
phoneилиemail. Для телефона покажитеauthorization_url, для email — форму ввода OTP. - Получите proofИспользуйте polling или подписанный webhook и проверяйте
phoneVerifiedлибоemailVerifiedв зависимости отproof.type.
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
Передайте ровно одно назначение: phone или email. Телефонный ответ содержит доступные messenger links и fallback-код. Email-ответ не содержит OTP: код отправляется только на указанный адрес. Старый запрос с phone полностью совместим.
Запрос
{
"phone": "+79991234567",
"externalUserRef": "user-4815",
"purpose": "login",
"expiresIn": 180,
"deliveryMode": "polling"
}201 Created
{
"id": "ch_...",
"challengeId": "ch_...",
"projectId": "prj_...",
"type": "phone",
"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.
Email-вариант
Запрос
{
"email": "user@example.com",
"externalUserRef": "user-4815",
"purpose": "registration",
"expiresIn": 180,
"deliveryMode": "polling"
}201 Created
{
"id": "ch_...",
"challengeId": "ch_...",
"projectId": "prj_...",
"type": "email",
"status": "created",
"purpose": "registration",
"maskedEmail": "us**@example.com",
"deliveryStatus": "sent",
"createdAt": "2026-07-23T12:00:00Z",
"expiresAt": "2026-07-23T12:03:00Z",
"expiresIn": 180,
"deliveryMode": "polling"
}OTP отсутствует в create response и никогда не должен попадать в frontend-логи. Пользователь получает его по email, а ваш backend передаёт код в /verify.
phone | emailstring · exactly oneТелефон либо адрес email. Одновременная передача обоих полей запрещена.externalUserRefstring · optionalВаш идентификатор пользователя, до 128 символов.purposestring · optionalНазначение проверки до 64 символов, например login.expiresInnumber · optionalTTL от 30 до 180 секунд.deliveryModepolling | webhookСпособ получения terminal result.webhookUrlstring · conditionalCallback URL до 2048 символов из разрешённых origins проекта./v1/challenges/:id/verifyПроверить email OTP
Endpoint используется только для email challenge. Передавайте код server-to-server строкой из шести цифр. После пяти неверных попыток challenge становится rejected и возвращает 429 verification_attempts_exhausted.
Запрос
{
"code": "482193"
}200 OK
{
"id": "ch_...",
"challengeId": "ch_...",
"type": "email",
"status": "confirmed",
"purpose": "registration",
"deliveryMode": "polling",
"messenger": null,
"emailVerified": true,
"deliveryStatus": "sent",
"resolvedAt": "2026-07-23T12:01:00Z",
"expiresAt": "2026-07-23T12:03:00Z"
}/v1/challenges/:id/statusПолучить статус
Безопасный endpoint для polling. Он не возвращает полный телефон, полный email или proof. Поле type определяет вариант challenge.
{
"id": "ch_...",
"challengeId": "ch_...",
"type": "phone",
"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"
}Для email дополнительно приходят emailVerified и deliveryStatus. Для rejected поле reason может быть phone_mismatch, foreign_contact, invalid_phone или verification_attempts_exhausted. Рекомендуемый интервал 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"
}EmailProof
{
"challengeId": "ch_...",
"projectId": "prj_...",
"externalUserRef": "user-4815",
"purpose": "registration",
"proof": {
"type": "email",
"method": "email_otp",
"email": "user@example.com",
"emailVerified": true
},
"resolvedAt": "2026-07-23T12:01:00Z"
}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"
}
}Webhook передаёт тот же типизированный proof, что и consume: PhoneProof или EmailProof. Примеры ниже показывают телефонный сценарий; для email отправьте поле email, после ввода пользователем OTP вызовите /verify, а в webhook проверяйте proof.type === "email" и emailVerified === true.
Каждый пример показывает полный цикл на 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` и `/verify` только с доверенного backend.
- Ветвить результат по
proof.typeи создавать сессию только приphoneVerified: trueлибоemailVerified: true. - Сопоставлять
externalUserRefс текущей операцией пользователя. - Не считать открытие messenger-ссылки или отправку email подтверждением без terminal proof.
- Использовать HTTPS, таймауты и уникальный
Idempotency-Key. - Не хранить proof, телефон и email дольше, чем требуется вашему продукту и законодательству.