API SMSCode для разработчиков: полное руководство по интеграции

API SMSCode для разработчиков: полное руководство по интеграции

Если вам нужно автоматизировать SMS-верификацию — будь то тестирование собственного продукта, массовая регистрация аккаунтов или интеграция верификации в бизнес-процесс — ручной интерфейс быстро становится узким местом. Именно для таких задач существует API SMSCode.

Это руководство охватывает все ключевые аспекты работы с API: от получения ключа до написания полноценного клиента с обработкой ошибок и реальными примерами кода на Python и TypeScript.

TL;DR: API SMSCode — REST API с Bearer-аутентификацией. Основной цикл: выбрать продукт → создать заказ (назначение номера может ещё ожидаться) → опросить входящие SMS → отменить заказ при необходимости. Полная документация в /docs.

Базовые концепции API

Аутентификация

API использует Bearer-токен аутентификацию. Токен генерируется в настройках аккаунта SMSCode — нужна только регистрация по email.

Authorization: Bearer ваш_api_токен

Храните токен в переменных окружения — никогда не коммитьте его в репозиторий и не вставляйте в исходный код напрямую. Используйте .env файлы или менеджеры секретов (AWS Secrets Manager, HashiCorp Vault, GitHub Secrets).

Базовый URL

Все внешние запросы идут на:

https://api.smscode.gg/v1/

Формат ответов

API возвращает JSON. Успешный ответ всегда содержит success: true и объект data:

{
  "success": true,
  "data": {}
}

При ошибке:

{
  "success": false,
  "error": {
    "code": "INSUFFICIENT_BALANCE",
    "message": "Недостаточно средств на балансе"
  }
}

Жизненный цикл заказа

Понимание состояний заказа критически важно для корректной интеграции:

Статус Значение Действие
ACTIVE Ожидаем SMS Продолжаем polling
OTP_RECEIVED Сервер зафиксировал SMS Применить правило монотонной sms_revision; сам статус не является условием доставки
COMPLETED Заказ завершён после доставки Остановить polling
CANCELED Заказ отменён Остановить polling
EXPIRED Сессия истекла Остановить polling

Основные эндпоинты

Каталог номеров

Получить доступные страны и сервисы с ценами и наличием:

GET /v1/catalog/products HTTP/1.1

Фильтрация по стране и платформе:

GET /v1/catalog/products?country_id=7&platform_id=1 HTTP/1.1

Ответ содержит catalog_product_id, целочисленную цену в IDR и целочисленный остаток available (0 означает отсутствие доступных номеров). Сначала выберите продукт, затем используйте его идентификатор в единственной логической покупке.

Заказ номера

POST /v1/orders/create HTTP/1.1
Content-Type: application/json
Idempotency-Key: order-example-001

{
  "catalog_product_id": 88,
  "quantity": 1
}

Ответ:

{
  "success": true,
  "data": {
    "orders": [{
      "id": 90210,
      "status": "ACTIVE",
      "phone_number": "+919876543210",
      "otp_code": null,
      "otp_received_at": null,
      "expires_at": "2026-03-16T12:15:00Z",
      "failed_reason": null,
      "product_id": 1024,
      "catalog_product_id": 88,
      "operator_id": null,
      "operator_name": null,
      "amount": 750000,
      "can_finish": false,
      "can_resend": false,
      "can_cancel": false,
      "can_replace": false,
      "can_reactivate": false,
      "resend_available_at": null,
      "cancel_available_at": "2026-03-16T12:02:00Z",
      "replace_available_at": "2026-03-16T12:02:00Z"
    }],
    "failed_count": 0
  }
}

Поле phone_number содержит международный номер после назначения, но до этого оно опционально и может быть null. После разрешённого create используйте только непустую строку; полный durable-поток делает не более одного ограниченного чтения того же order_id и при всё ещё отсутствующем, null или пустом значении сохраняет явный результат pending_assignment. При неоднозначном сетевом или 5xx-результате повторяйте только тот же Idempotency-Key с абсолютно тем же JSON body; не меняйте продукт, ключ или тело до сверки исходной попытки.

