Files
MySMSAPio/API_DOCUMENTATION.md
2025-10-22 17:22:17 +08:00

25 KiB

MySMSAPio - API Documentation for Android Integration

Overview

MySMSAPio provides a complete REST API and WebSocket interface for SMS gateway management. This documentation is specifically designed for Android developers building SMS gateway applications.

Version: 1.0 Base URL: http://your-server.com or https://your-server.com Protocol: HTTP/HTTPS (REST API) + WebSocket (Action Cable)


Table of Contents

  1. Authentication
  2. Gateway Registration
  3. WebSocket Connection
  4. Heartbeat System
  5. Receiving SMS
  6. Sending SMS
  7. Delivery Reports
  8. Error Handling
  9. Rate Limits
  10. Android Code Examples

Authentication

API Key Types

Gateway API Key (for Android devices):

  • Format: gw_live_ + 64 hex characters
  • Example: gw_live_a6e2b250dade8f6501256a8717723fc3f8ab7d4e7cb26aad470d65ee8478a82c
  • Obtained during gateway registration via admin interface

Authentication Methods

HTTP Headers:

Authorization: Bearer gw_live_your_api_key_here
Content-Type: application/json

WebSocket Query Parameter:

ws://your-server.com/cable?api_key=gw_live_your_api_key_here

Security Notes

  • API keys are SHA256 hashed on the server
  • Always use HTTPS in production
  • Never expose API keys in logs or public repositories
  • Store securely using Android Keystore

Gateway Registration

Endpoint

POST /api/v1/gateway/register

Description

Register a new gateway device with the system. This is typically done once during initial setup.

Request Headers

Content-Type: application/json

Request Body

{
  "device_id": "samsung-galaxy-001",
  "name": "My Android Phone",
  "device_info": {
    "manufacturer": "Samsung",
    "model": "Galaxy S21",
    "os_version": "Android 13",
    "app_version": "1.0.0"
  }
}

Response (201 Created)

{
  "success": true,
  "message": "Gateway registered successfully",
  "gateway": {
    "id": 1,
    "device_id": "samsung-galaxy-001",
    "name": "My Android Phone",
    "api_key": "gw_live_a6e2b250dade8f6501256a8717723fc3...",
    "status": "offline",
    "active": true
  }
}

Response (400 Bad Request)

{
  "success": false,
  "error": "Device ID already exists"
}

Android Example

suspend fun registerGateway(deviceId: String, name: String): String? {
    val client = OkHttpClient()
    val json = JSONObject().apply {
        put("device_id", deviceId)
        put("name", name)
        put("device_info", JSONObject().apply {
            put("manufacturer", Build.MANUFACTURER)
            put("model", Build.MODEL)
            put("os_version", "Android ${Build.VERSION.RELEASE}")
            put("app_version", BuildConfig.VERSION_NAME)
        })
    }

    val body = json.toString().toRequestBody("application/json".toMediaType())
    val request = Request.Builder()
        .url("$BASE_URL/api/v1/gateway/register")
        .post(body)
        .build()

    return withContext(Dispatchers.IO) {
        try {
            val response = client.newCall(request).execute()
            val responseBody = response.body?.string()
            if (response.isSuccessful && responseBody != null) {
                val jsonResponse = JSONObject(responseBody)
                jsonResponse.getJSONObject("gateway").getString("api_key")
            } else null
        } catch (e: Exception) {
            Log.e(TAG, "Registration failed", e)
            null
        }
    }
}

WebSocket Connection

Connection URL

ws://your-server.com/cable?api_key=gw_live_your_api_key_here

Connection Flow

  1. Connect to WebSocket endpoint with API key
  2. Receive welcome message
  3. Subscribe to GatewayChannel
  4. Receive confirmation
  5. Start sending/receiving messages

Message Format

All WebSocket messages use JSON format:

{
  "identifier": "{\"channel\":\"GatewayChannel\"}",
  "command": "message",
  "data": {
    "action": "action_name",
    "payload": {}
  }
}

Connection States

State Description
connecting WebSocket connection initiating
connected WebSocket open, not subscribed
subscribed Subscribed to GatewayChannel
disconnected Connection closed
error Connection error occurred

Android Example

import okhttp3.*
import java.util.concurrent.TimeUnit

