Документация

Руководствопо API

REST API DevSMS: отправка SMS (стандартные, универсальный OTP, с телефона), баланс, история, статус, устройства, callback и подпись webhook, международные цены. Примеры на cURL, PHP, Python и Node.

Введение и аутентификация

DevSMS API — REST-интерфейс, возвращающий JSON. Все запросы отправляются по HTTPS с заголовком Authorization. Основные адреса — /api/sms/send, /api/balance, /api/sms/history, /api/sms/status, /api/devices. Версионированный API v1 (последний раздел) дополнительно даёт защиту от повторов, коды ошибок и права ключей.

Базовый URLhttps://devsms.uz/api
AuthorizationAuthorization: Bearer YOUR_API_TOKEN

Важно

  • Токен: в каждом запросе Authorization: Bearer YOUR_API_TOKEN. Токен берётся в кабинете на странице API-ключи (/app/api-keys).
  • Заблокированный аккаунт на всех endpoint получает 403 и текст Sizning hisobingiz bloklangan; отсутствующий или неверный токен — 401.
  • Личный ключ сверх лимита тарифа — 403, error_code: token_locked_by_plan: работают самые старые ключи в пределах тарифа. Повысьте тариф или удалите старый ключ — следующий заработает сразу. Основной (старый) API-ключ никогда не блокируется.
  • Лимиты: /api/sms/send — 300 запросов в минуту на пользователя и 500 запросов в минуту с одного IP; при превышении 429. На остальных endpoint лимита нет.
  • Формат ответа: при успехе {"success":true,"message":"…","data":{…}}, при ошибке {"success":false,"error":"…"} (в некоторых ошибках есть дополнительные ключи: charged, sms_id, reject_streak).
  • Номер телефона в любом виде: 998901234567, +998 (90) 123-45-67, 901234567. Международные номера принимаются, только если для аккаунта включены международные SMS.
  • Время в формате Y-m-d H:i:s, ташкентское (UTC+5).
Получить токен

Отправка SMS

POST /api/sms/send

POST /api/sms/send — отправляет SMS по шаблону (type=sms; так же, если type не указан). Тело — JSON (Content-Type: application/json) или форма. SMS отправляется только если текст совпадает с утверждённым шаблоном; деньги списываются до отправки и возвращаются, если провайдер однозначно отклонил SMS.

Параметры

  • phonestringОбязательный: Да
    Номер телефона (998901234567 или международный: 12025551234).
  • messagestringОбязательный: Да
    Текст SMS (до 1000 байт).
  • fromstringОбязательный: Нет
    Имя отправителя (до 11 байт). По умолчанию 4546.
  • typestringОбязательный: Нет
    sms (по умолчанию), universal_otp или device.
  • callback_urlstringОбязательный: Нет
    Адрес http(s):// (до 500 символов), на который отправляется POST при смене статуса. Требования — в разделе «Callback и webhook».
  • template_idintegerОбязательный: Нет
    ID шаблона — только помогает определить ценовую категорию, отправку не отклоняет.

Пример запроса

curl -X POST "https://devsms.uz/api/sms/send" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "998901234567",
    "message": "Salom!",
    "callback_url": "https://your-site.uz/sms-webhook"
}'

Ответы

200Успешно (номер Узбекистана)
{
    "success": true,
    "message": "SMS muvaffaqiyatli yuborildi",
    "data": {
        "sms_id": 123,
        "request_id": "dfc46fc5-0d38-4d07-aa74-95c58af3694f",
        "status": "sent",
        "parts_count": 1,
        "total_cost": 200,
        "balance": 4800,
        "type": "sms",
        "callback_url": "https://your-site.uz/sms-webhook"
    }
}
200Успешно (международный номер — добавляются country, country_code)
{
    "success": true,
    "message": "SMS muvaffaqiyatli yuborildi",
    "data": {
        "sms_id": 124,
        "request_id": "a5b1c3d2-7e4f-4a9b-8c6d-1e2f3a4b5c6d",
        "status": "sent",
        "parts_count": 1,
        "total_cost": 625,
        "balance": 4175,
        "type": "sms",
        "country": "Russia",
        "country_code": "ru"
    }
}
400Недостаточно средств
{
    "success": false,
    "error": "Balansda yetarli mablag' yo'q"
}
400Результат неизвестен — деньги списаны
{
    "success": false,
    "error": "SMS yuborish natijasi noma'lum (provayder javob bermadi). Balansdan yechildi; qayta yuborishdan oldin holatni tekshiring.",
    "charged": true,
    "sms_id": 125
}
403Отклонено модерацией (деньги не списываются)
{
    "success": false,
    "error": "Xabar bloklandi: Nomaqbul so'z. SMS yuborilmadi, to'lov yechilmadi.",
    "charged": false,
    "reject_streak": 1,
    "remaining_attempts": 19
}

Ошибки

