├── cookies.json ├── requirements.txt ├── vercel.json ├── LICENSE ├── README.md └── app.py /cookies.json: -------------------------------------------------------------------------------- 1 | #Your Json Formated Cookies Here -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask 2 | requests 3 | beautifulsoup4 4 | cloudscraper 5 | brotli -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "builds": [ 4 | { 5 | "src": "app.py", 6 | "use": "@vercel/python" 7 | } 8 | ], 9 | "routes": [ 10 | { 11 | "src": "/(.*)", 12 | "dest": "app.py" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 @ISmartCoder 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Here’s an updated version of the README.md with your requested changes, including a warning against re-uploading to GitHub without proper credit to @ISmartCoder, a recommendation to fork instead, and some cool emojis! 😎🚀 2 | 3 | 4 | 5 | # IVAS SMS Panel API 🎉 6 | 7 | Welcome to the IVAS SMS Panel API, a powerful Flask-based application designed to interact with the IVAS SMS service programmatically. This API allows you to retrieve OTP messages and SMS statistics efficiently. 🌟 8 | 9 | - **Repository**: [github.com/TheSmartDevs/IvasSMSPanelAPI](https://github.com/TheSmartDevs/IvasSMSPanelAPI) 10 | - **Project Owner**: [@ISmartCoder](https://github.com/abirxdhack) 11 | 12 | ## Features ✨ 13 | 14 | - **OTP Retrieval**: Fetch OTP messages for specified phone ranges and date ranges. 15 | - **SMS Statistics**: Retrieve counts of SMS (total, paid, unpaid) and revenue data. 16 | - **Cookie-Based Authentication**: Secure login using stored cookies to maintain sessions. 17 | - **Flexible Querying**: Support for custom date ranges and limits on OTP retrieval. 18 | - **Robust Error Handling**: Detailed logging and error messages for debugging. 19 | - **Response Compression**: Handles gzip and brotli encoded responses seamlessly. 20 | 21 | ## Requirements 📋 22 | 23 | - A fresh, valid `cookies.json` file with authentication cookies for IVAS SMS Panel. 24 | - **Note**: Cookies must be updated regularly as they may expire. The API includes an alert system to notify about potential cookie expiration but does not log out to preserve the session. 25 | - Python 3.x 26 | - Required packages (listed in `requirements.txt`) 27 | 28 | ## Installation 🛠️ 29 | 30 | 1. Clone the repository: 31 | ```bash 32 | git clone https://github.com/TheSmartDevs/IvasSMSPanelAPI.git 33 | cd IvasSMSPanelAPI 34 | ``` 35 | 36 | 2. Install dependencies: 37 | ```bash 38 | pip install -r requirements.txt 39 | ``` 40 | 41 | 3. Update `cookies.json` with valid authentication cookies. 42 | 43 | 4. Run the application: 44 | ```bash 45 | python app.py 46 | ``` 47 | 48 | ## Hosting on Vercel 🌐 49 | 50 | 1. Fork the repository on GitHub. 51 | 2. Update the `cookies.json` file with valid authentication cookies. 52 | 3. Go to [vercel.com](https://vercel.com), sign in, and select the forked repository. 53 | 4. Deploy the project. Vercel will automatically handle the build and deployment process. 54 | 55 | ## API Endpoints 📡 56 | 57 | - **Welcome Endpoint**: 58 | - URL: `/` 59 | - Method: `GET` 60 | - Response Type: `application/json` 61 | - Example Response: 62 | ```json 63 | { 64 | "message": "Welcome to the IVAS SMS API", 65 | "status": "API is alive", 66 | "endpoints": { 67 | "/sms": "Get OTP messages for a specific date (format: DD/MM/YYYY) with optional limit. Example: /sms?date=01/05/2025&limit=10" 68 | } 69 | } 70 | ``` 71 | 72 | - **SMS Retrieval Endpoint**: 73 | - URL: `/sms` 74 | - Method: `GET` 75 | - Query Parameters: 76 | - `date` (required): Date in `DD/MM/YYYY` format. 77 | - `to_date` (optional): End date in `DD/MM/YYYY` format. 78 | - `limit` (optional): Maximum number of OTP messages to return. 79 | - Response Type: `application/json` 80 | - Example Response: 81 | ```json 82 | { 83 | "status": "success", 84 | "from_date": "01/05/2025", 85 | "to_date": "Not specified", 86 | "limit": "Not specified", 87 | "sms_stats": { 88 | "count_sms": "50", 89 | "paid_sms": "30", 90 | "unpaid_sms": "20", 91 | "revenue": "25.50" 92 | }, 93 | "otp_messages": [ 94 | { 95 | "range": "+1", 96 | "phone_number": "+1234567890", 97 | "otp_message": "Your OTP is 123456" 98 | } 99 | ] 100 | } 101 | ``` 102 | 103 | ## Important Notice ⚠️ 104 | 105 | - **Do not re-upload this code to GitHub**! If you must share or use it elsewhere, please provide proper credit to the original author, [@ISmartCoder](https://github.com/ISmartCoder). 🚫 106 | - **Forking Recommended**: Instead of re-uploading, fork this repository to contribute or use it for your projects. This helps maintain the integrity of the original work. 🙌 107 | 108 | ## Contributing 🤝 109 | 110 | Contributions are welcome! Please fork the repository and submit pull requests. For major changes, please open an issue first to discuss what you would like to change. 111 | 112 | ## License 📜 113 | 114 | This project is open-source. For more details, see the `LICENSE` file (if included). 115 | 116 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | #Copyright @ISmartCoder 2 | #Updates Channel t.me/TheSmartDev 3 | from flask import Flask, request, jsonify 4 | from datetime import datetime 5 | import cloudscraper 6 | import json 7 | from bs4 import BeautifulSoup 8 | import logging 9 | import os 10 | import gzip 11 | from io import BytesIO 12 | import brotli 13 | 14 | logging.basicConfig(level=logging.DEBUG) 15 | logger = logging.getLogger(__name__) 16 | 17 | class IVASSMSClient: 18 | def __init__(self): 19 | self.scraper = cloudscraper.create_scraper() 20 | self.base_url = "https://www.ivasms.com" 21 | self.logged_in = False 22 | self.csrf_token = None 23 | 24 | self.scraper.headers.update({ 25 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36', 26 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7', 27 | 'Accept-Language': 'en-US,en;q=0.9', 28 | 'Accept-Encoding': 'gzip, deflate, br', 29 | 'Connection': 'keep-alive', 30 | 'Upgrade-Insecure-Requests': '1', 31 | 'Sec-Fetch-Dest': 'document', 32 | 'Sec-Fetch-Mode': 'navigate', 33 | 'Sec-Fetch-Site': 'none', 34 | 'Sec-Fetch-User': '?1', 35 | 'Cache-Control': 'max-age=0', 36 | }) 37 | 38 | def decompress_response(self, response): 39 | """Decompress response content if encoded with gzip or brotli.""" 40 | encoding = response.headers.get('Content-Encoding', '').lower() 41 | content = response.content 42 | try: 43 | if encoding == 'gzip': 44 | logger.debug("Decompressing gzip response") 45 | content = gzip.decompress(content) 46 | elif encoding == 'br': 47 | logger.debug("Decompressing brotli response") 48 | content = brotli.decompress(content) 49 | return content.decode('utf-8', errors='replace') 50 | except Exception as e: 51 | logger.error(f"Error decompressing response: {e}") 52 | return response.text 53 | 54 | def load_cookies(self, file_path="cookies.json"): 55 | try: 56 | if os.getenv("COOKIES_JSON"): 57 | cookies_raw = json.loads(os.getenv("COOKIES_JSON")) 58 | logger.debug("Loaded cookies from environment variable") 59 | else: 60 | with open(file_path, 'r') as file: 61 | cookies_raw = json.load(file) 62 | logger.debug("Loaded cookies from file") 63 | 64 | if isinstance(cookies_raw, dict): 65 | logger.debug("Cookies loaded as dictionary") 66 | return cookies_raw 67 | elif isinstance(cookies_raw, list): 68 | cookies = {} 69 | for cookie in cookies_raw: 70 | if 'name' in cookie and 'value' in cookie: 71 | cookies[cookie['name']] = cookie['value'] 72 | logger.debug("Cookies loaded as list") 73 | return cookies 74 | else: 75 | logger.error("Cookies are in an unsupported format") 76 | raise ValueError("Cookies are in an unsupported format.") 77 | except FileNotFoundError: 78 | logger.error("cookies.json file not found") 79 | return None 80 | except json.JSONDecodeError: 81 | logger.error("Invalid JSON format in cookies.json") 82 | return None 83 | except Exception as e: 84 | logger.error(f"Error loading cookies: {e}") 85 | return None 86 | 87 | def login_with_cookies(self, cookies_file="cookies.json"): 88 | logger.debug("Attempting to login with cookies") 89 | cookies = self.load_cookies(cookies_file) 90 | if not cookies: 91 | logger.error("No valid cookies loaded") 92 | return False 93 | 94 | for name, value in cookies.items(): 95 | self.scraper.cookies.set(name, value, domain="www.ivasms.com") 96 | 97 | try: 98 | response = self.scraper.get(f"{self.base_url}/portal/sms/received", timeout=10) 99 | logger.debug(f"Response headers: {response.headers}") 100 | if response.status_code == 200: 101 | html_content = self.decompress_response(response) 102 | soup = BeautifulSoup(html_content, 'html.parser') 103 | csrf_input = soup.find('input', {'name': '_token'}) 104 | if csrf_input: 105 | self.csrf_token = csrf_input.get('value') 106 | self.logged_in = True 107 | logger.debug(f"Logged in successfully with CSRF token: {self.csrf_token}") 108 | return True 109 | else: 110 | logger.error("Could not find CSRF token. Dumping response HTML for debugging:") 111 | logger.error(f"Response HTML (first 2000 chars): {html_content[:2000]}") 112 | logger.error(f"Full response length: {len(html_content)}") 113 | return False 114 | logger.error(f"Login failed with status code: {response.status_code}") 115 | return False 116 | except Exception as e: 117 | logger.error(f"Login error: {e}") 118 | return False 119 | 120 | def check_otps(self, from_date="", to_date=""): 121 | if not self.logged_in: 122 | logger.error("Not logged in") 123 | return None 124 | 125 | if not self.csrf_token: 126 | logger.error("No CSRF token available") 127 | return None 128 | 129 | logger.debug(f"Checking OTPs from {from_date} to {to_date}") 130 | try: 131 | payload = { 132 | 'from': from_date, 133 | 'to': to_date, 134 | '_token': self.csrf_token 135 | } 136 | 137 | headers = { 138 | 'Accept': 'text/html, */*; q=0.01', 139 | 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 140 | 'X-Requested-With': 'XMLHttpRequest', 141 | 'Origin': self.base_url, 142 | 'Referer': f"{self.base_url}/portal/sms/received" 143 | } 144 | 145 | response = self.scraper.post( 146 | f"{self.base_url}/portal/sms/received/getsms", 147 | data=payload, 148 | headers=headers, 149 | timeout=10 150 | ) 151 | 152 | if response.status_code == 200: 153 | logger.debug("Successfully retrieved SMS data") 154 | html_content = self.decompress_response(response) 155 | soup = BeautifulSoup(html_content, 'html.parser') 156 | 157 | count_sms = soup.select_one("#CountSMS").text if soup.select_one("#CountSMS") else '0' 158 | paid_sms = soup.select_one("#PaidSMS").text if soup.select_one("#PaidSMS") else '0' 159 | unpaid_sms = soup.select_one("#UnpaidSMS").text if soup.select_one("#UnpaidSMS") else '0' 160 | revenue_sms = soup.select_one("#RevenueSMS").text.replace(' USD', '') if soup.select_one("#RevenueSMS") else '0' 161 | 162 | sms_details = [] 163 | items = soup.select("div.item") 164 | for item in items: 165 | country_number = item.select_one(".col-sm-4").text.strip() 166 | count = item.select_one(".col-3:nth-child(2) p").text.strip() 167 | paid = item.select_one(".col-3:nth-child(3) p").text.strip() 168 | unpaid = item.select_one(".col-3:nth-child(4) p").text.strip() 169 | revenue = item.select_one(".col-3:nth-child(5) p span.currency_cdr").text.strip() 170 | 171 | sms_details.append({ 172 | 'country_number': country_number, 173 | 'count': count, 174 | 'paid': paid, 175 | 'unpaid': unpaid, 176 | 'revenue': revenue 177 | }) 178 | 179 | result = { 180 | 'count_sms': count_sms, 181 | 'paid_sms': paid_sms, 182 | 'unpaid_sms': unpaid_sms, 183 | 'revenue': revenue_sms, 184 | 'sms_details': sms_details 185 | } 186 | result['raw_response'] = html_content 187 | logger.debug(f"Retrieved {len(sms_details)} SMS detail records: {sms_details}") 188 | return result 189 | logger.error(f"Failed to check OTPs. Status code: {response.status_code}, Response: {self.decompress_response(response)[:2000]}") 190 | return None 191 | except Exception as e: 192 | logger.error(f"Error checking OTPs: {e}") 193 | return None 194 | 195 | def get_sms_details(self, phone_range, from_date="", to_date=""): 196 | if not self.logged_in: 197 | logger.error("Not logged in") 198 | return None 199 | 200 | logger.debug(f"Fetching SMS details for range: {phone_range}, from {from_date} to {to_date}") 201 | try: 202 | payload = { 203 | '_token': self.csrf_token, 204 | 'start': from_date, 205 | 'end': to_date, 206 | 'range': phone_range 207 | } 208 | 209 | headers = { 210 | 'Accept': 'text/html, */*; q=0.01', 211 | 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 212 | 'X-Requested-With': 'XMLHttpRequest', 213 | 'Origin': self.base_url, 214 | 'Referer': f"{self.base_url}/portal/sms/received" 215 | } 216 | 217 | response = self.scraper.post( 218 | f"{self.base_url}/portal/sms/received/getsms/number", 219 | data=payload, 220 | headers=headers, 221 | timeout=10 222 | ) 223 | 224 | if response.status_code == 200: 225 | html_content = self.decompress_response(response) 226 | soup = BeautifulSoup(html_content, 'html.parser') 227 | number_details = [] 228 | items = soup.select("div.card.card-body") 229 | for item in items: 230 | phone_number = item.select_one(".col-sm-4").text.strip() 231 | count = item.select_one(".col-3:nth-child(2) p").text.strip() 232 | paid = item.select_one(".col-3:nth-child(3) p").text.strip() 233 | unpaid = item.select_one(".col-3:nth-child(4) p").text.strip() 234 | revenue = item.select_one(".col-3:nth-child(5) p span.currency_cdr").text.strip() 235 | onclick = item.select_one(".col-sm-4").get('onclick', '') 236 | id_number = onclick.split("'")[3] if onclick else '' 237 | 238 | number_details.append({ 239 | 'phone_number': phone_number, 240 | 'count': count, 241 | 'paid': paid, 242 | 'unpaid': unpaid, 243 | 'revenue': revenue, 244 | 'id_number': id_number 245 | }) 246 | logger.debug(f"Retrieved {len(number_details)} number details for range {phone_range}: {number_details}") 247 | return number_details 248 | logger.error(f"Failed to get SMS details for {phone_range}. Status code: {response.status_code}, Response: {self.decompress_response(response)[:2000]}") 249 | return None 250 | except Exception as e: 251 | logger.error(f"Error getting SMS details for {phone_range}: {e}") 252 | return None 253 | 254 | def get_otp_message(self, phone_number, phone_range, from_date="", to_date=""): 255 | if not self.logged_in: 256 | logger.error("Not logged in") 257 | return None 258 | 259 | logger.debug(f"Fetching OTP message for phone: {phone_number}, range: {phone_range}, from {from_date} to {to_date}") 260 | try: 261 | payload = { 262 | '_token': self.csrf_token, 263 | 'start': from_date, 264 | 'end': to_date, 265 | 'Number': phone_number, 266 | 'Range': phone_range 267 | } 268 | 269 | headers = { 270 | 'Accept': 'text/html, */*; q=0.01', 271 | 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', 272 | 'X-Requested-With': 'XMLHttpRequest', 273 | 'Origin': self.base_url, 274 | 'Referer': f"{self.base_url}/portal/sms/received" 275 | } 276 | 277 | response = self.scraper.post( 278 | f"{self.base_url}/portal/sms/received/getsms/number/sms", 279 | data=payload, 280 | headers=headers, 281 | timeout=10 282 | ) 283 | 284 | if response.status_code == 200: 285 | html_content = self.decompress_response(response) 286 | soup = BeautifulSoup(html_content, 'html.parser') 287 | message = soup.select_one(".col-9.col-sm-6 p").text.strip() if soup.select_one(".col-9.col-sm-6 p") else None 288 | logger.debug(f"Retrieved OTP message for {phone_number}: {message}") 289 | return message 290 | logger.error(f"Failed to get OTP message for {phone_number}. Status code: {response.status_code}, Response: {self.decompress_response(response)[:2000]}") 291 | return None 292 | except Exception as e: 293 | logger.error(f"Error getting OTP message for {phone_number}: {e}") 294 | return None 295 | 296 | def get_all_otp_messages(self, sms_details, from_date="", to_date="", limit=None): 297 | all_otp_messages = [] 298 | 299 | logger.debug(f"Processing {len(sms_details)} SMS details for OTP messages with limit {limit}") 300 | for detail in sms_details: 301 | phone_range = detail['country_number'] 302 | number_details = self.get_sms_details(phone_range, from_date, to_date) 303 | 304 | if number_details: 305 | for number_detail in number_details: 306 | if limit is not None and len(all_otp_messages) >= limit: 307 | logger.debug(f"Reached limit of {limit} OTP messages, stopping") 308 | return all_otp_messages 309 | phone_number = number_detail['phone_number'] 310 | otp_message = self.get_otp_message(phone_number, phone_range, from_date, to_date) 311 | if otp_message: 312 | all_otp_messages.append({ 313 | 'range': phone_range, 314 | 'phone_number': phone_number, 315 | 'otp_message': otp_message 316 | }) 317 | logger.debug(f"Added OTP message for {phone_number}: {otp_message}") 318 | else: 319 | logger.warning(f"No number details found for range: {phone_range}") 320 | 321 | logger.debug(f"Collected {len(all_otp_messages)} OTP messages") 322 | return all_otp_messages 323 | 324 | app = Flask(__name__) 325 | client = IVASSMSClient() 326 | 327 | with app.app_context(): 328 | if not client.login_with_cookies(): 329 | logger.error("Failed to initialize client with cookies") 330 | 331 | @app.route('/') 332 | def welcome(): 333 | return jsonify({ 334 | 'message': 'Welcome to the IVAS SMS API', 335 | 'status': 'API is alive', 336 | 'endpoints': { 337 | '/sms': 'Get OTP messages for a specific date (format: DD/MM/YYYY) with optional limit. Example: /sms?date=01/05/2025&limit=10' 338 | } 339 | }) 340 | 341 | @app.route('/sms') 342 | def get_sms(): 343 | date_str = request.args.get('date') 344 | limit = request.args.get('limit') 345 | 346 | if not date_str: 347 | return jsonify({ 348 | 'error': 'Date parameter is required in DD/MM/YYYY format' 349 | }), 400 350 | 351 | try: 352 | parsed_date = datetime.strptime(date_str, '%d/%m/%Y') 353 | from_date = date_str 354 | to_date = request.args.get('to_date', '') 355 | if to_date: 356 | datetime.strptime(to_date, '%d/%m/%Y') 357 | except ValueError: 358 | return jsonify({ 359 | 'error': 'Invalid date format. Use DD/MM/YYYY' 360 | }), 400 361 | 362 | if limit: 363 | try: 364 | limit = int(limit) 365 | if limit <= 0: 366 | return jsonify({ 367 | 'error': 'Limit must be a positive integer' 368 | }), 400 369 | except ValueError: 370 | return jsonify({ 371 | 'error': 'Limit must be a valid integer' 372 | }), 400 373 | else: 374 | limit = None 375 | 376 | if not client.logged_in: 377 | return jsonify({ 378 | 'error': 'Client not authenticated' 379 | }), 401 380 | 381 | logger.debug(f"Fetching SMS for date range: {from_date} to {to_date or 'empty'} with limit {limit}") 382 | result = client.check_otps(from_date=from_date, to_date=to_date) 383 | 384 | if not result: 385 | return jsonify({ 386 | 'error': 'Failed to fetch OTP data' 387 | }), 500 388 | 389 | otp_messages = client.get_all_otp_messages(result.get('sms_details', []), from_date=from_date, to_date=to_date, limit=limit) 390 | 391 | return jsonify({ 392 | 'status': 'success', 393 | 'from_date': from_date, 394 | 'to_date': to_date or 'Not specified', 395 | 'limit': limit if limit is not None else 'Not specified', 396 | 'sms_stats': { 397 | 'count_sms': result['count_sms'], 398 | 'paid_sms': result['paid_sms'], 399 | 'unpaid_sms': result['unpaid_sms'], 400 | 'revenue': result['revenue'] 401 | }, 402 | 'otp_messages': otp_messages 403 | }) 404 | 405 | if __name__ == '__main__': 406 | app.run(host='0.0.0.0', port=5000, debug=False) 407 | --------------------------------------------------------------------------------