SMS API
Send SMS and read their delivery results from your own systems. JSON over HTTPS. The panel is at /panel/api, where you create your keys.
Contents
Ready-made code · Authentication · Send · Message status · Delivery reports · Contacts · Groups · Blacklist · Balance · Limits · Errors
Ready-made code
The same steps in six languages: set up, send, check one message, read the delivery results, read the balance, send us your contacts, send to a group, and send safely with retries. Every example was run against the real API before it was published. Replace YOUR_KEY with your key (or set the environment variable SMS_API_KEY) and the sender with one approved on your account.
Needs only curl. The examples are shell commands.
Set up
# Keep the key in an environment variable, not in your scripts. export SMS_API_KEY="YOUR_KEY"
Send an SMS
curl -X POST https://api.isms.gr/v1/messages \
-H "Authorization: Bearer $SMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"sender": "MyShop", "to": ["306900000000"], "text": "Your order is on its way", "client_ref": "order-1042"}'
# 201 with the message when it is accepted. Any other answer is {"error": "...", "message": "..."}
Check one message
# By the id you received when you sent it curl https://api.isms.gr/v1/messages/KN-7K3M9-Q2XPD \ -H "Authorization: Bearer $SMS_API_KEY" # or by your own reference (this answers with the summary, without the list of recipients) curl "https://api.isms.gr/v1/messages?client_ref=order-1042" \ -H "Authorization: Bearer $SMS_API_KEY"
Read the delivery results
# First call. You get the results that nobody has confirmed yet, and an "ack". curl "https://api.isms.gr/v1/reports?limit=500" \ -H "Authorization: Bearer $SMS_API_KEY" # Store what you got, then confirm it by sending the ack back. You get the next batch. curl "https://api.isms.gr/v1/reports?limit=500&ack=THE_ACK_YOU_RECEIVED" \ -H "Authorization: Bearer $SMS_API_KEY" # An empty list means nothing is new: wait "next_poll_after" seconds before asking again.
Read the balance
curl https://api.isms.gr/v1/credits \
-H "Authorization: Bearer $SMS_API_KEY"
# {"credits": "12.34000", "currency": "EUR"}
Send your contacts
# The fields a contact can have, with their type (the administrator can add more at any time)
curl https://api.isms.gr/v1/contact-fields \
-H "Authorization: Bearer $SMS_API_KEY"
# Send contacts: up to 1,000 a request. A number we do not have is created, one we have is updated.
# "nextAppointment" is an example: use the names that /v1/contact-fields gives you.
curl -X PUT https://api.isms.gr/v1/contacts \
-H "Authorization: Bearer $SMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"contacts": [{"mobile": "306900000000", "name": "Maria", "surname": "P", "fields": {"nextAppointment": "2026-11-12T14:30:00Z"}, "groups": ["Clients"]}]}'
# {"created": 1, "updated": 0, "rejected": [], "warnings": [], ...}
# A contact we cannot accept is listed in "rejected" (index, error, message); the others are saved.
# Add "replace_groups": true to make each group named in the request hold exactly the contacts of that request.
Send to a group
# One message to every contact of a group, by its name. Retries are safe with client_ref, as for any send.
curl -X POST https://api.isms.gr/v1/messages \
-H "Authorization: Bearer $SMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"sender": "MyShop", "group": "Clients", "text": "Your appointment is tomorrow", "client_ref": "reminders-2026-11-11"}'
# 201 with the message; "recipients" is the number of contacts it goes to. Any other answer is {"error": "...", "message": "..."}
Send safely, with retries
# The same client_ref on every attempt: if an attempt reached us but you never saw the answer,
# the next one does not send or charge a second time (you get 200 and "duplicate": true).
for attempt in 1 2 3 4 5; do
code=$(curl -s -o /tmp/sms-answer.json -w "%{http_code}" --max-time 15 \
-X POST https://api.isms.gr/v1/messages \
-H "Authorization: Bearer $SMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{"sender": "MyShop", "to": ["306900000000"], "text": "Your order is on its way", "client_ref": "order-1042"}')
case "$code" in
200|201) cat /tmp/sms-answer.json; break ;; # sent (or already sent before)
429|503|000) sleep $((attempt * 2)) ;; # slow down, no provider for a moment, or no answer: try again
*) cat /tmp/sms-answer.json; break ;; # the request itself is wrong: retrying will not help
esac
done
Python 3.8 or newer and the requests package (pip install requests).
Set up
import os
import time
import requests
BASE = "https://api.isms.gr"
KEY = os.environ.get("SMS_API_KEY", "YOUR_KEY") # keep the key in an environment variable, not in your code
HEADERS = {"Authorization": f"Bearer {KEY}"}
Send an SMS
def send():
response = requests.post(
f"{BASE}/v1/messages",
headers=HEADERS,
json={
"sender": "MyShop",
"to": ["306900000000"],
"text": "Your order is on its way",
"client_ref": "order-1042", # your own reference: makes a retry safe (see the last example)
},
timeout=15,
)
data = response.json()
if response.status_code in (200, 201):
print(data["id"], data["status"], "cost", data["cost"], "again" if data["duplicate"] else "new")
return data["id"]
print("Refused:", data["error"], "-", data["message"])
Check one message
def status(message_id):
response = requests.get(f"{BASE}/v1/messages/{message_id}", headers=HEADERS, timeout=15)
data = response.json()
print(data["status"], data["counts"]) # for example {"queued": 0, "sent": 0, "delivered": 1, ...}
for row in data["items"]:
print(row["mobile"], row["status"])
# or by your own reference (this answers with the summary, without the list of recipients):
# requests.get(f"{BASE}/v1/messages", params={"client_ref": "order-1042"}, headers=HEADERS, timeout=15)
Read the delivery results
def read_reports(save, rounds=1):
"""save(report) is YOUR code: store the result, keyed by (message_id, mobile). Repeats are possible and harmless."""
ack = None
for _ in range(rounds): # in a real program: while True
params = {"limit": 500}
if ack:
params["ack"] = ack # confirms the previous batch and asks for the next
response = requests.get(f"{BASE}/v1/reports", headers=HEADERS, params=params, timeout=15)
if response.status_code == 429:
time.sleep(int(response.headers.get("Retry-After", 10)))
continue
response.raise_for_status()
batch = response.json()
for report in batch["reports"]:
save(report) # {"message_id", "client_ref", "mobile", "status", "reason", "at"}
ack = batch["ack"] # None when nothing was new
if not batch["reports"]:
time.sleep(batch["next_poll_after"])
Read the balance
def balance():
data = requests.get(f"{BASE}/v1/credits", headers=HEADERS, timeout=15).json()
print(data["credits"], data["currency"])
Send your contacts
def list_fields():
"""The fields a contact can have and their type. The administrator can add more at any time, so ask instead of writing the list into your code."""
data = requests.get(f"{BASE}/v1/contact-fields", headers=HEADERS, timeout=15).json()
for item in data["fields"]:
print(item["name"], item["type"]) # for example: nextAppointment date
def push_contacts():
"""Up to 1,000 contacts a request. A number we do not have is created, one we have is updated; what you do not send stays as it was."""
response = requests.put(
f"{BASE}/v1/contacts",
headers=HEADERS,
json={
"contacts": [{
"mobile": "306900000000",
"name": "Maria",
"surname": "P",
"fields": {"nextAppointment": "2026-11-12T14:30:00Z"}, # an example: use the names from list_fields()
"groups": ["Clients"], # created when it does not exist yet
}],
# "replace_groups": True, # each group named here then holds exactly the contacts of this request
},
timeout=15,
)
data = response.json()
if response.status_code != 200:
print("Refused:", data["error"], "-", data["message"])
return
print("created", data["created"], "updated", data["updated"], "rejected", len(data["rejected"]))
for row in data["rejected"]: # a contact we cannot accept; the others were saved
print(" #%d %s: %s" % (row["index"], row["error"], row["message"]))
for row in data["warnings"]: # saved, but worth a look, for example birthday_not_read
print(" warning", row["mobile"], row["warning"])
Send to a group
def send_to_group():
"""One message to every contact of a group, by its name. The members are those at the moment of the request."""
response = requests.post(
f"{BASE}/v1/messages",
headers=HEADERS,
json={"sender": "MyShop", "group": "Clients", "text": "Your appointment is tomorrow", "client_ref": "reminders-2026-11-11"},
timeout=15,
)
data = response.json()
if response.status_code in (200, 201):
print(data["id"], data["status"], "to", data["recipients"], "contacts, cost", data["cost"], "again" if data["duplicate"] else "new")
return data["id"]
print("Refused:", data["error"], "-", data["message"])
Send safely, with retries
def send_safely(sender, numbers, text, client_ref):
"""Retries only what can succeed on a retry. The same client_ref on every attempt means no second SMS and no second charge."""
for attempt in range(5):
try:
response = requests.post(
f"{BASE}/v1/messages", headers=HEADERS, timeout=15,
json={"sender": sender, "to": numbers, "text": text, "client_ref": client_ref},
)
except requests.RequestException: # timeout or no connection: we do not know if it arrived
time.sleep(2 ** attempt)
continue
data = response.json()
if response.status_code in (200, 201):
return data # data["duplicate"] is True when an earlier attempt had arrived
if response.status_code == 429:
time.sleep(int(response.headers.get("Retry-After", 5)))
elif response.status_code == 503: # no provider for a moment: nothing was charged
time.sleep(2 ** attempt)
else: # 400, 401, 402, 403 ...: the request itself must change
raise RuntimeError(f'{data["error"]}: {data["message"]}')
raise RuntimeError("The service could not be reached. Try again later with the same client_ref.")
PHP 8 with the curl extension (standard on most servers). Run with php sms.php.
Set up
<?php
const BASE = 'https://api.isms.gr';
$key = getenv('SMS_API_KEY') ?: 'YOUR_KEY'; // keep the key in an environment variable, not in your code
/** One request. Returns [HTTP status, decoded JSON, Retry-After seconds or null]. Throws when there is no answer at all. */
function api(string $method, string $path, ?array $body = null): array
{
global $key;
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $key, 'Content-Type: application/json'],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HEADER => true,
CURLOPT_TIMEOUT => 15,
]);
if ($body !== null) {
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
}
$raw = curl_exec($ch);
if ($raw === false) {
$message = curl_error($ch);
curl_close($ch);
throw new RuntimeException($message);
}
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
curl_close($ch);
$retryAfter = null;
foreach (explode("\r\n", substr($raw, 0, $headerSize)) as $line) {
if (stripos($line, 'Retry-After:') === 0) {
$retryAfter = (int) trim(substr($line, 12));
}
}
return [$status, json_decode(substr($raw, $headerSize), true), $retryAfter];
}
Send an SMS
function sendSms(): ?string
{
[$status, $data] = api('POST', '/v1/messages', [
'sender' => 'MyShop',
'to' => ['306900000000'],
'text' => 'Your order is on its way',
'client_ref' => 'order-1042', // your own reference: makes a retry safe (see the last example)
]);
if ($status === 200 || $status === 201) {
echo $data['id'], ' ', $data['status'], ' cost ', $data['cost'], ' ', $data['duplicate'] ? 'again' : 'new', "\n";
return $data['id'];
}
echo 'Refused: ', $data['error'], ' - ', $data['message'], "\n";
return null;
}
Check one message
function messageStatus(string $id): void
{
[, $data] = api('GET', '/v1/messages/' . rawurlencode($id));
echo $data['status'], ' ', json_encode($data['counts']), "\n"; // for example {"queued":0,"sent":0,"delivered":1,...}
foreach ($data['items'] as $row) {
echo $row['mobile'], ' ', $row['status'], "\n";
}
}
// or by your own reference (this answers with the summary, without the list of recipients):
// api('GET', '/v1/messages?client_ref=' . rawurlencode('order-1042'));
Read the delivery results
/** $save is YOUR function: store the result, keyed by message_id and mobile. Repeats are possible and harmless. */
function readReports(callable $save, int $rounds = 1): void
{
$ack = null;
for ($i = 0; $i < $rounds; $i++) { // in a real program: while (true)
$path = '/v1/reports?limit=500' . ($ack ? '&ack=' . rawurlencode($ack) : ''); // the ack confirms the previous batch
[$status, $batch, $retryAfter] = api('GET', $path);
if ($status === 429) {
sleep($retryAfter ?? 10);
continue;
}
foreach ($batch['reports'] as $report) {
$save($report); // message_id, client_ref, mobile, status, reason, at
}
$ack = $batch['ack']; // null when nothing was new
if (!$batch['reports']) {
sleep($batch['next_poll_after']);
}
}
}
Read the balance
function balance(): void
{
[, $data] = api('GET', '/v1/credits');
echo $data['credits'], ' ', $data['currency'], "\n";
}
Send your contacts
function listFields(): void
{
// The fields a contact can have and their type. The administrator can add more at any time, so ask instead of writing the list into your code.
[, $data] = api('GET', '/v1/contact-fields');
foreach ($data['fields'] as $item) {
echo $item['name'], ' ', $item['type'], "\n"; // for example: nextAppointment date
}
}
/** Up to 1,000 contacts a request. A number we do not have is created, one we have is updated; what you do not send stays as it was. */
function pushContacts(): void
{
[$status, $data] = api('PUT', '/v1/contacts', [
'contacts' => [[
'mobile' => '306900000000',
'name' => 'Maria',
'surname' => 'P',
'fields' => ['nextAppointment' => '2026-11-12T14:30:00Z'], // an example: use the names from listFields()
'groups' => ['Clients'], // created when it does not exist yet
]],
// 'replace_groups' => true, // each group named here then holds exactly the contacts of this request
]);
if ($status !== 200) {
echo 'Refused: ', $data['error'], ' - ', $data['message'], "\n";
return;
}
echo 'created ', $data['created'], ' updated ', $data['updated'], ' rejected ', count($data['rejected']), "\n";
foreach ($data['rejected'] as $row) { // a contact we cannot accept; the others were saved
echo ' #', $row['index'], ' ', $row['error'], ': ', $row['message'], "\n";
}
foreach ($data['warnings'] as $row) { // saved, but worth a look, for example birthday_not_read
echo ' warning ', $row['mobile'], ' ', $row['warning'], "\n";
}
}
Send to a group
/** One message to every contact of a group, by its name. The members are those at the moment of the request. */
function sendToGroup(): ?string
{
[$status, $data] = api('POST', '/v1/messages', [
'sender' => 'MyShop', 'group' => 'Clients', 'text' => 'Your appointment is tomorrow', 'client_ref' => 'reminders-2026-11-11',
]);
if ($status === 200 || $status === 201) {
echo $data['id'], ' ', $data['status'], ' to ', $data['recipients'], ' contacts, cost ', $data['cost'], ' ', $data['duplicate'] ? 'again' : 'new', "\n";
return $data['id'];
}
echo 'Refused: ', $data['error'], ' - ', $data['message'], "\n";
return null;
}
Send safely, with retries
/** Retries only what can succeed on a retry. The same client_ref on every attempt means no second SMS and no second charge. */
function sendSafely(string $sender, array $numbers, string $text, string $clientRef): array
{
for ($attempt = 0; $attempt < 5; $attempt++) {
try {
[$status, $data, $retryAfter] = api('POST', '/v1/messages', [
'sender' => $sender, 'to' => $numbers, 'text' => $text, 'client_ref' => $clientRef,
]);
} catch (RuntimeException $e) { // timeout or no connection: we do not know if it arrived
sleep(2 ** $attempt);
continue;
}
if ($status === 200 || $status === 201) {
return $data; // $data['duplicate'] is true when an earlier attempt had arrived
}
if ($status === 429) {
sleep($retryAfter ?? 5);
} elseif ($status === 503) { // no provider for a moment: nothing was charged
sleep(2 ** $attempt);
} else { // 400, 401, 402, 403 ...: the request itself must change
throw new RuntimeException($data['error'] . ': ' . $data['message']);
}
}
throw new RuntimeException('The service could not be reached. Try again later with the same client_ref.');
}
Node.js 18 or newer (it has fetch built in). Save as sms.mjs and run with node sms.mjs.
Set up
const BASE = "https://api.isms.gr";
const KEY = process.env.SMS_API_KEY || "YOUR_KEY"; // keep the key in an environment variable, not in your code
const HEADERS = { Authorization: `Bearer ${KEY}`, "Content-Type": "application/json" };
const sleep = (seconds) => new Promise((resolve) => setTimeout(resolve, seconds * 1000));
Send an SMS
async function send() {
const response = await fetch(`${BASE}/v1/messages`, {
method: "POST",
headers: HEADERS,
body: JSON.stringify({
sender: "MyShop",
to: ["306900000000"],
text: "Your order is on its way",
client_ref: "order-1042", // your own reference: makes a retry safe (see the last example)
}),
signal: AbortSignal.timeout(15000),
});
const data = await response.json();
if (response.ok) {
console.log(data.id, data.status, "cost", data.cost, data.duplicate ? "again" : "new");
return data.id;
}
console.log("Refused:", data.error, "-", data.message);
}
Check one message
async function status(messageId) {
const response = await fetch(`${BASE}/v1/messages/${encodeURIComponent(messageId)}`, { headers: HEADERS, signal: AbortSignal.timeout(15000) });
const data = await response.json();
console.log(data.status, data.counts); // for example { queued: 0, sent: 0, delivered: 1, ... }
for (const row of data.items) console.log(row.mobile, row.status);
}
// or by your own reference (this answers with the summary, without the list of recipients):
// fetch(`${BASE}/v1/messages?client_ref=${encodeURIComponent("order-1042")}`, { headers: HEADERS });
Read the delivery results
// save(report) is YOUR function: store the result, keyed by message_id and mobile. Repeats are possible and harmless.
async function readReports(save, rounds = 1) {
let ack = null;
for (let i = 0; i < rounds; i++) { // in a real program: while (true)
const url = `${BASE}/v1/reports?limit=500` + (ack ? `&ack=${encodeURIComponent(ack)}` : ""); // the ack confirms the previous batch
const response = await fetch(url, { headers: HEADERS, signal: AbortSignal.timeout(15000) });
if (response.status === 429) {
await sleep(Number(response.headers.get("Retry-After") || 10));
continue;
}
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const batch = await response.json();
for (const report of batch.reports) await save(report); // message_id, client_ref, mobile, status, reason, at
ack = batch.ack; // null when nothing was new
if (batch.reports.length === 0) await sleep(batch.next_poll_after);
}
}
Read the balance
async function balance() {
const response = await fetch(`${BASE}/v1/credits`, { headers: HEADERS, signal: AbortSignal.timeout(15000) });
const data = await response.json();
console.log(data.credits, data.currency);
}
Send your contacts
// The fields a contact can have and their type. The administrator can add more at any time, so ask instead of writing the list into your code.
async function listFields() {
const response = await fetch(`${BASE}/v1/contact-fields`, { headers: HEADERS, signal: AbortSignal.timeout(15000) });
const data = await response.json();
for (const item of data.fields) console.log(item.name, item.type); // for example: nextAppointment date
}
// Up to 1,000 contacts a request. A number we do not have is created, one we have is updated; what you do not send stays as it was.
async function pushContacts() {
const response = await fetch(`${BASE}/v1/contacts`, {
method: "PUT",
headers: HEADERS,
body: JSON.stringify({
contacts: [{
mobile: "306900000000",
name: "Maria",
surname: "P",
fields: { nextAppointment: "2026-11-12T14:30:00Z" }, // an example: use the names from listFields()
groups: ["Clients"], // created when it does not exist yet
}],
// replace_groups: true, // each group named here then holds exactly the contacts of this request
}),
signal: AbortSignal.timeout(15000),
});
const data = await response.json();
if (!response.ok) {
console.log("Refused:", data.error, "-", data.message);
return;
}
console.log("created", data.created, "updated", data.updated, "rejected", data.rejected.length);
for (const row of data.rejected) console.log(` #${row.index} ${row.error}: ${row.message}`); // a contact we cannot accept; the others were saved
for (const row of data.warnings) console.log(" warning", row.mobile, row.warning); // saved, but worth a look, for example birthday_not_read
}
Send to a group
// One message to every contact of a group, by its name. The members are those at the moment of the request.
async function sendToGroup() {
const response = await fetch(`${BASE}/v1/messages`, {
method: "POST",
headers: HEADERS,
body: JSON.stringify({ sender: "MyShop", group: "Clients", text: "Your appointment is tomorrow", client_ref: "reminders-2026-11-11" }),
signal: AbortSignal.timeout(15000),
});
const data = await response.json();
if (response.ok) {
console.log(data.id, data.status, "to", data.recipients, "contacts, cost", data.cost, data.duplicate ? "again" : "new");
return data.id;
}
console.log("Refused:", data.error, "-", data.message);
}
Send safely, with retries
// Retries only what can succeed on a retry. The same client_ref on every attempt means no second SMS and no second charge.
async function sendSafely(sender, numbers, text, clientRef) {
for (let attempt = 0; attempt < 5; attempt++) {
let response;
try {
response = await fetch(`${BASE}/v1/messages`, {
method: "POST",
headers: HEADERS,
body: JSON.stringify({ sender, to: numbers, text, client_ref: clientRef }),
signal: AbortSignal.timeout(15000),
});
} catch (error) { // timeout or no connection: we do not know if it arrived
await sleep(2 ** attempt);
continue;
}
const data = await response.json();
if (response.ok) return data; // data.duplicate is true when an earlier attempt had arrived
if (response.status === 429) await sleep(Number(response.headers.get("Retry-After") || 5));
else if (response.status === 503) await sleep(2 ** attempt); // no provider for a moment: nothing was charged
else throw new Error(`${data.error}: ${data.message}`); // 400, 401, 402, 403 ...: the request itself must change
}
throw new Error("The service could not be reached. Try again later with the same client_ref.");
}
Java 11 or newer, no libraries. Save as Sms.java and run with java Sms.java. The small field() reader is only for these examples: use Jackson or Gson in your own code.
Set up
import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class Sms {
static final String BASE = "https://api.isms.gr";
static final String KEY = System.getenv().getOrDefault("SMS_API_KEY", "YOUR_KEY"); // keep the key in an environment variable
static final HttpClient CLIENT = HttpClient.newHttpClient();
/** One request. Returns the response (status code, headers, body as text). Throws when there is no answer at all. */
static HttpResponse<String> call(String method, String path, String json) throws Exception {
HttpRequest.Builder request = HttpRequest.newBuilder(URI.create(BASE + path))
.timeout(Duration.ofSeconds(15))
.header("Authorization", "Bearer " + KEY)
.header("Content-Type", "application/json");
request = json == null
? request.method(method, HttpRequest.BodyPublishers.noBody())
: request.method(method, HttpRequest.BodyPublishers.ofString(json, StandardCharsets.UTF_8));
return CLIENT.send(request.build(), HttpResponse.BodyHandlers.ofString());
}
/** Reads one simple field of a JSON answer (text, number, true/false or null). For these examples only: use Jackson or Gson in your own code. */
static String field(String json, String name) {
Matcher m = Pattern.compile("\"" + name + "\"\\s*:\\s*(\"((?:[^\"\\\\]|\\\\.)*)\"|[0-9.]+|true|false|null)").matcher(json);
if (!m.find()) return null;
if (m.group(2) != null) return m.group(2);
return m.group(1).equals("null") ? null : m.group(1);
}
// The methods below go inside this class, before the last closing brace.
Send an SMS
static String send() throws Exception {
String body = "{\"sender\":\"MyShop\",\"to\":[\"306900000000\"],\"text\":\"Your order is on its way\",\"client_ref\":\"order-1042\"}";
HttpResponse<String> response = call("POST", "/v1/messages", body); // client_ref: your own reference, makes a retry safe
if (response.statusCode() == 200 || response.statusCode() == 201) {
System.out.println(field(response.body(), "id") + " " + field(response.body(), "status") + " cost " + field(response.body(), "cost")
+ (field(response.body(), "duplicate").equals("true") ? " again" : " new"));
return field(response.body(), "id");
}
System.out.println("Refused: " + field(response.body(), "error") + " - " + field(response.body(), "message"));
return null;
}
Check one message
static void status(String messageId) throws Exception {
HttpResponse<String> response = call("GET", "/v1/messages/" + URLEncoder.encode(messageId, StandardCharsets.UTF_8), null);
System.out.println(response.body()); // status, counts per recipient state, and the recipients (mobile, status)
}
// or by your own reference (the summary, without the list of recipients): call("GET", "/v1/messages?client_ref=" + URLEncoder.encode("order-1042", StandardCharsets.UTF_8), null)
Read the delivery results
interface Saver { void save(String reportJson); }
/** save is YOUR code: store the result, keyed by message_id and mobile. Repeats are possible and harmless. */
static void readReports(Saver saver, int rounds) throws Exception {
String ack = null;
for (int i = 0; i < rounds; i++) { // in a real program: while (true)
String path = "/v1/reports?limit=500" + (ack != null ? "&ack=" + URLEncoder.encode(ack, StandardCharsets.UTF_8) : ""); // the ack confirms the previous batch
HttpResponse<String> response = call("GET", path, null);
if (response.statusCode() == 429) {
Thread.sleep(Long.parseLong(response.headers().firstValue("Retry-After").orElse("10")) * 1000);
continue;
}
if (response.statusCode() != 200) throw new RuntimeException("HTTP " + response.statusCode());
String body = response.body();
Matcher report = Pattern.compile("\\{\"message_id\":[^}]*\\}").matcher(body); // one report: message_id, client_ref, mobile, status, reason, at
boolean any = false;
while (report.find()) { saver.save(report.group()); any = true; }
ack = field(body, "ack"); // null when nothing was new
if (!any) Thread.sleep(Long.parseLong(field(body, "next_poll_after")) * 1000);
}
}
Read the balance
static void balance() throws Exception {
HttpResponse<String> response = call("GET", "/v1/credits", null);
System.out.println(field(response.body(), "credits") + " " + field(response.body(), "currency"));
}
Send your contacts
static void listFields() throws Exception {
// The fields a contact can have and their type. The administrator can add more at any time, so ask instead of writing the list into your code.
System.out.println(call("GET", "/v1/contact-fields", null).body()); // {"fields":[{"name":"nextAppointment","type":"date"}, ...]}
}
/** Up to 1,000 contacts a request. A number we do not have is created, one we have is updated; what you do not send stays as it was. */
static void pushContacts() throws Exception {
String body = "{\"contacts\":[{\"mobile\":\"306900000000\",\"name\":\"Maria\",\"surname\":\"P\","
+ "\"fields\":{\"nextAppointment\":\"2026-11-12T14:30:00Z\"}," // an example: use the names from listFields()
+ "\"groups\":[\"Clients\"]}]}"; // a group is created when it does not exist yet
HttpResponse<String> response = call("PUT", "/v1/contacts", body); // add "replace_groups":true to make each group named here hold exactly these contacts
String answer = response.body();
if (response.statusCode() != 200) {
System.out.println("Refused: " + field(answer, "error") + " - " + field(answer, "message"));
return;
}
System.out.println("created " + field(answer, "created") + " updated " + field(answer, "updated") + " rejected " + (answer.contains("\"rejected\":[]") ? "0" : "some, see below"));
if (!answer.contains("\"rejected\":[]")) System.out.println(answer); // a contact we cannot accept is listed with index, error and message; the others were saved
}
Send to a group
/** One message to every contact of a group, by its name. The members are those at the moment of the request. */
static String sendToGroup() throws Exception {
String body = "{\"sender\":\"MyShop\",\"group\":\"Clients\",\"text\":\"Your appointment is tomorrow\",\"client_ref\":\"reminders-2026-11-11\"}";
HttpResponse<String> response = call("POST", "/v1/messages", body);
String answer = response.body();
if (response.statusCode() == 200 || response.statusCode() == 201) {
System.out.println(field(answer, "id") + " " + field(answer, "status") + " to " + field(answer, "recipients") + " contacts, cost " + field(answer, "cost")
+ (field(answer, "duplicate").equals("true") ? " again" : " new"));
return field(answer, "id");
}
System.out.println("Refused: " + field(answer, "error") + " - " + field(answer, "message"));
return null;
}
Send safely, with retries
/** Retries only what can succeed on a retry. The same client_ref on every attempt means no second SMS and no second charge. */
static String sendSafely(String sender, String number, String text, String clientRef) throws Exception {
String body = "{\"sender\":\"" + sender + "\",\"to\":[\"" + number + "\"],\"text\":\"" + text.replace("\\", "\\\\").replace("\"", "\\\"")
+ "\",\"client_ref\":\"" + clientRef + "\"}";
for (int attempt = 0; attempt < 5; attempt++) {
HttpResponse<String> response;
try {
response = call("POST", "/v1/messages", body);
} catch (java.io.IOException e) { // timeout or no connection: we do not know if it arrived
Thread.sleep((1L << attempt) * 1000);
continue;
}
int status = response.statusCode();
if (status == 200 || status == 201) return response.body(); // "duplicate": true when an earlier attempt had arrived
if (status == 429) Thread.sleep(Long.parseLong(response.headers().firstValue("Retry-After").orElse("5")) * 1000);
else if (status == 503) Thread.sleep((1L << attempt) * 1000); // no provider for a moment: nothing was charged
else throw new RuntimeException(field(response.body(), "error") + ": " + field(response.body(), "message")); // 400, 401, 402, 403 ...: the request itself must change
}
throw new RuntimeException("The service could not be reached. Try again later with the same client_ref.");
}
Go 1.20 or newer, standard library only. Save as main.go and run with go run main.go.
Set up
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"time"
)
const base = "https://api.isms.gr"
var key = envOr("SMS_API_KEY", "YOUR_KEY") // keep the key in an environment variable, not in your code
var client = &http.Client{Timeout: 15 * time.Second}
func envOr(name, fallback string) string {
if value := os.Getenv(name); value != "" {
return value
}
return fallback
}
// call makes one request. It returns the HTTP status, the decoded JSON answer and the Retry-After header in seconds (0 when absent).
// An error means there was no answer at all.
func call(method, path string, body any) (int, map[string]any, int, error) {
var reader io.Reader
if body != nil {
raw, err := json.Marshal(body)
if err != nil {
return 0, nil, 0, err
}
reader = bytes.NewReader(raw)
}
request, err := http.NewRequest(method, base+path, reader)
if err != nil {
return 0, nil, 0, err
}
request.Header.Set("Authorization", "Bearer "+key)
request.Header.Set("Content-Type", "application/json")
response, err := client.Do(request)
if err != nil {
return 0, nil, 0, err
}
defer response.Body.Close()
var data map[string]any
_ = json.NewDecoder(response.Body).Decode(&data)
wait, _ := strconv.Atoi(response.Header.Get("Retry-After"))
return response.StatusCode, data, wait, nil
}
Send an SMS
func send() (string, error) {
status, data, _, err := call("POST", "/v1/messages", map[string]any{
"sender": "MyShop",
"to": []string{"306900000000"},
"text": "Your order is on its way",
"client_ref": "order-1042", // your own reference: makes a retry safe (see the last example)
})
if err != nil {
return "", err
}
if status == 200 || status == 201 {
again := "new"
if data["duplicate"] == true {
again = "again"
}
fmt.Println(data["id"], data["status"], "cost", data["cost"], again)
return data["id"].(string), nil
}
fmt.Println("Refused:", data["error"], "-", data["message"])
return "", nil
}
Check one message
func messageStatus(id string) error {
_, data, _, err := call("GET", "/v1/messages/"+url.PathEscape(id), nil)
if err != nil {
return err
}
fmt.Println(data["status"], data["counts"]) // for example map[queued:0 sent:0 delivered:1 ...]
for _, row := range data["items"].([]any) {
item := row.(map[string]any)
fmt.Println(item["mobile"], item["status"])
}
return nil
}
// or by your own reference (the summary, without the list of recipients): call("GET", "/v1/messages?client_ref="+url.QueryEscape("order-1042"), nil)
Read the delivery results
// save is YOUR function: store the result, keyed by message_id and mobile. Repeats are possible and harmless.
func readReports(save func(report map[string]any), rounds int) error {
ack := ""
for i := 0; i < rounds; i++ { // in a real program: for {
path := "/v1/reports?limit=500"
if ack != "" {
path += "&ack=" + url.QueryEscape(ack) // confirms the previous batch and asks for the next
}
status, batch, wait, err := call("GET", path, nil)
if err != nil {
return err
}
if status == 429 {
if wait == 0 {
wait = 10
}
time.Sleep(time.Duration(wait) * time.Second)
continue
}
if status != 200 {
return fmt.Errorf("HTTP %d", status)
}
reports, _ := batch["reports"].([]any)
for _, item := range reports {
save(item.(map[string]any)) // message_id, client_ref, mobile, status, reason, at
}
ack, _ = batch["ack"].(string) // empty when nothing was new
if len(reports) == 0 {
time.Sleep(time.Duration(batch["next_poll_after"].(float64)) * time.Second)
}
}
return nil
}
Read the balance
func balance() error {
_, data, _, err := call("GET", "/v1/credits", nil)
if err != nil {
return err
}
fmt.Println(data["credits"], data["currency"])
return nil
}
Send your contacts
// listFields prints the fields a contact can have and their type. The administrator can add more at any time, so ask instead of writing the list into your code.
func listFields() error {
_, data, _, err := call("GET", "/v1/contact-fields", nil)
if err != nil {
return err
}
for _, entry := range data["fields"].([]any) {
item := entry.(map[string]any)
fmt.Println(item["name"], item["type"]) // for example: nextAppointment date
}
return nil
}
// pushContacts sends up to 1,000 contacts a request. A number we do not have is created, one we have is updated; what you do not send stays as it was.
func pushContacts() error {
status, data, _, err := call("PUT", "/v1/contacts", map[string]any{
"contacts": []map[string]any{{
"mobile": "306900000000",
"name": "Maria",
"surname": "P",
"fields": map[string]any{"nextAppointment": "2026-11-12T14:30:00Z"}, // an example: use the names from listFields()
"groups": []string{"Clients"}, // created when it does not exist yet
}},
// "replace_groups": true, // each group named here then holds exactly the contacts of this request
})
if err != nil {
return err
}
if status != 200 {
fmt.Println("Refused:", data["error"], "-", data["message"])
return nil
}
rejected, _ := data["rejected"].([]any)
fmt.Println("created", data["created"], "updated", data["updated"], "rejected", len(rejected))
for _, entry := range rejected { // a contact we cannot accept; the others were saved
row := entry.(map[string]any)
fmt.Printf(" #%v %v: %v\n", row["index"], row["error"], row["message"])
}
warnings, _ := data["warnings"].([]any)
for _, entry := range warnings { // saved, but worth a look, for example birthday_not_read
row := entry.(map[string]any)
fmt.Println(" warning", row["mobile"], row["warning"])
}
return nil
}
Send to a group
// sendToGroup sends one message to every contact of a group, by its name. The members are those at the moment of the request.
func sendToGroup() (string, error) {
status, data, _, err := call("POST", "/v1/messages", map[string]any{
"sender": "MyShop", "group": "Clients", "text": "Your appointment is tomorrow", "client_ref": "reminders-2026-11-11",
})
if err != nil {
return "", err
}
if status == 200 || status == 201 {
again := "new"
if data["duplicate"] == true {
again = "again"
}
fmt.Println(data["id"], data["status"], "to", data["recipients"], "contacts, cost", data["cost"], again)
return data["id"].(string), nil
}
fmt.Println("Refused:", data["error"], "-", data["message"])
return "", nil
}
Send safely, with retries
// sendSafely retries only what can succeed on a retry. The same client_ref on every attempt means no second SMS and no second charge.
func sendSafely(sender string, numbers []string, text, clientRef string) (map[string]any, error) {
for attempt := 0; attempt < 5; attempt++ {
status, data, wait, err := call("POST", "/v1/messages", map[string]any{
"sender": sender, "to": numbers, "text": text, "client_ref": clientRef,
})
if err != nil { // timeout or no connection: we do not know if it arrived
time.Sleep(time.Duration(1<<attempt) * time.Second)
continue
}
switch {
case status == 200 || status == 201:
return data, nil // data["duplicate"] is true when an earlier attempt had arrived
case status == 429:
if wait == 0 {
wait = 5
}
time.Sleep(time.Duration(wait) * time.Second)
case status == 503: // no provider for a moment: nothing was charged
time.Sleep(time.Duration(1<<attempt) * time.Second)
default: // 400, 401, 402, 403 ...: the request itself must change
return nil, fmt.Errorf("%v: %v", data["error"], data["message"])
}
}
return nil, fmt.Errorf("the service could not be reached; try again later with the same client_ref")
}
Authentication
Create a key in the panel (API page). It is shown once and cannot be recovered, so store it safely. Send it in every request:
Authorization: Bearer kn_xxxxxxxxxxxxxxxxxxxxxxxx
A key has four rights: send (send SMS and read the balance), read (read message status and delivery reports), blacklist (see and change your blacklist; off by default) and contacts (send us your contacts and manage your groups; off by default). Give each system only what it needs. You can restrict a key to a list of IP addresses, and revoke it at any time. Every request must use HTTPS.
You must sign in to your account to create a key. If you use the API, it is your responsibility to check the Terms of use from time to time and to accept them when they change.
Send: POST /v1/messages
curl -X POST https://api.isms.gr/v1/messages \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"sender": "MyShop", "to": ["306900000000"], "text": "Your order is on its way", "client_ref": "order-1042"}'
| Field | Required | Meaning |
|---|---|---|
sender | yes | Up to 11 Latin letters, digits or dots (it starts and ends with a letter or digit, for example iSMS.gr), or up to 15 digits. It must be a sender approved on your account. |
group | instead of to | The name of one of your groups (capitals do not matter). The message goes to the contacts in the group at the moment of the request. See Groups. Give to or group, not both. |
to | yes, or group | List of mobile numbers, each with its country code (for example 306900000000; a leading + or 00 is accepted). A number without its country code, or one that is not a valid mobile number, is refused with invalid_number and the whole request is rejected: nothing is sent or charged. A single text is accepted for one number. Up to 1,000 numbers per request. |
text | yes | The message. Stored exactly as sent. |
unicode | no | true to send as Unicode (70 characters per part, then 67). Default false: GSM (160, then 153). |
schedule_at | no | ISO 8601 time, for example 2026-10-11T19:30:00Z. |
client_ref | no | Your own reference, 1 to 64 characters, unique per account. See below. |
{
"id": "KN-7K3M9-Q2XPD",
"client_ref": "order-1042",
"status": "queued",
"created_at": "2026-10-12T08:20:30Z",
"scheduled_at": null,
"cost": "0.03572",
"recipients": 1,
"counts": {"queued": 1, "sent": 0, "delivered": 0, "failed": 0, "unknown": 0, "blocked": 0},
"duplicate": false
}
id is the reference of the message: 10 random characters, written KN-XXXXX-XXXXX. It is the same reference the customer sees in the panel. It does not show how many messages exist. Keep it as text; capital letters and hyphens may be left out when you send it back.
The first time you get 201. The price is charged when the message is accepted, to the account balance.
Retries are safe with client_ref. If a request times out and you do not know whether it arrived, send it again with the same client_ref. No second SMS is sent or charged: you get 200 with the first message and "duplicate": true. A client_ref is remembered permanently.
Message status
curl https://api.isms.gr/v1/messages/KN-7K3M9-Q2XPD \ -H "Authorization: Bearer YOUR_KEY" curl "https://api.isms.gr/v1/messages?client_ref=order-1042" \ -H "Authorization: Bearer YOUR_KEY"
Returns the same summary as sending, with page, pages and items (mobile and status for each recipient, 1,000 per page, ?page=2 for the next). The same message can be asked about at most 10 times a minute. For many messages use the delivery reports below instead of asking one by one.
Recipient statuses:
| Status | Meaning |
|---|---|
queued | Accepted, not handed to the operator yet. |
sent | Handed to the operator, or possibly handed (we did not get a clear answer), and no final result yet. Do not send it again: it may already have been delivered. |
delivered | Final. The handset received it. |
failed | Final. It was not delivered. See reason in the delivery report. |
blocked | Final. The number was on your blacklist when the message was due. It was not sent and is still charged. |
unknown | Final. No result arrived within 72 hours. The message may or may not have been delivered. It is still charged. |
Delivery reports: GET /v1/reports
The recommended way to learn the result of your messages. You receive each final result once, in small batches, and you do not need to track anything yourself. It covers messages sent through the API. Needs the read right.
curl "https://api.isms.gr/v1/reports?limit=500" \ -H "Authorization: Bearer YOUR_KEY"
{
"reports": [
{"message_id": "KN-7K3M9-Q2XPD", "client_ref": "order-1042", "mobile": "306900000000",
"status": "delivered", "reason": null, "at": "2026-10-12T08:20:36Z"}
],
"count": 1,
"ack": "tVEgYU10LopOB6biUNbftQ",
"next_poll_after": 5
}
- Process the batch, then call again with
?ack=<the ack you received>. This confirms the batch and returns the next one. - If you do not send the
ack(your system crashed before saving), you get the same batch again. Nothing is lost. Handle repeats bymessage_idandmobile. - When nothing is new you get an empty list and
ack: null. Waitnext_poll_afterseconds before asking again. It grows from 10 up to 60 seconds while nothing arrives, and returns to 5 when there are reports.
status is delivered, failed, unknown or blocked (reason is then blocked too). For failed, reason is one of:
| Reason | Meaning |
|---|---|
invalid_number | The number does not exist or is malformed. |
unreachable | The handset was off or out of coverage until the message expired. |
rejected | The operator refused the message. |
blocked | Blocked by the operator or the recipient (a number on your own blacklist gets the status blocked instead). |
expired | The message expired before delivery. |
other | Any other failure. |
limit is 500 by default and at most 1,000. Reports you have confirmed are kept for 30 days, then removed.
Contacts: /v1/contacts
Keep your contacts in step with your own application: it sends them to us, and we keep a copy in your account. One direction: your application is the source. A contact is known by its mobile number only. Needs the contacts right.
curl -X PUT https://api.isms.gr/v1/contacts \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"contacts": [{"mobile": "306900000000", "name": "Maria", "surname": "P",
"fields": {"nextAppointment": "2026-11-12T14:30:00Z"}, "groups": ["Clients"]}]}'
{"created": 1, "updated": 0, "rejected": [], "warnings": [], "groups_created": 1, "removed_from_groups": 0, "blacklisted": 0}
- Create or update, up to 1,000 contacts a request. A number we do not have is created; one we have is updated. What you do not send stays as it was. An empty value (
""ornull) removes a field. A new number is a new contact: the old one stays until you delete it. - One bad contact does not stop the others. It is listed in
rejectedwith its position in your list (index), the number you gave, anerrorand amessage:invalid_number,unknown_field,invalid_field,invalid_group,invalid_request,duplicate_mobile(the same number twice in one request: the first is used). nameandsurname: text, up to 100 characters.fields: only the fields of this service, see below.groups: names of groups; a group that does not exist is created (see Groups).- Consent. For each contact that is new, or newly in a group, we record that your account declared consent through the API (the date, the key and the group). You are responsible for having that consent. A number on your blacklist is saved and put in the groups, but stays blocked, gets no consent record, and is counted in
blacklisted. warnings: the contact was saved, but something is worth a look.birthday_not_read: thebirthdayis not a date we can read (we readdd/mm/yyyy,dd/mmandyyyy-mm-dd), so no birthday message will go out for it.GET /v1/contacts?mobile=306900000000returns one contact;GET /v1/contactslists them, 1,000 a page (?page=2), or only those of a group (?group=Clients):{"total": 2, "page": 1, "pages": 1, "items": [{"mobile": "306900000000", "name": "Maria", "surname": "P", "fields": {...}, "groups": ["Clients"]}]}.DELETE /v1/contacts/306900000000:{"mobile": "306900000000", "deleted": true, "reason": null}. A number on your blacklist is not deleted here ("deleted": false, "reason": "blacklisted"): that list is changed with/v1/blacklist. A number we do not have gives"reason": "not_found", not an error.- An account can hold up to 100,000 contacts and 500 groups (
contact_limit,group_limit; ask us if you need more).
Fields: GET /v1/contact-fields
The fields a contact can have are set by the service and can change. Ask for the list instead of writing it into your code:
{"fields": [{"name": "birthday", "type": "text"}, {"name": "nextAppointment", "type": "date"}, {"name": "balance", "type": "number"}, {"name": "vip", "type": "yesno"}]}
| Type | What we accept |
|---|---|
text | Any text up to 500 characters (a number sent as a JSON number is stored as its text). |
date | ISO 8601: 2026-11-12, or with a time 2026-11-12T14:30:00Z or 2026-11-12T14:30:00+02:00. Nothing else (not 12/11/2026). |
number | A JSON number or text such as 12.5: a dot, no thousands separator, up to 15 digits. |
yesno | true or false (also "yes", "no"). Stored as yes or no. |
Field names are not case sensitive. A name that is not in the list gives unknown_field for that contact. birthday is a text field that we read as dd/mm/yyyy, dd/mm (no year) or yyyy-mm-dd. Values are used in your messages as {fieldName}.
Groups: /v1/groups and sending to a group
A group is a name you give to a set of contacts, so that you can send one message to all of them instead of listing the numbers. A group name is unique in your account, whatever the capitals. Needs the contacts right (sending to a group needs only the send right).
GET /v1/groups:{"groups": [{"name": "Clients", "contacts": 120}]}.POST /v1/groupswith{"name": "Clients"}:201 {"name": "Clients", "created": true}, or200with"created": falsewhen it exists. A group is also created when a contact names it. The nameBlacklistis kept for the blacklist.DELETE /v1/groups?name=Clients: the group is deleted, its contacts stay.DELETE /v1/groups/members?name=Clients: every contact leaves the group (they stay as contacts). Use it before you send a list longer than 1,000 in several requests.
Keeping a group equal to your own list. Add "replace_groups": true to a contacts request: each group named in the request then holds exactly the contacts listed with it, and the others leave the group (they stay as contacts, and a group you do not name is not touched). The whole list must be in that one request and valid; if one contact is rejected, the whole request is refused with invalid_contacts and nothing is changed. For a list longer than 1,000: empty the group with DELETE /v1/groups/members, then send the list in requests of up to 1,000.
curl -X POST https://api.isms.gr/v1/messages \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"sender": "MyShop", "group": "Clients", "text": "Your appointment is tomorrow", "client_ref": "reminders-2026-11-11"}'
The answer is the same as for any send, and the same rules apply: the blacklist, the price, the balance, schedule_at, unicode and client_ref (a repeat gives the first message with "duplicate": true). The recipients are the contacts in the group at the moment of the request; a contact added later does not receive it. Compare recipients with the size of the group to see how many were left out. A group of more than 20,000 contacts is not sent through the API (group_too_large): send it from the panel or split it. An empty group is refused (empty_group), and an unknown name gives 404 group_not_found. In every refusal nothing is sent or charged.
Blacklist: /v1/blacklist
Numbers on your blacklist are never sent to and never charged. Use these calls to keep it in step with your own system, for example when a customer replies STOP. Needs the blacklist right. Numbers need their country code, as in a send.
curl -X POST https://api.isms.gr/v1/blacklist \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"to": ["306900000000", "306900000001"]}'
{"added": 2, "already_listed": 0, "with_scheduled_messages": 0}
POST /v1/blacklist: add up to 1,000 numbers (to, a list or a single number). A number that is already listed is not an error. One wrong number rejects the whole request (invalid_number) and nothing is added.with_scheduled_messagesis how many of the new numbers are waiting in a message you scheduled earlier: that message is still charged for them and they end asblocked(see Message status), unless you remove them before the send time.GET /v1/blacklist?mobile=306900000000:{"mobile": "306900000000", "blacklisted": true}.GET /v1/blacklist: the whole list, 1,000 a page (?page=2):{"total": 1050, "page": 1, "pages": 2, "items": [{"mobile": "306900000000"}]}.DELETE /v1/blacklist/306900000000:{"mobile": "306900000000", "removed": true}. A number that was not listed gives"removed": false, not an error.
When you send, a number on the blacklist is left out and not charged, and the answer does not list it: compare recipients with the number of different numbers you sent. If every number is on the blacklist the request is refused with 400 invalid_request ("Recipients are on the blacklist") and nothing is charged. A message scheduled for later is checked again when it is due.
Balance: GET /v1/credits
curl https://api.isms.gr/v1/credits \
-H "Authorization: Bearer YOUR_KEY"
{"credits": "12.34000", "currency": "EUR"}
Limits
- 120 requests per minute per key (your account manager can change it for a key). Over it:
429 rate_limitedwithRetry-After. - Delivery reports: at least 5 seconds between two calls (
429 too_soon). - Message status: 10 questions a minute for the same message (
429 too_many_requests_for_message). - Up to 1,000 numbers per send. Request body up to 1 MB. Up to 1,000 rows in any list.
- A key that keeps breaking the limits is paused for 15 minutes (
429 locked). RespectRetry-Afterandnext_poll_afterand you will never see it.
Errors
Every error is JSON with a machine-readable error and a readable message:
{"error": "insufficient_balance", "message": "..."}
| HTTP | error | Meaning |
|---|---|---|
| 400 | invalid_json, invalid_request, invalid_sender, invalid_number, too_many_recipients, blocked, empty_group, group_too_large, too_many_contacts, invalid_contacts, invalid_group, contact_limit, group_limit | The request is wrong. Fix it before retrying. blocked means the text was refused by the spam filter. |
| 401 | unauthorized, invalid_key | Missing, unknown or revoked key. |
| 402 | insufficient_balance | Not enough balance. Nothing was sent or charged. |
| 403 | forbidden, account_inactive, ip_not_allowed | The key lacks the right, the account is not active, or the request came from an address that is not allowed. |
| 404 | not_found, group_not_found | No such message, contact or group. |
| 409 | conflict | Another request changed the same contacts or groups at the same moment. Send it again. |
| 413 | too_large | Body over 1 MB. |
| 429 | rate_limited, too_soon, too_many_requests_for_message, locked | Slow down. Wait for Retry-After seconds. |
| 503 | no_provider | Temporarily unable to send. Nothing was charged. Retry later with the same client_ref. |