HTTPЗначение
400Неверный формат телефона / текст SMS не указан / callback_url не соответствует требованиям / недостаточно средств / международные SMS не включены / провайдер отклонил / результат неизвестен.
401Токен не найден или неверен.
403Аккаунт заблокирован или текст не прошёл модерацию (charged: false).
429Превышен лимит запросов.
500Внутренняя ошибка сервера ({"success":false,"error":"Ichki server xatosi"}).

Важно

  • total_cost — число частей × цена части; balance — баланс на начало запроса минус total_cost (при параллельных запросах может отличаться от реального; точный баланс — /api/balance).
  • Результат неизвестен (важно): если провайдер не ответил (5xx, таймаут), ответ приходит с 400, в нём "charged": true и sms_id. Деньги не возвращаются — SMS могла быть доставлена. Перед повторной отправкой проверьте статус через /api/sms/status?sms_id=…, иначе клиент может получить SMS дважды.
  • Если провайдер однозначно отклонил SMS (например, шаблон не найден), деньги автоматически возвращаются, причина указывается в error.
  • Если type не указан, в ответе data.type — sms. Другое значение (например, foo) отправляется как стандартное SMS и возвращается как есть в data.type. type=simple больше не поддерживается — используйте sms.

Универсальный OTP

POST /api/sms/send

POST /api/sms/send с type=universal_otp — отправляет код подтверждения по одному из 4 готовых шаблонов, заранее утверждённых оператором. Вы передаёте только название компании и код; название проходит автоматическую модерацию.

Параметры

  • phonestringОбязательный: Да
    Номер телефона.
  • typestringОбязательный: Да
    universal_otp
  • template_typeintegerОбязательный: Да
    1 — подтверждение операции, 2 — восстановление пароля, 3 — регистрация, 4 — вход в систему.
  • service_namestringОбязательный: Да
    Название компании/сервиса: 2–50 символов, только буквы, цифры, пробел, точка и дефис.
  • otp_codestringОбязательный: Да
    Код подтверждения: 4–8 цифр.
  • callback_urlstringОбязательный: Нет
    Адрес callback статуса.

Пример запроса

curl -X POST "https://devsms.uz/api/sms/send" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "998901234567",
    "type": "universal_otp",
    "template_type": 1,
    "service_name": "TechShop",
    "otp_code": "5678"
}'

Ответы

200Успешно
{
    "success": true,
    "message": "SMS muvaffaqiyatli yuborildi",
    "data": {
        "sms_id": 126,
        "request_id": "3f1c2b7a-5d6e-4f80-9a1b-2c3d4e5f6a7b",
        "status": "sent",
        "parts_count": 1,
        "total_cost": 200,
        "balance": 4600,
        "type": "universal_otp"
    }
}
403Название компании отклонено (деньги не списываются)
{
    "success": false,
    "error": "Korxona nomi nomaqbul deb topildi: Nomaqbul so'z. SMS yuborilmadi, to'lov yechilmadi.",
    "charged": false,
    "reject_streak": 3,
    "remaining_attempts": 17
}
403Блокировка на 24 часа
{
    "success": false,
    "error": "Korxona nomi ketma-ket 20 marta rad etilgani uchun universal OTP xizmati vaqtincha to'xtatilgan. Qayta ochilish vaqti: 2026-09-29 19:18:35. Admin bilan bog'laning.",
    "charged": false,
    "blocked_until": "2026-09-29 19:18:35"
}

Ошибки

HTTPЗначение
400Неверный тип шаблона, формат названия или кода, недостаточно средств.
403Название компании отклонено или сервис отключён на 24 часа.

Готовые шаблоны

template_typeТекст
1MyService tizimi: {service_name} xizmatida amaliyotni tasdiqlash kodi: {otp_code}
2MyService tizimi: {service_name} xizmatida parolni tiklash uchun tasdiqlash kodi: {otp_code}
3MyService tizimi: {service_name} xizmatiga ro'yxatdan o'tish uchun tasdiqlash kodi: {otp_code}
4MyService tizimi: {service_name} xizmatiga kirish uchun tasdiqlash kodi: {otp_code}

Важно

  • Если название компании неприемлемо, SMS не отправляется и деньги не списываются — в ответе charged: false. request_id при этом не создаётся, callback тоже не приходит.
  • Защита от злоупотреблений: после 20 отклонений подряд универсальный OTP отключается на 24 часа (403, blocked_until). Как только модерация одобрит название, счётчик обнуляется. В каждом ответе-отказе есть reject_streak и remaining_attempts.
  • Обычный текст message, похожий на один из этих шаблонов, проходит ту же модерацию и подчиняется тому же правилу блокировки.

Голосовой OTP (звонок)

POST /api/v1/voice/otp

POST /api/v1/voice/otp — на номер совершается автоматический звонок, и код подтверждения произносится голосом на узбекском языке ("…kodingiz: To'rt, Sakkiz, Ikki, Olti"). Только номера Узбекистана. Ключу нужно право voice:send; статус — GET /api/v1/voice/{id} (voice:read).

