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

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