Получение статуса и SMS

После получения непустого назначенного номера и его ввода на целевом сервисе опрашивайте статус заказа:

GET /v1/orders/{order_id} HTTP/1.1

Проекция V1OrderSummary в ожидании SMS (выбранные поля, не полный wire-ответ):

{
  "success": true,
  "data": {
    "id": 90210,
    "status": "ACTIVE",
    "otp_code": null,
    "otp_message": null,
    "sms_revision": 0,
    "can_cancel": true
  }
}

Для каждого заказа начните принятую ревизию с -1. В каждом snapshot прочитайте sms_revision и otp_message до проверки жизненного цикла: принимайте только целую (не boolean) ревизию строго больше предыдущей вместе с непустой строкой сообщения. Сначала обновите ревизию и передайте сообщение, независимо от status и otp_code, и только затем обрабатывайте терминальные состояния. Неверная или старая ревизия и пустое сообщение не меняют принятую ревизию.

Проекция V1OrderSummary после SMS (выбранные поля, не полный wire-ответ):

{
  "success": true,
  "data": {
    "id": 90210,
    "status": "OTP_RECEIVED",
    "otp_code": null,
    "otp_message": "Your Telegram code: 12345",
    "sms_revision": 1,
    "can_cancel": false
  }
}

Отмена заказа

Если номер больше не нужен, сначала прочитайте текущий snapshot. Отправляйте отмену только при can_cancel: true:

POST /v1/orders/cancel HTTP/1.1
Content-Type: application/json

{"id": 90210}

Ответ сервера и политика заказа определяют эффект для баланса; клиент не должен обещать возврат только по факту отправки запроса.

Баланс аккаунта

GET /v1/balance HTTP/1.1
{
  "success": true,
  "data": {
    "currency": "IDR",
    "balance": 1250000
  }
}

Полезно для мониторинга: добавьте алёрт, если баланс упадёт ниже определённого порога, чтобы не прерывать автоматические процессы.

Практическая реализация

Python-клиент (с httpx)

Полноценный клиент с ожиданием SMS и обработкой таймаута:

import httpx
import time
import os
import uuid

API_TOKEN = os.environ["SMSCODE_API_TOKEN"]
BASE_URL = "https://api.smscode.gg/v1"
headers = {"Authorization": f"Bearer {API_TOKEN}"}

def order_number(catalog_product_id: int) -> dict:
    body = {"catalog_product_id": catalog_product_id, "quantity": 1}
    idempotency_key = str(uuid.uuid4())
    resp = httpx.post(
        f"{BASE_URL}/orders/create",
        json=body,
        headers={**headers, "Idempotency-Key": idempotency_key},
        timeout=httpx.Timeout(30.0, connect=5.0),
    )
    resp.raise_for_status()
    order = resp.json()["data"]["orders"][0]

    # `phone_number` опционально/nullable до назначения. Разрешённый create
    # остаётся разрешённым: одно ограниченное чтение по тому же id, никогда
    # второй платный create.
    if not is_assigned(order.get("phone_number")):
        current = httpx.get(
            f"{BASE_URL}/orders/{order['id']}",
            headers=headers,
            timeout=httpx.Timeout(30.0, connect=5.0),
        ).json()
        if current.get("success"):
            order["phone_number"] = current["data"].get("phone_number")
    return order

def is_assigned(phone) -> bool:
    return isinstance(phone, str) and bool(phone.strip())

def wait_for_sms(order_id: int, timeout: int = 120) -> str | None:
    deadline = time.time() + timeout
    last_seen_revision = -1
    while time.time() < deadline:
        resp = httpx.get(
            f"{BASE_URL}/orders/{order_id}",
            headers=headers,
            timeout=httpx.Timeout(30.0, connect=5.0),
        )
        data = resp.json()["data"]
        revision = data.get("sms_revision")
        message = data.get("otp_message")
        if (
            type(revision) is int
            and revision > last_seen_revision
            and isinstance(message, str)
            and message.strip()
        ):
            last_seen_revision = revision
            return message

        lifecycle_status = data["status"]
        if lifecycle_status in ("COMPLETED", "CANCELED", "EXPIRED"):
            return None

        # ACTIVE и OTP_RECEIVED продолжают polling без нового валидного сообщения.
        time.sleep(3)  # Локальная настройка примера, не квота API
    return None