Параметры

  • phonestringОбязательный: Да
    Номер телефона: 998XXXXXXXXX.
  • template_typeintegerОбязательный: Нет
    Готовый шаблон: 1 — подтверждение операции, 2 — восстановление пароля, 3 — регистрация, 4 — вход в систему. Обязателен, если не передан text.
  • service_namestringОбязательный: Нет
    Название компании/сервиса (обязательно с template_type): 2–50 символов, только буквы, цифры, пробел, точка и дефис.
  • textstringОбязательный: Нет
    Свой текст (1–300 символов) — только для аккаунтов с разрешением администратора; не передаётся вместе с template_type и service_name. Код произносится после текста.
  • codestringОбязательный: Нет
    Код: 4–8 цифр. Если не передан, создаётся 6-значный код и возвращается в ответе (data.code).
  • repeatbooleanОбязательный: Нет
    Повторить текст и код второй раз ("Takrorlayman, …"). По умолчанию: true.
  • Idempotency-Keystring (UUID)Обязательный: Нет
    Заголовок: чтобы при повторной попытке не было второго звонка и повторного списания. Правила — в разделе «Защита от повторов».

Пример запроса

curl -X POST "https://devsms.uz/api/v1/voice/otp" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Idempotency-Key: 9a7c3e10-52b4-4d8f-a1e6-0c4b7d2f9e31" \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "998901234567",
    "template_type": 1,
    "service_name": "TechShop",
    "code": "4826",
    "repeat": true
}'

Ответы

200Успешно: звонок поставлен в очередь
{
    "success": true,
    "message": "Ovozli OTP yuborildi",
    "data": {
        "id": 4821,
        "kind": "otp",
        "phone": "998901234567",
        "status": "queued",
        "result": null,
        "outcome": "sent",
        "template_type": 1,
        "service_name": "TechShop",
        "code": "4826",
        "text": "Diqqat! TechShop xizmatida amaliyotni tasdiqlash kodingiz:",
        "spoken_text": "Diqqat! TechShop xizmatida amaliyotni tasdiqlash kodingiz: To'rt, Sakkiz, Ikki, Olti. Takrorlayman, Diqqat! TechShop xizmatida amaliyotni tasdiqlash kodingiz: To'rt, Sakkiz, Ikki, Olti.",
        "repeat": true,
        "price": "90.00",
        "currency": "UZS",
        "provider_call_id": "293",
        "duration_seconds": 0,
        "billable_seconds": 0,
        "dtmf_digits": null,
        "error": null,
        "sent_via": "api",
        "created_at": "2026-10-08T14:30:05+05:00",
        "provider_called_at": "2026-10-08T14:30:05+05:00",
        "answered_at": null,
        "ended_at": null,
        "synced_at": null
    }
}
503Голосовой сервис занят (Retry-After), деньги не списаны
{
    "success": false,
    "message": "Ovozli xizmat band — bir necha soniyadan keyin qayta urinib ko'ring",
    "error_code": "voice_busy",
    "meta": {
        "charged": false,
        "call_id": 4822
    }
}
502Результат неизвестен — повторите с тем же Idempotency-Key или проверьте статус
{
    "success": false,
    "message": "Qo'ng'iroq natijasi noma'lum (provayder javob bermadi). Balansdan yechildi; qayta yuborishdan oldin holatni tekshiring.",
    "error_code": "provider_outcome_unknown",
    "meta": {
        "call_id": 4823,
        "charged": true
    }
}
402Недостаточно средств
{
    "success": false,
    "message": "Balansda yetarli mablag' yo'q",
    "error_code": "insufficient_balance"
}

Ошибки

HTTPЗначение
402insufficient_balance — недостаточно средств.
403voice_free_text_disabled — нет разрешения на свой текст; moderation_rejected, otp_suspended, account_blocked.
422validation_failed, unsupported_country (только Узбекистан), provider_rejected (деньги возвращены), idempotency_key_reused.
502provider_outcome_unknown — деньги списаны и не возвращены; проверьте статус по meta.call_id.
503voice_disabled — сервис временно выключен; voice_busy — повторите через Retry-After секунд.

Готовые шаблоны (произносимый текст, на узбекском)

template_typeТекст
1Diqqat! {service_name} xizmatida amaliyotni tasdiqlash kodingiz: {code}
2Diqqat! {service_name} xizmatida parolni tiklash kodingiz: {code}
3Diqqat! {service_name} xizmatiga ro'yxatdan o'tish kodingiz: {code}
4Diqqat! {service_name} xizmatiga kirish kodingiz: {code}

Статусы звонка (status)

statusЗначение
queuedВ очереди: провайдер принял
calling, ringingНабор номера, звонит
answered, completedОтвечен, завершён (код произнесён)
no_answer, busyНет ответа, занято — стоимость списана
failed, rejected, cancelledНе удалось, отклонён, отменён — стоимость списана
not_sentПровайдер не принял запрос — деньги возвращены
unknownРезультат неизвестен — деньги списаны, автоматически не возвращаются
blockedОтклонено модерацией — деньги не списаны