class GatewayWebSocketClient(
    private val baseUrl: String,
    private val apiKey: String,
    private val listener: WebSocketEventListener
) {
    private var webSocket: WebSocket? = null
    private val client = OkHttpClient.Builder()
        .readTimeout(30, TimeUnit.SECONDS)
        .writeTimeout(30, TimeUnit.SECONDS)
        .pingInterval(30, TimeUnit.SECONDS)
        .build()

    fun connect() {
        val wsUrl = baseUrl.replace("http", "ws")
        val request = Request.Builder()
            .url("$wsUrl/cable?api_key=$apiKey")
            .build()

        webSocket = client.newWebSocket(request, object : WebSocketListener() {
            override fun onOpen(webSocket: WebSocket, response: Response) {
                Log.d(TAG, "WebSocket connected")
                listener.onConnected()
                subscribe()
            }

            override fun onMessage(webSocket: WebSocket, text: String) {
                Log.d(TAG, "Received: $text")
                handleMessage(text)
            }

            override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
                Log.e(TAG, "WebSocket error", t)
                listener.onError(t)
                scheduleReconnect()
            }

            override fun onClosed(webSocket: WebSocket, code: Int, reason: String) {
                Log.d(TAG, "WebSocket closed: $reason")
                listener.onDisconnected()
                scheduleReconnect()
            }
        })
    }

    private fun subscribe() {
        val message = JSONObject().apply {
            put("command", "subscribe")
            put("identifier", JSONObject().apply {
                put("channel", "GatewayChannel")
            }.toString())
        }
        webSocket?.send(message.toString())
    }

    private fun handleMessage(json: String) {
        try {
            val message = JSONObject(json)
            val type = message.optString("type")

            when (type) {
                "welcome" -> {
                    Log.d(TAG, "Welcome received")
                    listener.onWelcome()
                }
                "ping" -> {
                    Log.d(TAG, "Ping received")
                }
                "confirm_subscription" -> {
                    Log.d(TAG, "Subscription confirmed")
                    listener.onSubscribed()
                }
                else -> {
                    // Handle data messages
                    val messageData = message.optJSONObject("message")
                    if (messageData != null) {
                        handleDataMessage(messageData)
                    }
                }
            }
        } catch (e: Exception) {
            Log.e(TAG, "Error parsing message", e)
        }
    }

    private fun handleDataMessage(data: JSONObject) {
        val action = data.optString("action")
        when (action) {
            "send_sms" -> listener.onSendSmsRequest(data)
            "ping" -> listener.onHeartbeatRequest()
            else -> Log.w(TAG, "Unknown action: $action")
        }
    }

    fun send(action: String, payload: JSONObject) {
        val message = JSONObject().apply {
            put("command", "message")
            put("identifier", JSONObject().apply {
                put("channel", "GatewayChannel")
            }.toString())
            put("data", JSONObject().apply {
                put("action", action)
                put("payload", payload)
            }.toString())
        }
        webSocket?.send(message.toString())
    }

    fun disconnect() {
        webSocket?.close(1000, "Normal closure")
    }

    private fun scheduleReconnect() {
        Handler(Looper.getMainLooper()).postDelayed({
            connect()
        }, 5000)
    }

    interface WebSocketEventListener {
        fun onConnected()
        fun onWelcome()
        fun onSubscribed()
        fun onDisconnected()
        fun onError(throwable: Throwable)
        fun onSendSmsRequest(data: JSONObject)
        fun onHeartbeatRequest()
    }

    companion object {
        private const val TAG = "GatewayWebSocket"
    }
}

Heartbeat System

Purpose

Heartbeats keep the gateway marked as "online" in the system. If no heartbeat is received for 2 minutes, the gateway is automatically marked as "offline".

Heartbeat Interval

  • Recommended: Every 60 seconds
  • Maximum: 120 seconds (2 minutes)
  • Minimum: 30 seconds

HTTP Heartbeat

POST /api/v1/gateway/heartbeat

Request Headers

Authorization: Bearer gw_live_your_api_key_here
Content-Type: application/json

Request Body

{
  "status": "online",
  "battery_level": 85,
  "signal_strength": 4,
  "messages_sent": 10,
  "messages_received": 5
}

Response (200 OK)

{
  "success": true,
  "message": "Heartbeat received",
  "gateway_status": "online"
}

WebSocket Heartbeat

Send via WebSocket for more efficient communication:

{
  "command": "message",
  "identifier": "{\"channel\":\"GatewayChannel\"}",
  "data": "{\"action\":\"heartbeat\",\"payload\":{\"status\":\"online\",\"battery_level\":85}}"
}

Android Example

