Documentation

APIguide

DevSMS REST API: send SMS (standard, universal OTP, from your phone), balance, history, status, devices, callbacks and webhook signatures, international prices. Examples in cURL, PHP, Python and Node.

Introduction and authentication

The DevSMS API is a REST interface that returns JSON. Every request goes over HTTPS with an Authorization header. The main endpoints are /api/sms/send, /api/balance, /api/sms/history, /api/sms/status and /api/devices. The versioned API v1 (last section) adds duplicate protection, error codes and key abilities.

Base URLhttps://devsms.uz/api
AuthorizationAuthorization: Bearer YOUR_API_TOKEN

Important

  • Token: send Authorization: Bearer YOUR_API_TOKEN with every request. Get your token on the API keys page of the cabinet (/app/api-keys).
  • A blocked account gets 403 and the text Sizning hisobingiz bloklangan on every endpoint; a missing or invalid token gets 401.
  • A personal key over the plan limit gets 403 with error_code: token_locked_by_plan: the oldest keys keep working up to the plan limit. Upgrade or delete an older key — the next one works immediately. The main (legacy) API key is never blocked.
  • Limits: /api/sms/send allows 300 requests per minute per user and 500 requests per minute per IP; beyond that you get 429. Other endpoints have no limit.
  • Response shape: on success {"success":true,"message":"…","data":{…}}, on error {"success":false,"error":"…"} (some errors carry extra keys: charged, sms_id, reject_streak).
  • Phone numbers are accepted in any format: 998901234567, +998 (90) 123-45-67, 901234567. International numbers are accepted only if international SMS is enabled for your account.
  • Times use the Y-m-d H:i:s format in Tashkent time (UTC+5).
Get a token

Send SMS

POST /api/sms/send

POST /api/sms/send — sends a template-based SMS (type=sms; also when type is omitted). The body is JSON (Content-Type: application/json) or a form. The SMS is sent only if the text matches an approved template; the cost is charged before sending and refunded if the provider clearly rejects the SMS.

Parameters

  • phonestringRequired: Yes
    Phone number (998901234567 or international: 12025551234).
  • messagestringRequired: Yes
    SMS text (up to 1000 bytes).
  • fromstringRequired: No
    Sender name (up to 11 bytes). Default 4546.
  • typestringRequired: No
    sms (default), universal_otp or device.
  • callback_urlstringRequired: No
    An http(s):// address (up to 500 characters) that receives a POST when the status changes. Requirements are in "Callbacks and webhooks".
  • template_idintegerRequired: No
    Template ID — only helps pick the price category, it never rejects the send.

Request example

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"
}'

Responses

200Success (Uzbekistan number)
{
    "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"
    }
}
200Success (international number — country and country_code are added)
{
    "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"
    }
}
400Insufficient balance
{
    "success": false,
    "error": "Balansda yetarli mablag' yo'q"
}
400Outcome unknown — money was charged
{
    "success": false,
    "error": "SMS yuborish natijasi noma'lum (provayder javob bermadi). Balansdan yechildi; qayta yuborishdan oldin holatni tekshiring.",
    "charged": true,
    "sms_id": 125
}
403Rejected by moderation (no charge)
{
    "success": false,
    "error": "Xabar bloklandi: Nomaqbul so'z. SMS yuborilmadi, to'lov yechilmadi.",
    "charged": false,
    "reject_streak": 1,
    "remaining_attempts": 19
}

Errors

HTTPMeaning
400Invalid phone format / SMS text missing / callback_url does not meet the requirements / insufficient balance / international SMS not enabled / provider rejected / outcome unknown.
401Token not found or invalid.
403Account blocked, or the text failed moderation (charged: false).
429Request limit exceeded.
500Internal server error ({"success":false,"error":"Ichki server xatosi"}).