def cancel_order(order_id: int) -> dict | None:
    current = httpx.get(
        f"{BASE_URL}/orders/{order_id}",
        headers=headers,
        timeout=httpx.Timeout(30.0, connect=5.0),
    ).json()["data"]
    if not current["can_cancel"]:
        return None
    response = httpx.post(
        f"{BASE_URL}/orders/cancel",
        json={"id": order_id},
        headers=headers,
        timeout=httpx.Timeout(30.0, connect=5.0),
    )
    response.raise_for_status()
    payload = response.json()
    if not payload.get("success"):
        raise RuntimeError(payload.get("error", {}).get("message", "Отмена отклонена"))
    result = payload["data"]
    if result["status"] != "CANCELED":
        raise RuntimeError("Сервер не подтвердил отмену")
    return result

def get_balance() -> int:
    resp = httpx.get(
        f"{BASE_URL}/balance",
        headers=headers,
        timeout=httpx.Timeout(30.0, connect=5.0),
    )
    return resp.json()["data"]["balance"]

# Использование
def verify_telegram(catalog_product_id: int = 88) -> tuple[str, str] | None:
    """Возвращает (phone, otp_message) или None при неудаче."""
    order = order_number(catalog_product_id)
    order_id = order["id"]
    phone = order.get("phone_number")
    if not is_assigned(phone):
        # pending_assignment: заказ остаётся разрешённым и оплаченным. Не вводите
        # номер нигде и не создавайте второй заказ.
        print(f"pending_assignment: заказу {order_id} ещё не назначен номер")
        return None

    print(f"Используйте номер: {phone}")
    # Здесь ваша логика: ввести номер на целевом сервисе

    otp_message = wait_for_sms(order_id)
    if otp_message:
        print(f"SMS получено: {otp_message}")
        return phone, otp_message
    else:
        cancellation = cancel_order(order_id)
        if cancellation:
            print(
                f"Отмена подтверждена; возврат Rp {cancellation['refund_amount']}; "
                f"новый баланс Rp {cancellation['new_balance']}"
            )
        else:
            print("SMS не получено; отмена сейчас недоступна, сохраните заказ для сверки")
        return None

Node.js / TypeScript

import { Agent, fetch } from "undici";

const API_TOKEN = process.env.SMSCODE_API_TOKEN!;
const BASE_URL = "https://api.smscode.gg/v1";
const orderDispatcher = new Agent({ connectTimeout: 5_000 });
const orderTransport = () => ({
  dispatcher: orderDispatcher,
  signal: AbortSignal.timeout(30_000)
});

const headers = {
  Authorization: `Bearer ${API_TOKEN}`,
  "Content-Type": "application/json",
};

type OrderState = "ACTIVE" | "OTP_RECEIVED" | "COMPLETED" | "CANCELED" | "EXPIRED";

interface CreatedOrderView {
  id: number;
  phone_number: string | null;
  expires_at: string | null;
  status: OrderState;
}

interface OrderSnapshotView {
  id: number;
  status: OrderState;
  otp_code: string | null;
  otp_message: string | null;
  sms_revision: number;
  can_cancel: boolean;
}

const lastSeenRevisionByOrder = new Map<number, number>();

async function orderNumber(catalogProductId: number): Promise<CreatedOrderView> {
  const body = { catalog_product_id: catalogProductId, quantity: 1 };
  const idempotencyKey = crypto.randomUUID();
  const res = await fetch(`${BASE_URL}/orders/create`, {
    method: "POST",
    headers: { ...headers, "Idempotency-Key": idempotencyKey },
    body: JSON.stringify(body),
    ...orderTransport(),
  });
  const json = await res.json() as any;
  if (!json.success) throw new Error(json.error.message);
  return json.data.orders[0];
}

