SMS Gora API

Android SMS gateway API

SMS Gora sends and receives SMS through Android phones you link to your Tekservix account. Your software calls this REST API; the phone sends the text from its own SIM card and reports back sent and delivered status. Incoming SMS reach you by webhook.

Base URL   https://tekservix.com/smsgora/api/v1
Format     JSON (UTF-8) in and out
Auth       x-api-key: <key>   (or  Authorization: Bearer <key>)

Set up a phone

  1. On an Android phone (7.0 or newer) with a SIM card, download the SMS Gora app and install it.
  2. In the dashboard, press Add phone. A QR code and an 8-character code appear.
  3. In the app, tap Scan QR code (or Enter code) and allow the SMS permissions. The phone shows Online.
  4. Note the phone’s ID (shown on its card) and create an API key under API keys.
Keep the phone charged and online, and let the app ignore battery optimisation (it asks). Texts are billed by your mobile operator like any SMS you send by hand.

Authentication

API keys start with sgk_ and work for every phone in your account. Send the key in the x-api-key header — Authorization: Bearer sgk_… works too.

curl https://tekservix.com/smsgora/api/v1/devices -H "x-api-key: sgk_your_key"

Responses & errors

Every response has success. Successful calls return data; failures return message and, for invalid fields, errors.

{ "success": true, "message": "SMS added to the queue", "data": { "batch_id": null, "message_ids": [1873], "status": "queued", … } }
{ "success": false, "message": "\"recipients\" is required: a phone number or a list of numbers.", "errors": { "recipients": "required" } }
HTTPMeaning
400 / 422Invalid request — check message and errors.
401Missing or wrong API key.
402No active plan, or the phone isn’t covered by your plan (upgrade or remove a phone).
403The account or phone is suspended.
404Unknown endpoint, phone or message.
409The phone isn’t paired, or sending is switched off for it, or the action doesn’t fit the message’s state.
429Rate limit or your plan’s daily/monthly SMS limit reached — see message and Retry-After.

Limits & delivery

  • Requests: 300 per minute per key (X-RateLimit-Remaining header).
  • Recipients: up to 1,000 per send-sms call, 5,000 per bulk call. Use campaigns for bigger lists.
  • Message length: up to 1,600 characters. Long texts are sent as a multi-part SMS (160 GSM characters per SMS, or 70 with emoji and special letters).
  • Plans: every plan has unlimited SMS; plans differ by the number of phones. Without an active plan, sending answers 402 and queued messages wait until a plan is active.
  • Pace: each phone waits 2.5 s between texts by default (change it per phone) so operators don’t flag the SIM.
  • Offline phones: messages wait in the queue and go out when the phone reconnects. A message the phone took but never confirmed is handed out again after 10 minutes (up to 3 times).

Send SMS

POST/v1/devices/{deviceId}/send-sms
FieldTypeDescription
recipientsarray or stringPhone numbers, ideally in international format (+250788123456). A comma-separated string works too. Also accepted as to.
messagestringThe text, up to 1,600 characters. Also accepted as text.
sim_slotinteger, optional0 = SIM 1, 1 = SIM 2. Leave out to use the phone’s default SIM.
schedule_atISO 8601, optionalSend later, e.g. 2026-10-01T08:00:00Z (up to a year ahead).
curl -X POST https://tekservix.com/smsgora/api/v1/devices/42/send-sms \
  -H "x-api-key: sgk_your_key" -H "Content-Type: application/json" \
  -d '{"recipients": ["+250788123456", "+250722000000"], "message": "Your appointment is tomorrow at 10:00."}'
{ "success": true, "message": "SMS added to the queue",
  "data": { "batch_id": 311, "smsBatchId": 311, "message_ids": [1873, 1874], "recipientCount": 2,
            "status": "queued", "device_id": 42, "device_online": true, "invalid_recipients": [] } }

Node.js