Important

  • total_cost is parts × price per part; balance is the balance at the start of the request minus total_cost (with parallel requests it can differ from the real balance; use /api/balance for the exact value).
  • Outcome unknown (important): if the provider does not answer (5xx, timeout), the response is 400 with "charged": true and an sms_id. The money is not refunded — the SMS may have been delivered. Before retrying, check the status with /api/sms/status?sms_id=…, otherwise your customer may receive the SMS twice.
  • If the provider clearly rejects the SMS (for example, no matching template), the money is refunded automatically and the reason is in error.
  • When type is omitted, data.type in the response is sms. Any other value (for example foo) is sent as a standard SMS and echoed back unchanged in data.type. type=simple is no longer supported — use sms.

Universal OTP

POST /api/sms/send

POST /api/sms/send with type=universal_otp — sends a verification code using one of 4 ready-made templates pre-approved by the operator. You provide only the company name and the code; the company name goes through automatic moderation.

Parameters

  • phonestringRequired: Yes
    Phone number.
  • typestringRequired: Yes
    universal_otp
  • template_typeintegerRequired: Yes
    1 — confirm an action, 2 — password reset, 3 — sign-up, 4 — sign-in.
  • service_namestringRequired: Yes
    Company/service name: 2–50 characters, letters, digits, spaces, dots and hyphens only.
  • otp_codestringRequired: Yes
    Verification code: 4–8 digits.
  • callback_urlstringRequired: No
    Status callback address.

Request example

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"
}'

Responses

200Success
{
    "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"
    }
}
403Company name rejected (no charge)
{
    "success": false,
    "error": "Korxona nomi nomaqbul deb topildi: Nomaqbul so'z. SMS yuborilmadi, to'lov yechilmadi.",
    "charged": false,
    "reject_streak": 3,
    "remaining_attempts": 17
}
40324-hour block
{
    "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"
}

Errors

HTTPMeaning
400Invalid template type, company name or code format, insufficient balance.
403Company name rejected, or the service is suspended for 24 hours.

Ready-made templates

template_typeText
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}

Important

  • If the company name is unacceptable, the SMS is not sent and no money is charged — the response has charged: false. No request_id is created and no callback arrives.
  • Abuse protection: after 20 rejections in a row the universal OTP service is suspended for 24 hours (403, blocked_until). As soon as moderation approves a name, the counter resets to zero. Every rejection response carries reject_streak and remaining_attempts.
  • A regular message text that looks like one of these templates goes through the same moderation and follows the same blocking rule.

Voice OTP (call)

POST /api/v1/voice/otp

POST /api/v1/voice/otp places an automated call and reads the verification code aloud in Uzbek ("…kodingiz: To'rt, Sakkiz, Ikki, Olti"). Uzbekistan numbers only. The key needs the voice:send ability; status via GET /api/v1/voice/{id} (voice:read).

Parameters

  • phonestringRequired: Yes
    Phone number: 998XXXXXXXXX.
  • template_typeintegerRequired: No
    Ready template: 1 — confirm operation, 2 — password reset, 3 — sign up, 4 — sign in. Required when text is not sent.
  • service_namestringRequired: No
    Company/service name (required with template_type): 2–50 characters, letters, digits, space, dot and dash only.
  • textstringRequired: No
    Your own text (1–300 characters) — only for accounts the administrator allowed; not combined with template_type and service_name. The code is read after the text.
  • codestringRequired: No
    Code: 4–8 digits. If omitted, a 6-digit code is generated and returned (data.code).
  • repeatbooleanRequired: No
    Repeat the text and code a second time ("Takrorlayman, …"). Default: true.
  • Idempotency-Keystring (UUID)Required: No
    Header: a retry neither places a second call nor charges again. Rules — in the "Retry protection" section.

Request example

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
}'

Responses