async function waitForSms(
  orderId: number,
  timeoutMs = 120_000
): Promise<string | null> {
  const deadline = Date.now() + timeoutMs;
  let lastSeenRevision = -1;
  const storedRevision = lastSeenRevisionByOrder.get(orderId);
  if (storedRevision !== undefined) lastSeenRevision = storedRevision;

  while (Date.now() < deadline) {
    const res = await fetch(`${BASE_URL}/orders/${orderId}`, {
      headers,
      ...orderTransport()
    });
    const json = await res.json() as any;
    const order: OrderSnapshotView = json.data;

    const smsRevision = order.sms_revision;
    const otpMessage = order.otp_message;
    if (
      Number.isInteger(smsRevision) &&
      smsRevision > lastSeenRevision &&
      typeof otpMessage === "string" &&
      otpMessage.trim().length > 0
    ) {
      lastSeenRevision = smsRevision;
      lastSeenRevisionByOrder.set(orderId, lastSeenRevision);
      return otpMessage;
    }

    const lifecycleStatus = order.status;
    if (["COMPLETED", "CANCELED", "EXPIRED"].includes(lifecycleStatus)) return null;
    await new Promise((r) => setTimeout(r, 3000));
  }
  return null;
}

interface CancelResult {
  order_id: number;
  status: "CANCELED";
  refund_amount: number;
  new_balance: number;
}

async function cancelOrder(orderId: number): Promise<CancelResult | null> {
  const response = await fetch(`${BASE_URL}/orders/${orderId}`, {
    headers,
    ...orderTransport()
  });
  const current = (await response.json() as any).data as OrderSnapshotView;
  if (!current.can_cancel) return null;
  const cancelResponse = await fetch(`${BASE_URL}/orders/cancel`, {
    method: "POST",
    headers,
    body: JSON.stringify({ id: orderId }),
    ...orderTransport(),
  });
  const payload = await cancelResponse.json() as any;
  if (!cancelResponse.ok || !payload.success || payload.data.status !== "CANCELED") {
    throw new Error(payload.error?.message || "Сервер не подтвердил отмену");
  }
  return payload.data as CancelResult;
}

Rust-клиент

Для Rust используйте тот же HTTP-контракт из примеров выше и проверяйте ответы по опубликованной схеме OpenAPI.

Обработка ошибок

Хорошая интеграция предполагает обработку всех возможных ситуаций:

Код ошибки Ситуация Рекомендуемое действие
INSUFFICIENT_BALANCE Нет средств Пополнить баланс, отправить алёрт
NO_OFFER_AVAILABLE Нет продукта для выбранной политики Завершить эту покупку; другой продукт — новое явное действие
NOT_FOUND Заказ не существует Проверить order_id
CONFLICT Сессия истекла Создать новый заказ
RATE_LIMIT_EXCEEDED Слишком много запросов Добавить задержку, exponential backoff
VALIDATION_ERROR Неверное имя сервиса Проверить каталог /v1/catalog/products

Для polling GET используйте ограниченный retry с экспоненциальной задержкой. Для платного create только NO_OFFER_AVAILABLE, VALIDATION_ERROR, PROVIDER_ERROR и IDEMPOTENCY_KEY_REUSED считаются окончательными ошибками этой интеграции. INSUFFICIENT_BALANCE завершает поток после одного POST. При REQUEST_IN_PROGRESS ограниченно повторяйте тот же key и body. Неизвестный код, повреждённый JSON или неоднозначный ответ требуют сверки без второй покупки.

Лучшие практики

Интервал polling. Задайте в приложении ограниченный интервал и общий timeout. При 429 используйте положительный Retry-After; если заголовок отсутствует или некорректен, применяйте ограниченную резервную задержку. Локальный интервал не является гарантированной квотой API.

Всегда устанавливайте timeout. После локального timeout перечитайте актуальный snapshot заказа и отменяйте его только при can_cancel: true. Не считайте локальный timeout доказательством отмены или возврата.

