ConfirmatorREST API docsВойти

Confirmator REST API

Проверка телефона и email единым API

Backend создаёт короткоживущий challenge с одним назначением: phone подтверждается через Telegram, MAX или WhatsApp, а email — шестизначным одноразовым кодом из письма. Status, consume и webhook общие для обоих вариантов.

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

Машиночитаемый контракт: 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 · Быстрый старт

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

  1. Создайте проектЗарегистрируйтесь, пополните баланс, настройте нужные каналы и выпустите API-ключ.
  2. Создайте challengeПередайте ровно одно поле: phone или email. Для телефона покажите authorization_url, для email — форму ввода OTP.
  3. Получите proofИспользуйте polling или подписанный webhook и проверяйте phoneVerified либо emailVerified в зависимости от proof.type.

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

Передайте ровно одно назначение: 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 проекта.
POST/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"
}
GET/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 нужно остановить.

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"
}

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"
  }
}
400 invalid_json, invalid_request, invalid_phone, invalid_email, invalid_verification_code, invalid_challenge_type, 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, challenge_not_ready, challenge_expired, already_consumed, channel_unavailable, webhook_not_configured413 payload_too_large429 verification_attempts_exhausted502 email_delivery_failed503 email_channel_unavailable500 internal_error

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

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

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