200Success: the call is queued
{
    "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
    }
}
503The voice service is busy (Retry-After), not charged
{
    "success": false,
    "message": "Ovozli xizmat band — bir necha soniyadan keyin qayta urinib ko'ring",
    "error_code": "voice_busy",
    "meta": {
        "charged": false,
        "call_id": 4822
    }
}
502Result unknown — retry with the same Idempotency-Key or check the status
{
    "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
    }
}
402Insufficient balance
{
    "success": false,
    "message": "Balansda yetarli mablag' yo'q",
    "error_code": "insufficient_balance"
}

Errors

HTTPMeaning
402insufficient_balance — insufficient balance.
403voice_free_text_disabled — custom text is not allowed; moderation_rejected, otp_suspended, account_blocked.
422validation_failed, unsupported_country (Uzbekistan only), provider_rejected (refunded), idempotency_key_reused.
502provider_outcome_unknown — charged and not refunded; check the status by meta.call_id.
503voice_disabled — the service is temporarily off; voice_busy — retry after Retry-After seconds.

Ready templates (spoken text, in Uzbek)

template_typeText
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}

Call statuses (status)

statusMeaning
queuedQueued: accepted by the provider
calling, ringingCalling, ringing
answered, completedAnswered, completed (code read)
no_answer, busyNo answer, busy — the call was charged
failed, rejected, cancelledFailed, rejected, cancelled — the call was charged
not_sentThe provider rejected the request — refunded
unknownResult unknown — charged, not refunded automatically
blockedRejected by moderation — not charged

Important

  • The price is per call (90 UZS by default) — charged even if the subscriber does not answer or the line is busy. Money is refunded only when the provider rejects the request (not_sent).
  • data.code is the code that was read (generated if you did not send one). Compare it with the code your user enters.
  • The status updates within a few seconds to a minute: GET /api/v1/voice/{id}; list — GET /api/v1/voice (status, from, to, cursor, limit).
  • Idempotency-Key follows the SMS rules: a retry with the same key places no new call and charges nothing again (Idempotent-Replayed: true); the key with a different phone, text or code — 422 idempotency_key_reused; the first request is still running — 409, Retry-After: 2, meta.call_id.
  • When moderation is on, the company name and text are checked: a rejection is not charged (403 moderation_rejected, remaining_attempts); 20 rejections in a row — a 24-hour block (otp_suspended).

SMS from your phone

POST /api/sms/send

POST /api/sms/send with type=device — the SMS is sent from your own phone (no template needed). First install the DevSMS Sender app and pair the phone in the cabinet. The SMS is queued and sent when the phone is online; the service fee is charged per SMS and refunded if the SMS is not sent.

Parameters

  • phonestringRequired: Yes
    Phone number.
  • typestringRequired: Yes
    device
  • messagestringRequired: Yes
    SMS text.
  • device_idintegerRequired: No
    Which phone to send from (device_id from /api/devices). If omitted, the active phone with the most recent heartbeat is chosen.
  • expires_inintegerRequired: No
    Waiting time in seconds (60–86400, default 3600). When it runs out the money is refunded.
  • client_refstringRequired: No
    Your own order ID (up to 100 characters) — returned unchanged in the status response and the callback.
  • callback_urlstringRequired: No
    Final-status callback address.

Request example

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"
}'

Responses

200Queued
{
    "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"
    }
}

Errors

HTTPMeaning
400Invalid phone or text.
402Insufficient balance.
403Account is not active.
404No active phone found for the given device_id (someone else's device gives the same error).
409device_id was not given and there is no paired active phone.

Important

  • device_online — whether the phone was online at request time. Even with false the SMS stays queued and is sent once the phone connects.
  • Statuses: queued → sent_to_device → sending → sent → delivered; final: delivered, failed, expired, cancelled.
  • Requirement: the phone must run DevSMS Sender version 2.0.0 or newer.

Balance

GET /api/balance

GET /api/balance — current balance, the price of one SMS and statistics. No parameters.

Request example

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

Responses

200Success
{
    "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"
        }
    }
}

