# 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](#authentication) 2. [Gateway Registration](#gateway-registration) 3. [WebSocket Connection](#websocket-connection) 4. [Heartbeat System](#heartbeat-system) 5. [Receiving SMS](#receiving-sms) 6. [Sending SMS](#sending-sms) 7. [Delivery Reports](#delivery-reports) 8. [Error Handling](#error-handling) 9. [Rate Limits](#rate-limits) 10. [Android Code Examples](#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 ```json { "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) ```json { "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) ```json { "success": false, "error": "Device ID already exists" } ``` ### Android Example ```kotlin 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: ```json { "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 ```kotlin 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 ```json { "status": "online", "battery_level": 85, "signal_strength": 4, "messages_sent": 10, "messages_received": 5 } ``` #### Response (200 OK) ```json { "success": true, "message": "Heartbeat received", "gateway_status": "online" } ``` ### WebSocket Heartbeat Send via WebSocket for more efficient communication: ```json { "command": "message", "identifier": "{\"channel\":\"GatewayChannel\"}", "data": "{\"action\":\"heartbeat\",\"payload\":{\"status\":\"online\",\"battery_level\":85}}" } ``` ### Android Example ```kotlin 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 ```json { "phone_number": "+959123456789", "message_body": "Hello, this is a test message", "received_at": "2025-10-20T13:45:30Z", "sim_slot": 1 } ``` #### Response (201 Created) ```json { "success": true, "message": "SMS received and processed", "message_id": "msg_abc123def456" } ``` ### WebSocket Method ```json { "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 ```kotlin 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**: ```xml ``` --- ## 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) ```json { "type": "message", "message": { "action": "send_sms", "payload": { "message_id": "msg_abc123def456", "phone_number": "+959123456789", "message_body": "Your OTP is 123456", "priority": "high" } } } ``` ### Android Example ```kotlin 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() val deliveredIntents = ArrayList() 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**: ```xml ``` --- ## 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 ```json { "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) ```json { "success": true, "message": "Status updated" } ``` ### WebSocket Method ```json { "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 ```kotlin class RetryPolicy { suspend fun 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 ```kotlin 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](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: ```kotlin 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