Важно

  • Цена за каждый звонок (по умолчанию 90 сум) — списывается, даже если абонент не ответил или номер занят. Деньги возвращаются только если провайдер не принял запрос (not_sent).
  • data.code — произнесённый код (если вы его не передали — сгенерированный). Сравнивайте его с кодом, введённым пользователем.
  • Статус обновляется от нескольких секунд до минуты: GET /api/v1/voice/{id}; список — GET /api/v1/voice (status, from, to, cursor, limit).
  • Idempotency-Key — те же правила, что и для SMS: повтор с тем же ключом не делает новый звонок и не списывает повторно (Idempotent-Replayed: true); ключ с другим телефоном, текстом или кодом — 422 idempotency_key_reused; первый запрос ещё не завершён — 409, Retry-After: 2, meta.call_id.
  • Если модерация включена, название компании и текст проверяются: при отклонении деньги не списываются (403 moderation_rejected, remaining_attempts); 20 отказов подряд — блокировка на 24 часа (otp_suspended).

SMS с телефона

POST /api/sms/send

POST /api/sms/send с type=device — SMS отправляется с вашего телефона (шаблон не нужен). Сначала установите приложение DevSMS Sender и подключите телефон в кабинете. SMS ставится в очередь и уходит, когда телефон онлайн; плата за сервис списывается за каждую SMS и возвращается, если SMS не отправлена.

Параметры

  • phonestringОбязательный: Да
    Номер телефона.
  • typestringОбязательный: Да
    device
  • messagestringОбязательный: Да
    Текст SMS.
  • device_idintegerОбязательный: Нет
    С какого телефона отправлять (device_id из /api/devices). Если не указан — выбирается активный телефон с самым свежим heartbeat.
  • expires_inintegerОбязательный: Нет
    Срок ожидания в секундах (60–86400, по умолчанию 3600). По истечении деньги возвращаются.
  • client_refstringОбязательный: Нет
    ID заказа в вашей системе (до 100 символов) — возвращается без изменений в ответе статуса и callback.
  • callback_urlstringОбязательный: Нет
    Адрес callback финального статуса.

Пример запроса

curl -X POST "https://devsms.uz/api/sms/send" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "998901234567",
    "type": "device",
    "message": "Kod: 4821",
    "device_id": 3001,
    "expires_in": 3600,
    "client_ref": "ORDER-123",
    "callback_url": "https://your-site.uz/sms-webhook"
}'

Ответы

200Поставлено в очередь
{
    "success": true,
    "message": "SMS navbatga qo'yildi",
    "data": {
        "device_sms_id": 77,
        "status": "queued",
        "device_id": 3001,
        "device_online": true,
        "expires_at": "2026-09-28 20:18:35",
        "fee": 15,
        "balance": 4985,
        "type": "device"
    }
}

Ошибки

HTTPЗначение
400Неверный телефон или текст.
402Недостаточно средств на балансе.
403Аккаунт неактивен.
404Активный телефон с указанным device_id не найден (чужое устройство — та же ошибка).
409device_id не указан, и подключённого активного телефона нет.

Важно

  • device_online — был ли телефон онлайн в момент запроса. Даже при false SMS остаётся в очереди и уйдёт, когда телефон подключится.
  • Статусы: queued → sent_to_device → sending → sent → delivered; финальные: delivered, failed, expired, cancelled.
  • Требование: на телефоне должно быть приложение DevSMS Sender версии 2.0.0 или выше.

Баланс

GET /api/balance

GET /api/balance — текущий баланс, цена одной SMS и статистика. Параметров нет.

Пример запроса

curl -X GET "https://devsms.uz/api/balance" \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Ответы

200Успешно
{
    "success": true,
    "message": "Success",
    "data": {
        "balance": "5000.00",
        "sms_price": "200.00",
        "statistics": {
            "total_sms": 120,
            "total_spent": 24000,
            "total_failed": 2,
            "today_sms": 10,
            "today_spent": 2000,
            "today_failed": 0,
            "month_sms": 85,
            "month_spent": 17000,
            "month_failed": 1,
            "balance": "5000.00"
        }
    }
}

Важно

  • balance и sms_price — строки в сумах ("5000.00"); *_spent — числа.
  • Статистика охватывает только SMS, отправленные через кабинет и API (групповые и телефонные SMS не входят). «Отправленные» — не заблокированные и не отклонённые провайдером.
  • today_* — сегодня, month_* — текущий месяц (ташкентское время).

История SMS

GET /api/sms/history

GET /api/sms/history — список отправленных SMS, сначала новые. Параметры — в строке запроса (?limit=20&offset=0).

Параметры

  • limitintegerОбязательный: Нет
    Сколько записей (по умолчанию 50, максимум 200).
  • offsetintegerОбязательный: Нет
    Сколько записей пропустить (по умолчанию 0).
  • statusstringОбязательный: Нет
    Фильтр по статусу: pending, sent, delivered, failed, blocked.

Пример запроса

curl -X GET "https://devsms.uz/api/sms/history?limit=20&offset=0&status=delivered" \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Ответы