Логируйте order_id. Если что-то пошло не так, order_id поможет службе поддержки найти проблему. Сохраняйте его в структурированных логах вместе с timestamp и именем сервиса.

Отменяйте только по разрешению сервера. После ошибки или таймаута перечитайте заказ и вызывайте POST /v1/orders/cancel только при can_cancel: true. Не обещайте возврат до ответа сервера.

Выбирайте страну динамически. Перед заказом проверяйте наличие номеров через /v1/catalog/products. Выбирайте страну с ненулевым количеством номеров и приемлемой ценой.

Мониторьте баланс. Добавьте алёрт при падении баланса ниже порогового значения — автоматические процессы не должны останавливаться из-за нулевого баланса.

Типичные сценарии автоматизации

Автотест регистрации

В CI/CD пайплайне можно автоматически создавать тестовых пользователей для проверки флоу регистрации вашего продукта. Каждый запуск — новый виртуальный номер, новый аккаунт, чистые данные.

Пример для pytest:

@pytest.fixture
def fresh_account():
    order = order_number(88)
    phone = order.get("phone_number")
    if not is_assigned(phone):
        # pending_assignment: заказ остаётся разрешённым и оплаченным. В этой
        # ветке нет ни второго чтения, ни polling, ни отмены, ни нового платного
        # create — отмена изменила бы оплаченный заказ. Сверку по `order_id`
        # выполняет оператор.
        pytest.skip(f"pending_assignment: заказу {order['id']} ещё не назначен номер")
    yield phone
    cancel_order(order["id"])

Мониторинг доставки SMS

Используйте API для проверки, что ваша система SMS-уведомлений работает корректно. Заказывайте номер, инициируйте отправку SMS из вашей системы, проверяйте получение. Это замечательно вписывается в synthetic monitoring через Grafana или Datadog.

Параллельная обработка

Для создания нескольких аккаунтов одновременно — заказывайте несколько номеров параллельно. В Python используйте asyncio.gather(), в Node.js — Promise.all():

import asyncio
import httpx
import uuid

async def order_multiple(catalog_product_ids: list[int]) -> list[dict]:
    requests_to_send = [
        {
            "body": {"catalog_product_id": catalog_id, "quantity": 1},
            "key": str(uuid.uuid4()),
        }
        for catalog_id in catalog_product_ids
    ]
    async with httpx.AsyncClient(
        headers=headers,
        timeout=httpx.Timeout(30.0, connect=5.0),
    ) as client:
        tasks = [
            client.post(
                f"{BASE_URL}/orders/create",
                json=request["body"],
                headers={"Idempotency-Key": request["key"]},
            )
            for request in requests_to_send
        ]
        responses = await asyncio.gather(*tasks)
        return [r.json()["data"]["orders"][0] for r in responses]

Интеграция в очереди задач

Для масштабируемых пайплайнов используйте очереди задач (Celery, Sidekiq, BullMQ). Задача получает номер, создаёт заказ в SMSCode, ждёт SMS и обновляет статус в базе данных.

Безопасность при интеграции

Ротация токенов. Периодически обновляйте API-токен — особенно после смены сотрудников с доступом к секретам. Новый токен генерируется в настройках аккаунта.

Минимум привилегий. Если вы используете отдельные аккаунты для разных проектов — это правильная практика изоляции. Один проект — один токен — один баланс.

Не логируйте токен. Убедитесь, что библиотеки HTTP не логируют заголовки Authorization по умолчанию. Настройте whitelist логируемых заголовков.

Проверяйте ответы. Всегда проверяйте поле success в ответе перед обращением к data. Не полагайтесь только на HTTP status code.

Больше деталей и примеров — в полной документации API. Для бизнес-сценариев читайте статью о виртуальных номерах для бизнеса.

Интеграция в CI/CD пайплайн

Одно из ключевых применений API SMSCode — автоматическое тестирование флоу верификации в рамках непрерывной интеграции.

GitHub Actions пример:

