Все линии заняты¶
POST /api/v1/verifications/ может ответить 503 с кодом
no_dial_number_available. Это значит: на момент запроса свободного
номера для приёма звонка не нашлось, верификация не создана.
Это ожидаемая ситуация под нагрузкой, а не инцидент. Ниже — что именно произошло, что сделать в коде прямо сейчас и что изменить, чтобы упираться в отказ реже.
Что именно произошло¶
Верификация работает через аренду номера: на время жизни запроса
(ttl_seconds, по умолчанию 60 секунд) один наш номер закрепляется за
одной верификацией. Пока аренда активна, этот номер никому больше не
выдаётся — иначе мы не смогли бы однозначно сопоставить входящий
звонок с конкретной верификацией.
Номер возвращается в пул, когда происходит любое из событий:
| Событие | Когда | Сколько держался номер |
|---|---|---|
| Пришёл звонок | Пользователь позвонил, Caller-ID зафиксирован | обычно 15–40 секунд |
| Истёк TTL | Звонка не было за ttl_seconds |
весь TTL (по умолчанию 60 с) |
| Вы отменили верификацию | Вызов POST /verifications/{id}/cancel/ |
до момента отмены |
Номер может быть выдан, только если выполнены все условия:
- Сам номер активен;
- SIP-линия, к которой он привязан, активна;
- эта линия сейчас зарегистрирована у провайдера;
- номер подходит абоненту по коду страны.
Если ни один номер не проходит все четыре условия и при этом свободен —
приходит 503.
Очереди нет
Запрос не ставится в ожидание свободного номера — отказ приходит сразу, синхронно. Решение о повторе принимает ваш код.
Пул общий
Номера не закреплены за клиентами: занятость зависит не только от вашего трафика, но и от общей нагрузки на сервис. Если вам нужен гарантированный объём одновременных верификаций — напишите в поддержку, обсудим выделенную ёмкость.
Пулы российских и международных номеров считаются отдельно¶
Абоненту выдаётся номер, на который он реально может позвонить: телефону с
кодом +7 — российский номер, любому другому — международный. Смысл простой:
звонок на номер другой страны для абонента дорогой или невозможный, и
верификация просто истечёт по таймауту.
Поэтому пул не один, а два, и заканчиваются они независимо:
Номер абонента (phone) |
Из какого пула выдаётся |
|---|---|
+7… (Россия, Казахстан) |
российские номера |
| любой другой код страны | международные номера |
Отсюда неочевидное следствие: 503 может прийти на международный номер в тот
момент, когда российские номера свободны — и наоборот. Ретрай в такой
ситуации помогает ровно так же, как при любой другой занятости: свободным
должен стать номер того же пула.
Как выглядит отказ¶
HTTP/1.1 503 Service Unavailable
Retry-After: 60
Content-Type: application/json
{
"error": "no_dial_number_available",
"message": "Сервис временно занят, все линии заняты. Попробуйте позже.",
"retry_after": 60
}
| Поле | Стабильность | Назначение |
|---|---|---|
error |
стабильно | Машинно-читаемый код. Завязывайте логику на него. |
message |
может меняться | Человекочитаемый текст. Не парсите его. |
retry_after |
стабильно | Дублирует заголовок Retry-After (сейчас всегда 60). |
На что завязывать код
Ловите HTTP-код 503 и error == "no_dial_number_available".
Сравнение по message сломается при первой же правке текста.
В ответе нет verification_id — верификация с номером не была
создана, читать через GET /verifications/{id}/ нечего. Факт отказа
виден только в дашборде (см. «Что видно в дашборде» ниже).
Webhook по такому отказу не отправляется: вам уже ответили
синхронно кодом 503.
Что делать прямо сейчас¶
1. Повторить с экспоненциальным backoff и jitter¶
| Попытка | Пауза перед ней | Прошло с начала |
|---|---|---|
| 1 | — | 0 |
| 2 | 60 с ± 15 с | ~1 мин |
| 3 | 120 с ± 30 с | ~3 мин |
Больше трёх попыток смысла не имеет: к четвёртой (≈7 минут) человек уже ушёл с экрана ввода номера.
Jitter обязателен. Без случайной добавки все ваши инстансы, получившие отказ в одну секунду, синхронно ударят в API ровно через 60 секунд и получат отказ снова — уже своими же руками.
Уважайте Retry-After: повтор раньше указанного времени почти
гарантированно получит тот же 503, но при этом израсходует квоту
запросов.
2. Помните про лимит 120 запросов в минуту¶
Отклонённый запрос тоже расходует квоту вашего API-ключа
(120 req/min, счётчик общий на создание, чтение и отмену — см.
Лимиты). Ретрай в плотном цикле приведёт к
тому, что поверх 503 вы получите ещё и 429, и не сможете создать
верификацию даже когда номера освободятся.
3. Учтите, что отказы попадают в вашу статистику¶
Каждая отклонённая попытка записывается отдельной строкой со статусом
failed и причиной no_dial_number_available. Они входят в счётчик
«Всего» и снижают «Долю успешных» на дашборде. Три агрессивных ретрая
на одного пользователя — это три отказа в отчётности вместо одного.
4. Покажите пользователю понятный текст¶
Не показывайте код ошибки. Подойдёт формулировка вида:
Сейчас все линии заняты. Попробуйте через минуту.
и кнопка Повторить — вместо бесконечного спиннера. Если после трёх попыток номера так и не нашлось, предложите альтернативу (другой способ входа) или явное «попробуйте через 5 минут».
Пример: Python¶
import random
import time
import requests
API = "https://app.truenum.ru/api/v1/verifications/"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}
BACKOFF = (60, 120) # паузы перед 2-й и 3-й попытками
class LinesBusy(Exception):
"""Пул номеров исчерпан — все попытки израсходованы."""
def create_verification(payload: dict) -> dict:
for attempt, pause in enumerate((0, *BACKOFF)):
if pause:
# jitter ±25%: иначе все инстансы ретраят синхронно
time.sleep(pause * random.uniform(0.75, 1.25))
resp = requests.post(API, json=payload, headers=HEADERS, timeout=10)
if resp.status_code != 503:
resp.raise_for_status()
return resp.json()
body = resp.json()
if body.get("error") != "no_dial_number_available":
raise RuntimeError(f"Неожиданный 503: {body}")
raise LinesBusy
Пример: Node.js¶
const BACKOFF = [60, 120]; // секунды: паузы перед 2-й и 3-й попытками
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
export async function createVerification(payload) {
for (const pause of [0, ...BACKOFF]) {
if (pause) {
const jitter = 0.75 + Math.random() * 0.5; // ±25%
await sleep(pause * jitter * 1000);
}
const resp = await fetch("https://app.truenum.ru/api/v1/verifications/", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TRUENUM_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
if (resp.status !== 503) {
if (!resp.ok) throw new Error(`TrueNum ${resp.status}`);
return resp.json();
}
const body = await resp.json();
if (body.error !== "no_dial_number_available") {
throw new Error(`Неожиданный 503: ${body.message}`);
}
}
throw new Error("LINES_BUSY");
}
Как упираться в 503 реже¶
Отказ возникает, когда скорость выдачи номеров превышает скорость их возврата. Ретраи лечат симптом; ниже — что уменьшает саму частоту отказов, по убыванию эффекта.
1. Отменяйте брошенные верификации¶
Пользователь открыл экран «позвоните на номер» и ушёл — номер остаётся
занятым весь TTL, то есть по умолчанию минуту, а если вы подняли
ttl_seconds — ровно столько, сколько попросили. Отмена возвращает
номер в пул сразу же; чем длиннее ваш TTL, тем больше выигрыш.
Вызывайте POST /verifications/{id}/cancel/,
как только верификация перестала быть нужной:
- пользователь закрыл модальное окно / ушёл с экрана;
- пользователь выбрал другой способ входа;
- ваша сессия оформления истекла;
- вы решили создать новую верификацию вместо старой.
curl -X POST \
https://app.truenum.ru/api/v1/verifications/ver_01HXYZ.../cancel/ \
-H "Authorization: Bearer tn_live_<prefix>_<secret>"
Отмена идемпотентна: повторный вызов на уже завершённой верификации вернёт её как есть, не сломав успешный результат.
2. Подберите ttl_seconds под свой сценарий¶
TTL — это верхняя граница, сколько номер простоит занятым при неудачной попытке. Диапазон: 30–600 секунд, по умолчанию 60 (значения вне диапазона подтягиваются к границе).
Реальный звонок укладывается в 15–40 секунд, поэтому дефолтной минуты
хватает большинству сценариев и номер быстро возвращается в пул. Если
между созданием верификации и звонком у ваших пользователей проходит
больше времени (экран показывается не сразу, номер переписывают на
другой телефон) — поднимите ttl_seconds до 120–180.
Следите за долей expired
Слишком короткий TTL даёт verification.expired у пользователей,
которые просто набирали номер медленно. Посмотрите на распределение
времени до звонка в дашборде (колонка «Время», подпись «освободился
через») и корректируйте ttl_seconds под свой сценарий.
3. Создавайте верификацию в момент показа экрана¶
Не создавайте её «заранее» — на этапе формы регистрации или в фоне при
входе на сайт. Номер занимается в момент вызова POST, а не в момент,
когда пользователь его увидел. Каждая секунда между созданием и
показом экрана — секунда впустую занятого номера.
4. Не ретрайте POST вслепую после сетевого таймаута¶
POST /verifications/ не идемпотентен. Если запрос дошёл, а ответ
потерялся, слепой ретрай заберёт второй номер под того же
пользователя: пул расходуется вдвое быстрее, а пользователь видит два
разных номера. Подробнее — Лимиты, раздел
«Идемпотентность и ретраи».
5. Переиспользуйте активную верификацию¶
Если пользователь нажал «Отправить ещё раз» / перезагрузил страницу, а
его предыдущая верификация всё ещё pending — покажите тот же
dial_number, а не создавайте новую. Храните verification_id в своей
сессии и проверяйте статус через GET /verifications/{id}/.
Если всё-таки нужна новая — сначала отмените старую (пункт 1).
6. Ограничьте параллелизм на своей стороне¶
При массовых сценариях (миграция базы пользователей, рассылка с
приглашением подтвердить номер) не запускайте верификации пачкой.
Пропускайте их через очередь с ограничением на количество
одновременных pending-верификаций — так вы не выедаете пул целиком и
не мешаете живому трафику.
Сколько верификаций выдерживает пул¶
Простая оценка пропускной способности:
Среднее удержание считается по всем попыткам, а не только по успешным:
Пример на пуле из 10 номеров при 70% дозвонов за 30 секунд:
| Сценарий | Среднее удержание | Верификаций в час |
|---|---|---|
| TTL 60 с (дефолт), отмены нет | 0,7×30 + 0,3×60 = 39 с | ~920 |
| TTL 60 с, отмена через 20 с | 0,7×30 + 0,3×20 = 27 с | ~1330 |
| TTL 300 с, отмены нет | 0,7×30 + 0,3×300 = 111 с | ~320 |
То есть дефолтная минута уже забирает основную часть выигрыша, а отмена
брошенных верификаций добавляет сверху — и особенно заметна, если вы
подняли ttl_seconds ради медленных пользователей.
Держите запас
Трафик приходит не равномерно, а всплесками. Планируйте нагрузку примерно на 70% от расчётной пропускной способности: на 100% отказы начнутся задолго до достижения расчётного потолка.
Занятость или авария?¶
С точки зрения клиента авария SIP-линии выглядит так же, как
занятый пул: если линия потеряла регистрацию у провайдера, её номера
временно исключаются из выдачи, и запрос получает тот же 503
no_dial_number_available.
Отличить помогает характер отказов:
| Признак | Скорее занятость | Скорее авария |
|---|---|---|
| Длительность | Секунды–минуты, отказы чередуются с успехами | Все запросы подряд, минуты и дольше |
| Связь с нагрузкой | Совпадает с вашим пиком | Отказы на любом, даже низком трафике |
| Помогает ли ретрай | Да, 2–3-я попытка обычно проходит | Нет |
| Живые верификации | Есть pending, они завершаются |
Ни одна не завершается звонком |
Если картина похожа на правую колонку — не ретрайте до бесконечности, сразу пишите в поддержку.
Что видно в дашборде¶
Каждый отказ записывается как отдельная верификация со статусом «Ошибка» — чтобы просадка доли успешных не выглядела беспричинной.
На app.truenum.ru:
- Обзор → плитка «Отказы» в блоках «За 24 часа» и «За 7 дней» — сколько запросов было отклонено. Растёт вместе с «Всего» и тянет вниз «Долю успешных».
- Обзор → таблица «Последние верификации»: у отказа в колонке «Статус» под пилюлей «Ошибка» подписана причина — «Все линии заняты», а колонка «Куда звонить» пустая (номера не выдавалось).
- Верификации → фильтр по статусу «Ошибка» — полный список отказов с временем, номером, полем «Инфо» и API-ключом, по которому пришёл запрос.
- Верификации → колонка «Время», подпись «освободился через …» — сколько номер реально был занят конкретной верификацией. Это главный источник данных для расчёта выше: если у большинства строк там стоит полный TTL, вы упираетесь в брошенные верификации, а не в нехватку номеров.
Что происходит на нашей стороне¶
- Каждый отказ пишется в серверный лог на уровне
WARNINGвместе с количеством номеров, доступных на тот момент. - Устойчивый рост частоты отказов — сигнал к расширению пула номеров.
- Потеря регистрации SIP-линии отслеживается отдельно от занятости.
Отдельных действий с вашей стороны для «разблокировки» не требуется: номера возвращаются в пул автоматически.
Когда писать в поддержку¶
Стоит написать, если:
- доля
503в ваших запросах стабильно выше нескольких процентов; - отказы идут подряд дольше 5 минут независимо от нагрузки (похоже на аварию);
- вы планируете рост трафика и хотите заранее убедиться в ёмкости;
- вам нужна гарантированная ёмкость под пиковые сценарии.
Приложите к обращению:
- Период (с датой и временем, с указанием часового пояса).
prefixAPI-ключа, по которому шли запросы.- Примерное число запросов и долю отказов за период.
- Ваши
ttl_secondsи используете ли вы отмену верификаций.
Смотрите также¶
- Ошибки — полный каталог HTTP-кодов и форматов тел.
- Верификации — эндпоинты создания, чтения и отмены, жизненный цикл номера.
- Лимиты — квота 120 req/min и идемпотентность.
- Поиск проблем — остальные типовые ошибки.