200Успешно
{
    "success": true,
    "message": "Success",
    "data": {
        "history": [
            {
                "id": 123,
                "user_id": 1001,
                "phone_number": "998901234567",
                "message": "Salom!",
                "provider_request_id": "dfc46fc5-0d38-4d07-aa74-95c58af3694f",
                "status": "delivered",
                "parts_count": 1,
                "price": "200.00",
                "total_cost": "200.00",
                "from_number": "4546",
                "sms_type": "regular",
                "sent_via": "api",
                "error_message": null,
                "sent_at": "2026-09-28 19:14:25",
                "delivered_at": "2026-09-28 19:14:31",
                "failed_at": null,
                "created_at": "2026-09-28 19:14:25"
            }
        ],
        "count": 1,
        "limit": 20,
        "offset": 0
    }
}

Важно

  • count — число записей в этом ответе (не общее); для следующей страницы увеличьте offset.
  • Порядок стабилен даже для записей, созданных в одну секунду (больший id — первым).
  • В каждой записи столбцы: id, phone_number, message, status, parts_count, price, total_cost, from_number, sms_type, sent_via, error_message, sent_at, delivered_at, failed_at, created_at и другие. Денежные поля — строки в сумах.

Статус SMS

GET /api/sms/status

GET /api/sms/status — статус одной SMS. Видны только ваши SMS.

Параметры

  • sms_idintegerОбязательный: Нет
    ID SMS в DevSMS (sms_id из ответа /api/sms/send).
  • request_idstringОбязательный: Нет
    ID запроса у провайдера (request_id). Нужен sms_id или request_id.
  • device_sms_idintegerОбязательный: Нет
    ID SMS с телефона — если указан, возвращается статус телефонной SMS.

Пример запроса

curl -X GET "https://devsms.uz/api/sms/status?sms_id=123" \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Ответы

200Обычная SMS
{
    "success": true,
    "message": "Success",
    "data": {
        "id": 123,
        "phone_number": "998901234567",
        "message": "Salom!",
        "status": "delivered",
        "parts_count": 1,
        "total_cost": "200.00",
        "sent_at": "2026-09-28 19:14:25",
        "delivered_at": "2026-09-28 19:14:31",
        "failed_at": null,
        "error_message": null
    }
}
200SMS с телефона
{
    "success": true,
    "message": "Success",
    "data": {
        "device_sms_id": 77,
        "type": "device",
        "status": "queued",
        "phone": "998901234567",
        "client_ref": "ORDER-123",
        "device_id": 3001,
        "expires_at": "2026-09-28 20:18:35",
        "sent_at": null,
        "delivered_at": null,
        "failed_at": null,
        "error_message": null,
        "fee": 15,
        "refunded": false
    }
}
404Не найдено (чужая SMS даёт тот же ответ)
{
    "success": false,
    "error": "SMS topilmadi"
}

Ошибки

HTTPЗначение
400SMS ID yoki request_id kiritilmagan.
404SMS topilmadi.

Устройства

GET /api/devices

GET /api/devices — подключённые телефоны. По умолчанию только status=active; подключаемые (pending) не возвращаются никогда.

Параметры

  • statusstringОбязательный: Нет
    При all добавляются неактивные (inactive, blocked) устройства. Регистр важен.

Пример запроса

curl -X GET "https://devsms.uz/api/devices" \
  -H "Authorization: Bearer YOUR_API_TOKEN"

Ответы

200Успешно
{
    "success": true,
    "message": "Success",
    "data": {
        "devices": [
            {
                "device_id": 3001,
                "name": "Ish telefoni",
                "model": "Samsung Galaxy S24",
                "android_version": "14",
                "app_version": "2.1.0",
                "status": "active",
                "online": true,
                "last_heartbeat_at": "2026-09-28 19:18:29",
                "sim_operator": "Beeline",
                "sim_slot": 1,
                "battery_level": 87,
                "signal_strength": "good",
                "daily_limit": 2000,
                "sent_today": 143,
                "remaining_today": 1857,
                "sms_interval": 5,
                "supports_api": true,
                "connected_at": "2026-08-01 10:12:03"
            }
        ],
        "total": 1
    }
}

Важно

  • device_id — ID для передачи в /api/sms/send с type=device.
  • online — true, если последний heartbeat был менее 2 минут назад. remaining_today — остаток SMS от дневного лимита.
  • Возвращаются только ваши телефоны; ключи и код сопряжения не отдаются никогда.

Статусы

Поле status записи SMS принимает одно из значений ниже. Статус обновляется через callback (webhook) и /api/sms/status.

Обычная SMS

СтатусЗначение
pendingПринята, отправляется провайдеру или ожидает.
sentПровайдер принял (передано оператору).
deliveredДоставлена получателю.
failedНе доставлена или отклонена. Если провайдер однозначно отклонил — деньги возвращаются.
blockedЗаблокирована модерацией или правилами аккаунта, деньги не списаны.

SMS с телефона (type=device)