- name: Run verification flow tests
  env:
    SMSCODE_API_TOKEN: ${{ secrets.SMSCODE_API_TOKEN }}
  run: pytest tests/test_verification.py -v

Тест создаёт виртуальный номер, запускает верификацию через ваш сервис и проверяет корректность доставки OTP. При каждом деплое вы можете быть уверены, что SMS-верификация работает.

Стоимость тестов. Перед каждым прогоном выбирайте активный продукт по текущей целочисленной цене IDR из каталога и ограничивайте число платных заказов. Не закладывайте в CI фиксированную цену или автоматический возврат.

Мониторинг и алертинг

Для продакшен-систем добавьте проактивный мониторинг:

Алёрт на низкий баланс:

balance = get_balance()
if balance < 100000:  # меньше Rp 100 000
    print("SMSCode balance low", balance)

Синтетический тест верификации. Раз в час запускайте тест: заказ номера → отправка тестового SMS → проверка получения. Если цепочка ломается — алёрт немедленно.

Логирование метрик. Отслеживайте процент успешных верификаций по странам. Если индийские номера вдруг начали чаще давать ошибку NO_OFFER_AVAILABLE — пора добавить Индонезию как запасной вариант.

SDK и библиотеки сообщества

Примеры в этой статье обращаются к REST API напрямую. Перед выбором сторонней обёртки или SDK сверяйтесь с актуальной официальной документацией и не полагайтесь на обещания из roadmap.

Для быстрого старта рекомендуем:

  • Python: скопируйте пример выше, добавьте обработку INSUFFICIENT_BALANCE и NO_OFFER_AVAILABLE
  • TypeScript/Node.js: типизированный интерфейс из примера легко расширить под нужды проекта
  • Rust: используйте тот же HTTP-контракт и проверяйте модели по опубликованной OpenAPI-схеме

Если вы написали клиент на другом языке — делитесь в нашем чате поддержки. Полная документация API доступна на сайте.


FAQ

Как получить API-ключ SMSCode?

Зарегистрируйтесь на SMSCode, перейдите в настройки аккаунта и сгенерируйте API-токен. Токен отображается только один раз при создании — сохраните его в безопасном месте немедленно. Если потеряли — сгенерируйте новый в тех же настройках.

Есть ли лимиты на количество запросов?

Да. При 429 используйте положительный Retry-After; если заголовок отсутствует или некорректен, применяйте ограниченную резервную задержку. Не фиксируйте общую числовую квоту в клиенте.

Можно ли использовать API для тестирования на локальном окружении?

Да. API работает через HTTPS из любого места с доступом в интернет. Для изоляции тестовой среды создайте отдельный SMSCode-аккаунт с небольшим балансом специально для тестов.

Как получать уведомления о входящих SMS без polling?

Webhook поддерживается. Проверяйте X-Webhook-Signature в формате sha256={hex} по сырым байтам тела, используя сравнение с постоянным временем, до разбора JSON. Обработайте и надёжно сохраните или поставьте событие в очередь до ответа 2xx; polling используйте для сверки.

Что делать при ошибке NO_OFFER_AVAILABLE?

Это значит, что продукт не соответствует выбранной политике. Завершите эту попытку. Другой catalog_product_id — отдельное явное действие, допустимое только после того, как исходный create однозначно завершён или сверен.

Как обрабатывать несколько SMS на один номер?

Храните принятую sms_revision отдельно для каждого заказа, начиная с -1. Обрабатывайте непустой otp_message только при строго большей целой (не boolean) ревизии и обновляйте baseline лишь после принятия; status и otp_code не являются условиями доставки.

Поддерживается ли получение голосовых звонков с кодом?

Нет, SMSCode API работает только с текстовыми SMS. Если целевой сервис предлагает только голосовую верификацию — это не поддерживается. Большинство сервисов предлагают SMS как основной вариант, а голосовой звонок — как резервный.

Хотите попробовать SMSCode?

Создайте аккаунт и получите первый виртуальный номер менее чем за две минуты.

Начать →