SMS gateway api service for android application
This commit is contained in:
719
SAAS_INTEGRATION_GUIDE.md
Normal file
719
SAAS_INTEGRATION_GUIDE.md
Normal file
@@ -0,0 +1,719 @@
|
|||||||
|
# SaaS Application Integration Guide
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This guide shows how to integrate MySMSAPio into your SaaS application to send SMS and OTP messages.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
1. **MySMSAPio Server**: Running and accessible (e.g., `http://192.168.18.66:3005`)
|
||||||
|
2. **API Key**: Client API key (starts with `api_live_...`)
|
||||||
|
3. **Gateway Device**: At least one Android gateway online
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### 1. Create an API Key
|
||||||
|
|
||||||
|
**Via Admin Interface**:
|
||||||
|
1. Go to `http://your-server:3005/admin/api_keys`
|
||||||
|
2. Click "New API Key"
|
||||||
|
3. Enter name (e.g., "My SaaS App")
|
||||||
|
4. Enable permissions: `send_sms`, `receive_sms`
|
||||||
|
5. Copy the API key (shown only once!)
|
||||||
|
|
||||||
|
**Via Rails Console**:
|
||||||
|
```ruby
|
||||||
|
result = ApiKey.generate!(
|
||||||
|
name: "My SaaS App",
|
||||||
|
permissions: {
|
||||||
|
send_sms: true,
|
||||||
|
receive_sms: true,
|
||||||
|
send_otp: true,
|
||||||
|
verify_otp: true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
puts result[:raw_key]
|
||||||
|
# Save this key: api_live_abc123...
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Test Connection
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X GET http://your-server:3005/up \
|
||||||
|
-H "Authorization: Bearer api_live_your_key_here"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: `200 OK`
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### Base URL
|
||||||
|
```
|
||||||
|
http://your-server:3005/api/v1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Authentication
|
||||||
|
All requests require the `Authorization` header:
|
||||||
|
```
|
||||||
|
Authorization: Bearer api_live_your_key_here
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sending SMS
|
||||||
|
|
||||||
|
### Endpoint
|
||||||
|
```
|
||||||
|
POST /api/v1/sms/send
|
||||||
|
```
|
||||||
|
|
||||||
|
### Request
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"to": "+959123456789",
|
||||||
|
"message": "Your message here"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response (Success - 202 Accepted)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message_id": "msg_abc123def456...",
|
||||||
|
"status": "queued"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response (Error - 422 Unprocessable Entity)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "Invalid phone number format"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example: cURL
|
||||||
|
```bash
|
||||||
|
curl -X POST http://your-server:3005/api/v1/sms/send \
|
||||||
|
-H "Authorization: Bearer api_live_your_key_here" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"to": "+959123456789",
|
||||||
|
"message": "Hello from My SaaS App!"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example: JavaScript/Node.js
|
||||||
|
```javascript
|
||||||
|
const axios = require('axios');
|
||||||
|
|
||||||
|
async function sendSMS(phoneNumber, message) {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(
|
||||||
|
'http://your-server:3005/api/v1/sms/send',
|
||||||
|
{
|
||||||
|
to: phoneNumber,
|
||||||
|
message: message
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer api_live_your_key_here',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('SMS sent!');
|
||||||
|
console.log('Message ID:', response.data.message_id);
|
||||||
|
console.log('Status:', response.data.status);
|
||||||
|
|
||||||
|
return response.data.message_id;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to send SMS:', error.response?.data || error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
sendSMS('+959123456789', 'Welcome to our service!')
|
||||||
|
.then(messageId => console.log('Sent:', messageId))
|
||||||
|
.catch(err => console.error('Error:', err));
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example: Python
|
||||||
|
```python
|
||||||
|
import requests
|
||||||
|
|
||||||
|
def send_sms(phone_number, message):
|
||||||
|
url = "http://your-server:3005/api/v1/sms/send"
|
||||||
|
headers = {
|
||||||
|
"Authorization": "Bearer api_live_your_key_here",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
data = {
|
||||||
|
"to": phone_number,
|
||||||
|
"message": message
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(url, json=data, headers=headers)
|
||||||
|
|
||||||
|
if response.status_code == 202:
|
||||||
|
result = response.json()
|
||||||
|
print(f"SMS sent! Message ID: {result['message_id']}")
|
||||||
|
return result['message_id']
|
||||||
|
else:
|
||||||
|
print(f"Failed: {response.json()}")
|
||||||
|
raise Exception(response.json().get('error', 'Unknown error'))
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
message_id = send_sms("+959123456789", "Welcome to our service!")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example: PHP
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
function sendSMS($phoneNumber, $message) {
|
||||||
|
$url = 'http://your-server:3005/api/v1/sms/send';
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
'to' => $phoneNumber,
|
||||||
|
'message' => $message
|
||||||
|
];
|
||||||
|
|
||||||
|
$options = [
|
||||||
|
'http' => [
|
||||||
|
'header' => [
|
||||||
|
"Authorization: Bearer api_live_your_key_here",
|
||||||
|
"Content-Type: application/json"
|
||||||
|
],
|
||||||
|
'method' => 'POST',
|
||||||
|
'content' => json_encode($data)
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
$context = stream_context_create($options);
|
||||||
|
$result = file_get_contents($url, false, $context);
|
||||||
|
|
||||||
|
if ($result === FALSE) {
|
||||||
|
throw new Exception('Failed to send SMS');
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = json_decode($result, true);
|
||||||
|
echo "SMS sent! Message ID: " . $response['message_id'] . "\n";
|
||||||
|
|
||||||
|
return $response['message_id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
$messageId = sendSMS("+959123456789", "Welcome to our service!");
|
||||||
|
?>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example: Ruby
|
||||||
|
```ruby
|
||||||
|
require 'httparty'
|
||||||
|
|
||||||
|
def send_sms(phone_number, message)
|
||||||
|
response = HTTParty.post(
|
||||||
|
'http://your-server:3005/api/v1/sms/send',
|
||||||
|
headers: {
|
||||||
|
'Authorization' => 'Bearer api_live_your_key_here',
|
||||||
|
'Content-Type' => 'application/json'
|
||||||
|
},
|
||||||
|
body: {
|
||||||
|
to: phone_number,
|
||||||
|
message: message
|
||||||
|
}.to_json
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.code == 202
|
||||||
|
puts "SMS sent! Message ID: #{response['message_id']}"
|
||||||
|
response['message_id']
|
||||||
|
else
|
||||||
|
puts "Failed: #{response['error']}"
|
||||||
|
raise StandardError, response['error']
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
message_id = send_sms("+959123456789", "Welcome to our service!")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sending OTP
|
||||||
|
|
||||||
|
### Endpoint
|
||||||
|
```
|
||||||
|
POST /api/v1/otp/send
|
||||||
|
```
|
||||||
|
|
||||||
|
### Request
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"phone_number": "+959123456789"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response (Success - 200 OK)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "OTP sent successfully",
|
||||||
|
"expires_in": 300
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response (Error - Rate Limited)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "Rate limit exceeded. Maximum 3 OTP per hour for this number"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example: JavaScript
|
||||||
|
```javascript
|
||||||
|
async function sendOTP(phoneNumber) {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(
|
||||||
|
'http://your-server:3005/api/v1/otp/send',
|
||||||
|
{
|
||||||
|
phone_number: phoneNumber
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer api_live_your_key_here',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('OTP sent!');
|
||||||
|
console.log('Expires in:', response.data.expires_in, 'seconds');
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to send OTP:', error.response?.data || error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
sendOTP('+959123456789')
|
||||||
|
.then(() => console.log('OTP sent successfully'))
|
||||||
|
.catch(err => console.error('Error:', err));
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example: Python
|
||||||
|
```python
|
||||||
|
def send_otp(phone_number):
|
||||||
|
url = "http://your-server:3005/api/v1/otp/send"
|
||||||
|
headers = {
|
||||||
|
"Authorization": "Bearer api_live_your_key_here",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
data = {
|
||||||
|
"phone_number": phone_number
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(url, json=data, headers=headers)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
result = response.json()
|
||||||
|
print(f"OTP sent! Expires in {result['expires_in']} seconds")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f"Failed: {response.json()}")
|
||||||
|
raise Exception(response.json().get('error', 'Unknown error'))
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
send_otp("+959123456789")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Verifying OTP
|
||||||
|
|
||||||
|
### Endpoint
|
||||||
|
```
|
||||||
|
POST /api/v1/otp/verify
|
||||||
|
```
|
||||||
|
|
||||||
|
### Request
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"phone_number": "+959123456789",
|
||||||
|
"code": "123456"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response (Success - 200 OK)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"message": "OTP verified successfully"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response (Error - Invalid)
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "Invalid or expired OTP"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example: JavaScript
|
||||||
|
```javascript
|
||||||
|
async function verifyOTP(phoneNumber, code) {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(
|
||||||
|
'http://your-server:3005/api/v1/otp/verify',
|
||||||
|
{
|
||||||
|
phone_number: phoneNumber,
|
||||||
|
code: code
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer api_live_your_key_here',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('OTP verified!');
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('OTP verification failed:', error.response?.data || error.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
const isValid = await verifyOTP('+959123456789', '123456');
|
||||||
|
if (isValid) {
|
||||||
|
console.log('User verified!');
|
||||||
|
} else {
|
||||||
|
console.log('Invalid OTP');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example: Python
|
||||||
|
```python
|
||||||
|
def verify_otp(phone_number, code):
|
||||||
|
url = "http://your-server:3005/api/v1/otp/verify"
|
||||||
|
headers = {
|
||||||
|
"Authorization": "Bearer api_live_your_key_here",
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
}
|
||||||
|
data = {
|
||||||
|
"phone_number": phone_number,
|
||||||
|
"code": code
|
||||||
|
}
|
||||||
|
|
||||||
|
response = requests.post(url, json=data, headers=headers)
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
print("OTP verified successfully!")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
print(f"Verification failed: {response.json()}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
if verify_otp("+959123456789", "123456"):
|
||||||
|
print("User authenticated!")
|
||||||
|
else:
|
||||||
|
print("Authentication failed")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Checking Message Status
|
||||||
|
|
||||||
|
### Endpoint
|
||||||
|
```
|
||||||
|
GET /api/v1/sms/status/:message_id
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"message_id": "msg_abc123def456...",
|
||||||
|
"status": "delivered",
|
||||||
|
"sent_at": "2025-10-22T10:30:00Z",
|
||||||
|
"delivered_at": "2025-10-22T10:30:05Z",
|
||||||
|
"failed_at": null,
|
||||||
|
"error_message": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Status Values
|
||||||
|
- `queued` - Waiting for gateway
|
||||||
|
- `pending` - No gateway available, will retry
|
||||||
|
- `sent` - Sent to gateway device
|
||||||
|
- `delivered` - Successfully delivered to recipient
|
||||||
|
- `failed` - Failed to send
|
||||||
|
|
||||||
|
### Example: JavaScript
|
||||||
|
```javascript
|
||||||
|
async function checkMessageStatus(messageId) {
|
||||||
|
try {
|
||||||
|
const response = await axios.get(
|
||||||
|
`http://your-server:3005/api/v1/sms/status/${messageId}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer api_live_your_key_here'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('Status:', response.data.status);
|
||||||
|
console.log('Sent at:', response.data.sent_at);
|
||||||
|
console.log('Delivered at:', response.data.delivered_at);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to check status:', error.response?.data || error.message);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
const status = await checkMessageStatus('msg_abc123def456');
|
||||||
|
```
|
||||||
|
|
||||||
|
## Complete Integration Example
|
||||||
|
|
||||||
|
### User Registration with OTP (JavaScript)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const MySMSAPI = {
|
||||||
|
baseURL: 'http://your-server:3005/api/v1',
|
||||||
|
apiKey: 'api_live_your_key_here',
|
||||||
|
|
||||||
|
headers() {
|
||||||
|
return {
|
||||||
|
'Authorization': `Bearer ${this.apiKey}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
async sendSMS(phoneNumber, message) {
|
||||||
|
const response = await axios.post(
|
||||||
|
`${this.baseURL}/sms/send`,
|
||||||
|
{ to: phoneNumber, message },
|
||||||
|
{ headers: this.headers() }
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async sendOTP(phoneNumber) {
|
||||||
|
const response = await axios.post(
|
||||||
|
`${this.baseURL}/otp/send`,
|
||||||
|
{ phone_number: phoneNumber },
|
||||||
|
{ headers: this.headers() }
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
|
async verifyOTP(phoneNumber, code) {
|
||||||
|
try {
|
||||||
|
const response = await axios.post(
|
||||||
|
`${this.baseURL}/otp/verify`,
|
||||||
|
{ phone_number: phoneNumber, code },
|
||||||
|
{ headers: this.headers() }
|
||||||
|
);
|
||||||
|
return response.data.success;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async checkStatus(messageId) {
|
||||||
|
const response = await axios.get(
|
||||||
|
`${this.baseURL}/sms/status/${messageId}`,
|
||||||
|
{ headers: this.headers() }
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Usage Example: User Registration Flow
|
||||||
|
async function registerUser(phoneNumber, userData) {
|
||||||
|
try {
|
||||||
|
// Step 1: Send OTP
|
||||||
|
console.log('Sending OTP...');
|
||||||
|
await MySMSAPI.sendOTP(phoneNumber);
|
||||||
|
console.log('OTP sent! Check your phone.');
|
||||||
|
|
||||||
|
// Step 2: Wait for user to enter OTP (in real app, this would be a form)
|
||||||
|
const userEnteredCode = '123456'; // Get from user input
|
||||||
|
|
||||||
|
// Step 3: Verify OTP
|
||||||
|
console.log('Verifying OTP...');
|
||||||
|
const isValid = await MySMSAPI.verifyOTP(phoneNumber, userEnteredCode);
|
||||||
|
|
||||||
|
if (isValid) {
|
||||||
|
console.log('✅ Phone verified!');
|
||||||
|
|
||||||
|
// Step 4: Create user account
|
||||||
|
const user = await createUserInDatabase(phoneNumber, userData);
|
||||||
|
|
||||||
|
// Step 5: Send welcome SMS
|
||||||
|
const result = await MySMSAPI.sendSMS(
|
||||||
|
phoneNumber,
|
||||||
|
`Welcome to our service, ${userData.name}! Your account is now active.`
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('Welcome SMS sent:', result.message_id);
|
||||||
|
|
||||||
|
return { success: true, user };
|
||||||
|
} else {
|
||||||
|
console.log('❌ Invalid OTP');
|
||||||
|
return { success: false, error: 'Invalid OTP' };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Registration failed:', error);
|
||||||
|
return { success: false, error: error.message };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example usage
|
||||||
|
registerUser('+959123456789', { name: 'John Doe', email: 'john@example.com' });
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Common Errors
|
||||||
|
|
||||||
|
| Status | Error | Cause | Solution |
|
||||||
|
|--------|-------|-------|----------|
|
||||||
|
| 401 | Unauthorized | Invalid API key | Check API key is correct |
|
||||||
|
| 422 | Unprocessable Entity | Invalid phone number | Use international format (+959...) |
|
||||||
|
| 429 | Too Many Requests | Rate limit exceeded | Wait before retrying |
|
||||||
|
| 404 | Not Found | Message ID not found | Check message ID is correct |
|
||||||
|
| 503 | Service Unavailable | No gateway online | Ensure gateway is connected |
|
||||||
|
|
||||||
|
### Rate Limits
|
||||||
|
|
||||||
|
- **SMS Send**: 100 per minute per API key
|
||||||
|
- **OTP Send**: 3 per hour per phone number
|
||||||
|
- **OTP Verify**: 3 attempts per OTP code
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### 1. Store API Key Securely
|
||||||
|
```javascript
|
||||||
|
// ❌ Don't hardcode in frontend
|
||||||
|
const apiKey = 'api_live_abc123...';
|
||||||
|
|
||||||
|
// ✅ Use environment variables
|
||||||
|
const apiKey = process.env.SMS_API_KEY;
|
||||||
|
|
||||||
|
// ✅ Store in backend config
|
||||||
|
const apiKey = config.get('sms.apiKey');
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Handle Errors Gracefully
|
||||||
|
```javascript
|
||||||
|
async function sendSMSWithRetry(phoneNumber, message, maxRetries = 3) {
|
||||||
|
for (let i = 0; i < maxRetries; i++) {
|
||||||
|
try {
|
||||||
|
return await MySMSAPI.sendSMS(phoneNumber, message);
|
||||||
|
} catch (error) {
|
||||||
|
if (i === maxRetries - 1) throw error;
|
||||||
|
await sleep(1000 * (i + 1)); // Exponential backoff
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Validate Phone Numbers
|
||||||
|
```javascript
|
||||||
|
function validatePhoneNumber(phone) {
|
||||||
|
// Myanmar phone numbers
|
||||||
|
const myanmarRegex = /^\+959\d{8,9}$/;
|
||||||
|
return myanmarRegex.test(phone);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Monitor Message Status
|
||||||
|
```javascript
|
||||||
|
async function sendAndTrack(phoneNumber, message) {
|
||||||
|
const result = await MySMSAPI.sendSMS(phoneNumber, message);
|
||||||
|
|
||||||
|
// Poll status until delivered
|
||||||
|
const maxAttempts = 30; // 5 minutes (30 * 10 seconds)
|
||||||
|
for (let i = 0; i < maxAttempts; i++) {
|
||||||
|
await sleep(10000); // Wait 10 seconds
|
||||||
|
|
||||||
|
const status = await MySMSAPI.checkStatus(result.message_id);
|
||||||
|
|
||||||
|
if (status.status === 'delivered') {
|
||||||
|
console.log('✅ Message delivered!');
|
||||||
|
return status;
|
||||||
|
} else if (status.status === 'failed') {
|
||||||
|
console.log('❌ Message failed:', status.error_message);
|
||||||
|
throw new Error(status.error_message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('⏱️ Delivery timeout');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Test API Key
|
||||||
|
Use the API Tester in the admin interface:
|
||||||
|
```
|
||||||
|
http://your-server:3005/admin/api_tester
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test from Command Line
|
||||||
|
```bash
|
||||||
|
# Send SMS
|
||||||
|
curl -X POST http://your-server:3005/api/v1/sms/send \
|
||||||
|
-H "Authorization: Bearer api_live_your_key" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"to":"+959123456789","message":"Test"}'
|
||||||
|
|
||||||
|
# Send OTP
|
||||||
|
curl -X POST http://your-server:3005/api/v1/otp/send \
|
||||||
|
-H "Authorization: Bearer api_live_your_key" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"phone_number":"+959123456789"}'
|
||||||
|
|
||||||
|
# Verify OTP
|
||||||
|
curl -X POST http://your-server:3005/api/v1/otp/verify \
|
||||||
|
-H "Authorization: Bearer api_live_your_key" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"phone_number":"+959123456789","code":"123456"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Messages Stuck in "pending"
|
||||||
|
**Cause**: No gateway online
|
||||||
|
**Solution**: Ensure at least one Android gateway is connected
|
||||||
|
|
||||||
|
### "Invalid phone number" error
|
||||||
|
**Cause**: Wrong format
|
||||||
|
**Solution**: Use international format: `+[country_code][number]`
|
||||||
|
|
||||||
|
### "Rate limit exceeded"
|
||||||
|
**Cause**: Too many requests
|
||||||
|
**Solution**: Implement exponential backoff and caching
|
||||||
|
|
||||||
|
### "Unauthorized" error
|
||||||
|
**Cause**: Invalid or expired API key
|
||||||
|
**Solution**: Generate new API key in admin interface
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
- **API Documentation**: See `API_DOCUMENTATION.md`
|
||||||
|
- **Admin Interface**: `http://your-server:3005/admin`
|
||||||
|
- **API Tester**: `http://your-server:3005/admin/api_tester`
|
||||||
|
- **Logs**: `http://your-server:3005/admin/logs`
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
✅ **Create API Key** → Get `api_live_...` key
|
||||||
|
✅ **Send SMS** → `POST /api/v1/sms/send`
|
||||||
|
✅ **Send OTP** → `POST /api/v1/otp/send`
|
||||||
|
✅ **Verify OTP** → `POST /api/v1/otp/verify`
|
||||||
|
✅ **Check Status** → `GET /api/v1/sms/status/:id`
|
||||||
|
|
||||||
|
Your SaaS app is now ready to send SMS and OTP! 🚀
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
module Admin
|
module Admin
|
||||||
class LogsController < BaseController
|
class LogsController < BaseController
|
||||||
def index
|
def index
|
||||||
|
items_per_page = params[:items]&.to_i || 50
|
||||||
|
items_per_page = 50 unless [10, 25, 50, 100].include?(items_per_page)
|
||||||
|
|
||||||
@pagy, @messages = pagy(
|
@pagy, @messages = pagy(
|
||||||
apply_filters(SmsMessage).order(created_at: :desc),
|
apply_filters(SmsMessage).order(created_at: :desc),
|
||||||
items: 50
|
items: items_per_page
|
||||||
)
|
)
|
||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
|
|||||||
@@ -128,7 +128,7 @@
|
|||||||
<%= msg.phone_number %>
|
<%= msg.phone_number %>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 text-sm text-gray-700">
|
<td class="px-6 py-4 text-sm text-gray-700">
|
||||||
<div class="max-w-xs truncate"><%= msg.message_body %></div>
|
<div class="max-w-md whitespace-pre-wrap break-words"><%= msg.message_body %></div>
|
||||||
</td>
|
</td>
|
||||||
<td class="whitespace-nowrap px-6 py-4 text-sm">
|
<td class="whitespace-nowrap px-6 py-4 text-sm">
|
||||||
<% if msg.direction == "outbound" %>
|
<% if msg.direction == "outbound" %>
|
||||||
@@ -209,8 +209,71 @@
|
|||||||
<!-- Pagination -->
|
<!-- Pagination -->
|
||||||
<% if @pagy.pages > 1 %>
|
<% if @pagy.pages > 1 %>
|
||||||
<div class="border-t border-gray-200 px-6 py-4 bg-gray-50">
|
<div class="border-t border-gray-200 px-6 py-4 bg-gray-50">
|
||||||
<div class="flex items-center justify-center">
|
<div class="flex items-center justify-between">
|
||||||
<%== pagy_nav(@pagy) %>
|
<!-- Page info -->
|
||||||
|
<div class="text-sm text-gray-700">
|
||||||
|
Showing
|
||||||
|
<span class="font-semibold"><%= @pagy.from %></span>
|
||||||
|
to
|
||||||
|
<span class="font-semibold"><%= @pagy.to %></span>
|
||||||
|
of
|
||||||
|
<span class="font-semibold"><%= @pagy.count %></span>
|
||||||
|
results
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pagination controls -->
|
||||||
|
<nav class="isolate inline-flex -space-x-px rounded-md shadow-sm" aria-label="Pagination">
|
||||||
|
<% if @pagy.prev %>
|
||||||
|
<%= link_to admin_logs_path(params.permit!.merge(page: @pagy.prev)),
|
||||||
|
class: "relative inline-flex items-center rounded-l-md px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 transition-colors" do %>
|
||||||
|
<i class="fas fa-chevron-left"></i>
|
||||||
|
<span class="sr-only">Previous</span>
|
||||||
|
<% end %>
|
||||||
|
<% else %>
|
||||||
|
<span class="relative inline-flex items-center rounded-l-md px-3 py-2 text-sm font-semibold text-gray-400 ring-1 ring-inset ring-gray-300 cursor-not-allowed">
|
||||||
|
<i class="fas fa-chevron-left"></i>
|
||||||
|
</span>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% @pagy.series.each do |item| %>
|
||||||
|
<% if item.is_a?(Integer) %>
|
||||||
|
<% if item == @pagy.page %>
|
||||||
|
<span class="relative z-10 inline-flex items-center bg-blue-600 px-4 py-2 text-sm font-semibold text-white ring-1 ring-inset ring-blue-600"><%= item %></span>
|
||||||
|
<% else %>
|
||||||
|
<%= link_to item, admin_logs_path(params.permit!.merge(page: item)),
|
||||||
|
class: "relative inline-flex items-center px-4 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 transition-colors" %>
|
||||||
|
<% end %>
|
||||||
|
<% elsif item == :gap %>
|
||||||
|
<span class="relative inline-flex items-center px-4 py-2 text-sm font-semibold text-gray-700 ring-1 ring-inset ring-gray-300">...</span>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
|
||||||
|
<% if @pagy.next %>
|
||||||
|
<%= link_to admin_logs_path(params.permit!.merge(page: @pagy.next)),
|
||||||
|
class: "relative inline-flex items-center rounded-r-md px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 transition-colors" do %>
|
||||||
|
<span class="sr-only">Next</span>
|
||||||
|
<i class="fas fa-chevron-right"></i>
|
||||||
|
<% end %>
|
||||||
|
<% else %>
|
||||||
|
<span class="relative inline-flex items-center rounded-r-md px-3 py-2 text-sm font-semibold text-gray-400 ring-1 ring-inset ring-gray-300 cursor-not-allowed">
|
||||||
|
<i class="fas fa-chevron-right"></i>
|
||||||
|
</span>
|
||||||
|
<% end %>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Page size selector -->
|
||||||
|
<div class="text-sm text-gray-700">
|
||||||
|
<%= form_with url: admin_logs_path, method: :get, local: true, class: "inline-flex items-center gap-2" do |f| %>
|
||||||
|
<label for="items" class="font-medium">Per page:</label>
|
||||||
|
<%= select_tag :items,
|
||||||
|
options_for_select([10, 25, 50, 100], params[:items] || 50),
|
||||||
|
onchange: "this.form.submit()",
|
||||||
|
class: "rounded-md border-gray-300 text-sm focus:border-blue-500 focus:ring-blue-500" %>
|
||||||
|
<% params.except(:items, :authenticity_token).each do |key, value| %>
|
||||||
|
<%= hidden_field_tag key, value %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<% end %>
|
<% end %>
|
||||||
|
|||||||
Reference in New Issue
Block a user