class HeartbeatManager(
    private val apiKey: String,
    private val httpClient: OkHttpClient,
    private val webSocketClient: GatewayWebSocketClient?
) {
    private val handler = Handler(Looper.getMainLooper())
    private var isRunning = false

    fun start() {
        isRunning = true
        scheduleNextHeartbeat()
    }

    fun stop() {
        isRunning = false
        handler.removeCallbacksAndMessages(null)
    }

    private fun scheduleNextHeartbeat() {
        if (!isRunning) return

        handler.postDelayed({
            sendHeartbeat()
            scheduleNextHeartbeat()
        }, 60_000) // 60 seconds
    }

    private fun sendHeartbeat() {
        val batteryLevel = getBatteryLevel()
        val signalStrength = getSignalStrength()

        if (webSocketClient != null) {
            // Send via WebSocket (preferred)
            val payload = JSONObject().apply {
                put("status", "online")
                put("battery_level", batteryLevel)
                put("signal_strength", signalStrength)
            }
            webSocketClient.send("heartbeat", payload)
        } else {
            // Fallback to HTTP
            sendHttpHeartbeat(batteryLevel, signalStrength)
        }
    }

    private fun sendHttpHeartbeat(batteryLevel: Int, signalStrength: Int) {
        val json = JSONObject().apply {
            put("status", "online")
            put("battery_level", batteryLevel)
            put("signal_strength", signalStrength)
        }

        val body = json.toString().toRequestBody("application/json".toMediaType())
        val request = Request.Builder()
            .url("$BASE_URL/api/v1/gateway/heartbeat")
            .header("Authorization", "Bearer $apiKey")
            .post(body)
            .build()

        httpClient.newCall(request).enqueue(object : Callback {
            override fun onFailure(call: Call, e: IOException) {
                Log.e(TAG, "Heartbeat failed", e)
            }

            override fun onResponse(call: Call, response: Response) {
                if (response.isSuccessful) {
                    Log.d(TAG, "Heartbeat sent successfully")
                }
            }
        })
    }

    private fun getBatteryLevel(): Int {
        val batteryManager = context.getSystemService(Context.BATTERY_SERVICE) as BatteryManager
        return batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
    }

    private fun getSignalStrength(): Int {
        // Implement signal strength detection
        // Return value 0-5
        return 4
    }

    companion object {
        private const val TAG = "HeartbeatManager"
    }
}

Receiving SMS

Flow

When your Android app receives an SMS:

  1. Intercept SMS via BroadcastReceiver
  2. Extract sender, message, timestamp
  3. Send to server via HTTP or WebSocket
  4. Receive confirmation

HTTP Method

POST /api/v1/gateway/sms/received

Request Headers

Authorization: Bearer gw_live_your_api_key_here
Content-Type: application/json

Request Body

{
  "phone_number": "+959123456789",
  "message_body": "Hello, this is a test message",
  "received_at": "2025-10-20T13:45:30Z",
  "sim_slot": 1
}

Response (201 Created)

{
  "success": true,
  "message": "SMS received and processed",
  "message_id": "msg_abc123def456"
}

WebSocket Method

{
  "command": "message",
  "identifier": "{\"channel\":\"GatewayChannel\"}",
  "data": "{\"action\":\"sms_received\",\"payload\":{\"phone_number\":\"+959123456789\",\"message_body\":\"Hello\",\"received_at\":\"2025-10-20T13:45:30Z\"}}"
}

Android Example

class SmsReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        if (intent.action == Telephony.Sms.Intents.SMS_RECEIVED_ACTION) {
            val messages = Telephony.Sms.Intents.getMessagesFromIntent(intent)

            for (smsMessage in messages) {
                val sender = smsMessage.displayOriginatingAddress
                val messageBody = smsMessage.messageBody
                val timestamp = Date(smsMessage.timestampMillis)

                // Send to server
                sendSmsToServer(sender, messageBody, timestamp)
            }
        }
    }

    private fun sendSmsToServer(sender: String, body: String, timestamp: Date) {
        CoroutineScope(Dispatchers.IO).launch {
            try {
                val json = JSONObject().apply {
                    put("phone_number", sender)
                    put("message_body", body)
                    put("received_at", SimpleDateFormat(
                        "yyyy-MM-dd'T'HH:mm:ss'Z'",
                        Locale.US
                    ).apply {
                        timeZone = TimeZone.getTimeZone("UTC")
                    }.format(timestamp))
                    put("sim_slot", getSimSlot())
                }

                val client = OkHttpClient()
                val requestBody = json.toString()
                    .toRequestBody("application/json".toMediaType())

                val request = Request.Builder()
                    .url("$BASE_URL/api/v1/gateway/sms/received")
                    .header("Authorization", "Bearer $API_KEY")
                    .post(requestBody)
                    .build()

                val response = client.newCall(request).execute()
                if (response.isSuccessful) {
                    Log.d(TAG, "SMS sent to server successfully")
                } else {
                    Log.e(TAG, "Failed to send SMS: ${response.code}")
                }
            } catch (e: Exception) {
                Log.e(TAG, "Error sending SMS to server", e)
            }
        }
    }

    private fun getSimSlot(): Int {
        // Detect SIM slot (1 or 2) for dual SIM devices
        return 1
    }

    companion object {
        private const val TAG = "SmsReceiver"
        private const val BASE_URL = "http://your-server.com"
        private const val API_KEY = "gw_live_your_key"
    }
}

