Перейти к содержанию

Все линии заняты

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/ до момента отмены

Номер может быть выдан, только если выполнены все условия:

  1. Сам номер активен;
  2. SIP-линия, к которой он привязан, активна;
  3. эта линия сейчас зарегистрирована у провайдера;
  4. номер подходит абоненту по коду страны.

Если ни один номер не проходит все четыре условия и при этом свободен — приходит 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 до 120180.

Следите за долей expired

Слишком короткий TTL даёт verification.expired у пользователей, которые просто набирали номер медленно. Посмотрите на распределение времени до звонка в дашборде (колонка «Время», подпись «освободился через») и корректируйте ttl_seconds под свой сценарий.

3. Создавайте верификацию в момент показа экрана

Не создавайте её «заранее» — на этапе формы регистрации или в фоне при входе на сайт. Номер занимается в момент вызова POST, а не в момент, когда пользователь его увидел. Каждая секунда между созданием и показом экрана — секунда впустую занятого номера.

4. Не ретрайте POST вслепую после сетевого таймаута

POST /verifications/ не идемпотентен. Если запрос дошёл, а ответ потерялся, слепой ретрай заберёт второй номер под того же пользователя: пул расходуется вдвое быстрее, а пользователь видит два разных номера. Подробнее — Лимиты, раздел «Идемпотентность и ретраи».

5. Переиспользуйте активную верификацию

Если пользователь нажал «Отправить ещё раз» / перезагрузил страницу, а его предыдущая верификация всё ещё pending — покажите тот же dial_number, а не создавайте новую. Храните verification_id в своей сессии и проверяйте статус через GET /verifications/{id}/.

Если всё-таки нужна новая — сначала отмените старую (пункт 1).

6. Ограничьте параллелизм на своей стороне

При массовых сценариях (миграция базы пользователей, рассылка с приглашением подтвердить номер) не запускайте верификации пачкой. Пропускайте их через очередь с ограничением на количество одновременных pending-верификаций — так вы не выедаете пул целиком и не мешаете живому трафику.


Сколько верификаций выдерживает пул

Простая оценка пропускной способности:

верификаций в час ≈ (число доступных номеров) × 3600 / (среднее удержание номера, сек)

Среднее удержание считается по всем попыткам, а не только по успешным:

среднее удержание = доля дозвонившихся × время до звонка
                  + доля ушедших × (TTL или время до отмены)

Пример на пуле из 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 минут независимо от нагрузки (похоже на аварию);
  • вы планируете рост трафика и хотите заранее убедиться в ёмкости;
  • вам нужна гарантированная ёмкость под пиковые сценарии.

Приложите к обращению:

  1. Период (с датой и временем, с указанием часового пояса).
  2. prefix API-ключа, по которому шли запросы.
  3. Примерное число запросов и долю отказов за период.
  4. Ваши ttl_seconds и используете ли вы отмену верификаций.

Смотрите также

  • Ошибки — полный каталог HTTP-кодов и форматов тел.
  • Верификации — эндпоинты создания, чтения и отмены, жизненный цикл номера.
  • Лимиты — квота 120 req/min и идемпотентность.
  • Поиск проблем — остальные типовые ошибки.