Getting started
- Create an account and wait for it to be activated.
- In the panel, open DLT templates and add your entity ID, sender ID and approved templates.
- Open API and webhooks and create an API key.
All requests go to this base URL over HTTPS. Request and response bodies are JSON.
https://cloudsms.in/api/v1Authentication
Send your API key in the Authorization header of every request.
Authorization: Bearer csk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxKeep the key on your server and never put it in a mobile app or web page. You can also limit the key to your server's IP addresses in the panel. If your system cannot set headers, you may pass api_key as a query parameter instead.
Send SMS
POST /sms/send
Sends one approved DLT template to one or more numbers. Each {#var#} in the template is filled, in order, from variables.
| Field | Required | Description |
|---|---|---|
template_id | Yes | DLT content template ID, as saved in the panel. |
variables | If the template has values | Array of values, or one string separated by |. Up to 30 characters each. |
to | Yes | Array of 10-digit mobile numbers, or one comma-separated string. +91 and 0 are removed. |
sender | No | Sender ID. If given, it must match the template's sender. |
schedule_at | No | Send later, for example 2026-11-01T10:30:00+05:30. 5 minutes to 30 days ahead. |
curl -X POST https://cloudsms.in/api/v1/sms/send \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"template_id": "1207168xxxxxxxx412",
"variables": ["Ravi", "482913"],
"to": ["9876543210", "9123456780"]
}'$ch = curl_init('https://cloudsms.in/api/v1/sms/send');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer YOUR_API_KEY',
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'template_id' => '1207168xxxxxxxx412',
'variables' => ['Ravi', '482913'],
'to' => ['9876543210', '9123456780'],
]),
]);
$result = json_decode(curl_exec($ch), true);const res = await fetch("https://cloudsms.in/api/v1/sms/send", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
template_id: "1207168xxxxxxxx412",
variables: ["Ravi", "482913"],
to: ["9876543210", "9123456780"],
}),
});
const result = await res.json();import requests
r = requests.post(
"https://cloudsms.in/api/v1/sms/send",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={
"template_id": "1207168xxxxxxxx412",
"variables": ["Ravi", "482913"],
"to": ["9876543210", "9123456780"],
},
)
print(r.json())Response when the numbers are handed to the operators:
{
"success": true,
"batch_id": 1042,
"request_id": "SLBZPIxP4mRVdf4",
"status": "submitted",
"sender": "ACMEIN",
"template_id": "1207168xxxxxxxx412",
"numbers": 2,
"sms_parts": 1,
"credits": 2,
"sent": 2,
"failed": 0,
"scheduled_at": null,
"created_at": "2026-10-22T10:15:02+05:30",
"invalid_numbers": 0
}Requests with more than 200 numbers return 202 Accepted with status: "queued" and are sent in the background. Use delivery reports to follow them.
For systems that can only call a URL, the same request works as a GET:
https://cloudsms.in/api/v1/sms/send?api_key=YOUR_API_KEY&template_id=1207168xxxxxxxx412&variables=Ravi|482913&to=9876543210Send an OTP
Create the code in your application, send it with your approved OTP template, and check it in your own system when the user types it. Keep OTP templates on a service route, so they reach numbers on the DND list.
$otp = (string) random_int(100000, 999999);
// Save a hash of $otp with the user's number and an expiry time.
$ch = curl_init('https://cloudsms.in/api/v1/sms/send');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Authorization: Bearer YOUR_API_KEY', 'Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode([
'template_id' => 'YOUR_OTP_TEMPLATE_ID',
'variables' => [$otp],
'to' => [$mobile],
]),
]);
$result = json_decode(curl_exec($ch), true);import crypto from "node:crypto";
const otp = String(crypto.randomInt(100000, 1000000));
// Save a hash of otp with the user's number and an expiry time.
await fetch("https://cloudsms.in/api/v1/sms/send", {
method: "POST",
headers: { Authorization: "Bearer YOUR_API_KEY", "Content-Type": "application/json" },
body: JSON.stringify({ template_id: "YOUR_OTP_TEMPLATE_ID", variables: [otp], to: [mobile] }),
});import secrets, requests
otp = str(secrets.randbelow(900000) + 100000)
# Save a hash of otp with the user's number and an expiry time.
requests.post(
"https://cloudsms.in/api/v1/sms/send",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={"template_id": "YOUR_OTP_TEMPLATE_ID", "variables": [otp], "to": [mobile]},
)Different values per number
To send each number its own values, such as a name and an amount, use messages instead of to and variables.
{
"template_id": "1207168xxxxxxxx412",
"messages": [
{ "to": "9876543210", "variables": ["Ravi", "1,200"] },
{ "to": "9123456780", "variables": ["Sita", "860"] }
]
}Send full text
If you prefer to send the final message yourself, pass message with the exact approved text and the DLT template_id. The sender ID must be added to your account in the panel. entity_id is optional; the sender's entity is used by default.
{
"sender": "ACMEIN",
"template_id": "1207168xxxxxxxx412",
"message": "Dear Ravi, 482913 is your login OTP for Acme. Do not share it.",
"to": "9876543210"
}The text must match the registered template exactly, or the operator blocks the message.
Check balance
GET /balance
{
"success": true,
"balance": 1000.00,
"sms_credits": 6666,
"rate": 0.15,
"currency": "INR"
}rate is your price per SMS credit in rupees, without GST.
List templates
GET /templates returns your sender IDs and templates with the number of values each one needs.
{
"success": true,
"templates": [
{
"template_id": "1207168xxxxxxxx412",
"name": "Login OTP",
"sender": "ACMEIN",
"entity_id": "1201160xxxxxxxx790",
"category": "service_implicit",
"variables": 2,
"content": "Dear {#var#}, {#var#} is your login OTP for Acme. Do not share it.",
"usable": true
}
]
}Delivery reports
GET /reports with one of these parameters:
| Parameter | Returns |
|---|---|
batch_id or request_id | The batch and the status of each number, 1,000 per page (page). |
mobile | The last 100 messages sent to one number. |
from, to | Your batches in a date range, for example from=2026-10-01&to=2026-10-07. |
{
"success": true,
"batch": { "batch_id": 1042, "status": "submitted", "numbers": 2, "...": "..." },
"page": 1,
"pages": 1,
"numbers": [
{
"mobile": "9876543210",
"status": "delivered",
"description": "Delivered successfully",
"sms_parts": 1,
"sent_at": "2026-10-22T10:15:02+05:30",
"delivered_at": "2026-10-22T10:15:05+05:30",
"request_id": "SLBZPIxP4mRVdf4"
}
]
}Number status is one of sent (waiting for the operator), delivered, failed, undelivered, rejected, expired or dnd.
Webhooks
Add your URL in the panel under API and webhooks. When delivery reports arrive, we POST them to your URL in groups of up to 200.
{
"events": [
{
"event": "sms.status",
"batch_id": 1042,
"sender": "ACMEIN",
"template_id": "1207168xxxxxxxx412",
"mobile": "9876543210",
"status": "delivered",
"description": "Delivered successfully",
"sms_parts": 1,
"sent_at": "2026-10-22T10:15:02+05:30",
"delivered_at": "2026-10-22T10:15:05+05:30",
"request_id": "SLBZPIxP4mRVdf4"
}
]
}Each request has two headers. X-CloudSMS-Timestamp is the Unix time, and X-CloudSMS-Signature is sha256= followed by the HMAC-SHA256 of timestamp + "." + body, keyed with your signing secret. Check it before trusting the data, and reply with any 2xx status. Failed deliveries are retried every minute, up to 5 times.
$body = file_get_contents('php://input');
$ts = $_SERVER['HTTP_X_CLOUDSMS_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_X_CLOUDSMS_SIGNATURE'] ?? '';
$good = 'sha256=' . hash_hmac('sha256', $ts . '.' . $body, 'YOUR_SIGNING_SECRET');
if (!hash_equals($good, $sig) || abs(time() - (int) $ts) > 300) {
http_response_code(401);
exit;
}
foreach (json_decode($body, true)['events'] as $event) {
// update your records with $event['mobile'] and $event['status']
}
http_response_code(200);import crypto from "node:crypto";
import express from "express";
const app = express();
app.post("/sms-status", express.raw({ type: "application/json" }), (req, res) => {
const ts = req.get("X-CloudSMS-Timestamp") || "";
const sig = req.get("X-CloudSMS-Signature") || "";
const good = "sha256=" + crypto.createHmac("sha256", "YOUR_SIGNING_SECRET")
.update(ts + "." + req.body).digest("hex");
if (sig.length !== good.length || !crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(good))) {
return res.sendStatus(401);
}
for (const event of JSON.parse(req.body).events) {
// update your records
}
res.sendStatus(200);
});Errors
Errors use a 4xx or 5xx status and this body:
{
"success": false,
"error": {
"code": "INSUFFICIENT_CREDITS",
"message": "This needs 600 SMS credits; the account has 555."
}
}| HTTP | Code | Meaning |
|---|---|---|
| 400 | INVALID_JSON | The body is not valid JSON. |
| 401 | UNAUTHORIZED | Missing or wrong API key. |
| 402 | INSUFFICIENT_CREDITS | Not enough SMS credits. Add balance. |
| 403 | IP_NOT_ALLOWED | The request came from an IP address not on your list. |
| 403 | ACCOUNT_NOT_ACTIVE | The account is waiting for approval or disabled. |
| 404 | TEMPLATE_NOT_FOUND | No active template with this template_id in your account. |
| 404 | NOT_FOUND | No batch for this batch_id or request_id. |
| 405 | METHOD_NOT_ALLOWED | Wrong HTTP method for the endpoint. |
| 422 | INVALID_REQUEST | A required field is missing or wrong. |
| 422 | INVALID_VARIABLES | A value is missing, empty or longer than 30 characters. |
| 422 | INVALID_NUMBERS | No valid mobile number in the request. |
| 422 | SENDER_NOT_ALLOWED | The sender ID is not in your account, or does not match the template. |
| 422 | TOO_MANY_NUMBERS | More numbers than allowed in one request. |
| 422 | INVALID_SCHEDULE | schedule_at is in the past or too far ahead. |
| 429 | RATE_LIMITED | Too many requests in a minute. |
| 502 | GATEWAY_ERROR | The operator network refused the message. The message explains why. |
Limits and SMS length
- 300 requests a minute per API key.
- Up to 1,00,000 numbers per request.
- Each value is up to 30 characters, a DLT rule.
- One SMS credit covers 160 characters in English, or 70 in Unicode (Hindi, Bengali and other scripts). Longer messages are split into parts of 153 or 67 characters, and each part uses one credit.
Questions about the API? Write to support@cloudsms.in.