const res = await fetch('https://tekservix.com/smsgora/api/v1/devices/42/send-sms', {
  method: 'POST',
  headers: { 'x-api-key': process.env.SMSGORA_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({ recipients: ['+250788123456'], message: 'Your code is 482913' }),
});
console.log(await res.json());

PHP

$ch = curl_init('https://tekservix.com/smsgora/api/v1/devices/42/send-sms');
curl_setopt_array($ch, [
  CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => ['x-api-key: ' . getenv('SMSGORA_KEY'), 'Content-Type: application/json'],
  CURLOPT_POSTFIELDS => json_encode(['recipients' => ['+250788123456'], 'message' => 'Your code is 482913']),
]);
echo curl_exec($ch);

Python

import os, requests
r = requests.post('https://tekservix.com/smsgora/api/v1/devices/42/send-sms',
    headers={'x-api-key': os.environ['SMSGORA_KEY']},
    json={'recipients': ['+250788123456'], 'message': 'Your code is 482913'})
print(r.json())

Send without a phone ID

POST/v1/send-sms

Same body as above. SMS Gora picks your best phone: online, sending switched on, covered by your plan, with the shortest queue. Pass device_id to choose one.

Bulk SMS

POST/v1/devices/{deviceId}/send-bulk-sms

Different texts for different people in one call. messageTemplate is used for items without their own message.

{ "messageTemplate": "Hello, your parcel is ready for pickup.",
  "messages": [
    { "recipients": ["+250788123456"], "message": "Hi Aline, parcel #1042 is ready." },
    { "recipients": ["+250722000000", "+250733000000"] }
  ] }

Answer: { "batch_ids": [...], "message_ids": [...], "recipientCount": 3 }.

Message statuses

StatusMeaning
scheduledWaiting for schedule_at.
queuedWaiting for the phone to collect it.
dispatchedThe phone has it and is sending.
sentThe phone handed it to the operator.
deliveredThe operator confirmed delivery to the recipient’s phone (when the network supports delivery reports).
failedNot sent — see error (no signal, no credit, SIM blocked, invalid number…).
cancelledCancelled before the phone collected it.
receivedAn incoming SMS.

List messages

GET/v1/messages

Newest first. Query: direction (in/out), status, device_id, phone, since (ISO date), after_id, page, limit (≤ 100).

{ "success": true, "data": { "data": [
  { "id": 1874, "device_id": 42, "direction": "out", "to": "+250722000000", "message": "Your appointment…", "status": "delivered",
    "error": null, "sim_slot": 0, "parts": 1, "source": "api", "batch_id": 311, "campaign_id": null,
    "scheduled_at": null, "sent_at": "2026-10-01T09:30:14Z", "delivered_at": "2026-10-01T09:30:19Z", "received_at": null, "created_at": "2026-10-01T09:30:11Z" }
  ], "page": 1, "limit": 50, "total": 1 } }

Received SMS

GET/v1/devices/{deviceId}/get-received-sms
GET/v1/received-sms

Incoming texts (for one phone, or all). Same filters and shape as List messages; incoming items have from instead of to. Tip: poll with after_id, or better, use the sms.received webhook.

One message & cancel

GET/v1/messages/{id}
POST/v1/messages/{id}/cancel

Cancelling works while the message is queued or scheduled.

Batches

GET/v1/batches/{id}

A send to several recipients creates a batch. Returns total, counts by status and the messages.

Phones

GET/v1/devices
GET/v1/devices/{id}
{ "id": 42, "name": "Office phone", "status": "online", "paired": true, "enabled": true, "receive_enabled": true,
  "brand": "samsung", "model": "SM-A145F", "android_version": "14", "app_version": "1.0.0",
  "sims": [ { "slot": 0, "carrier": "MTN", "number": "+250788…" }, { "slot": 1, "carrier": "Airtel", "number": "" } ],
  "default_sim": null, "battery": 83, "charging": false, "network": "wifi", "last_seen_at": "2026-10-01 09:31:02",
  "msgs_sent": 1284, "msgs_failed": 3, "msgs_received": 37, "covered": true }

status is online, offline (no contact for 5 minutes), pending (not paired) or suspended.

Plan & usage

GET/v1/account
{ "plan": "Pro", "devices_allowed": 3, "devices_used": 1, "daily_limit": 0, "monthly_limit": 0,
  "usage": { "today": 12, "month": 140 }, "renews_at": "2026-10-29 16:40:12" }

0 means unlimited. plan is null when the account has no active plan.

Webhooks: setup & signature

Add webhooks under Webhooks (up to 10, for all phones or one). We POST JSON to your https URL:

POST /your/webhook
Content-Type: application/json
X-SmsGora-Event: sms.received
X-SmsGora-Delivery: 99812
X-SmsGora-Signature: sha256=5d3c…

{ "event": "sms.received", "timestamp": "2026-10-01T09:30:12Z", "device_id": 42,
  "data": { "id": 1875, "device_id": 42, "direction": "in", "from": "+250788123456",
            "message": "YES, confirm my booking", "status": "received", "sim_slot": 0,
            "received_at": "2026-10-01T09:30:10Z", … } }

Check the signature: compute HMAC-SHA256 of the raw request body with your webhook’s secret and compare it with the header (after sha256=). Reply with any 2xx within 10 seconds. Failed deliveries are retried after 1 min, 5 min, 30 min, 2 h and 6 h.

// Node.js (Express)
app.post('/sms/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = 'sha256=' + crypto.createHmac('sha256', process.env.SMSGORA_SECRET).update(req.body).digest('hex');
  if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(req.get('X-SmsGora-Signature') || ''))) return res.sendStatus(401);
  const e = JSON.parse(req.body);
  if (e.event === 'sms.received') console.log(e.data.from, e.data.message);
  res.sendStatus(200);
});
// PHP
$raw = file_get_contents('php://input');
$ok = hash_equals('sha256=' . hash_hmac('sha256', $raw, getenv('SMSGORA_SECRET')), $_SERVER['HTTP_X_SMSGORA_SIGNATURE'] ?? '');
if (!$ok) { http_response_code(401); exit; }
$e = json_decode($raw, true);

Events

EventWhendata
sms.receivedAn SMS arrives on a phone.The message (from, message, sim_slot, received_at…)
sms.sentThe phone handed the SMS to the operator.The message
sms.deliveredThe operator confirmed delivery.The message
sms.failedThe SMS couldn’t be sent.The message, with error
device.onlineA phone paired or came back online.{ device_id, name }
device.offlineNo contact from a phone for 5 minutes.{ device_id, name }

Manage webhooks by API

GET/v1/webhooks
POST/v1/webhooks
DELETE/v1/webhooks/{id}
POST /v1/webhooks
{ "url": "https://your-app.com/sms/webhook", "events": ["sms.received", "sms.failed"], "device_id": null }
→ { "id": 7, "url": "…", "events": [...], "secret": "3f9c…" }   (the secret is only returned here)

TextBee compatibility

SMS Gora accepts TextBee-style requests, so most integrations switch by changing the base URL and the key:

TextBeeSMS Gora
https://api.textbee.dev/api/v1https://tekservix.com/smsgora/api/v1
POST /gateway/devices/{id}/send-sms {recipients, message}Same path and body (the /gateway prefix is optional)
POST /gateway/devices/{id}/send-bulk-smsSame path and body
GET /gateway/devices/{id}/get-received-smsSame path
x-api-key headerSame header
Answer data.smsBatchIdAlso returned (with message_ids and batch_id)

Webhook payloads differ: see Events.