СтатусЗначение
queuedВ очереди — уйдёт, когда телефон будет онлайн.
sent_to_deviceПередана телефону.
sendingТелефон отправляет.
sentУшла с телефона.
deliveredДоставлена.
failedНе отправлена (деньги возвращаются).
expiredСрок истёк (деньги возвращаются).
cancelledОтменена.

HTTP-коды

При ошибке тело ответа всегда {"success":false,"error":"…"} (иногда с дополнительными ключами).

Коды

КодЗначение
200Успешно.
400Неверный запрос: ошибка ввода, недостаточно средств, провайдер отклонил или результат неизвестен.
401Ошибка аутентификации: токена нет или он неверен.
402Недостаточно средств (SMS с телефона).
403Нет доступа: аккаунт заблокирован либо блокировка модерации/OTP.
404Не найдено (SMS или устройство).
409Конфликт: нет подключённого активного телефона.
429Слишком много запросов. Подождите и повторите.
500Внутренняя ошибка сервера.
503Сервис временно недоступен.

Callback и webhook

POST callback_url

Если при отправке SMS указан callback_url, при смене статуса на этот адрес отправляется POST (JSON): для обычной SMS — sent, delivered, failed; для SMS с телефона — один раз при финальном статусе.

Ответы

200Payload обычной SMS
{
    "sms_id": 123,
    "request_id": "dfc46fc5-0d38-4d07-aa74-95c58af3694f",
    "phone": "998901234567",
    "status": "delivered",
    "sent_at": "2026-09-28 19:14:25",
    "delivered_at": "2026-09-28 19:14:31",
    "failed_at": null,
    "timestamp": "2026-09-28 19:14:32"
}
200Payload SMS с телефона
{
    "type": "device",
    "device_sms_id": 77,
    "client_ref": "ORDER-123",
    "phone": "998901234567",
    "status": "delivered",
    "sent_at": "2026-09-28 19:20:02",
    "delivered_at": "2026-09-28 19:20:09",
    "failed_at": null,
    "error_message": null,
    "timestamp": "2026-09-28 19:20:10"
}

Заголовки

ЗаголовокЗначение
Content-Typeapplication/json
User-AgentDevSMS-Callback/1.0
X-DevSMS-TimestampВремя запроса (unix-секунды).
X-DevSMS-Signaturesha256= + подпись HMAC-SHA256 (ниже).

Требования к адресу

ПравилоПодробности
ПротоколТолько http:// и https://; длина до 500 символов.
Отклоняютсяlocalhost, внутренние и зарезервированные IP (127.x, 10.x, 172.16–31.x, 192.168.x, 169.254.x, ::1), тестовые домены (example.com, test.com…), домены без DNS-записи.
При доставкеАдрес проверяется заново и привязывается к проверенному IP (защита от DNS-rebinding). Редиректы не выполняются.
Ваш ответВерните 2xx/3xx (ниже 400) в течение 5 секунд — тогда доставка считается успешной. Тело ответа — не более 10 КБ, оно вам не возвращается.

Важно

  • Повторы: если ваш адрес вернул ошибку (выше 400) или недоступен, доставка повторяется через очередь: через 1, 5, 15, 30 минут, затем каждый час — до 24 часов с первой попытки. После этого попытки прекращаются.
  • Дубликаты (dedup): callback с одинаковыми sms_id и status может прийти несколько раз, а статусы — в произвольном порядке (например, sent после delivered). Делайте обработку идемпотентной: дедуплицируйте по sms_id + status и не возвращайте финальный статус (delivered, failed) к предыдущему.
  • Ключ подписи: в кабинете на странице API-ключи (/app/api-keys → секрет webhook). Никому его не передавайте; после замены старый ключ перестаёт работать сразу.
  • Для SMS с телефона, если в статусе sent отчёт о доставке не пришёл за 30 минут, callback отправляется со статусом sent (деньги не возвращаются).

Порядок проверки

  1. Проверьте, что X-DevSMS-Timestamp отличается от текущего времени не более чем на 5 минут (защита от повторов).
  2. Вычислите HMAC-SHA256: ключ — секрет webhook (whsec_… целиком), сообщение — {timestamp}.{сырое_тело}.
  3. Сравните результат с X-DevSMS-Signature (с префиксом sha256=) сравнением за постоянное время.

Подпись считается по СЫРОМУ (raw) телу. Не разбирайте JSON и не кодируйте заново — порядок ключей и символы изменятся, и подпись не совпадёт.

Пример проверки подписи

<?php
$secret    = 'whsec_...';                       // /app/api-keys sahifasidagi kalit
$body      = file_get_contents('php://input');  // XOM body — json_decode qilmang
$timestamp = $_SERVER['HTTP_X_DEVSMS_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_DEVSMS_SIGNATURE'] ?? '';

if (abs(time() - (int) $timestamp) > 300) {
    http_response_code(400);  // eskirgan so'rov (replay)
    exit;
}

$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $body, $secret);

if (!hash_equals($expected, $signature)) {
    http_response_code(401);
    exit;
}

