noria-sendkit 0.1.0__py3-none-any.whl
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.
- noria_sendkit-0.1.0.dist-info/METADATA +794 -0
- noria_sendkit-0.1.0.dist-info/RECORD +25 -0
- noria_sendkit-0.1.0.dist-info/WHEEL +5 -0
- noria_sendkit-0.1.0.dist-info/licenses/LICENSE +21 -0
- noria_sendkit-0.1.0.dist-info/top_level.txt +1 -0
- sendkit/__init__.py +255 -0
- sendkit/core/__init__.py +0 -0
- sendkit/core/config.py +51 -0
- sendkit/core/errors.py +111 -0
- sendkit/core/http.py +546 -0
- sendkit/core/types.py +84 -0
- sendkit/core/utils.py +188 -0
- sendkit/events.py +31 -0
- sendkit/providers/__init__.py +0 -0
- sendkit/providers/sms/__init__.py +71 -0
- sendkit/providers/sms/africastalking.py +714 -0
- sendkit/providers/sms/client.py +716 -0
- sendkit/providers/sms/types.py +186 -0
- sendkit/providers/whatsapp/__init__.py +123 -0
- sendkit/providers/whatsapp/client.py +2065 -0
- sendkit/providers/whatsapp/types.py +573 -0
- sendkit/py.typed +1 -0
- sendkit/sms.py +6 -0
- sendkit/webhooks.py +117 -0
- sendkit/whatsapp.py +6 -0
|
@@ -0,0 +1,794 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: noria-sendkit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Sync and async Python SDK for WhatsApp and bulk SMS gateways (Meta, Onfon, Africa's Talking).
|
|
5
|
+
Author: Noria Labs
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: sms,whatsapp,bulk-sms,africastalking,onfon,meta,messaging,httpx,async,sdk
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
16
|
+
Classifier: Topic :: Communications
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: httpx>=0.28.1
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest>=9.0.3; extra == "dev"
|
|
24
|
+
Requires-Dist: pytest-cov>=7.1.0; extra == "dev"
|
|
25
|
+
Requires-Dist: ruff>=0.15.9; extra == "dev"
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# `sendkit`
|
|
29
|
+
|
|
30
|
+
Modular Python SDK for WhatsApp and bulk SMS providers.
|
|
31
|
+
|
|
32
|
+
Python `>=3.11` is required. SendKit ships **sync and async** clients backed by
|
|
33
|
+
`httpx`, and is designed for services, workers, and serverless messaging flows.
|
|
34
|
+
|
|
35
|
+
Use `sendkit` when you want direct provider wrappers:
|
|
36
|
+
|
|
37
|
+
- Meta WhatsApp Cloud API
|
|
38
|
+
- Onfon bulk SMS
|
|
39
|
+
- Africa's Talking SMS
|
|
40
|
+
|
|
41
|
+
Each provider exposes a synchronous client and an asynchronous `Async*`
|
|
42
|
+
counterpart that share the same request models, payload building, and response
|
|
43
|
+
parsing — one definition, two execution models.
|
|
44
|
+
|
|
45
|
+
## Install
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install noria-sendkit
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The distribution is published as `noria-sendkit`; the import package is `sendkit`.
|
|
52
|
+
|
|
53
|
+
## Imports
|
|
54
|
+
|
|
55
|
+
Import the whole package:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
from sendkit import (
|
|
59
|
+
AfricasTalkingSmsClient,
|
|
60
|
+
MetaWhatsAppClient,
|
|
61
|
+
OnfonSmsClient,
|
|
62
|
+
)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Or import the async clients:
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from sendkit import (
|
|
69
|
+
AsyncAfricasTalkingSmsClient,
|
|
70
|
+
AsyncMetaWhatsAppClient,
|
|
71
|
+
AsyncOnfonSmsClient,
|
|
72
|
+
)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Modular subpackages are also available:
|
|
76
|
+
|
|
77
|
+
- `sendkit` — everything
|
|
78
|
+
- `sendkit.sms` — SMS clients and models
|
|
79
|
+
- `sendkit.whatsapp` — WhatsApp client and models
|
|
80
|
+
|
|
81
|
+
## Quick Start
|
|
82
|
+
|
|
83
|
+
### WhatsApp
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from sendkit import MetaWhatsAppClient, WhatsAppTextRequest
|
|
87
|
+
|
|
88
|
+
whatsapp = MetaWhatsAppClient(
|
|
89
|
+
access_token="...",
|
|
90
|
+
phone_number_id="...",
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
with whatsapp:
|
|
94
|
+
whatsapp.send_text(
|
|
95
|
+
WhatsAppTextRequest(recipient="254700123456", text="Hello from Noria")
|
|
96
|
+
)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The async client mirrors the sync API:
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
import asyncio
|
|
103
|
+
|
|
104
|
+
from sendkit import AsyncMetaWhatsAppClient, WhatsAppTextRequest
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
async def main() -> None:
|
|
108
|
+
async with AsyncMetaWhatsAppClient(
|
|
109
|
+
access_token="...",
|
|
110
|
+
phone_number_id="...",
|
|
111
|
+
) as whatsapp:
|
|
112
|
+
await whatsapp.send_text(
|
|
113
|
+
WhatsAppTextRequest(recipient="254700123456", text="Hello from Noria")
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
asyncio.run(main())
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Onfon SMS
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
from sendkit import OnfonSmsClient, SmsMessage, SmsSendRequest
|
|
124
|
+
|
|
125
|
+
sms = OnfonSmsClient(
|
|
126
|
+
access_key="...",
|
|
127
|
+
api_key="...",
|
|
128
|
+
client_id="...",
|
|
129
|
+
default_sender_id="NORIALABS",
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
sms.send(
|
|
133
|
+
SmsSendRequest(
|
|
134
|
+
messages=[
|
|
135
|
+
SmsMessage(recipient="254700123456", text="Your OTP is 123456", reference="otp-1"),
|
|
136
|
+
]
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Africa's Talking SMS
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
from sendkit import AfricasTalkingSmsClient, SmsMessage, SmsSendRequest
|
|
145
|
+
|
|
146
|
+
sms = AfricasTalkingSmsClient(
|
|
147
|
+
api_key="...",
|
|
148
|
+
username="...",
|
|
149
|
+
default_sender_id="NORIALABS",
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
sms.send(
|
|
153
|
+
SmsSendRequest(
|
|
154
|
+
messages=[
|
|
155
|
+
SmsMessage(recipient="+254700123456", text="Your OTP is 123456", reference="otp-1"),
|
|
156
|
+
]
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## Provider Coverage
|
|
162
|
+
|
|
163
|
+
| Provider | Capabilities |
|
|
164
|
+
| --- | --- |
|
|
165
|
+
| Meta WhatsApp | Text, templates, media by id or URL, media upload/get/delete, location, contacts, reactions, interactive buttons/lists, catalog, single product, product list, flows, mark-read, typing indicator, template management, delivery parsing, inbound parsing |
|
|
166
|
+
| Onfon SMS | Bulk SMS send, scheduled SMS, Unicode/flash flags, balance, groups, templates, delivery report parsing |
|
|
167
|
+
| Africa's Talking SMS | Bulk SMS send, premium SMS reply, incoming message fetch, subscription create/delete, balance, delivery report parsing |
|
|
168
|
+
|
|
169
|
+
Non-SMS Africa's Talking products such as Airtime, Voice, USSD, Payments, and
|
|
170
|
+
Data Bundles are intentionally outside the current SMS scope.
|
|
171
|
+
|
|
172
|
+
## Shared Transport
|
|
173
|
+
|
|
174
|
+
Every provider client accepts the same transport options:
|
|
175
|
+
|
|
176
|
+
- `client`: a custom `httpx.Client` / `httpx.AsyncClient`
|
|
177
|
+
- `timeout_seconds`: request timeout
|
|
178
|
+
- `default_headers`: extra default headers
|
|
179
|
+
- `retry`: a `RetryPolicy` (or `False` to disable)
|
|
180
|
+
- `hooks`: `Hooks(before_request=..., after_response=..., on_error=...)`
|
|
181
|
+
|
|
182
|
+
Per-request options are passed via `RequestOptions`:
|
|
183
|
+
|
|
184
|
+
- `headers`
|
|
185
|
+
- `timeout_seconds`
|
|
186
|
+
- `retry`
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
from sendkit import Hooks, OnfonSmsClient, RetryPolicy
|
|
190
|
+
|
|
191
|
+
sms = OnfonSmsClient(
|
|
192
|
+
access_key="access-key",
|
|
193
|
+
api_key="api-key",
|
|
194
|
+
client_id="client-id",
|
|
195
|
+
timeout_seconds=15.0,
|
|
196
|
+
retry=RetryPolicy(
|
|
197
|
+
max_attempts=3,
|
|
198
|
+
retry_methods=("GET", "POST"),
|
|
199
|
+
retry_on_statuses=(429, 500, 502, 503, 504),
|
|
200
|
+
retry_on_network_error=True,
|
|
201
|
+
base_delay_seconds=0.25,
|
|
202
|
+
),
|
|
203
|
+
hooks=Hooks(
|
|
204
|
+
before_request=lambda ctx: ctx.headers.__setitem__("x-trace-id", "trace-123"),
|
|
205
|
+
),
|
|
206
|
+
)
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Construction From Environment
|
|
210
|
+
|
|
211
|
+
Each client exposes a `from_env(...)` classmethod.
|
|
212
|
+
|
|
213
|
+
```python
|
|
214
|
+
sms = OnfonSmsClient.from_env()
|
|
215
|
+
whatsapp = MetaWhatsAppClient.from_env()
|
|
216
|
+
at = AfricasTalkingSmsClient.from_env()
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Onfon env vars
|
|
220
|
+
|
|
221
|
+
- `ONFON_ACCESS_KEY`
|
|
222
|
+
- `ONFON_API_KEY`
|
|
223
|
+
- `ONFON_CLIENT_ID`
|
|
224
|
+
- `ONFON_SENDER_ID`
|
|
225
|
+
- `ONFON_BASE_URL`
|
|
226
|
+
- `ONFON_TIMEOUT_SECONDS`
|
|
227
|
+
|
|
228
|
+
### Meta WhatsApp env vars
|
|
229
|
+
|
|
230
|
+
- `META_WHATSAPP_ACCESS_TOKEN`
|
|
231
|
+
- `META_WHATSAPP_PHONE_NUMBER_ID`
|
|
232
|
+
- `META_WHATSAPP_WHATSAPP_BUSINESS_ACCOUNT_ID`
|
|
233
|
+
- `META_WHATSAPP_APP_SECRET`
|
|
234
|
+
- `META_WHATSAPP_WEBHOOK_VERIFY_TOKEN`
|
|
235
|
+
- `META_WHATSAPP_API_VERSION`
|
|
236
|
+
- `META_WHATSAPP_BASE_URL`
|
|
237
|
+
- `META_WHATSAPP_TIMEOUT_SECONDS`
|
|
238
|
+
|
|
239
|
+
`whatsapp_business_account_id` is required only for template management methods.
|
|
240
|
+
|
|
241
|
+
### Africa's Talking env vars
|
|
242
|
+
|
|
243
|
+
- `AFRICASTALKING_API_KEY`
|
|
244
|
+
- `AFRICASTALKING_USERNAME`
|
|
245
|
+
- `AFRICASTALKING_SENDER_ID`
|
|
246
|
+
- `AFRICASTALKING_BASE_URL`
|
|
247
|
+
- `AFRICASTALKING_TIMEOUT_SECONDS`
|
|
248
|
+
|
|
249
|
+
Fallback names are also accepted: `AFRICAS_TALKING_API_KEY`,
|
|
250
|
+
`AFRICAS_TALKING_USERNAME`, `AFRICAS_TALKING_SENDER_ID`,
|
|
251
|
+
`AFRICAS_TALKING_BASE_URL`.
|
|
252
|
+
|
|
253
|
+
## WhatsApp: Meta Cloud API
|
|
254
|
+
|
|
255
|
+
### WhatsApp Method Reference
|
|
256
|
+
|
|
257
|
+
| Method | Purpose |
|
|
258
|
+
| --- | --- |
|
|
259
|
+
| `send_text(request, options=None)` | Send text messages with optional URL preview |
|
|
260
|
+
| `send_template(request, options=None)` | Send approved template messages |
|
|
261
|
+
| `send_media(request, options=None)` | Send image, audio, document, sticker, or video by media id or URL |
|
|
262
|
+
| `send_location(request, options=None)` | Send a location pin |
|
|
263
|
+
| `send_contacts(request, options=None)` | Send one or more contacts |
|
|
264
|
+
| `send_reaction(request, options=None)` | React to an existing message |
|
|
265
|
+
| `send_interactive(request, options=None)` | Send reply-button or list interactive messages |
|
|
266
|
+
| `send_catalog(request, options=None)` | Send catalog messages |
|
|
267
|
+
| `send_product(request, options=None)` | Send a single-product message |
|
|
268
|
+
| `send_product_list(request, options=None)` | Send a multi-product list |
|
|
269
|
+
| `send_flow(request, options=None)` | Send a WhatsApp Flow interactive message |
|
|
270
|
+
| `mark_message_read(request, options=None)` | Mark an inbound message as read |
|
|
271
|
+
| `send_typing_indicator(request, options=None)` | Mark as read and show a typing indicator |
|
|
272
|
+
| `upload_media(request, options=None)` | Upload media bytes to Meta |
|
|
273
|
+
| `get_media(media_id, options=None)` | Get media metadata and download URL |
|
|
274
|
+
| `delete_media(media_id, options=None)` | Delete uploaded media |
|
|
275
|
+
| `list_templates(request=None, options=None)` | List templates for a WABA |
|
|
276
|
+
| `get_template(template_id, fields=None, options=None)` | Fetch one template |
|
|
277
|
+
| `create_template(request, options=None)` | Create a template |
|
|
278
|
+
| `update_template(template_id, request, options=None)` | Update a template |
|
|
279
|
+
| `delete_template(request, options=None)` | Delete a template by name, id, or ids |
|
|
280
|
+
| `parse_events(payload)` | Parse delivery/read/failed webhook statuses |
|
|
281
|
+
| `parse_inbound_messages(payload)` | Parse inbound messages |
|
|
282
|
+
| `parse_event(payload)` | Return the first parsed delivery event, or `None` |
|
|
283
|
+
| `parse_inbound_message(payload)` | Return the first parsed inbound message, or `None` |
|
|
284
|
+
|
|
285
|
+
### Text
|
|
286
|
+
|
|
287
|
+
```python
|
|
288
|
+
from sendkit import WhatsAppTextRequest
|
|
289
|
+
|
|
290
|
+
whatsapp.send_text(
|
|
291
|
+
WhatsAppTextRequest(
|
|
292
|
+
recipient="254700123456",
|
|
293
|
+
text="Plain text message",
|
|
294
|
+
preview_url=True,
|
|
295
|
+
reply_to_message_id="wamid.previous",
|
|
296
|
+
)
|
|
297
|
+
)
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### Templates
|
|
301
|
+
|
|
302
|
+
Template messages support text, media, and button parameters. For media headers
|
|
303
|
+
pass a parameter of type `image`, `video`, or `document` and either a `value`
|
|
304
|
+
(an uploaded media id) or a provider-specific object via `provider_options`.
|
|
305
|
+
|
|
306
|
+
```python
|
|
307
|
+
from sendkit import (
|
|
308
|
+
WhatsAppTemplateComponent,
|
|
309
|
+
WhatsAppTemplateParameter,
|
|
310
|
+
WhatsAppTemplateRequest,
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
whatsapp.send_template(
|
|
314
|
+
WhatsAppTemplateRequest(
|
|
315
|
+
recipient="254700123456",
|
|
316
|
+
template_name="order_update",
|
|
317
|
+
language_code="en",
|
|
318
|
+
components=[
|
|
319
|
+
WhatsAppTemplateComponent(
|
|
320
|
+
type="header",
|
|
321
|
+
parameters=[
|
|
322
|
+
WhatsAppTemplateParameter(
|
|
323
|
+
type="document",
|
|
324
|
+
provider_options={
|
|
325
|
+
"document": {"id": "media-id", "filename": "invoice.pdf"}
|
|
326
|
+
},
|
|
327
|
+
)
|
|
328
|
+
],
|
|
329
|
+
),
|
|
330
|
+
WhatsAppTemplateComponent(
|
|
331
|
+
type="body",
|
|
332
|
+
parameters=[
|
|
333
|
+
WhatsAppTemplateParameter(type="text", value="NORIA-123"),
|
|
334
|
+
WhatsAppTemplateParameter(type="text", value="Ready for pickup"),
|
|
335
|
+
],
|
|
336
|
+
),
|
|
337
|
+
WhatsAppTemplateComponent(
|
|
338
|
+
type="button",
|
|
339
|
+
sub_type="quick_reply",
|
|
340
|
+
index=0,
|
|
341
|
+
parameters=[WhatsAppTemplateParameter(type="payload", value="track-order")],
|
|
342
|
+
),
|
|
343
|
+
],
|
|
344
|
+
)
|
|
345
|
+
)
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### Media And Attachments
|
|
349
|
+
|
|
350
|
+
Send media by public URL, by uploaded id, or upload bytes first and use the
|
|
351
|
+
returned media id.
|
|
352
|
+
|
|
353
|
+
```python
|
|
354
|
+
from sendkit import WhatsAppMediaRequest, WhatsAppMediaUploadRequest
|
|
355
|
+
|
|
356
|
+
whatsapp.send_media(
|
|
357
|
+
WhatsAppMediaRequest(
|
|
358
|
+
recipient="254700123456",
|
|
359
|
+
media_type="image",
|
|
360
|
+
link="https://example.com/product.jpg",
|
|
361
|
+
caption="Preview",
|
|
362
|
+
)
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
uploaded = whatsapp.upload_media(
|
|
366
|
+
WhatsAppMediaUploadRequest(
|
|
367
|
+
filename="menu.pdf",
|
|
368
|
+
mime_type="application/pdf",
|
|
369
|
+
content=b"file-bytes",
|
|
370
|
+
)
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
whatsapp.send_media(
|
|
374
|
+
WhatsAppMediaRequest(
|
|
375
|
+
recipient="254700123456",
|
|
376
|
+
media_type="document",
|
|
377
|
+
media_id=uploaded.media_id,
|
|
378
|
+
filename="menu.pdf",
|
|
379
|
+
)
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
whatsapp.get_media(uploaded.media_id)
|
|
383
|
+
whatsapp.delete_media(uploaded.media_id)
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
Supported media types: `image`, `audio`, `document`, `sticker`, `video`.
|
|
387
|
+
|
|
388
|
+
### Location, Contacts, And Reactions
|
|
389
|
+
|
|
390
|
+
```python
|
|
391
|
+
from sendkit import (
|
|
392
|
+
WhatsAppContact,
|
|
393
|
+
WhatsAppContactName,
|
|
394
|
+
WhatsAppContactPhone,
|
|
395
|
+
WhatsAppContactsRequest,
|
|
396
|
+
WhatsAppLocationRequest,
|
|
397
|
+
WhatsAppReactionRequest,
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
whatsapp.send_location(
|
|
401
|
+
WhatsAppLocationRequest(
|
|
402
|
+
recipient="254700123456",
|
|
403
|
+
latitude=-1.286389,
|
|
404
|
+
longitude=36.817223,
|
|
405
|
+
name="Nairobi Office",
|
|
406
|
+
address="Nairobi, Kenya",
|
|
407
|
+
)
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
whatsapp.send_contacts(
|
|
411
|
+
WhatsAppContactsRequest(
|
|
412
|
+
recipient="254700123456",
|
|
413
|
+
contacts=[
|
|
414
|
+
WhatsAppContact(
|
|
415
|
+
name=WhatsAppContactName(formatted_name="Noria Support", first_name="Noria"),
|
|
416
|
+
phones=[WhatsAppContactPhone(phone="+254700000000", type="WORK")],
|
|
417
|
+
)
|
|
418
|
+
],
|
|
419
|
+
)
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
whatsapp.send_reaction(
|
|
423
|
+
WhatsAppReactionRequest(recipient="254700123456", message_id="wamid.inbound", emoji="👍")
|
|
424
|
+
)
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
### Interactive, Catalog, Product, And Flow Messages
|
|
428
|
+
|
|
429
|
+
```python
|
|
430
|
+
from sendkit import (
|
|
431
|
+
WhatsAppFlowMessageRequest,
|
|
432
|
+
WhatsAppInteractiveButton,
|
|
433
|
+
WhatsAppInteractiveHeader,
|
|
434
|
+
WhatsAppInteractiveRequest,
|
|
435
|
+
WhatsAppInteractiveRow,
|
|
436
|
+
WhatsAppInteractiveSection,
|
|
437
|
+
WhatsAppProductItem,
|
|
438
|
+
WhatsAppProductListRequest,
|
|
439
|
+
WhatsAppProductSection,
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
whatsapp.send_interactive(
|
|
443
|
+
WhatsAppInteractiveRequest(
|
|
444
|
+
recipient="254700123456",
|
|
445
|
+
interactive_type="button",
|
|
446
|
+
body_text="Choose one",
|
|
447
|
+
buttons=[
|
|
448
|
+
WhatsAppInteractiveButton(identifier="yes", title="Yes"),
|
|
449
|
+
WhatsAppInteractiveButton(identifier="no", title="No"),
|
|
450
|
+
],
|
|
451
|
+
)
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
whatsapp.send_interactive(
|
|
455
|
+
WhatsAppInteractiveRequest(
|
|
456
|
+
recipient="254700123456",
|
|
457
|
+
interactive_type="list",
|
|
458
|
+
body_text="Choose a product",
|
|
459
|
+
button_text="View options",
|
|
460
|
+
sections=[
|
|
461
|
+
WhatsAppInteractiveSection(
|
|
462
|
+
title="Products",
|
|
463
|
+
rows=[
|
|
464
|
+
WhatsAppInteractiveRow(identifier="sku-1", title="Starter"),
|
|
465
|
+
WhatsAppInteractiveRow(identifier="sku-2", title="Pro"),
|
|
466
|
+
],
|
|
467
|
+
)
|
|
468
|
+
],
|
|
469
|
+
)
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
whatsapp.send_product_list(
|
|
473
|
+
WhatsAppProductListRequest(
|
|
474
|
+
recipient="254700123456",
|
|
475
|
+
catalog_id="catalog-1",
|
|
476
|
+
header=WhatsAppInteractiveHeader(type="text", text="Featured"),
|
|
477
|
+
sections=[
|
|
478
|
+
WhatsAppProductSection(
|
|
479
|
+
title="Top Picks",
|
|
480
|
+
product_items=[WhatsAppProductItem(product_retailer_id="sku-1")],
|
|
481
|
+
)
|
|
482
|
+
],
|
|
483
|
+
)
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
whatsapp.send_flow(
|
|
487
|
+
WhatsAppFlowMessageRequest(
|
|
488
|
+
recipient="254700123456",
|
|
489
|
+
flow_cta="Start",
|
|
490
|
+
flow_id="flow-1",
|
|
491
|
+
flow_action="navigate",
|
|
492
|
+
)
|
|
493
|
+
)
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
### Read Receipts And Typing Indicator
|
|
497
|
+
|
|
498
|
+
```python
|
|
499
|
+
from sendkit import WhatsAppReadRequest
|
|
500
|
+
|
|
501
|
+
whatsapp.mark_message_read(WhatsAppReadRequest(message_id="wamid.inbound"))
|
|
502
|
+
whatsapp.send_typing_indicator(WhatsAppReadRequest(message_id="wamid.inbound"))
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
### Template Management
|
|
506
|
+
|
|
507
|
+
```python
|
|
508
|
+
from sendkit import (
|
|
509
|
+
WhatsAppTemplateCreateRequest,
|
|
510
|
+
WhatsAppTemplateDeleteRequest,
|
|
511
|
+
WhatsAppTemplateListRequest,
|
|
512
|
+
WhatsAppTemplateUpdateRequest,
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
result = whatsapp.list_templates(
|
|
516
|
+
WhatsAppTemplateListRequest(limit=20, status=["approved"])
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
whatsapp.get_template("template-id")
|
|
520
|
+
|
|
521
|
+
whatsapp.create_template(
|
|
522
|
+
WhatsAppTemplateCreateRequest(
|
|
523
|
+
name="order_update",
|
|
524
|
+
language="en_US",
|
|
525
|
+
category="utility",
|
|
526
|
+
)
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
whatsapp.update_template("template-id", WhatsAppTemplateUpdateRequest(category="utility"))
|
|
530
|
+
whatsapp.delete_template(WhatsAppTemplateDeleteRequest(template_id="template-id"))
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
### WhatsApp Webhooks
|
|
534
|
+
|
|
535
|
+
```python
|
|
536
|
+
delivery_events = whatsapp.parse_events(meta_webhook_payload)
|
|
537
|
+
inbound_messages = whatsapp.parse_inbound_messages(meta_webhook_payload)
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
`parse_inbound_messages` supports inbound text, media, location, contacts,
|
|
541
|
+
button replies, interactive replies, reactions, and unsupported message
|
|
542
|
+
fallback metadata.
|
|
543
|
+
|
|
544
|
+
## SMS: Shared Request Shape
|
|
545
|
+
|
|
546
|
+
All SMS providers use the shared `SmsSendRequest`:
|
|
547
|
+
|
|
548
|
+
```python
|
|
549
|
+
from datetime import datetime
|
|
550
|
+
|
|
551
|
+
from sendkit import SmsMessage, SmsSendRequest
|
|
552
|
+
|
|
553
|
+
sms.send(
|
|
554
|
+
SmsSendRequest(
|
|
555
|
+
sender_id="NORIALABS",
|
|
556
|
+
messages=[
|
|
557
|
+
SmsMessage(
|
|
558
|
+
recipient="254700123456",
|
|
559
|
+
text="Hello",
|
|
560
|
+
reference="internal-id",
|
|
561
|
+
metadata={"account_id": "acct_1"},
|
|
562
|
+
)
|
|
563
|
+
],
|
|
564
|
+
schedule_at=datetime(2026, 6, 26, 9, 0, 0),
|
|
565
|
+
is_unicode=False,
|
|
566
|
+
is_flash=False,
|
|
567
|
+
provider_options={},
|
|
568
|
+
)
|
|
569
|
+
)
|
|
570
|
+
```
|
|
571
|
+
|
|
572
|
+
`provider_options` is passed through to the underlying provider payload when you
|
|
573
|
+
need provider-specific fields.
|
|
574
|
+
|
|
575
|
+
## SMS: Onfon
|
|
576
|
+
|
|
577
|
+
### Onfon Method Reference
|
|
578
|
+
|
|
579
|
+
| Method | Purpose |
|
|
580
|
+
| --- | --- |
|
|
581
|
+
| `send(request, options=None)` | Send one or more SMS messages |
|
|
582
|
+
| `get_balance(options=None)` | Read SMS balance |
|
|
583
|
+
| `list_groups(options=None)` | List contact groups |
|
|
584
|
+
| `create_group(request, options=None)` | Create a contact group |
|
|
585
|
+
| `update_group(group_id, request, options=None)` | Update a contact group |
|
|
586
|
+
| `delete_group(group_id, options=None)` | Delete a contact group |
|
|
587
|
+
| `list_templates(options=None)` | List SMS templates |
|
|
588
|
+
| `create_template(request, options=None)` | Create an SMS template |
|
|
589
|
+
| `update_template(template_id, request, options=None)` | Update an SMS template |
|
|
590
|
+
| `delete_template(template_id, options=None)` | Delete an SMS template |
|
|
591
|
+
| `parse_delivery_report(payload)` | Parse delivery-report callbacks |
|
|
592
|
+
|
|
593
|
+
```python
|
|
594
|
+
from sendkit import SmsGroupUpsertRequest, SmsMessage, SmsSendRequest, SmsTemplateUpsertRequest
|
|
595
|
+
|
|
596
|
+
result = sms.send(
|
|
597
|
+
SmsSendRequest(
|
|
598
|
+
sender_id="NORIALABS",
|
|
599
|
+
messages=[
|
|
600
|
+
SmsMessage(recipient="254700123456", text="Hello there", reference="msg-1"),
|
|
601
|
+
SmsMessage(recipient="254711111111", text="Hello again", reference="msg-2"),
|
|
602
|
+
],
|
|
603
|
+
is_unicode=False,
|
|
604
|
+
)
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
sms.get_balance()
|
|
608
|
+
|
|
609
|
+
group = sms.create_group(SmsGroupUpsertRequest(name="VIP Customers"))
|
|
610
|
+
sms.update_group(group.resource_id, SmsGroupUpsertRequest(name="Priority Customers"))
|
|
611
|
+
sms.delete_group(group.resource_id)
|
|
612
|
+
|
|
613
|
+
template = sms.create_template(SmsTemplateUpsertRequest(name="otp", body="Your OTP is {{1}}"))
|
|
614
|
+
sms.update_template(template.resource_id, SmsTemplateUpsertRequest(name="otp", body="Use code {{1}}"))
|
|
615
|
+
sms.delete_template(template.resource_id)
|
|
616
|
+
|
|
617
|
+
report = sms.parse_delivery_report(
|
|
618
|
+
{"messageId": "abc123", "mobile": "254700123456", "status": "Delivered"}
|
|
619
|
+
)
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
## SMS: Africa's Talking
|
|
623
|
+
|
|
624
|
+
Use `AFRICASTALKING_SANDBOX_SMS_BASE_URL` for sandbox clients:
|
|
625
|
+
|
|
626
|
+
```python
|
|
627
|
+
from sendkit import AFRICASTALKING_SANDBOX_SMS_BASE_URL, AfricasTalkingSmsClient
|
|
628
|
+
|
|
629
|
+
sandbox_sms = AfricasTalkingSmsClient(
|
|
630
|
+
api_key="...",
|
|
631
|
+
username="sandbox",
|
|
632
|
+
base_url=AFRICASTALKING_SANDBOX_SMS_BASE_URL,
|
|
633
|
+
)
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
### Africa's Talking Method Reference
|
|
637
|
+
|
|
638
|
+
| Method | Purpose |
|
|
639
|
+
| --- | --- |
|
|
640
|
+
| `send(request, options=None)` | Send normal bulk SMS |
|
|
641
|
+
| `send_premium(request, options=None)` | Send premium SMS replies using keyword and link id |
|
|
642
|
+
| `fetch_messages(request=None, options=None)` | Fetch incoming SMS messages |
|
|
643
|
+
| `create_subscription(request, options=None)` | Opt a phone number into a premium SMS subscription |
|
|
644
|
+
| `delete_subscription(request, options=None)` | Remove a premium SMS subscription |
|
|
645
|
+
| `get_balance(options=None)` | Read account balance |
|
|
646
|
+
| `parse_delivery_report(payload)` | Parse delivery-report callbacks |
|
|
647
|
+
|
|
648
|
+
The client groups messages by text because Africa's Talking accepts one message
|
|
649
|
+
body per request and many recipients.
|
|
650
|
+
|
|
651
|
+
```python
|
|
652
|
+
from sendkit import (
|
|
653
|
+
AfricasTalkingFetchMessagesRequest,
|
|
654
|
+
AfricasTalkingPremiumSmsRequest,
|
|
655
|
+
AfricasTalkingSubscriptionRequest,
|
|
656
|
+
SmsMessage,
|
|
657
|
+
SmsSendRequest,
|
|
658
|
+
)
|
|
659
|
+
|
|
660
|
+
sms.send(
|
|
661
|
+
SmsSendRequest(
|
|
662
|
+
sender_id="NORIALABS",
|
|
663
|
+
messages=[
|
|
664
|
+
SmsMessage(recipient="+254700123456", text="Hello", reference="msg-1"),
|
|
665
|
+
SmsMessage(recipient="+254711111111", text="Hello", reference="msg-2"),
|
|
666
|
+
],
|
|
667
|
+
provider_options={"enqueue": "1"},
|
|
668
|
+
)
|
|
669
|
+
)
|
|
670
|
+
|
|
671
|
+
sms.send_premium(
|
|
672
|
+
AfricasTalkingPremiumSmsRequest(
|
|
673
|
+
recipient="+254700123456",
|
|
674
|
+
short_code="22384",
|
|
675
|
+
keyword="NORIA",
|
|
676
|
+
link_id="link-id-from-inbound-message",
|
|
677
|
+
text="Thanks for subscribing",
|
|
678
|
+
retry_duration_in_hours=2,
|
|
679
|
+
)
|
|
680
|
+
)
|
|
681
|
+
|
|
682
|
+
inbox = sms.fetch_messages(AfricasTalkingFetchMessagesRequest(last_received_id=42))
|
|
683
|
+
for message in inbox.messages:
|
|
684
|
+
print(message.provider_message_id, message.sender, message.text)
|
|
685
|
+
|
|
686
|
+
sms.create_subscription(
|
|
687
|
+
AfricasTalkingSubscriptionRequest(
|
|
688
|
+
phone_number="+254700123456", short_code="22384", keyword="NORIA"
|
|
689
|
+
)
|
|
690
|
+
)
|
|
691
|
+
sms.delete_subscription(
|
|
692
|
+
AfricasTalkingSubscriptionRequest(
|
|
693
|
+
phone_number="+254700123456", short_code="22384", keyword="NORIA"
|
|
694
|
+
)
|
|
695
|
+
)
|
|
696
|
+
|
|
697
|
+
sms.get_balance()
|
|
698
|
+
|
|
699
|
+
event = sms.parse_delivery_report(
|
|
700
|
+
{
|
|
701
|
+
"id": "at-message-id",
|
|
702
|
+
"phoneNumber": "+254700123456",
|
|
703
|
+
"status": "Success",
|
|
704
|
+
"networkCode": "63902",
|
|
705
|
+
"retryCount": "0",
|
|
706
|
+
}
|
|
707
|
+
)
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
## Webhooks
|
|
711
|
+
|
|
712
|
+
### Meta Verification Challenge
|
|
713
|
+
|
|
714
|
+
```python
|
|
715
|
+
from sendkit import resolve_meta_subscription_challenge
|
|
716
|
+
|
|
717
|
+
challenge = resolve_meta_subscription_challenge(
|
|
718
|
+
{
|
|
719
|
+
"hub.mode": "subscribe",
|
|
720
|
+
"hub.verify_token": "verify-me",
|
|
721
|
+
"hub.challenge": "12345",
|
|
722
|
+
},
|
|
723
|
+
"verify-me",
|
|
724
|
+
)
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
### Meta Signature Verification
|
|
728
|
+
|
|
729
|
+
```python
|
|
730
|
+
from sendkit import require_valid_meta_signature
|
|
731
|
+
|
|
732
|
+
require_valid_meta_signature(raw_body, signature_header, app_secret)
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
### SMS Delivery Reports
|
|
736
|
+
|
|
737
|
+
```python
|
|
738
|
+
from sendkit import parse_africastalking_sms_delivery_report, parse_onfon_delivery_report
|
|
739
|
+
|
|
740
|
+
onfon_event = parse_onfon_delivery_report(query_params, onfon_client)
|
|
741
|
+
at_event = parse_africastalking_sms_delivery_report(body, africastalking_client)
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
## Errors
|
|
745
|
+
|
|
746
|
+
Exported errors:
|
|
747
|
+
|
|
748
|
+
- `SendKitError`
|
|
749
|
+
- `ConfigurationError`
|
|
750
|
+
- `ApiError`
|
|
751
|
+
- `ProviderError`
|
|
752
|
+
- `NetworkError`
|
|
753
|
+
- `TimeoutError`
|
|
754
|
+
- `WebhookVerificationError`
|
|
755
|
+
|
|
756
|
+
Provider errors include provider response details where available.
|
|
757
|
+
|
|
758
|
+
```python
|
|
759
|
+
from sendkit import ProviderError, SmsSendRequest
|
|
760
|
+
|
|
761
|
+
try:
|
|
762
|
+
sms.send(SmsSendRequest(messages=[...]))
|
|
763
|
+
except ProviderError as error:
|
|
764
|
+
print(error.provider, error.error_code, error.response_body)
|
|
765
|
+
```
|
|
766
|
+
|
|
767
|
+
## Lifecycle
|
|
768
|
+
|
|
769
|
+
Sync clients are context managers and expose `close()`; async clients are async
|
|
770
|
+
context managers and expose `aclose()`. When you pass your own `httpx` client,
|
|
771
|
+
SendKit will not close it for you.
|
|
772
|
+
|
|
773
|
+
```python
|
|
774
|
+
with OnfonSmsClient(access_key="...", api_key="...", client_id="...") as sms:
|
|
775
|
+
sms.get_balance()
|
|
776
|
+
|
|
777
|
+
async with AsyncOnfonSmsClient(access_key="...", api_key="...", client_id="...") as sms:
|
|
778
|
+
await sms.get_balance()
|
|
779
|
+
```
|
|
780
|
+
|
|
781
|
+
## Runtime Exports
|
|
782
|
+
|
|
783
|
+
Root constant exports:
|
|
784
|
+
|
|
785
|
+
- `ONFON_BASE_URL`
|
|
786
|
+
- `ONFON_SMS_BASE_URL`
|
|
787
|
+
- `AFRICASTALKING_SMS_BASE_URL`
|
|
788
|
+
- `AFRICASTALKING_SANDBOX_SMS_BASE_URL`
|
|
789
|
+
- `META_GRAPH_BASE_URL`
|
|
790
|
+
- `META_GRAPH_API_VERSION`
|
|
791
|
+
|
|
792
|
+
Provider clients, request/result models, transport types (`RequestOptions`,
|
|
793
|
+
`RetryPolicy`, `Hooks`, `DeliveryEvent`, `DeliveryState`, `MessageChannel`), and
|
|
794
|
+
webhook helpers are all available from the top-level `sendkit` package.
|