connect24 0.1.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.
- connect24-0.1.0/.github/workflows/ci.yml +28 -0
- connect24-0.1.0/.github/workflows/publish.yml +40 -0
- connect24-0.1.0/.gitignore +9 -0
- connect24-0.1.0/LICENSE +21 -0
- connect24-0.1.0/PKG-INFO +213 -0
- connect24-0.1.0/README.md +163 -0
- connect24-0.1.0/pyproject.toml +68 -0
- connect24-0.1.0/src/connect24/__init__.py +61 -0
- connect24-0.1.0/src/connect24/_transport.py +127 -0
- connect24-0.1.0/src/connect24/client.py +129 -0
- connect24-0.1.0/src/connect24/errors.py +90 -0
- connect24-0.1.0/src/connect24/models.py +311 -0
- connect24-0.1.0/src/connect24/py.typed +0 -0
- connect24-0.1.0/src/connect24/resources/__init__.py +19 -0
- connect24-0.1.0/src/connect24/resources/account.py +28 -0
- connect24-0.1.0/src/connect24/resources/billing.py +29 -0
- connect24-0.1.0/src/connect24/resources/messages.py +191 -0
- connect24-0.1.0/src/connect24/resources/sending_domains.py +45 -0
- connect24-0.1.0/src/connect24/resources/suppressions.py +39 -0
- connect24-0.1.0/src/connect24/resources/templates.py +56 -0
- connect24-0.1.0/src/connect24/resources/webhooks.py +41 -0
- connect24-0.1.0/src/connect24/webhooks.py +120 -0
- connect24-0.1.0/tests/test_client.py +139 -0
- connect24-0.1.0/tests/test_webhook_signature.py +96 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
fail-fast: false
|
|
13
|
+
matrix:
|
|
14
|
+
# The floor and the current release. A library is installed into applications whose Python
|
|
15
|
+
# version nobody consulted us about, so both ends have to stay green.
|
|
16
|
+
python: ['3.9', '3.13']
|
|
17
|
+
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
|
|
21
|
+
- uses: actions/setup-python@v5
|
|
22
|
+
with:
|
|
23
|
+
python-version: ${{ matrix.python }}
|
|
24
|
+
|
|
25
|
+
- run: pip install pytest
|
|
26
|
+
- run: python -m pytest tests -q
|
|
27
|
+
env:
|
|
28
|
+
PYTHONPATH: src
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
# Tag-driven, so a release is a deliberate act with a name rather than whatever happened to land
|
|
4
|
+
# on main. Tag as v0.1.0 and the version in pyproject.toml must match — the job checks.
|
|
5
|
+
on:
|
|
6
|
+
push:
|
|
7
|
+
tags: ['v*']
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
publish:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
environment: pypi
|
|
13
|
+
|
|
14
|
+
permissions:
|
|
15
|
+
# The whole point of trusted publishing: GitHub mints a short-lived OIDC token for this run
|
|
16
|
+
# and PyPI verifies it came from this repo and this workflow. There is no API token to store,
|
|
17
|
+
# rotate, or leak — which is the only kind of secret that cannot be stolen from a CI log.
|
|
18
|
+
id-token: write
|
|
19
|
+
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@v4
|
|
22
|
+
|
|
23
|
+
- uses: actions/setup-python@v5
|
|
24
|
+
with:
|
|
25
|
+
python-version: '3.12'
|
|
26
|
+
|
|
27
|
+
- name: Refuse a tag that disagrees with the package version
|
|
28
|
+
run: |
|
|
29
|
+
TAG="${GITHUB_REF_NAME#v}"
|
|
30
|
+
VERSION=$(grep -m1 '^version' pyproject.toml | cut -d'"' -f2)
|
|
31
|
+
if [ "$TAG" != "$VERSION" ]; then
|
|
32
|
+
echo "Tag $GITHUB_REF_NAME does not match version $VERSION in pyproject.toml." >&2
|
|
33
|
+
exit 1
|
|
34
|
+
fi
|
|
35
|
+
echo "Publishing $VERSION"
|
|
36
|
+
|
|
37
|
+
- run: pip install build
|
|
38
|
+
- run: python -m build
|
|
39
|
+
|
|
40
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
connect24-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Connect24
|
|
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.
|
connect24-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: connect24
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python client for the Connect24 communications API — email, SMS and WhatsApp.
|
|
5
|
+
Project-URL: Homepage, https://connect24.co.za
|
|
6
|
+
Project-URL: Documentation, https://connect24.co.za/developer
|
|
7
|
+
Project-URL: Source, https://github.com/Lihle2018/connect24-python
|
|
8
|
+
Author: Connect24
|
|
9
|
+
License: MIT License
|
|
10
|
+
|
|
11
|
+
Copyright (c) 2026 Connect24
|
|
12
|
+
|
|
13
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
14
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
15
|
+
in the Software without restriction, including without limitation the rights
|
|
16
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
17
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
18
|
+
furnished to do so, subject to the following conditions:
|
|
19
|
+
|
|
20
|
+
The above copyright notice and this permission notice shall be included in all
|
|
21
|
+
copies or substantial portions of the Software.
|
|
22
|
+
|
|
23
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
24
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
25
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
26
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
27
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
28
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
29
|
+
SOFTWARE.
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Keywords: connect24,email,messaging api,popia,sms,sms api,south africa,whatsapp
|
|
32
|
+
Classifier: Development Status :: 4 - Beta
|
|
33
|
+
Classifier: Intended Audience :: Developers
|
|
34
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
35
|
+
Classifier: Programming Language :: Python :: 3
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
40
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
41
|
+
Classifier: Topic :: Communications
|
|
42
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
43
|
+
Classifier: Typing :: Typed
|
|
44
|
+
Requires-Python: >=3.9
|
|
45
|
+
Provides-Extra: dev
|
|
46
|
+
Requires-Dist: mypy>=1.8; extra == 'dev'
|
|
47
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
48
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
49
|
+
Description-Content-Type: text/markdown
|
|
50
|
+
|
|
51
|
+
# Connect24 Python SDK
|
|
52
|
+
|
|
53
|
+
Official Python client for the [Connect24](https://connect24.co.za) communications API —
|
|
54
|
+
one interface for email, SMS and WhatsApp.
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pip install connect24
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
No runtime dependencies. An SDK is a dependency of *your* application, and every package it drags
|
|
61
|
+
in is a version conflict that becomes yours to resolve.
|
|
62
|
+
|
|
63
|
+
## Quick start
|
|
64
|
+
|
|
65
|
+
Get your **account id** and an **API key** from the portal, under Settings → API keys.
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
import os
|
|
69
|
+
from connect24 import Connect24
|
|
70
|
+
|
|
71
|
+
client = Connect24(
|
|
72
|
+
account_id="acc_3f9c1a7b4e2d",
|
|
73
|
+
api_key=os.environ["CONNECT24_API_KEY"],
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
client.messages.send_sms("+27821234567", "Your payment is due tomorrow.")
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Or read both from the environment, which is what most deployments want:
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
client = Connect24.from_env() # CONNECT24_ACCOUNT_ID, CONNECT24_API_KEY
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## What you can reach
|
|
86
|
+
|
|
87
|
+
| | |
|
|
88
|
+
|---|---|
|
|
89
|
+
| `client.messages` | Send email, SMS and WhatsApp; read status and history |
|
|
90
|
+
| `client.templates` | Stored bodies with placeholders, so copy lives on the platform |
|
|
91
|
+
| `client.suppressions` | Addresses that will not be sent to |
|
|
92
|
+
| `client.webhooks` | Delivery events pushed to you, plus signature verification |
|
|
93
|
+
| `client.sending_domains` | Prove you control a domain, so mail goes out as you |
|
|
94
|
+
| `client.billing` | Prepaid credit, pricing and the statement |
|
|
95
|
+
| `client.account` | Who you are, and which channels can send right now |
|
|
96
|
+
|
|
97
|
+
## Every channel, one shape
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
# SMS
|
|
101
|
+
client.messages.send_sms("+27821234567", "Your delivery is on its way.")
|
|
102
|
+
|
|
103
|
+
# WhatsApp — free-form inside the 24-hour window, a template outside it
|
|
104
|
+
client.messages.send_whatsapp("+27821234567", "Your order has shipped.")
|
|
105
|
+
client.messages.send_whatsapp("+27821234567", template_name="payment_reminder")
|
|
106
|
+
|
|
107
|
+
# Email
|
|
108
|
+
client.messages.send_email(
|
|
109
|
+
to="customer@example.com",
|
|
110
|
+
subject="Payment reminder",
|
|
111
|
+
html="<p>Your account is overdue.</p>",
|
|
112
|
+
sender={"address": "collections@acme.co.za", "name": "Acme Collections"},
|
|
113
|
+
)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Two things that surprise people
|
|
117
|
+
|
|
118
|
+
**Your `sender` address is not used until you verify the domain.** Until then mail leaves from your
|
|
119
|
+
account's assigned address on `connect24.co.za` and yours becomes the Reply-To. Verify with
|
|
120
|
+
`client.sending_domains.add("acme.co.za")`, publish the returned DNS records, then `verify`.
|
|
121
|
+
|
|
122
|
+
**There is no sender for SMS.** South African traffic routes from a shared originator pool, and
|
|
123
|
+
naming an identity you do not own is rejected by the network.
|
|
124
|
+
|
|
125
|
+
## Not sending twice
|
|
126
|
+
|
|
127
|
+
Pass an idempotency key when a network failure leaves you unsure whether a send arrived. A repeat
|
|
128
|
+
with the same key returns the original message instead of sending again:
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
client.messages.send_sms(
|
|
132
|
+
"+27821234567",
|
|
133
|
+
"Your order has shipped.",
|
|
134
|
+
idempotency_key=f"order-{order.id}-shipped",
|
|
135
|
+
)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Use something stable and tied to the event — an order id, not a UUID generated at call time, which
|
|
139
|
+
would differ on the retry and defeat the point.
|
|
140
|
+
|
|
141
|
+
## Watch the emoji
|
|
142
|
+
|
|
143
|
+
An SMS holds 160 characters using the GSM-7 alphabet. A single emoji, curly quote or em dash
|
|
144
|
+
switches the whole message to UCS-2, which holds 70 characters per part. A 150-character message
|
|
145
|
+
with one emoji costs **three** SMS, not one. The portal shows the segment count while you write.
|
|
146
|
+
|
|
147
|
+
## Verifying a webhook
|
|
148
|
+
|
|
149
|
+
Verify against the **raw body**, before any framework parses it.
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
from connect24 import WebhookEvent, verify_signature
|
|
153
|
+
|
|
154
|
+
@app.post("/hooks/connect24")
|
|
155
|
+
def connect24_hook():
|
|
156
|
+
payload = request.get_data(as_text=True) # raw body, not request.json
|
|
157
|
+
signature = request.headers.get("X-Connect24-Signature", "")
|
|
158
|
+
|
|
159
|
+
if not verify_signature(payload, signature, WEBHOOK_SECRET):
|
|
160
|
+
return "", 401
|
|
161
|
+
|
|
162
|
+
event = WebhookEvent.parse(payload)
|
|
163
|
+
# Acknowledge fast — anything that is not 2xx is retried.
|
|
164
|
+
return "", 200
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Delivery is at-least-once. Deduplicate on `event.id`.
|
|
168
|
+
|
|
169
|
+
## Errors
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
from connect24 import Connect24ApiError, Connect24ConnectionError
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
client.messages.send_sms("+27821234567", "Hello")
|
|
176
|
+
except Connect24ApiError as e:
|
|
177
|
+
if e.status_code == 402:
|
|
178
|
+
... # out of credit; topping up is the only fix
|
|
179
|
+
elif e.status_code == 401:
|
|
180
|
+
... # key is wrong or revoked
|
|
181
|
+
print(e.errors) # field-level validation messages, when the API sent any
|
|
182
|
+
except Connect24ConnectionError:
|
|
183
|
+
... # never reached the API — the send may or may not have happened
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Rate limits, 5xx and connection failures are retried twice with backoff before either is raised.
|
|
187
|
+
A 4xx is never retried, because repeating it changes nothing.
|
|
188
|
+
|
|
189
|
+
## What the send path enforces
|
|
190
|
+
|
|
191
|
+
Messages sent through Connect24 are subject to South African law, applied where the message is
|
|
192
|
+
actually sent rather than left to you:
|
|
193
|
+
|
|
194
|
+
- **POPIA** — a lawful basis is recorded per contact, with where the details came from.
|
|
195
|
+
- **Consumer Protection Act** — no marketing on Sundays or public holidays, Saturdays 09:00–13:00
|
|
196
|
+
only, weekdays 08:00–20:00. A marketing send outside the window waits rather than going out late.
|
|
197
|
+
- **WASPA Code** — a working opt-out on every marketing message. Once used, it applies across every
|
|
198
|
+
list, permanently, and cannot be reversed by the sender.
|
|
199
|
+
|
|
200
|
+
Connect24 enforces these in the send path. You remain the responsible party under POPIA for the
|
|
201
|
+
data you upload and the consent you hold — see the [terms](https://connect24.co.za/terms).
|
|
202
|
+
|
|
203
|
+
## Development
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
pip install -e ".[dev]"
|
|
207
|
+
pytest
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Links
|
|
211
|
+
|
|
212
|
+
- [API documentation](https://connect24.co.za/developer)
|
|
213
|
+
- [Support](mailto:support@connect24.co.za)
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# Connect24 Python SDK
|
|
2
|
+
|
|
3
|
+
Official Python client for the [Connect24](https://connect24.co.za) communications API —
|
|
4
|
+
one interface for email, SMS and WhatsApp.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
pip install connect24
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
No runtime dependencies. An SDK is a dependency of *your* application, and every package it drags
|
|
11
|
+
in is a version conflict that becomes yours to resolve.
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
Get your **account id** and an **API key** from the portal, under Settings → API keys.
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
import os
|
|
19
|
+
from connect24 import Connect24
|
|
20
|
+
|
|
21
|
+
client = Connect24(
|
|
22
|
+
account_id="acc_3f9c1a7b4e2d",
|
|
23
|
+
api_key=os.environ["CONNECT24_API_KEY"],
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
client.messages.send_sms("+27821234567", "Your payment is due tomorrow.")
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Or read both from the environment, which is what most deployments want:
|
|
30
|
+
|
|
31
|
+
```python
|
|
32
|
+
client = Connect24.from_env() # CONNECT24_ACCOUNT_ID, CONNECT24_API_KEY
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## What you can reach
|
|
36
|
+
|
|
37
|
+
| | |
|
|
38
|
+
|---|---|
|
|
39
|
+
| `client.messages` | Send email, SMS and WhatsApp; read status and history |
|
|
40
|
+
| `client.templates` | Stored bodies with placeholders, so copy lives on the platform |
|
|
41
|
+
| `client.suppressions` | Addresses that will not be sent to |
|
|
42
|
+
| `client.webhooks` | Delivery events pushed to you, plus signature verification |
|
|
43
|
+
| `client.sending_domains` | Prove you control a domain, so mail goes out as you |
|
|
44
|
+
| `client.billing` | Prepaid credit, pricing and the statement |
|
|
45
|
+
| `client.account` | Who you are, and which channels can send right now |
|
|
46
|
+
|
|
47
|
+
## Every channel, one shape
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
# SMS
|
|
51
|
+
client.messages.send_sms("+27821234567", "Your delivery is on its way.")
|
|
52
|
+
|
|
53
|
+
# WhatsApp — free-form inside the 24-hour window, a template outside it
|
|
54
|
+
client.messages.send_whatsapp("+27821234567", "Your order has shipped.")
|
|
55
|
+
client.messages.send_whatsapp("+27821234567", template_name="payment_reminder")
|
|
56
|
+
|
|
57
|
+
# Email
|
|
58
|
+
client.messages.send_email(
|
|
59
|
+
to="customer@example.com",
|
|
60
|
+
subject="Payment reminder",
|
|
61
|
+
html="<p>Your account is overdue.</p>",
|
|
62
|
+
sender={"address": "collections@acme.co.za", "name": "Acme Collections"},
|
|
63
|
+
)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Two things that surprise people
|
|
67
|
+
|
|
68
|
+
**Your `sender` address is not used until you verify the domain.** Until then mail leaves from your
|
|
69
|
+
account's assigned address on `connect24.co.za` and yours becomes the Reply-To. Verify with
|
|
70
|
+
`client.sending_domains.add("acme.co.za")`, publish the returned DNS records, then `verify`.
|
|
71
|
+
|
|
72
|
+
**There is no sender for SMS.** South African traffic routes from a shared originator pool, and
|
|
73
|
+
naming an identity you do not own is rejected by the network.
|
|
74
|
+
|
|
75
|
+
## Not sending twice
|
|
76
|
+
|
|
77
|
+
Pass an idempotency key when a network failure leaves you unsure whether a send arrived. A repeat
|
|
78
|
+
with the same key returns the original message instead of sending again:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
client.messages.send_sms(
|
|
82
|
+
"+27821234567",
|
|
83
|
+
"Your order has shipped.",
|
|
84
|
+
idempotency_key=f"order-{order.id}-shipped",
|
|
85
|
+
)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Use something stable and tied to the event — an order id, not a UUID generated at call time, which
|
|
89
|
+
would differ on the retry and defeat the point.
|
|
90
|
+
|
|
91
|
+
## Watch the emoji
|
|
92
|
+
|
|
93
|
+
An SMS holds 160 characters using the GSM-7 alphabet. A single emoji, curly quote or em dash
|
|
94
|
+
switches the whole message to UCS-2, which holds 70 characters per part. A 150-character message
|
|
95
|
+
with one emoji costs **three** SMS, not one. The portal shows the segment count while you write.
|
|
96
|
+
|
|
97
|
+
## Verifying a webhook
|
|
98
|
+
|
|
99
|
+
Verify against the **raw body**, before any framework parses it.
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from connect24 import WebhookEvent, verify_signature
|
|
103
|
+
|
|
104
|
+
@app.post("/hooks/connect24")
|
|
105
|
+
def connect24_hook():
|
|
106
|
+
payload = request.get_data(as_text=True) # raw body, not request.json
|
|
107
|
+
signature = request.headers.get("X-Connect24-Signature", "")
|
|
108
|
+
|
|
109
|
+
if not verify_signature(payload, signature, WEBHOOK_SECRET):
|
|
110
|
+
return "", 401
|
|
111
|
+
|
|
112
|
+
event = WebhookEvent.parse(payload)
|
|
113
|
+
# Acknowledge fast — anything that is not 2xx is retried.
|
|
114
|
+
return "", 200
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Delivery is at-least-once. Deduplicate on `event.id`.
|
|
118
|
+
|
|
119
|
+
## Errors
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from connect24 import Connect24ApiError, Connect24ConnectionError
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
client.messages.send_sms("+27821234567", "Hello")
|
|
126
|
+
except Connect24ApiError as e:
|
|
127
|
+
if e.status_code == 402:
|
|
128
|
+
... # out of credit; topping up is the only fix
|
|
129
|
+
elif e.status_code == 401:
|
|
130
|
+
... # key is wrong or revoked
|
|
131
|
+
print(e.errors) # field-level validation messages, when the API sent any
|
|
132
|
+
except Connect24ConnectionError:
|
|
133
|
+
... # never reached the API — the send may or may not have happened
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Rate limits, 5xx and connection failures are retried twice with backoff before either is raised.
|
|
137
|
+
A 4xx is never retried, because repeating it changes nothing.
|
|
138
|
+
|
|
139
|
+
## What the send path enforces
|
|
140
|
+
|
|
141
|
+
Messages sent through Connect24 are subject to South African law, applied where the message is
|
|
142
|
+
actually sent rather than left to you:
|
|
143
|
+
|
|
144
|
+
- **POPIA** — a lawful basis is recorded per contact, with where the details came from.
|
|
145
|
+
- **Consumer Protection Act** — no marketing on Sundays or public holidays, Saturdays 09:00–13:00
|
|
146
|
+
only, weekdays 08:00–20:00. A marketing send outside the window waits rather than going out late.
|
|
147
|
+
- **WASPA Code** — a working opt-out on every marketing message. Once used, it applies across every
|
|
148
|
+
list, permanently, and cannot be reversed by the sender.
|
|
149
|
+
|
|
150
|
+
Connect24 enforces these in the send path. You remain the responsible party under POPIA for the
|
|
151
|
+
data you upload and the consent you hold — see the [terms](https://connect24.co.za/terms).
|
|
152
|
+
|
|
153
|
+
## Development
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
pip install -e ".[dev]"
|
|
157
|
+
pytest
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Links
|
|
161
|
+
|
|
162
|
+
- [API documentation](https://connect24.co.za/developer)
|
|
163
|
+
- [Support](mailto:support@connect24.co.za)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "connect24"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Official Python client for the Connect24 communications API — email, SMS and WhatsApp."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { file = "LICENSE" }
|
|
12
|
+
authors = [{ name = "Connect24" }]
|
|
13
|
+
keywords = [
|
|
14
|
+
"sms",
|
|
15
|
+
"email",
|
|
16
|
+
"whatsapp",
|
|
17
|
+
"messaging api",
|
|
18
|
+
"sms api",
|
|
19
|
+
"south africa",
|
|
20
|
+
"popia",
|
|
21
|
+
"connect24",
|
|
22
|
+
]
|
|
23
|
+
classifiers = [
|
|
24
|
+
"Development Status :: 4 - Beta",
|
|
25
|
+
"Intended Audience :: Developers",
|
|
26
|
+
"License :: OSI Approved :: MIT License",
|
|
27
|
+
"Programming Language :: Python :: 3",
|
|
28
|
+
"Programming Language :: Python :: 3.9",
|
|
29
|
+
"Programming Language :: Python :: 3.10",
|
|
30
|
+
"Programming Language :: Python :: 3.11",
|
|
31
|
+
"Programming Language :: Python :: 3.12",
|
|
32
|
+
"Programming Language :: Python :: 3.13",
|
|
33
|
+
"Topic :: Communications",
|
|
34
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
35
|
+
"Typing :: Typed",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
# No runtime dependencies, on purpose. An SDK is a dependency of somebody else's application, and
|
|
39
|
+
# every package it drags in is a version conflict that becomes theirs to resolve. JSON over HTTPS
|
|
40
|
+
# is something the standard library does perfectly well.
|
|
41
|
+
dependencies = []
|
|
42
|
+
|
|
43
|
+
[project.urls]
|
|
44
|
+
Homepage = "https://connect24.co.za"
|
|
45
|
+
Documentation = "https://connect24.co.za/developer"
|
|
46
|
+
Source = "https://github.com/Lihle2018/connect24-python"
|
|
47
|
+
|
|
48
|
+
[project.optional-dependencies]
|
|
49
|
+
dev = ["pytest>=7", "mypy>=1.8", "ruff>=0.4"]
|
|
50
|
+
|
|
51
|
+
[tool.hatch.build.targets.wheel]
|
|
52
|
+
packages = ["src/connect24"]
|
|
53
|
+
|
|
54
|
+
[tool.pytest.ini_options]
|
|
55
|
+
testpaths = ["tests"]
|
|
56
|
+
pythonpath = ["src"]
|
|
57
|
+
|
|
58
|
+
[tool.ruff]
|
|
59
|
+
line-length = 100
|
|
60
|
+
src = ["src", "tests"]
|
|
61
|
+
|
|
62
|
+
[tool.ruff.lint]
|
|
63
|
+
select = ["E", "F", "I", "UP", "B"]
|
|
64
|
+
|
|
65
|
+
[tool.mypy]
|
|
66
|
+
python_version = "3.9"
|
|
67
|
+
files = ["src/connect24"]
|
|
68
|
+
strict = true
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""Official Python client for the Connect24 communications API.
|
|
2
|
+
|
|
3
|
+
One interface for email, SMS and WhatsApp::
|
|
4
|
+
|
|
5
|
+
from connect24 import Connect24
|
|
6
|
+
|
|
7
|
+
client = Connect24.from_env()
|
|
8
|
+
client.messages.send_sms("+27821234567", "Your order has shipped.")
|
|
9
|
+
|
|
10
|
+
Two things surprise people, both of them deliberate:
|
|
11
|
+
|
|
12
|
+
**Your ``from`` address is not used until you verify the domain.** Until then mail leaves from your
|
|
13
|
+
account's assigned address on ``connect24.co.za`` and yours becomes the Reply-To.
|
|
14
|
+
|
|
15
|
+
**There is no sender for SMS.** South African traffic routes from a shared originator pool, and
|
|
16
|
+
naming an identity you do not own is rejected by the network.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from .client import DEFAULT_BASE_URL, Connect24
|
|
20
|
+
from .errors import Connect24ApiError, Connect24ConnectionError, Connect24Error
|
|
21
|
+
from .models import (
|
|
22
|
+
AccountInfo,
|
|
23
|
+
Balance,
|
|
24
|
+
ChannelStatus,
|
|
25
|
+
LedgerEntry,
|
|
26
|
+
Message,
|
|
27
|
+
MessageAccepted,
|
|
28
|
+
SendingDomain,
|
|
29
|
+
Suppression,
|
|
30
|
+
Template,
|
|
31
|
+
WebhookDelivery,
|
|
32
|
+
WebhookEndpoint,
|
|
33
|
+
WebhookEvent,
|
|
34
|
+
)
|
|
35
|
+
from .webhooks import DEFAULT_TOLERANCE_SECONDS, timestamp_of, verify_signature
|
|
36
|
+
|
|
37
|
+
__version__ = "0.1.0"
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
"Connect24",
|
|
41
|
+
"DEFAULT_BASE_URL",
|
|
42
|
+
"Connect24Error",
|
|
43
|
+
"Connect24ApiError",
|
|
44
|
+
"Connect24ConnectionError",
|
|
45
|
+
"verify_signature",
|
|
46
|
+
"timestamp_of",
|
|
47
|
+
"DEFAULT_TOLERANCE_SECONDS",
|
|
48
|
+
"WebhookEvent",
|
|
49
|
+
"AccountInfo",
|
|
50
|
+
"Balance",
|
|
51
|
+
"ChannelStatus",
|
|
52
|
+
"LedgerEntry",
|
|
53
|
+
"Message",
|
|
54
|
+
"MessageAccepted",
|
|
55
|
+
"SendingDomain",
|
|
56
|
+
"Suppression",
|
|
57
|
+
"Template",
|
|
58
|
+
"WebhookDelivery",
|
|
59
|
+
"WebhookEndpoint",
|
|
60
|
+
"__version__",
|
|
61
|
+
]
|