1021 lines
25 KiB
Markdown
1021 lines
25 KiB
Markdown
# 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
|
|
<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)
|
|
|
|
```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<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**:
|
|
|
|
```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
|
|
|
|
```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 <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
|
|
|
|
```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
|