smsgora 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
smsgora-1.0.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tekservix
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.
smsgora-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,156 @@
1
+ Metadata-Version: 2.4
2
+ Name: smsgora
3
+ Version: 1.0.0
4
+ Summary: Official Python client for the SMS Gora SMS Gateway: send and receive SMS through your own Android phones, with delivery reports and signed webhooks.
5
+ Author-email: Tekservix <support@tekservix.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://tekservix.com/smsgora/
8
+ Project-URL: Documentation, https://tekservix.com/smsgora/docs/
9
+ Project-URL: Source, https://github.com/ellyj164/smsgora-python
10
+ Project-URL: Issues, https://github.com/ellyj164/smsgora-python/issues
11
+ Keywords: sms,sms-gateway,sms-api,bulk-sms,send-sms,android-sms-gateway,otp,textbee,smsgora,tekservix
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Topic :: Communications :: Telephony
16
+ Classifier: Topic :: Software Development :: Libraries
17
+ Requires-Python: >=3.8
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Dynamic: license-file
21
+
22
+ # smsgora
23
+
24
+ Python client for the [SMS Gora SMS Gateway](https://tekservix.com/smsgora/) API.
25
+
26
+ SMS Gora turns Android phones with ordinary SIM cards into an SMS gateway. Your server calls the API, a linked phone sends the text, and replies and delivery reports come back to you. There's no sender ID registration and no per-message fee from us: your mobile operator bills the texts like any SMS.
27
+
28
+ - Zero dependencies (standard library only), Python 3.8+
29
+ - Works in Django, Flask, FastAPI, scripts and cron jobs
30
+ - Webhook signature check built in
31
+
32
+ ## Install
33
+
34
+ ```bash
35
+ pip install smsgora
36
+ ```
37
+
38
+ ## Quick start
39
+
40
+ Create an API key in the SMS Gora dashboard (SMS Gora -> API keys) and keep it on your server.
41
+
42
+ ```python
43
+ import os
44
+ from smsgora import SmsGora
45
+
46
+ sms = SmsGora(api_key=os.environ["SMSGORA_KEY"])
47
+
48
+ result = sms.send_sms(to="+15551234567", message="Your verification code is 482913")
49
+ print(result["message_ids"]) # [1873]
50
+ ```
51
+
52
+ ## Sending
53
+
54
+ ```python
55
+ from datetime import datetime, timezone
56
+
57
+ # Several recipients (up to 1,000 per call)
58
+ sms.send_sms(to=["+15551234567", "+447700900123"], message="Your order has shipped")
59
+
60
+ # From a chosen phone and SIM, scheduled for later
61
+ sms.send_sms(
62
+ to="+15551234567",
63
+ message="Reminder: your appointment is tomorrow at 10:00",
64
+ device_id=42,
65
+ sim_slot=1, # 0 = SIM 1, 1 = SIM 2
66
+ schedule_at=datetime(2026, 12, 1, 8, 0, tzinfo=timezone.utc),
67
+ )
68
+
69
+ # Different texts for different people in one call (up to 5,000 recipients)
70
+ sms.send_bulk_sms(
71
+ 42,
72
+ message_template="Hello, your parcel is ready for pickup.",
73
+ messages=[
74
+ {"recipients": ["+15551234567"], "message": "Hi Aline, parcel #1042 is ready."},
75
+ {"recipients": ["+15557654321"]},
76
+ ],
77
+ )
78
+ ```
79
+
80
+ ## Delivery status and incoming SMS
81
+
82
+ ```python
83
+ msg = sms.get_message(1873)
84
+ print(msg["status"]) # queued -> dispatched -> sent -> delivered (or failed)
85
+
86
+ sms.cancel_message(1874) # works while queued or scheduled
87
+
88
+ inbox = sms.list_received_sms(since="2026-11-01", limit=50)
89
+ phones = sms.list_devices()
90
+ account = sms.get_account()
91
+ ```
92
+
93
+ Anything else in the [API reference](https://tekservix.com/smsgora/docs/) is one call away:
94
+
95
+ ```python
96
+ batch = sms.request("GET", "/batches/311")
97
+ ```
98
+
99
+ ## Webhooks
100
+
101
+ Add your HTTPS URL in SMS Gora -> Webhooks (or with `create_webhook`) and keep the secret. Each request carries `X-SmsGora-Signature: sha256=...`, an HMAC-SHA256 of the raw body. Check it before trusting the request. Flask example:
102
+
103
+ ```python
104
+ import json, os
105
+ from flask import Flask, request, abort
106
+ from smsgora import verify_webhook
107
+
108
+ app = Flask(__name__)
109
+
110
+ @app.post("/sms/webhook")
111
+ def sms_webhook():
112
+ if not verify_webhook(request.get_data(), request.headers.get("X-SmsGora-Signature"),
113
+ os.environ["SMSGORA_WEBHOOK_SECRET"]):
114
+ abort(401)
115
+ event = json.loads(request.get_data())
116
+ if event["event"] == "sms.received":
117
+ print(event["data"]["from"], event["data"]["message"])
118
+ return "", 200 # answer within 10 seconds; queue slow work
119
+ ```
120
+
121
+ In Django use `request.body`; in FastAPI use `await request.body()`. Always pass the raw bytes, not re-encoded JSON.
122
+
123
+ ## Errors
124
+
125
+ Failed calls raise `SmsGoraError` with `status`, `message`, field `errors` and, on 429, `retry_after` in seconds.
126
+
127
+ ```python
128
+ from smsgora import SmsGoraError
129
+
130
+ try:
131
+ sms.send_sms(to="+15551234567", message="Hi")
132
+ except SmsGoraError as err:
133
+ if err.status == 429:
134
+ pass # rate limit (300 requests/minute per key) or plan limit reached
135
+ ```
136
+
137
+ Never ship the key inside a browser or mobile app: anyone could read it. Call the API from your server.
138
+
139
+ ## Migrating from TextBee
140
+
141
+ SMS Gora accepts TextBee-style requests, so most integrations switch by changing the base URL and key. See the [compatibility notes](https://tekservix.com/smsgora/docs/).
142
+
143
+ ## Good to know
144
+
145
+ A phone-based gateway suits OTPs, order alerts, reminders and two-way conversations at moderate volume. It isn't meant for mass marketing blasts, and in the US high-volume business texting belongs on registered 10DLC or toll-free numbers.
146
+
147
+ ## More from Tekservix
148
+
149
+ - [WAservix WhatsApp API Gateway](https://tekservix.com/waservix/): send WhatsApp messages from your own number (`pip install waservix`)
150
+ - [AI Sales Agent](https://tekservix.com/ai-sales-agent): answers customers on WhatsApp and SMS using your website's content
151
+ - [Website to App](https://tekservix.com/mobile-apps): turn your website into Android and iOS apps
152
+ - Also on npm: `npm install smsgora`
153
+
154
+ ## License
155
+
156
+ MIT
@@ -0,0 +1,135 @@
1
+ # smsgora
2
+
3
+ Python client for the [SMS Gora SMS Gateway](https://tekservix.com/smsgora/) API.
4
+
5
+ SMS Gora turns Android phones with ordinary SIM cards into an SMS gateway. Your server calls the API, a linked phone sends the text, and replies and delivery reports come back to you. There's no sender ID registration and no per-message fee from us: your mobile operator bills the texts like any SMS.
6
+
7
+ - Zero dependencies (standard library only), Python 3.8+
8
+ - Works in Django, Flask, FastAPI, scripts and cron jobs
9
+ - Webhook signature check built in
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ pip install smsgora
15
+ ```
16
+
17
+ ## Quick start
18
+
19
+ Create an API key in the SMS Gora dashboard (SMS Gora -> API keys) and keep it on your server.
20
+
21
+ ```python
22
+ import os
23
+ from smsgora import SmsGora
24
+
25
+ sms = SmsGora(api_key=os.environ["SMSGORA_KEY"])
26
+
27
+ result = sms.send_sms(to="+15551234567", message="Your verification code is 482913")
28
+ print(result["message_ids"]) # [1873]
29
+ ```
30
+
31
+ ## Sending
32
+
33
+ ```python
34
+ from datetime import datetime, timezone
35
+
36
+ # Several recipients (up to 1,000 per call)
37
+ sms.send_sms(to=["+15551234567", "+447700900123"], message="Your order has shipped")
38
+
39
+ # From a chosen phone and SIM, scheduled for later
40
+ sms.send_sms(
41
+ to="+15551234567",
42
+ message="Reminder: your appointment is tomorrow at 10:00",
43
+ device_id=42,
44
+ sim_slot=1, # 0 = SIM 1, 1 = SIM 2
45
+ schedule_at=datetime(2026, 12, 1, 8, 0, tzinfo=timezone.utc),
46
+ )
47
+
48
+ # Different texts for different people in one call (up to 5,000 recipients)
49
+ sms.send_bulk_sms(
50
+ 42,
51
+ message_template="Hello, your parcel is ready for pickup.",
52
+ messages=[
53
+ {"recipients": ["+15551234567"], "message": "Hi Aline, parcel #1042 is ready."},
54
+ {"recipients": ["+15557654321"]},
55
+ ],
56
+ )
57
+ ```
58
+
59
+ ## Delivery status and incoming SMS
60
+
61
+ ```python
62
+ msg = sms.get_message(1873)
63
+ print(msg["status"]) # queued -> dispatched -> sent -> delivered (or failed)
64
+
65
+ sms.cancel_message(1874) # works while queued or scheduled
66
+
67
+ inbox = sms.list_received_sms(since="2026-11-01", limit=50)
68
+ phones = sms.list_devices()
69
+ account = sms.get_account()
70
+ ```
71
+
72
+ Anything else in the [API reference](https://tekservix.com/smsgora/docs/) is one call away:
73
+
74
+ ```python
75
+ batch = sms.request("GET", "/batches/311")
76
+ ```
77
+
78
+ ## Webhooks
79
+
80
+ Add your HTTPS URL in SMS Gora -> Webhooks (or with `create_webhook`) and keep the secret. Each request carries `X-SmsGora-Signature: sha256=...`, an HMAC-SHA256 of the raw body. Check it before trusting the request. Flask example:
81
+
82
+ ```python
83
+ import json, os
84
+ from flask import Flask, request, abort
85
+ from smsgora import verify_webhook
86
+
87
+ app = Flask(__name__)
88
+
89
+ @app.post("/sms/webhook")
90
+ def sms_webhook():
91
+ if not verify_webhook(request.get_data(), request.headers.get("X-SmsGora-Signature"),
92
+ os.environ["SMSGORA_WEBHOOK_SECRET"]):
93
+ abort(401)
94
+ event = json.loads(request.get_data())
95
+ if event["event"] == "sms.received":
96
+ print(event["data"]["from"], event["data"]["message"])
97
+ return "", 200 # answer within 10 seconds; queue slow work
98
+ ```
99
+
100
+ In Django use `request.body`; in FastAPI use `await request.body()`. Always pass the raw bytes, not re-encoded JSON.
101
+
102
+ ## Errors
103
+
104
+ Failed calls raise `SmsGoraError` with `status`, `message`, field `errors` and, on 429, `retry_after` in seconds.
105
+
106
+ ```python
107
+ from smsgora import SmsGoraError
108
+
109
+ try:
110
+ sms.send_sms(to="+15551234567", message="Hi")
111
+ except SmsGoraError as err:
112
+ if err.status == 429:
113
+ pass # rate limit (300 requests/minute per key) or plan limit reached
114
+ ```
115
+
116
+ Never ship the key inside a browser or mobile app: anyone could read it. Call the API from your server.
117
+
118
+ ## Migrating from TextBee
119
+
120
+ SMS Gora accepts TextBee-style requests, so most integrations switch by changing the base URL and key. See the [compatibility notes](https://tekservix.com/smsgora/docs/).
121
+
122
+ ## Good to know
123
+
124
+ A phone-based gateway suits OTPs, order alerts, reminders and two-way conversations at moderate volume. It isn't meant for mass marketing blasts, and in the US high-volume business texting belongs on registered 10DLC or toll-free numbers.
125
+
126
+ ## More from Tekservix
127
+
128
+ - [WAservix WhatsApp API Gateway](https://tekservix.com/waservix/): send WhatsApp messages from your own number (`pip install waservix`)
129
+ - [AI Sales Agent](https://tekservix.com/ai-sales-agent): answers customers on WhatsApp and SMS using your website's content
130
+ - [Website to App](https://tekservix.com/mobile-apps): turn your website into Android and iOS apps
131
+ - Also on npm: `npm install smsgora`
132
+
133
+ ## License
134
+
135
+ MIT
@@ -0,0 +1,27 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "smsgora"
7
+ version = "1.0.0"
8
+ description = "Official Python client for the SMS Gora SMS Gateway: send and receive SMS through your own Android phones, with delivery reports and signed webhooks."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ authors = [{ name = "Tekservix", email = "support@tekservix.com" }]
13
+ requires-python = ">=3.8"
14
+ keywords = ["sms", "sms-gateway", "sms-api", "bulk-sms", "send-sms", "android-sms-gateway", "otp", "textbee", "smsgora", "tekservix"]
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3",
17
+ "Operating System :: OS Independent",
18
+ "Intended Audience :: Developers",
19
+ "Topic :: Communications :: Telephony",
20
+ "Topic :: Software Development :: Libraries",
21
+ ]
22
+
23
+ [project.urls]
24
+ Homepage = "https://tekservix.com/smsgora/"
25
+ Documentation = "https://tekservix.com/smsgora/docs/"
26
+ Source = "https://github.com/ellyj164/smsgora-python"
27
+ Issues = "https://github.com/ellyj164/smsgora-python/issues"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,175 @@
1
+ """Python client for the SMS Gora SMS Gateway API.
2
+
3
+ Docs: https://tekservix.com/smsgora/docs/
4
+ """
5
+
6
+ import datetime as _dt
7
+ import hashlib
8
+ import hmac
9
+ import json
10
+ import urllib.error
11
+ import urllib.parse
12
+ import urllib.request
13
+
14
+ __all__ = ["SmsGora", "SmsGoraError", "verify_webhook"]
15
+ __version__ = "1.0.0"
16
+
17
+ DEFAULT_BASE_URL = "https://tekservix.com/smsgora/api/v1"
18
+
19
+
20
+ class SmsGoraError(Exception):
21
+ """Raised when the API answers with an error.
22
+
23
+ Attributes: status (HTTP code), errors (field errors), body (parsed answer)
24
+ and retry_after (seconds, on 429).
25
+ """
26
+
27
+ def __init__(self, message, status=None, errors=None, body=None, retry_after=None):
28
+ super().__init__(message)
29
+ self.message = message
30
+ self.status = status
31
+ self.errors = errors
32
+ self.body = body
33
+ self.retry_after = retry_after
34
+
35
+
36
+ def _q(value):
37
+ return urllib.parse.quote(str(value), safe="")
38
+
39
+
40
+ def _query(params):
41
+ if not params:
42
+ return ""
43
+ clean = {k: v for k, v in params.items() if v is not None and v != ""}
44
+ return "?" + urllib.parse.urlencode(clean) if clean else ""
45
+
46
+
47
+ class SmsGora:
48
+ """Client for the SMS Gora SMS Gateway.
49
+
50
+ >>> sms = SmsGora(api_key="sgk_...")
51
+ >>> sms.send_sms(to="+15551234567", message="Your code is 482913")
52
+ """
53
+
54
+ def __init__(self, api_key, base_url=DEFAULT_BASE_URL, timeout=30):
55
+ if not api_key:
56
+ raise ValueError("SmsGora: api_key is required (create one in SMS Gora -> API keys)")
57
+ self.api_key = api_key
58
+ self.base_url = base_url.rstrip("/")
59
+ self.timeout = timeout
60
+
61
+ def request(self, method, path, body=None):
62
+ """Call any documented endpoint, e.g. request("GET", "/devices"). Returns the `data` field."""
63
+ headers = {
64
+ "x-api-key": self.api_key,
65
+ "Accept": "application/json",
66
+ "User-Agent": "smsgora-python/" + __version__,
67
+ }
68
+ data = None
69
+ if body is not None:
70
+ data = json.dumps(body).encode("utf-8")
71
+ headers["Content-Type"] = "application/json"
72
+ req = urllib.request.Request(self.base_url + path, data=data, headers=headers, method=method)
73
+ try:
74
+ with urllib.request.urlopen(req, timeout=self.timeout) as res:
75
+ status, text, retry = res.status, res.read().decode("utf-8"), None
76
+ except urllib.error.HTTPError as err:
77
+ status, text = err.code, err.read().decode("utf-8", "replace")
78
+ retry = err.headers.get("Retry-After") if err.headers else None
79
+ try:
80
+ parsed = json.loads(text) if text else None
81
+ except ValueError:
82
+ parsed = None
83
+ failed = status >= 400 or (isinstance(parsed, dict) and parsed.get("success") is False)
84
+ if failed:
85
+ msg = parsed.get("message") if isinstance(parsed, dict) else None
86
+ raise SmsGoraError(
87
+ msg or "HTTP %d" % status,
88
+ status=status,
89
+ errors=parsed.get("errors") if isinstance(parsed, dict) else None,
90
+ body=parsed if parsed is not None else text,
91
+ retry_after=float(retry) if retry else None,
92
+ )
93
+ if isinstance(parsed, dict) and "data" in parsed:
94
+ return parsed["data"]
95
+ return parsed
96
+
97
+ def send_sms(self, to, message, device_id=None, sim_slot=None, schedule_at=None):
98
+ """Send an SMS to one number or a list (up to 1,000). Picks your best online phone unless device_id is given.
99
+
100
+ sim_slot: 0 = SIM 1, 1 = SIM 2. schedule_at: datetime or ISO string.
101
+ """
102
+ recipients = [to] if isinstance(to, str) else list(to)
103
+ body = {"recipients": recipients, "message": message}
104
+ if sim_slot is not None:
105
+ body["sim_slot"] = sim_slot
106
+ if schedule_at:
107
+ if isinstance(schedule_at, _dt.datetime):
108
+ schedule_at = schedule_at.isoformat()
109
+ body["schedule_at"] = schedule_at
110
+ path = "/devices/%s/send-sms" % _q(device_id) if device_id else "/send-sms"
111
+ return self.request("POST", path, body)
112
+
113
+ def send_bulk_sms(self, device_id, messages, message_template=None):
114
+ """Different texts for different people in one call (up to 5,000 recipients).
115
+
116
+ messages: [{"recipients": ["+1555..."], "message": "optional own text"}, ...]
117
+ """
118
+ body = {"messages": messages}
119
+ if message_template is not None:
120
+ body["messageTemplate"] = message_template
121
+ return self.request("POST", "/devices/%s/send-bulk-sms" % _q(device_id), body)
122
+
123
+ def get_message(self, message_id):
124
+ """Status is queued, dispatched, sent, delivered, failed or cancelled."""
125
+ return self.request("GET", "/messages/%s" % _q(message_id))
126
+
127
+ def cancel_message(self, message_id):
128
+ """Cancel a queued or scheduled message."""
129
+ return self.request("POST", "/messages/%s/cancel" % _q(message_id))
130
+
131
+ def list_messages(self, **filters):
132
+ """Filters: direction (in/out), status, device_id, phone, since, after_id, page, limit."""
133
+ return self.request("GET", "/messages" + _query(filters))
134
+
135
+ def list_received_sms(self, device_id=None, **filters):
136
+ """Incoming SMS for all phones, or one phone with device_id."""
137
+ path = "/devices/%s/get-received-sms" % _q(device_id) if device_id else "/received-sms"
138
+ return self.request("GET", path + _query(filters))
139
+
140
+ def get_batch(self, batch_id):
141
+ return self.request("GET", "/batches/%s" % _q(batch_id))
142
+
143
+ def list_devices(self):
144
+ """Your linked Android phones."""
145
+ return self.request("GET", "/devices")
146
+
147
+ def get_device(self, device_id):
148
+ return self.request("GET", "/devices/%s" % _q(device_id))
149
+
150
+ def get_account(self):
151
+ """Account, plan and usage."""
152
+ return self.request("GET", "/account")
153
+
154
+ def list_webhooks(self):
155
+ return self.request("GET", "/webhooks")
156
+
157
+ def create_webhook(self, url, events=("sms.received",), device_id=None):
158
+ """Register a webhook. The answer includes the secret (only returned once)."""
159
+ return self.request("POST", "/webhooks", {"url": url, "events": list(events), "device_id": device_id})
160
+
161
+ def delete_webhook(self, webhook_id):
162
+ return self.request("DELETE", "/webhooks/%s" % _q(webhook_id))
163
+
164
+
165
+ def verify_webhook(raw_body, signature, secret):
166
+ """Check the X-SmsGora-Signature header (sha256=<hex HMAC-SHA256 of the raw body>).
167
+
168
+ raw_body: bytes or str exactly as received, before JSON parsing.
169
+ """
170
+ if not raw_body or not signature or not secret:
171
+ return False
172
+ if isinstance(raw_body, str):
173
+ raw_body = raw_body.encode("utf-8")
174
+ expected = "sha256=" + hmac.new(secret.encode("utf-8"), raw_body, hashlib.sha256).hexdigest()
175
+ return hmac.compare_digest(str(signature).encode("utf-8"), expected.encode("utf-8"))
@@ -0,0 +1,156 @@
1
+ Metadata-Version: 2.4
2
+ Name: smsgora
3
+ Version: 1.0.0
4
+ Summary: Official Python client for the SMS Gora SMS Gateway: send and receive SMS through your own Android phones, with delivery reports and signed webhooks.
5
+ Author-email: Tekservix <support@tekservix.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://tekservix.com/smsgora/
8
+ Project-URL: Documentation, https://tekservix.com/smsgora/docs/
9
+ Project-URL: Source, https://github.com/ellyj164/smsgora-python
10
+ Project-URL: Issues, https://github.com/ellyj164/smsgora-python/issues
11
+ Keywords: sms,sms-gateway,sms-api,bulk-sms,send-sms,android-sms-gateway,otp,textbee,smsgora,tekservix
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Topic :: Communications :: Telephony
16
+ Classifier: Topic :: Software Development :: Libraries
17
+ Requires-Python: >=3.8
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Dynamic: license-file
21
+
22
+ # smsgora
23
+
24
+ Python client for the [SMS Gora SMS Gateway](https://tekservix.com/smsgora/) API.
25
+
26
+ SMS Gora turns Android phones with ordinary SIM cards into an SMS gateway. Your server calls the API, a linked phone sends the text, and replies and delivery reports come back to you. There's no sender ID registration and no per-message fee from us: your mobile operator bills the texts like any SMS.
27
+
28
+ - Zero dependencies (standard library only), Python 3.8+
29
+ - Works in Django, Flask, FastAPI, scripts and cron jobs
30
+ - Webhook signature check built in
31
+
32
+ ## Install
33
+
34
+ ```bash
35
+ pip install smsgora
36
+ ```
37
+
38
+ ## Quick start
39
+
40
+ Create an API key in the SMS Gora dashboard (SMS Gora -> API keys) and keep it on your server.
41
+
42
+ ```python
43
+ import os
44
+ from smsgora import SmsGora
45
+
46
+ sms = SmsGora(api_key=os.environ["SMSGORA_KEY"])
47
+
48
+ result = sms.send_sms(to="+15551234567", message="Your verification code is 482913")
49
+ print(result["message_ids"]) # [1873]
50
+ ```
51
+
52
+ ## Sending
53
+
54
+ ```python
55
+ from datetime import datetime, timezone
56
+
57
+ # Several recipients (up to 1,000 per call)
58
+ sms.send_sms(to=["+15551234567", "+447700900123"], message="Your order has shipped")
59
+
60
+ # From a chosen phone and SIM, scheduled for later
61
+ sms.send_sms(
62
+ to="+15551234567",
63
+ message="Reminder: your appointment is tomorrow at 10:00",
64
+ device_id=42,
65
+ sim_slot=1, # 0 = SIM 1, 1 = SIM 2
66
+ schedule_at=datetime(2026, 12, 1, 8, 0, tzinfo=timezone.utc),
67
+ )
68
+
69
+ # Different texts for different people in one call (up to 5,000 recipients)
70
+ sms.send_bulk_sms(
71
+ 42,
72
+ message_template="Hello, your parcel is ready for pickup.",
73
+ messages=[
74
+ {"recipients": ["+15551234567"], "message": "Hi Aline, parcel #1042 is ready."},
75
+ {"recipients": ["+15557654321"]},
76
+ ],
77
+ )
78
+ ```
79
+
80
+ ## Delivery status and incoming SMS
81
+
82
+ ```python
83
+ msg = sms.get_message(1873)
84
+ print(msg["status"]) # queued -> dispatched -> sent -> delivered (or failed)
85
+
86
+ sms.cancel_message(1874) # works while queued or scheduled
87
+
88
+ inbox = sms.list_received_sms(since="2026-11-01", limit=50)
89
+ phones = sms.list_devices()
90
+ account = sms.get_account()
91
+ ```
92
+
93
+ Anything else in the [API reference](https://tekservix.com/smsgora/docs/) is one call away:
94
+
95
+ ```python
96
+ batch = sms.request("GET", "/batches/311")
97
+ ```
98
+
99
+ ## Webhooks
100
+
101
+ Add your HTTPS URL in SMS Gora -> Webhooks (or with `create_webhook`) and keep the secret. Each request carries `X-SmsGora-Signature: sha256=...`, an HMAC-SHA256 of the raw body. Check it before trusting the request. Flask example:
102
+
103
+ ```python
104
+ import json, os
105
+ from flask import Flask, request, abort
106
+ from smsgora import verify_webhook
107
+
108
+ app = Flask(__name__)
109
+
110
+ @app.post("/sms/webhook")
111
+ def sms_webhook():
112
+ if not verify_webhook(request.get_data(), request.headers.get("X-SmsGora-Signature"),
113
+ os.environ["SMSGORA_WEBHOOK_SECRET"]):
114
+ abort(401)
115
+ event = json.loads(request.get_data())
116
+ if event["event"] == "sms.received":
117
+ print(event["data"]["from"], event["data"]["message"])
118
+ return "", 200 # answer within 10 seconds; queue slow work
119
+ ```
120
+
121
+ In Django use `request.body`; in FastAPI use `await request.body()`. Always pass the raw bytes, not re-encoded JSON.
122
+
123
+ ## Errors
124
+
125
+ Failed calls raise `SmsGoraError` with `status`, `message`, field `errors` and, on 429, `retry_after` in seconds.
126
+
127
+ ```python
128
+ from smsgora import SmsGoraError
129
+
130
+ try:
131
+ sms.send_sms(to="+15551234567", message="Hi")
132
+ except SmsGoraError as err:
133
+ if err.status == 429:
134
+ pass # rate limit (300 requests/minute per key) or plan limit reached
135
+ ```
136
+
137
+ Never ship the key inside a browser or mobile app: anyone could read it. Call the API from your server.
138
+
139
+ ## Migrating from TextBee
140
+
141
+ SMS Gora accepts TextBee-style requests, so most integrations switch by changing the base URL and key. See the [compatibility notes](https://tekservix.com/smsgora/docs/).
142
+
143
+ ## Good to know
144
+
145
+ A phone-based gateway suits OTPs, order alerts, reminders and two-way conversations at moderate volume. It isn't meant for mass marketing blasts, and in the US high-volume business texting belongs on registered 10DLC or toll-free numbers.
146
+
147
+ ## More from Tekservix
148
+
149
+ - [WAservix WhatsApp API Gateway](https://tekservix.com/waservix/): send WhatsApp messages from your own number (`pip install waservix`)
150
+ - [AI Sales Agent](https://tekservix.com/ai-sales-agent): answers customers on WhatsApp and SMS using your website's content
151
+ - [Website to App](https://tekservix.com/mobile-apps): turn your website into Android and iOS apps
152
+ - Also on npm: `npm install smsgora`
153
+
154
+ ## License
155
+
156
+ MIT
@@ -0,0 +1,9 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/smsgora/__init__.py
5
+ src/smsgora.egg-info/PKG-INFO
6
+ src/smsgora.egg-info/SOURCES.txt
7
+ src/smsgora.egg-info/dependency_links.txt
8
+ src/smsgora.egg-info/top_level.txt
9
+ tests/test_smsgora.py
@@ -0,0 +1 @@
1
+ smsgora
@@ -0,0 +1,81 @@
1
+ import hashlib, hmac, json, threading, unittest
2
+ from http.server import BaseHTTPRequestHandler, HTTPServer
3
+
4
+ from smsgora import SmsGora, SmsGoraError, verify_webhook
5
+
6
+ CALLS = []
7
+
8
+
9
+ class Handler(BaseHTTPRequestHandler):
10
+ def _go(self):
11
+ n = int(self.headers.get("Content-Length") or 0)
12
+ body = json.loads(self.rfile.read(n)) if n else None
13
+ CALLS.append((self.command, self.path, {k.lower(): v for k, v in self.headers.items()}, body))
14
+ if self.path.startswith("/limit"):
15
+ self.send_response(429); self.send_header("Retry-After", "7"); self.end_headers()
16
+ self.wfile.write(b'{"success":false,"message":"Too many requests"}'); return
17
+ self.send_response(200); self.send_header("Content-Type", "application/json"); self.end_headers()
18
+ self.wfile.write(json.dumps({"success": True, "data": {"ok": True}}).encode())
19
+ do_GET = do_POST = do_DELETE = do_PUT = _go
20
+ def log_message(self, *a): pass
21
+
22
+
23
+ class T(unittest.TestCase):
24
+ @classmethod
25
+ def setUpClass(cls):
26
+ cls.srv = HTTPServer(("127.0.0.1", 0), Handler)
27
+ threading.Thread(target=cls.srv.serve_forever, daemon=True).start()
28
+ cls.c = SmsGora("sgk_test", base_url="http://127.0.0.1:%d/" % cls.srv.server_port)
29
+
30
+ @classmethod
31
+ def tearDownClass(cls):
32
+ cls.srv.shutdown()
33
+
34
+ def last(self):
35
+ return CALLS[-1]
36
+
37
+ def test_send(self):
38
+ self.assertEqual(self.c.send_sms(to="+1555", message="hi"), {"ok": True})
39
+ m, p, h, b = self.last()
40
+ self.assertEqual((m, p), ("POST", "/send-sms"))
41
+ self.assertEqual(h["x-api-key"], "sgk_test")
42
+ self.assertEqual(b, {"recipients": ["+1555"], "message": "hi"})
43
+
44
+ def test_send_device(self):
45
+ self.c.send_sms(to=["+1", "+2"], message="x", device_id=42, sim_slot=1, schedule_at="2026-12-01T08:00:00Z")
46
+ m, p, h, b = self.last()
47
+ self.assertEqual(p, "/devices/42/send-sms")
48
+ self.assertEqual(b["sim_slot"], 1)
49
+ self.assertEqual(b["schedule_at"], "2026-12-01T08:00:00Z")
50
+
51
+ def test_bulk_and_lists(self):
52
+ self.c.send_bulk_sms(42, [{"recipients": ["+1"]}], message_template="t")
53
+ self.assertEqual(self.last()[3], {"messages": [{"recipients": ["+1"]}], "messageTemplate": "t"})
54
+ self.c.list_received_sms(device_id=3, limit=5, since=None)
55
+ self.assertEqual(self.last()[1], "/devices/3/get-received-sms?limit=5")
56
+ self.c.list_messages(status="sent")
57
+ self.assertEqual(self.last()[1], "/messages?status=sent")
58
+ self.c.delete_webhook(9)
59
+ self.assertEqual(self.last()[:2], ("DELETE", "/webhooks/9"))
60
+
61
+ def test_error(self):
62
+ c = SmsGora("k", base_url="http://127.0.0.1:%d/limit" % self.srv.server_port)
63
+ with self.assertRaises(SmsGoraError) as e:
64
+ c.get_account()
65
+ self.assertEqual((e.exception.status, e.exception.retry_after, str(e.exception)), (429, 7.0, "Too many requests"))
66
+
67
+ def test_webhook(self):
68
+ raw = b'{"event":"sms.received"}'
69
+ sig = "sha256=" + hmac.new(b"s3cret", raw, hashlib.sha256).hexdigest()
70
+ self.assertTrue(verify_webhook(raw, sig, "s3cret"))
71
+ self.assertTrue(verify_webhook(raw.decode(), sig, "s3cret"))
72
+ self.assertFalse(verify_webhook(raw, sig, "wrong"))
73
+ self.assertFalse(verify_webhook(raw, None, "s3cret"))
74
+
75
+ def test_requires_key(self):
76
+ with self.assertRaises(ValueError):
77
+ SmsGora("")
78
+
79
+
80
+ if __name__ == "__main__":
81
+ unittest.main()