Important

  • balance and sms_price are strings in UZS ("5000.00"); *_spent are numbers.
  • Statistics cover only SMS sent through the cabinet and the API (group and phone SMS are not included). "Sent" means not blocked and not rejected by the provider.
  • today_* is today, month_* is the current month (Tashkent time).

SMS history

GET /api/sms/history

GET /api/sms/history — the list of sent SMS, newest first. Parameters go in the query string (?limit=20&offset=0).

Parameters

  • limitintegerRequired: No
    How many records (default 50, at most 200).
  • offsetintegerRequired: No
    How many records to skip (default 0).
  • statusstringRequired: No
    Filter by status: pending, sent, delivered, failed, blocked.

Request example

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

Responses

200Success
{
    "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
    }
}

Important

  • count is the number of records in this response (not the total); increase offset for the next page.
  • The order is stable even for records created in the same second (higher id first).
  • Each record has the columns 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 and others. Money fields are strings in UZS.

SMS status

GET /api/sms/status

GET /api/sms/status — the status of a single SMS. Only your own SMS are visible.

Parameters

  • sms_idintegerRequired: No
    SMS ID in DevSMS (sms_id from the /api/sms/send response).
  • request_idstringRequired: No
    Provider request ID (request_id). Either sms_id or request_id is required.
  • device_sms_idintegerRequired: No
    Phone SMS ID — when given, the status of the phone SMS is returned.

Request example

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

Responses

200Regular 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
    }
}
200Phone SMS
{
    "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
    }
}
404Not found (someone else's SMS gives the same answer)
{
    "success": false,
    "error": "SMS topilmadi"
}

Errors

HTTPMeaning
400SMS ID yoki request_id kiritilmagan.
404SMS topilmadi.

Devices

GET /api/devices

GET /api/devices — paired phones. By default only status=active devices; phones still pairing (pending) are never returned.

Parameters

  • statusstringRequired: No
    With all, inactive (inactive, blocked) devices are included too. Case-sensitive.

Request example

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

Responses

200Success
{
    "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
    }
}

Important

  • device_id is the ID to pass to /api/sms/send with type=device.
  • online is true when the last heartbeat is less than 2 minutes old. remaining_today is what is left of the daily limit.
  • Only your own phones are listed; keys and pairing codes are never returned.

Statuses

The status field of an SMS record takes one of the values below. The status is updated through callbacks (webhooks) and /api/sms/status.

Regular SMS

StatusMeaning
pendingAccepted, being handed to the provider or waiting.
sentAccepted by the provider (handed to the operator).
deliveredDelivered to the recipient.
failedNot delivered or rejected. If the provider clearly rejected it, the money is refunded.
blockedBlocked by moderation or account rules; no money charged.

Phone SMS (type=device)

StatusMeaning
queuedQueued — will be sent when the phone is online.
sent_to_deviceHanded to the phone.
sendingThe phone is sending.
sentLeft the phone.
deliveredDelivered.
failedNot sent (money refunded).
expiredExpired (money refunded).
cancelledCancelled.

HTTP codes

On error the response body is always {"success":false,"error":"…"} (sometimes with extra keys).

Codes

CodeMeaning
200Success.
400Bad request: input error, insufficient balance, provider rejected, or outcome unknown.
401Authentication error: token missing or invalid.
402Insufficient balance (phone SMS).
403Forbidden: account blocked, or a moderation/OTP block.
404Not found (SMS or device).
409Conflict: no paired active phone.
429Too many requests. Wait a bit and retry.
500Internal server error.
503Service temporarily unavailable.

Callbacks and webhooks

POST callback_url

If you pass a callback_url when sending, a POST (JSON) goes to that address whenever the status changes: sent, delivered, failed for a regular SMS; once, at the final status, for a phone SMS.

Responses

200Regular SMS payload
{
    "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"
}
200Phone SMS payload
{
    "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"
}

Headers