AndroidManifest.xml:

<uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.READ_SMS" />

<receiver
    android:name=".SmsReceiver"
    android:exported="true"
    android:permission="android.permission.BROADCAST_SMS">
    <intent-filter android:priority="1000">
        <action android:name="android.provider.Telephony.SMS_RECEIVED" />
    </intent-filter>
</receiver>

Sending SMS

Flow

Server sends SMS request to gateway:

  1. Receive send request via WebSocket
  2. Validate phone number and message
  3. Send SMS using Android SmsManager
  4. Report delivery status back to server

WebSocket Request (Server → Gateway)

{
  "type": "message",
  "message": {
    "action": "send_sms",
    "payload": {
      "message_id": "msg_abc123def456",
      "phone_number": "+959123456789",
      "message_body": "Your OTP is 123456",
      "priority": "high"
    }
  }
}

Android Example

class SmsSender(
    private val context: Context,
    private val webSocketClient: GatewayWebSocketClient
) {
    private val smsManager = context.getSystemService(SmsManager::class.java)

    fun handleSendRequest(data: JSONObject) {
        val payload = data.getJSONObject("payload")
        val messageId = payload.getString("message_id")
        val phoneNumber = payload.getString("phone_number")
        val messageBody = payload.getString("message_body")

        sendSms(messageId, phoneNumber, messageBody)
    }

    private fun sendSms(messageId: String, phoneNumber: String, messageBody: String) {
        try {
            // Create pending intents for delivery tracking
            val sentIntent = PendingIntent.getBroadcast(
                context,
                0,
                Intent("SMS_SENT").apply {
                    putExtra("message_id", messageId)
                },
                PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
            )

            val deliveredIntent = PendingIntent.getBroadcast(
                context,
                0,
                Intent("SMS_DELIVERED").apply {
                    putExtra("message_id", messageId)
                },
                PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
            )

            // Send SMS
            if (messageBody.length > 160) {
                // Split long messages
                val parts = smsManager.divideMessage(messageBody)
                val sentIntents = ArrayList<PendingIntent>()
                val deliveredIntents = ArrayList<PendingIntent>()

                for (i in parts.indices) {
                    sentIntents.add(sentIntent)
                    deliveredIntents.add(deliveredIntent)
                }

                smsManager.sendMultipartTextMessage(
                    phoneNumber,
                    null,
                    parts,
                    sentIntents,
                    deliveredIntents
                )
            } else {
                smsManager.sendTextMessage(
                    phoneNumber,
                    null,
                    messageBody,
                    sentIntent,
                    deliveredIntent
                )
            }

            Log.d(TAG, "SMS sent: $messageId")

            // Report sent status
            reportStatus(messageId, "sent")

        } catch (e: Exception) {
            Log.e(TAG, "Failed to send SMS", e)
            reportStatus(messageId, "failed", e.message)
        }
    }

    private fun reportStatus(messageId: String, status: String, error: String? = null) {
        val payload = JSONObject().apply {
            put("message_id", messageId)
            put("status", status)
            if (error != null) {
                put("error_message", error)
            }
        }

        webSocketClient.send("delivery_status", payload)
    }

    companion object {
        private const val TAG = "SmsSender"
    }
}

// Broadcast Receivers for SMS status
class SmsSentReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        val messageId = intent.getStringExtra("message_id") ?: return

        when (resultCode) {
            Activity.RESULT_OK -> {
                Log.d(TAG, "SMS sent successfully: $messageId")
                // Status already reported in sendSms()
            }
            SmsManager.RESULT_ERROR_GENERIC_FAILURE -> {
                reportError(context, messageId, "Generic failure")
            }
            SmsManager.RESULT_ERROR_NO_SERVICE -> {
                reportError(context, messageId, "No service")
            }
            SmsManager.RESULT_ERROR_NULL_PDU -> {
                reportError(context, messageId, "Null PDU")
            }
            SmsManager.RESULT_ERROR_RADIO_OFF -> {
                reportError(context, messageId, "Radio off")
            }
        }
    }

    private fun reportError(context: Context, messageId: String, error: String) {
        // Report to server via WebSocket or HTTP
        Log.e(TAG, "SMS send failed: $messageId - $error")
    }

    companion object {
        private const val TAG = "SmsSentReceiver"
    }
}

class SmsDeliveredReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        val messageId = intent.getStringExtra("message_id") ?: return

        when (resultCode) {
            Activity.RESULT_OK -> {
                Log.d(TAG, "SMS delivered: $messageId")
                // Report delivered status to server
            }
            Activity.RESULT_CANCELED -> {
                Log.e(TAG, "SMS not delivered: $messageId")
                // Report failed delivery to server
            }
        }
    }

    companion object {
        private const val TAG = "SmsDeliveredReceiver"
    }
}

AndroidManifest.xml:

<uses-permission android:name="android.permission.SEND_SMS" />

<receiver
    android:name=".SmsSentReceiver"
    android:exported="false">
    <intent-filter>
        <action android:name="SMS_SENT" />
    </intent-filter>
</receiver>

<receiver
    android:name=".SmsDeliveredReceiver"
    android:exported="false">
    <intent-filter>
        <action android:name="SMS_DELIVERED" />
    </intent-filter>
</receiver>

Delivery Reports

HTTP Method

POST /api/v1/gateway/sms/status

Request Headers

Authorization: Bearer gw_live_your_api_key_here
Content-Type: application/json

Request Body

{
  "message_id": "msg_abc123def456",
  "status": "delivered",
  "delivered_at": "2025-10-20T13:46:00Z"
}

Status Values:

  • sent - SMS sent from device
  • delivered - SMS delivered to recipient
  • failed - SMS failed to send
  • error - Error occurred

Response (200 OK)

{
  "success": true,
  "message": "Status updated"
}

WebSocket Method

{
  "command": "message",
  "identifier": "{\"channel\":\"GatewayChannel\"}",
  "data": "{\"action\":\"delivery_status\",\"payload\":{\"message_id\":\"msg_abc123def456\",\"status\":\"delivered\"}}"
}

Error Handling

HTTP Error Codes

Code Meaning Action
400 Bad Request Check request format
401 Unauthorized Check API key
403 Forbidden Gateway inactive
404 Not Found Check endpoint URL
422 Unprocessable Entity Validation failed
429 Too Many Requests Implement backoff
500 Server Error Retry with backoff
503 Service Unavailable Server down, retry later

Retry Strategy

class RetryPolicy {
    suspend fun <T> executeWithRetry(
        maxRetries: Int = 3,
        initialDelay: Long = 1000,
        maxDelay: Long = 10000,
        factor: Double = 2.0,
        block: suspend () -> T
    ): T? {
        var currentDelay = initialDelay
        repeat(maxRetries) { attempt ->
            try {
                return block()
            } catch (e: Exception) {
                Log.w(TAG, "Attempt ${attempt + 1} failed", e)

                if (attempt == maxRetries - 1) {
                    throw e
                }

                delay(currentDelay)
                currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay)
            }
        }
        return null
    }

    companion object {
        private const val TAG = "RetryPolicy"
    }
}

Rate Limits

Current Limits

  • Heartbeat: 1 per minute
  • SMS Received: 100 per minute
  • SMS Status: 1000 per minute
  • WebSocket Messages: No limit (use responsibly)

Rate Limit Headers

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1634567890

Handling Rate Limits

fun handleRateLimitError(response: Response) {
    val retryAfter = response.header("Retry-After")?.toIntOrNull() ?: 60

    Log.w(TAG, "Rate limited. Retry after $retryAfter seconds")

    Handler(Looper.getMainLooper()).postDelayed({
        // Retry request
    }, retryAfter * 1000L)
}

Complete Android Integration Example

See CABLE_DOCUMENTATION.md for complete WebSocket integration examples.


Testing

Test API Key

Use the admin interface test feature:

  • Navigate to /admin/gateways/:id/test
  • Send test SMS
  • Verify delivery

Debug Logging

Enable detailed logging:

if (BuildConfig.DEBUG) {
    val loggingInterceptor = HttpLoggingInterceptor().apply {
        level = HttpLoggingInterceptor.Level.BODY
    }

    OkHttpClient.Builder()
        .addInterceptor(loggingInterceptor)
        .build()
}

Support

For issues or questions:

  • Check server logs: /admin/logs
  • Test gateway connection: /admin/gateways/:id/test
  • Review error messages in responses
  • Check API key validity

Changelog

Version 1.0 (2025-10-20)

  • Initial API documentation
  • Gateway registration
  • WebSocket connection
  • SMS send/receive
  • Delivery reports
  • Android code examples