$event = json_decode($body, true);
// ... $event['sms_id'] va $event['status'] bo'yicha idempotent qayta ishlang
http_response_code(200);

API v1

API v1 — версионированный интерфейс: единый конверт ответа (success, message, data), чёткие коды ошибок (error_code), права ключей (sms:send, sms:read, balance:read, devices:read, voice:send, voice:read, templates:read, device_sms:send, device_sms:read), суммы строками, время в ISO 8601 (+05:00) и постраничная навигация (cursor).

Важно

  • Адреса: /api/v1/sms, /api/v1/balance, /api/v1/devices, /api/v1/templates, /api/v1/devices/sms (SMS с телефона), /api/v1/voice/otp (голосовой OTP).
  • Создайте персональный ключ на странице API-ключи в кабинете, выбрав нужные права.
  • POST /api/v1/sms принимает необязательный заголовок Idempotency-Key — чтобы при повторной попытке не отправлялась вторая SMS и повторно не списывались деньги (раздел «Защита от повторов» ниже).
  • Лимиты зависят от тарифа (в минуту, отправка POST / чтение GET): Start — 120 / 240, Pro — 300 / 600, Business — 1 500 / 3 000. При превышении 429. Ваш тариф — на странице Тариф в кабинете.
Руководство для ИИ-агентов

Защита от повторов: Idempotency-Key

POST /api/v1/sms

POST /api/v1/sms принимает необязательный заголовок Idempotency-Key. Если после обрыва сети, таймаута или ответа 502 вы повторите запрос с тем же ключом — вторая SMS не отправится и деньги повторно не спишутся, вернётся результат первого запроса. Запросы без заголовка работают как раньше.

Параметры

  • Idempotency-Keystring (UUID)Обязательный: Нет
    Заголовок (header), не поле тела. UUID — любая версия, например v4; без кавычек или в формате IETF в одной паре кавычек ("3f2b8a52-…") — обе записи это один ключ; регистр не важен. Для каждой новой SMS создавайте новый UUID и передавайте прежний только при повторной попытке.

Пример запроса

curl -X POST "https://devsms.uz/api/v1/sms" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Idempotency-Key: 3f2b8a52-7c1d-4e0a-9b4f-6d5c1a2e8b70" \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "998901234567",
    "message": "Salom!"
}'

Ответы

200Первый запрос (успех)
{
    "success": true,
    "message": "SMS yuborildi",
    "data": {
        "id": 123,
        "phone": "998901234567",
        "message": "Salom!",
        "status": "sent",
        "type": "regular",
        "parts": 1,
        "price_per_part": "200.00",
        "total_cost": "200.00",
        "currency": "UZS",
        "provider_request_id": "dfc46fc5-0d38-4d07-aa74-95c58af3694f",
        "country_code": null,
        "sent_via": "api",
        "error": null,
        "created_at": "2026-09-28T19:14:25+05:00",
        "sent_at": "2026-09-28T19:14:25+05:00",
        "delivered_at": null,
        "failed_at": null,
        "outcome": "sent"
    }
}
200Повтор: тот же ключ, телефон, текст и отправитель (from) — с заголовком Idempotent-Replayed: true, текущее состояние SMS
{
    "success": true,
    "message": "SMS yuborildi",
    "data": {
        "id": 123,
        "phone": "998901234567",
        "message": "Salom!",
        "status": "delivered",
        "type": "regular",
        "parts": 1,
        "price_per_part": "200.00",
        "total_cost": "200.00",
        "currency": "UZS",
        "provider_request_id": "dfc46fc5-0d38-4d07-aa74-95c58af3694f",
        "country_code": null,
        "sent_via": "api",
        "error": null,
        "created_at": "2026-09-28T19:14:25+05:00",
        "sent_at": "2026-09-28T19:14:25+05:00",
        "delivered_at": "2026-09-28T19:14:31+05:00",
        "failed_at": null,
        "outcome": "sent"
    }
}
502502, результат неизвестен — повторите с тем же ключом: ответом снова будет этот 502, новая SMS не отправится
{
    "success": false,
    "message": "SMS yuborish natijasi noma'lum (provayder javob bermadi). Balansdan yechildi; qayta yuborishdan oldin holatni tekshiring.",
    "error_code": "provider_outcome_unknown",
    "meta": {
        "sms_id": 125,
        "charged": true
    }
}
422Тот же ключ с другим телефоном, текстом или отправителем (деньги не списываются)
{
    "success": false,
    "message": "Idempotency-Key boshqa telefon, matn yoki yuboruvchi (from) bilan ishlatilgan. Har yangi so'rov uchun yangi UUID bering",
    "error_code": "idempotency_key_reused"
}
409Первый запрос ещё не завершён
{
    "success": false,
    "message": "Shu Idempotency-Key bilan oldingi so'rov hali bajarilmoqda. Birozdan keyin o'sha kalit bilan qayta yuboring",
    "error_code": "idempotency_request_in_progress",
    "meta": {
        "sms_id": 123
    }
}
422Неверный формат заголовка
{
    "success": false,
    "message": "Kiritilgan ma'lumotlar noto'g'ri",
    "error_code": "validation_failed",
    "errors": {
        "Idempotency-Key": [
            "Idempotency-Key UUID bo'lishi kerak (masalan 0b1c2d3e-4f50-4a61-8b72-93a4b5c6d7e8; qo'shtirnoqsiz yoki bitta juft qo'shtirnoq ichida) va bitta qiymat"
        ]
    }
}