HeaderValue
Content-Typeapplication/json
User-AgentDevSMS-Callback/1.0
X-DevSMS-TimestampRequest time (unix seconds).
X-DevSMS-Signaturesha256= + HMAC-SHA256 signature (below).

Address requirements

RuleDetails
ProtocolOnly http:// and https://; up to 500 characters.
Rejectedlocalhost, internal and reserved IPs (127.x, 10.x, 172.16–31.x, 192.168.x, 169.254.x, ::1), test/placeholder domains (example.com, test.com…), domains with no DNS record.
At deliveryThe address is checked again and pinned to the verified IP (DNS-rebinding protection). Redirects are not followed.
Your answerReturn 2xx/3xx (below 400) within 5 seconds — then delivery counts as successful. The response body must stay under 10 KB and is not passed back to you.

Important

  • Retries: if your address returns an error (above 400) or cannot be reached, delivery is retried through a queue: after 1, 5, 15 and 30 minutes, then hourly — until 24 hours after the first attempt. After that it stops.
  • Duplicates (dedup): a callback with the same sms_id and status can arrive more than once, and statuses can arrive out of order (for example sent after delivered). Make your handler idempotent: deduplicate by sms_id + status and never move a final status (delivered, failed) back to an earlier one.
  • Signing key: on the API keys page of the cabinet (/app/api-keys → webhook secret). Do not share it; after you rotate it the old key stops working immediately.
  • For a phone SMS, if no delivery report arrives within 30 minutes of sent, the callback is sent with status sent (money is not refunded).

Verification steps

  1. Check that X-DevSMS-Timestamp is within 5 minutes of the current time (replay protection).
  2. Compute HMAC-SHA256: key — your webhook secret (the whole whsec_… string), message — {timestamp}.{raw_body}.
  3. Compare the result with X-DevSMS-Signature (including the sha256= prefix) using a constant-time comparison.

The signature is computed over the RAW body. Do not parse the JSON and re-encode it — key order and characters would change and the signature would no longer match.

Signature verification example

<?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 is the versioned interface: one response envelope (success, message, data), explicit error codes (error_code), key abilities (sms:send, sms:read, balance:read, devices:read, voice:send, voice:read, templates:read, device_sms:send, device_sms:read), amounts as strings, ISO 8601 times (+05:00) and pagination (cursor).

Important

  • Addresses: /api/v1/sms, /api/v1/balance, /api/v1/devices, /api/v1/templates, /api/v1/devices/sms (SMS from your phone), /api/v1/voice/otp (voice OTP).
  • Create a personal key on the API keys page of the cabinet and pick the abilities it needs.
  • POST /api/v1/sms accepts an optional Idempotency-Key header, so a retry never sends a second SMS or charges twice (see "Duplicate protection" below).
  • Limits depend on your plan (per minute, sending POST / reading GET): Start — 120 / 240, Pro — 300 / 600, Business — 1 500 / 3 000. Beyond that you get 429. See your plan on the Plan page in your account.
Guide for AI agents

Duplicate protection: Idempotency-Key

POST /api/v1/sms

POST /api/v1/sms accepts an optional Idempotency-Key header. If you retry with the same key after a network drop, a timeout or a 502, no second SMS is sent and you are not charged again — the result of the first request is returned. Requests without the header work as before.

Parameters

  • Idempotency-Keystring (UUID)Required: No
    A header, not a body field. A UUID of any version, for example v4; with no quotes or in the IETF form inside one pair of quotes ("3f2b8a52-…") — both spellings are the same key; case-insensitive. Create a new UUID for every new SMS and send the same one only when retrying.

Request example

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!"
}'

Responses

200First request (success)
{
    "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"
    }
}
200Repeat: same key, phone, text and sender (from) — with the Idempotent-Replayed: true header, the current state of the 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, outcome unknown — retry with the same key: the response is this 502 again and no new SMS is sent
{
    "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
    }
}
422Same key with a different phone, text or sender (you are not charged)
{
    "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"
}
409The first request has not finished yet
{
    "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
    }
}
422Malformed header
{
    "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"
        ]
    }
}

