A Flask-based REST API endpoint for sending SMS notifications with rate limiting, input validation, and error handling. Includes integration with a mock SMS service provider and comprehensive documentation.
import os
import time
from datetime import datetime, timedelta
from functools import wraps
from flask import Flask, request, jsonify
from flask_cors import CORS
import re
app = Flask(__name__)
CORS(app)
class RateLimiter:
"""
Rate limiter to prevent API abuse.
Tracks requests per phone number with a time window.
"""
def __init__(self, max_requests=5, time_window=3600):
self.max_requests = max_requests
self.time_window = time_window
self.requests = {}
def is_allowed(self, identifier):
"""
Check if a request is allowed based on rate limit.
Returns True if request is within limits, False otherwise.
"""
now = time.time()
if identifier not in self.requests:
self.requests[identifier] = []
request_times = self.requests[identifier]
request_times = [req_time for req_time in request_times
if now - req_time < self.time_window]
if len(request_times) < self.max_requests:
request_times.append(now)
self.requests[identifier] = request_times
return True
return False
rate_limiter = RateLimiter(max_requests=5, time_window=3600)
class SMSValidator:
"""
Validates phone numbers and message content.
Ensures data integrity before processing.
"""
@staticmethod
def validate_phone(phone_number):
"""
Validate phone number format.
Accepts international format starting with + or standard format.
"""
pattern = r'^\+?[1-9]\d{1,14}$'
return re.match(pattern, phone_number.replace(' ', '').replace('-', '')) is not None
@staticmethod
def validate_message(message):
"""
Validate message content.
Message must be between 1 and 160 characters.
"""
if not message or len(message) == 0:
return False
if len(message) > 160:
return False
return True
@staticmethod
def validate_recipient(recipient_email):
"""
Validate recipient email address.
Uses basic email pattern validation.
"""
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
return re.match(pattern, recipient_email) is not None
class SMSService:
"""
Mock SMS service provider.
In production, integrate with real providers like Twilio, AWS SNS, etc.
"""
@staticmethod
def send_sms(phone_number, message):
"""
Send SMS to specified phone number.
Returns success status and delivery ID.
"""
delivery_id = f'SMS_{int(time.time())}_{hash(phone_number) % 10000}'
print(f'[SMS Service] Sending to {phone_number}: {message[:50]}...')
return {
'success': True,
'delivery_id': delivery_id,
'timestamp': datetime.utcnow().isoformat(),
'phone': phone_number,
'message_length': len(message)
}
@app.route('/api/sms/send', methods=['POST'])
def send_sms_endpoint():
"""
API endpoint to send SMS notification.
Request body:
{
'phone_number': '+1234567890' or '1234567890',
'message': 'Your notification message',
'sender_id': 'optional_sender_identifier'
}
Returns JSON response with status and delivery information.
"""
try:
data = request.get_json()
if not data:
return jsonify({
'success': False,
'error': 'Request body must be JSON',
'status_code': 400
}), 400
phone_number = data.get('phone_number', '').strip()
message = data.get('message', '').strip()
sender_id = data.get('sender_id', 'DefaultApp').strip()
if not phone_number:
return jsonify({
'success': False,
'error': 'phone_number is required',
'status_code': 400
}), 400
if not message:
return jsonify({
'success': False,
'error': 'message is required',
'status_code': 400
}), 400
if not SMSValidator.validate_phone(phone_number):
return jsonify({
'success': False,
'error': 'Invalid phone number format',
'status_code': 400
}), 400
if not SMSValidator.validate_message(message):
return jsonify({
'success': False,
'error': 'Message must be between 1 and 160 characters',
'status_code': 400
}), 400
if not rate_limiter.is_allowed(phone_number):
return jsonify({
'success': False,
'error': 'Rate limit exceeded. Maximum 5 SMS per hour per number',
'status_code': 429
}), 429
result = SMSService.send_sms(phone_number, message)
return jsonify({
'success': True,
'message': 'SMS sent successfully',
'delivery_id': result['delivery_id'],
'phone_number': phone_number,
'message_length': result['message_length'],
'timestamp': result['timestamp'],
'status_code': 200
}), 200
except Exception as e:
print(f'Error in send_sms_endpoint: {str(e)}')
return jsonify({
'success': False,
'error': 'Internal server error',
'details': str(e),
'status_code': 500
}), 500
@app.route('/api/sms/health', methods=['GET'])
def health_check():
"""
Health check endpoint.
Verifies API is running and operational.
"""
return jsonify({
'status': 'operational',
'service': 'SMS Notification API',
'timestamp': datetime.utcnow().isoformat(),
'version': '1.0.0'
}), 200
@app.errorhandler(404)
def not_found(error):
"""
Handle 404 Not Found errors.
"""
return jsonify({
'success': False,
'error': 'Endpoint not found',
'status_code': 404
}), 404
@app.errorhandler(405)
def method_not_allowed(error):
"""
Handle 405 Method Not Allowed errors.
"""
return jsonify({
'success': False,
'error': 'Method not allowed',
'status_code': 405
}), 405
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=5000)