Ошибки

HTTPЗначение
422validation_failed — Idempotency-Key не UUID или пуст (errors["Idempotency-Key"]); idempotency_key_reused — ключ использован с другим телефоном, текстом или отправителем (from): ничего не отправлено, деньги не списаны, передайте новый UUID.
409idempotency_request_in_progress — предыдущий запрос с этим ключом ещё выполняется; пока результат не готов, повторяйте запрос с тем же ключом через Retry-After: 2 секунды (meta.sms_id — для GET /api/v1/sms/{id}).
502provider_outcome_unknown — результат неизвестен, деньги не возвращены. Повторите запрос с тем же Idempotency-Key.

Важно

  • Необязательный. Без заголовка ничего не меняется: каждый запрос — отдельная SMS. Работает только в POST /api/v1/sms (/api/sms/send его игнорирует).
  • Формат: UUID (любая версия, например v4): без кавычек (3f2b8a52-7c1d-4e0a-9b4f-6d5c1a2e8b70) или в формате IETF в одной паре кавычек ("3f2b8a52-7c1d-4e0a-9b4f-6d5c1a2e8b70") — обе записи это один ключ; регистр не важен. Неверное значение, пустое "", одна или вложенная кавычка, пробел внутри кавычек или несколько значений — 422, SMS не отправляется.
  • Когда нужен новый ключ: для каждой новой SMS создавайте новый UUID и храните его до окончания повторных попыток; повторяйте прежний только при повторной отправке той же SMS (таймаут, обрыв, 5xx, 502). А 422 provider_rejected — окончательный для этого ключа: повтор с тем же ключом снова вернёт этот отказ, и SMS повторно не отправляется — чтобы отправить её заново (например, устранив причину), передайте новый UUID.
  • Ключ бессрочный: срока хранения нет. Старый ключ всегда возвращает результат первого запроса (или 422 idempotency_key_reused при другом теле запроса) — поэтому для каждой новой SMS создавайте новый UUID.
  • Ключ действует в рамках вашего аккаунта: все API-ключи (токены) аккаунта используют общее пространство ключей; ключи других пользователей не пересекаются с вашими.
  • Повтор (тот же ключ, телефон, текст и отправитель from): ни второй SMS, ни второго списания. Возвращается результат первого запроса — та же SMS (data.id), тот же HTTP-статус (200, 422 provider_rejected или 502 provider_outcome_unknown) и заголовок Idempotent-Replayed: true. data — текущее состояние SMS (например, позже delivered).
  • Что сравнивается: телефон, текст и отправитель (from; если не указан — стандартный 4546). callback_url и template_id не сравниваются — первый это метаданные доставки, второй определяется текстом: при повторе они игнорируются, SMS остаётся с параметрами первого запроса.
  • 502 provider_outcome_unknown: деньги не возвращаются, SMS могла быть доставлена. Повторите запрос с тем же Idempotency-Key — это безопасно: новая SMS не отправится, деньги повторно не спишутся, ответом снова будет этот 502 (Idempotent-Replayed: true). Следите за результатом через GET /api/v1/sms/{id} (meta.sms_id) или callback. Повтор без ключа отправит вторую SMS и спишет деньги второй раз.
  • Тот же ключ с другим телефоном, текстом или отправителем (from) — 422 idempotency_key_reused: ничего не отправляется, деньги не списываются. Для другой SMS передайте новый UUID.
  • Первый запрос ещё не завершён (параллельный или очень быстрый повтор) — 409 idempotency_request_in_progress, заголовок Retry-After: 2 (секунды) и meta.sms_id. Результат ещё не готов: вторая SMS не отправляется; пока результат не готов, повторяйте запрос с тем же ключом каждые ~2 секунды (или смотрите GET /api/v1/sms/{id}) — по завершении вернётся результат первого запроса. Если процесс первого запроса на сервере прервался (редко), 409 будет возвращаться до ≈15 минут — пока система автоматически не закроет этот запрос: если 409 длится несколько минут, повторяйте реже и проверяйте статус через GET /api/v1/sms/{id} по meta.sms_id (не отправляйте заново с новым ключом — SMS может уйти дважды). После закрытия повтор с тем же ключом вернёт окончательный результат: 422 provider_rejected (SMS не отправлена, деньги возвращены) или 502 provider_outcome_unknown.
  • Ключ привязывается к запросу, за который списаны деньги. Запрос, отклонённый до списания (402 недостаточно средств, 403, 422 ошибка проверки), ключ не «расходует» — исправьте и повторите с тем же ключом.