Errors

HTTPMeaning
422validation_failed — Idempotency-Key is not a UUID or is empty (errors["Idempotency-Key"]); idempotency_key_reused — the key was used with a different phone, text or sender (from): nothing was sent and you were not charged, pass a new UUID.
409idempotency_request_in_progress — the earlier request with this key is still running; until the result is ready, retry with the same key after Retry-After: 2 seconds (meta.sms_id is for GET /api/v1/sms/{id}).
502provider_outcome_unknown — the outcome is unknown and the money was not refunded. Retry with the same Idempotency-Key.

Important

  • Optional. Without the header nothing changes: every request is a separate SMS. It works only on POST /api/v1/sms (/api/sms/send ignores it).
  • Format: a UUID (any version, for example v4): with no quotes (3f2b8a52-7c1d-4e0a-9b4f-6d5c1a2e8b70) or in the IETF form inside one pair of quotes ("3f2b8a52-7c1d-4e0a-9b4f-6d5c1a2e8b70") — both spellings are the same key; case-insensitive. An invalid value, an empty "", a single or nested quote, a space inside the quotes or multiple values give 422 and no SMS is sent.
  • When to use a new key: create a new UUID for every new SMS and keep it until its retries are over; repeat the same one only when re-sending that same SMS (timeout, disconnect, 5xx, 502). 422 provider_rejected, however, is final for this key: a retry with the same key returns the same rejection and the SMS is not re-sent — to send it again (for example, after fixing the cause), pass a new UUID.
  • The key does not expire: there is no retention limit. An old key always returns the result of the first request (or 422 idempotency_key_reused with a different request body) — so create a new UUID for every new SMS.
  • The key is scoped to your account: all API keys (tokens) of your account share one key space; other users' keys never collide with yours.
  • A repeat (same key, phone, text and sender from): no second SMS and no second charge. The result of the first request is returned — the same SMS (data.id), the same HTTP status (200, 422 provider_rejected or 502 provider_outcome_unknown) and the Idempotent-Replayed: true header. data is the current state of the SMS (for example delivered later).
  • What is compared: the phone, the text and the sender (from; the default 4546 if omitted). callback_url and template_id are not compared — the first is delivery metadata, the second follows from the text: on a repeat they are ignored and the SMS keeps the first request's parameters.
  • 502 provider_outcome_unknown: the money is not refunded and the SMS may have been delivered. Retry with the same Idempotency-Key — it is safe: no new SMS is sent, you are not charged again, and the response is this 502 again (Idempotent-Replayed: true). Follow the result with GET /api/v1/sms/{id} (meta.sms_id) or the callback. Retrying without a key sends a second SMS and charges a second time.
  • The same key with a different phone, text or sender (from) — 422 idempotency_key_reused: nothing is sent and you are not charged. Pass a new UUID for a different SMS.
  • The first request has not finished yet (a parallel or very quick retry) — 409 idempotency_request_in_progress, a Retry-After: 2 header (seconds) and meta.sms_id. The result is not ready yet: no second SMS is sent; until the result is ready, retry with the same key every ~2 seconds (or check GET /api/v1/sms/{id}) — once it is done, the result of the first request is returned. If the first request's process was interrupted on the server (rare), 409 keeps coming back for up to ≈15 minutes, until the system closes that request automatically: if 409 lasts for several minutes, retry less often and check the status with GET /api/v1/sms/{id} using meta.sms_id (do not re-send with a new key — the SMS may go out twice). Once it is closed, a retry with the same key returns the final result: 422 provider_rejected (the SMS was not sent, you were refunded) or 502 provider_outcome_unknown.
  • The key binds to a request that was charged. A request rejected before charging (402 insufficient balance, 403, 422 validation) does not use the key up — fix it and retry with the same key.