Если вам нужно автоматизировать 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 как основной вариант, а голосовой звонок — как резервный.