Реализация
API подтверждения номера телефона: интеграция за час
Интеграция состоит из двух запросов: создать проверку и узнать её статус. Первый
возвращает выделенный номер, который нужно показать пользователю, второй — одно из трёх
состояний: PENDING, CONFIRMED или EXPIRED. Вебхуков нет, результат забирается
опросом — поэтому не нужен публичный обработчик, проверка подписи и защита от повторных
доставок. Ключ проекта используется только с сервера.
Интеграция подтверждения номера обычно занимает не больше рабочего дня. Ниже — весь путь: от первого запроса до обработки граничных случаев, с примерами, которые можно копировать.
Как устроен API
- Базовый адрес:
https://flashcall.ru/api/v1 - Формат: JSON, обычный REST.
- Авторизация: ключ проекта передаётся полем
token— в телеPOST /get-phoneили в query-строкеGET /status. Отдельных заголовков авторизации нет. - Формат номера: E.164 — знак плюс, код страны и номер без разделителей:
+79161234567. - Статусы проверки:
PENDING— ждём звонок,CONFIRMED— номер подтверждён,EXPIRED— за 60 секунд звонка не было.
Всего два запроса. Третий эндпоинт, POST /inbound-call, вызывает оператор телефонии при поступлении звонка — со стороны интеграции он не используется.
Если механизм в целом ещё не знаком, начните с разбора в статье про подтверждение номера телефона звонком: дальше по тексту предполагается, что схема «выдали номер — пользователь позвонил — забрали статус» уже понятна. Модель тарификации и критерии выбора провайдера — в материале про сервисы подтверждения номера.
Шаг 1. Создать проверку
curl -X POST "https://flashcall.ru/api/v1/get-phone" \
-H "Content-Type: application/json" \
-d '{ "token": "YOUR_API_KEY", "phone": "+79161234567" }'
Ответ:
{ "id": "c1x8f2k9b0001", "phone": "+74950000000" }
id — идентификатор проверки, по нему запрашивается статус. phone — выделенный номер, на который пользователь должен позвонить. Проверка создаётся в статусе PENDING и живёт 60 секунд.
Возможные ошибки:
| Код | Причина | Что делать |
|---|---|---|
| 400 | неверный формат номера | привести к E.164 до отправки |
| 400 | недостаточно средств на балансе | пополнить, заранее завести оповещение |
| 404 | ключ проекта не найден | проверить ключ и окружение |
| 503 | нет доступных номеров в пуле | повторить попытку, показать запасной способ |
Шаг 2. Показать номер пользователю
Во фронтенд отдаются только id и выделенный номер — ключ проекта наружу не уходит.
Что должно быть на экране: номер крупно и с разделителями, на мобильных — ссылкой tel:; таймер на 60 секунд; пояснение, что трубку брать не нужно; подсказка, что звонить надо именно с подтверждаемого номера. Разбор интерфейса — в материалах про подтверждение на сайте и в мобильном приложении.
Шаг 3. Получить результат
curl "https://flashcall.ru/api/v1/status?token=YOUR_API_KEY&id=c1x8f2k9b0001"
{
"id": "c1x8f2k9b0001",
"status": "PENDING",
"callerPhone": "+79161234567",
"cost": "0.20",
"createdAt": "2026-08-08T18:00:00.000Z"
}
Вебхуков нет — результат забирается опросом. Это заметно упрощает интеграцию: не нужен публично доступный обработчик, не нужна проверка подписи, не нужна защита от повторной доставки.
Правила опроса:
- Интервал одна-две секунды. Чаще бессмысленно, реже — заметная задержка на экране.
- Три условия остановки:
CONFIRMED,EXPIREDи истечение локального таймера. Без последнего цикл может не завершиться никогда. - Решение «номер подтверждён» принимается на сервере по ответу API, а не по тому, что прислал браузер.
- Привязка номера должна отрабатывать один раз, даже если
CONFIRMEDувиден несколькими запросами подряд.
Примеры интеграции
Node.js
const API = 'https://flashcall.ru/api/v1'
const TOKEN = process.env.FLASHCALL_TOKEN
export async function createVerification(phone) {
const res = await fetch(`${API}/get-phone`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token: TOKEN, phone }),
})
if (!res.ok) throw new Error(`get-phone: ${res.status}`)
// callTo показываем пользователю — на него нужно позвонить
const { id, phone: callTo } = await res.json()
return { id, callTo }
}
export async function getStatus(id) {
const url = `${API}/status?token=${encodeURIComponent(TOKEN)}&id=${encodeURIComponent(id)}`
const res = await fetch(url)
if (!res.ok) throw new Error(`status: ${res.status}`)
return (await res.json()).status
}
/** Ждём результат, пока проверка жива: TTL 60 секунд. */
export async function waitForCall(id, timeoutMs = 60_000) {
const deadline = Date.now() + timeoutMs
while (Date.now() < deadline) {
const status = await getStatus(id)
if (status === 'CONFIRMED') return true
if (status === 'EXPIRED') return false
await new Promise((r) => setTimeout(r, 2000))
}
return false
}
PHP
<?php
const FLASHCALL_API = 'https://flashcall.ru/api/v1';
function flashcallRequest(string $url, ?array $body = null): array {
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
if ($body !== null) {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
}
$response = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($code >= 400) {
throw new RuntimeException("FlashCall API вернул $code: $response");
}
return json_decode($response, true);
}
function createVerification(string $phone): array {
return flashcallRequest(FLASHCALL_API . '/get-phone', [
'token' => getenv('FLASHCALL_TOKEN'),
'phone' => $phone,
]);
}
function getStatus(string $id): string {
$query = http_build_query(['token' => getenv('FLASHCALL_TOKEN'), 'id' => $id]);
return flashcallRequest(FLASHCALL_API . '/status?' . $query)['status'];
}
Python
import os, time, requests
API = "https://flashcall.ru/api/v1"
TOKEN = os.environ["FLASHCALL_TOKEN"]
def create_verification(phone: str) -> tuple[str, str]:
r = requests.post(f"{API}/get-phone", json={"token": TOKEN, "phone": phone}, timeout=10)
r.raise_for_status()
data = r.json()
# второй элемент — номер, на который звонит пользователь
return data["id"], data["phone"]
def get_status(check_id: str) -> str:
r = requests.get(f"{API}/status", params={"token": TOKEN, "id": check_id}, timeout=10)
r.raise_for_status()
return r.json()["status"]
def wait_for_call(check_id: str, timeout: float = 60.0) -> bool:
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
status = get_status(check_id)
if status == "CONFIRMED":
return True
if status == "EXPIRED":
return False
time.sleep(2)
return False
Фронтенд
Оба примера ходят в ваш эндпоинт, а не в API напрямую: ключ проекта в браузере размещать нельзя.
Vue
<script setup>
import { ref, onUnmounted } from 'vue'
const callTo = ref('')
const secondsLeft = ref(0)
const done = ref(false)
let timer = null
async function start(phone) {
const res = await fetch('/api/verification/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phone }),
})
const { id, callTo: number } = await res.json()
callTo.value = number
secondsLeft.value = 60
timer = setInterval(async () => {
secondsLeft.value--
const { status } = await (await fetch(`/api/verification/${id}`)).json()
if (status === 'CONFIRMED') {
done.value = true
stop()
} else if (status === 'EXPIRED' || secondsLeft.value <= 0) {
stop()
}
}, 2000)
}
function stop() {
clearInterval(timer)
timer = null
}
onUnmounted(stop)
</script>
<template>
<a v-if="callTo && !done" :href="`tel:${callTo}`">Позвонить на {{ callTo }}</a>
<p v-if="secondsLeft > 0">Ждём звонок: {{ secondsLeft }} с</p>
</template>
React
import { useEffect, useRef, useState } from 'react'
export function CallVerification({ phone, onConfirmed }) {
const [callTo, setCallTo] = useState('')
const [secondsLeft, setSecondsLeft] = useState(0)
const idRef = useRef(null)
useEffect(() => {
let cancelled = false
async function start() {
const res = await fetch('/api/verification/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phone }),
})
const data = await res.json()
if (cancelled) return
idRef.current = data.id
setCallTo(data.callTo)
setSecondsLeft(60)
}
start()
return () => { cancelled = true }
}, [phone])
useEffect(() => {
if (!idRef.current || secondsLeft <= 0) return
const timer = setTimeout(async () => {
const { status } = await (await fetch(`/api/verification/${idRef.current}`)).json()
if (status === 'CONFIRMED') onConfirmed()
else setSecondsLeft((s) => s - 2)
}, 2000)
return () => clearTimeout(timer)
}, [secondsLeft, onConfirmed])
if (!callTo) return null
return (
<>
<a href={`tel:${callTo}`}>Позвонить на {callTo}</a>
{secondsLeft > 0 && <p>Ждём звонок: {secondsLeft} с</p>}
</>
)
}
Типичные ошибки при интеграции
- Не обрабатывают
EXPIRED. Пользователь видит вечный спиннер вместо кнопки «Позвонить ещё раз». Самая частая ошибка. - Держат ключ во фронтенде. Ключ забирают из исходников страницы и тратят ваш баланс.
- Опрашивают статус слишком часто. Десять запросов в секунду ничего не ускоряют.
- Не останавливают опрос. Без условия по таймеру цикл продолжается после ухода пользователя со страницы.
- Не проверяют баланс. При нулевом балансе проверка не создастся, и форма перестанет работать молча.
- Не ограничивают частоту на своём эндпоинте. Создание проверок стоит денег — лимит на попытки обязателен.
- Не приводят номер к E.164. Номер с восьмёркой, скобками и дефисами не пройдёт валидацию.
- Забывают предупредить, что звонить надо с подтверждаемого номера. Звонок с рабочего телефона не подтвердится, а причина будет неочевидна.
Сколько это занимает
| Шаг | Время |
|---|---|
| Регистрация, проект, ключ | 5 минут |
| Два эндпоинта на бэкенде | 1–2 часа |
Экран ожидания с таймером и tel: | 1–2 часа |
| Обработка граничных случаев и лимиты | 1–2 часа |
| Тестирование на реальных номерах | 1 час |
Итого рабочий день на разработчика. Дольше обычно идёт не техническая часть, а согласование интерфейса и сравнение воронок.
Актуальные цены — на странице тарифов. Инструкция с вашим собственным ключом открывается в кабинете на странице проекта сразу после регистрации.
Частые вопросы
- Есть ли вебхуки?
Нет. Результат забирается опросом
GET /status. Это упрощает интеграцию: не нужен публично доступный обработчик, проверка подписи и защита от повторной доставки. Если вебхуки появятся, опрос продолжит работать.- Можно ли вызывать API прямо из браузера?
Нет. Запрос содержит ключ проекта, а всё, что попало во фронтенд, доступно пользователю. Оба запроса должны идти с вашего сервера, а браузер общается с вашим собственным эндпоинтом.
- Сколько живёт проверка?
60 секунд с момента создания. После этого она переходит в
EXPIRED, и звонок, поступивший позже, к ней уже не привяжется. Создавайте новую проверку вместо продления старой.- Что вернётся, если на балансе нет денег?
Создание проверки завершится ошибкой о недостатке средств — проверка не будет создана. Уже существующие проверки при этом дорабатывают штатно.
- Нужен ли SDK?
Нет. Это обычный REST с JSON: два запроса, которые делаются штатным HTTP-клиентом любого языка. Отдельной библиотеки не требуется.