whatsapp-cloud-api-py 0.1.0__py3-none-any.whl → 0.1.1__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.
- whatsapp_cloud_api_py-0.1.1.dist-info/METADATA +623 -0
- {whatsapp_cloud_api_py-0.1.0.dist-info → whatsapp_cloud_api_py-0.1.1.dist-info}/RECORD +4 -4
- whatsapp_cloud_api_py-0.1.0.dist-info/METADATA +0 -24
- {whatsapp_cloud_api_py-0.1.0.dist-info → whatsapp_cloud_api_py-0.1.1.dist-info}/WHEEL +0 -0
- {whatsapp_cloud_api_py-0.1.0.dist-info → whatsapp_cloud_api_py-0.1.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: whatsapp-cloud-api-py
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Async Python SDK for WhatsApp Business Cloud API with Pydantic V2
|
|
5
|
+
Project-URL: Homepage, https://github.com/HeiCg/whatsapp-cloud-api-py
|
|
6
|
+
Project-URL: Repository, https://github.com/HeiCg/whatsapp-cloud-api-py
|
|
7
|
+
Project-URL: Issues, https://github.com/HeiCg/whatsapp-cloud-api-py/issues
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Python: >=3.11
|
|
11
|
+
Requires-Dist: httpx[http2]>=0.27
|
|
12
|
+
Requires-Dist: pydantic>=2.7
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
15
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
16
|
+
Requires-Dist: pyventus>=0.7.2; extra == 'dev'
|
|
17
|
+
Requires-Dist: respx>=0.22; extra == 'dev'
|
|
18
|
+
Requires-Dist: ruff>=0.8; extra == 'dev'
|
|
19
|
+
Provides-Extra: events
|
|
20
|
+
Requires-Dist: pyventus>=0.7.2; extra == 'events'
|
|
21
|
+
Provides-Extra: server
|
|
22
|
+
Requires-Dist: cryptography>=43.0; extra == 'server'
|
|
23
|
+
Provides-Extra: webhooks
|
|
24
|
+
Requires-Dist: starlette>=0.37; extra == 'webhooks'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# whatsapp-cloud-api-py
|
|
28
|
+
|
|
29
|
+
Community-built async Python SDK for the WhatsApp Business Cloud API.
|
|
30
|
+
|
|
31
|
+
> **Note:** This is an **independent Python implementation** — not a port or fork. It was inspired by the excellent [`@kapso/whatsapp-cloud-api`](https://github.com/gokapso/whatsapp-cloud-api-js) (TypeScript), but written from scratch in Python with its own architecture, design choices, and API surface.
|
|
32
|
+
|
|
33
|
+
Built with **httpx** (HTTP/2 + connection pooling), **Pydantic V2** (Rust-powered validation), and optional **pyventus** event-driven webhooks.
|
|
34
|
+
|
|
35
|
+
## Features
|
|
36
|
+
|
|
37
|
+
- **Fully async** — all I/O uses `async`/`await` with httpx
|
|
38
|
+
- **HTTP/2** — connection pooling and multiplexing out of the box
|
|
39
|
+
- **Pydantic V2** — fast, type-safe input/response models with Rust-powered validation
|
|
40
|
+
- **27 message types** — text, image, video, audio, document, sticker, location, contacts, reaction, template, interactive (buttons, list, flow, CTA URL, catalog), mark as read
|
|
41
|
+
- **Media operations** — upload, get metadata, download, delete (with auto-retry on auth failures)
|
|
42
|
+
- **Template management** — list, create, delete message templates
|
|
43
|
+
- **Phone number management** — registration, verification, business profile
|
|
44
|
+
- **WhatsApp Flows** — create and deploy (auto-publish)
|
|
45
|
+
- **Webhook handling** — HMAC-SHA256 signature verification + payload normalization
|
|
46
|
+
- **Event-driven webhooks** — optional pyventus integration with 18 typed events
|
|
47
|
+
- **Error categorization** — 14 error categories with retry hints (but no forced auto-retry)
|
|
48
|
+
|
|
49
|
+
## Installation
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
uv add whatsapp-cloud-api-py
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
With extras:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# Event-driven webhooks (pyventus)
|
|
59
|
+
uv add "whatsapp-cloud-api-py[events]"
|
|
60
|
+
|
|
61
|
+
# All extras
|
|
62
|
+
uv add "whatsapp-cloud-api-py[events,webhooks,server]"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Quick Start
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
import asyncio
|
|
69
|
+
from whatsapp_cloud_api import WhatsAppClient, TextMessage
|
|
70
|
+
|
|
71
|
+
async def main():
|
|
72
|
+
async with WhatsAppClient(access_token="YOUR_TOKEN") as client:
|
|
73
|
+
response = await client.messages.send_text(TextMessage(
|
|
74
|
+
phone_number_id="PHONE_NUMBER_ID",
|
|
75
|
+
to="5511999999999",
|
|
76
|
+
body="Hello from Python!",
|
|
77
|
+
))
|
|
78
|
+
print(response.messages[0].id)
|
|
79
|
+
|
|
80
|
+
asyncio.run(main())
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Sending Messages
|
|
84
|
+
|
|
85
|
+
All message types return a `SendMessageResponse` with `contacts` and `messages` fields.
|
|
86
|
+
|
|
87
|
+
### Text
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from whatsapp_cloud_api import TextMessage
|
|
91
|
+
|
|
92
|
+
await client.messages.send_text(TextMessage(
|
|
93
|
+
phone_number_id="PHONE_ID",
|
|
94
|
+
to="5511999999999",
|
|
95
|
+
body="Hello!",
|
|
96
|
+
preview_url=True, # enable link previews
|
|
97
|
+
))
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Image
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
from whatsapp_cloud_api import ImageMessage
|
|
104
|
+
from whatsapp_cloud_api.resources.messages import MediaById, MediaByLink
|
|
105
|
+
|
|
106
|
+
# By media ID (from upload)
|
|
107
|
+
await client.messages.send_image(ImageMessage(
|
|
108
|
+
phone_number_id="PHONE_ID",
|
|
109
|
+
to="5511999999999",
|
|
110
|
+
image=MediaById(id="MEDIA_ID", caption="Check this out"),
|
|
111
|
+
))
|
|
112
|
+
|
|
113
|
+
# By URL
|
|
114
|
+
await client.messages.send_image(ImageMessage(
|
|
115
|
+
phone_number_id="PHONE_ID",
|
|
116
|
+
to="5511999999999",
|
|
117
|
+
image=MediaByLink(link="https://example.com/photo.jpg"),
|
|
118
|
+
))
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Audio / Video / Document / Sticker
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
from whatsapp_cloud_api import AudioMessage, VideoMessage, DocumentMessage, StickerMessage
|
|
125
|
+
from whatsapp_cloud_api.resources.messages import (
|
|
126
|
+
AudioPayloadByLink, MediaByLink, DocumentPayloadByLink, StickerByLink,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
await client.messages.send_audio(AudioMessage(
|
|
130
|
+
phone_number_id="PHONE_ID", to="5511999999999",
|
|
131
|
+
audio=AudioPayloadByLink(link="https://example.com/audio.mp3"),
|
|
132
|
+
))
|
|
133
|
+
|
|
134
|
+
await client.messages.send_video(VideoMessage(
|
|
135
|
+
phone_number_id="PHONE_ID", to="5511999999999",
|
|
136
|
+
video=MediaByLink(link="https://example.com/video.mp4", caption="Watch this"),
|
|
137
|
+
))
|
|
138
|
+
|
|
139
|
+
await client.messages.send_document(DocumentMessage(
|
|
140
|
+
phone_number_id="PHONE_ID", to="5511999999999",
|
|
141
|
+
document=DocumentPayloadByLink(
|
|
142
|
+
link="https://example.com/file.pdf",
|
|
143
|
+
filename="report.pdf",
|
|
144
|
+
caption="Monthly report",
|
|
145
|
+
),
|
|
146
|
+
))
|
|
147
|
+
|
|
148
|
+
await client.messages.send_sticker(StickerMessage(
|
|
149
|
+
phone_number_id="PHONE_ID", to="5511999999999",
|
|
150
|
+
sticker=StickerByLink(link="https://example.com/sticker.webp"),
|
|
151
|
+
))
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Location
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
from whatsapp_cloud_api import LocationMessage
|
|
158
|
+
from whatsapp_cloud_api.resources.messages import LocationPayload
|
|
159
|
+
|
|
160
|
+
await client.messages.send_location(LocationMessage(
|
|
161
|
+
phone_number_id="PHONE_ID",
|
|
162
|
+
to="5511999999999",
|
|
163
|
+
location=LocationPayload(
|
|
164
|
+
latitude=-23.5505,
|
|
165
|
+
longitude=-46.6333,
|
|
166
|
+
name="Sao Paulo",
|
|
167
|
+
address="Av. Paulista, 1000",
|
|
168
|
+
),
|
|
169
|
+
))
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Contacts
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
from whatsapp_cloud_api import ContactsMessage
|
|
176
|
+
from whatsapp_cloud_api.resources.messages import Contact, ContactName, ContactPhone
|
|
177
|
+
|
|
178
|
+
await client.messages.send_contacts(ContactsMessage(
|
|
179
|
+
phone_number_id="PHONE_ID",
|
|
180
|
+
to="5511999999999",
|
|
181
|
+
contacts=[Contact(
|
|
182
|
+
name=ContactName(formatted_name="Maria Silva", first_name="Maria"),
|
|
183
|
+
phones=[ContactPhone(phone="+5511988887777", type="MOBILE")],
|
|
184
|
+
)],
|
|
185
|
+
))
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Reaction
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
from whatsapp_cloud_api import ReactionMessage
|
|
192
|
+
from whatsapp_cloud_api.resources.messages import ReactionPayload
|
|
193
|
+
|
|
194
|
+
await client.messages.send_reaction(ReactionMessage(
|
|
195
|
+
phone_number_id="PHONE_ID",
|
|
196
|
+
to="5511999999999",
|
|
197
|
+
reaction=ReactionPayload(message_id="wamid.xxx", emoji="👍"),
|
|
198
|
+
))
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Template
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
from whatsapp_cloud_api import TemplateMessage
|
|
205
|
+
from whatsapp_cloud_api.resources.messages import TemplatePayload, TemplateLanguage
|
|
206
|
+
|
|
207
|
+
await client.messages.send_template(TemplateMessage(
|
|
208
|
+
phone_number_id="PHONE_ID",
|
|
209
|
+
to="5511999999999",
|
|
210
|
+
template=TemplatePayload(
|
|
211
|
+
name="hello_world",
|
|
212
|
+
language=TemplateLanguage(code="en_US"),
|
|
213
|
+
),
|
|
214
|
+
))
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Interactive Buttons
|
|
218
|
+
|
|
219
|
+
```python
|
|
220
|
+
from whatsapp_cloud_api import InteractiveButtonsMessage
|
|
221
|
+
from whatsapp_cloud_api.resources.messages import InteractiveButton
|
|
222
|
+
|
|
223
|
+
await client.messages.send_interactive_buttons(InteractiveButtonsMessage(
|
|
224
|
+
phone_number_id="PHONE_ID",
|
|
225
|
+
to="5511999999999",
|
|
226
|
+
body_text="Choose an option:",
|
|
227
|
+
buttons=[
|
|
228
|
+
InteractiveButton(id="opt_1", title="Option 1"),
|
|
229
|
+
InteractiveButton(id="opt_2", title="Option 2"),
|
|
230
|
+
InteractiveButton(id="opt_3", title="Option 3"),
|
|
231
|
+
],
|
|
232
|
+
))
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### Interactive List
|
|
236
|
+
|
|
237
|
+
```python
|
|
238
|
+
from whatsapp_cloud_api import InteractiveListMessage
|
|
239
|
+
from whatsapp_cloud_api.resources.messages import ListSection, ListRow
|
|
240
|
+
|
|
241
|
+
await client.messages.send_interactive_list(InteractiveListMessage(
|
|
242
|
+
phone_number_id="PHONE_ID",
|
|
243
|
+
to="5511999999999",
|
|
244
|
+
body_text="Pick a product:",
|
|
245
|
+
button_text="View options",
|
|
246
|
+
sections=[ListSection(
|
|
247
|
+
title="Products",
|
|
248
|
+
rows=[
|
|
249
|
+
ListRow(id="p1", title="Product A", description="$10.00"),
|
|
250
|
+
ListRow(id="p2", title="Product B", description="$20.00"),
|
|
251
|
+
],
|
|
252
|
+
)],
|
|
253
|
+
))
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### Interactive Flow
|
|
257
|
+
|
|
258
|
+
```python
|
|
259
|
+
from whatsapp_cloud_api import InteractiveFlowMessage
|
|
260
|
+
from whatsapp_cloud_api.resources.messages import FlowParameters
|
|
261
|
+
|
|
262
|
+
await client.messages.send_interactive_flow(InteractiveFlowMessage(
|
|
263
|
+
phone_number_id="PHONE_ID",
|
|
264
|
+
to="5511999999999",
|
|
265
|
+
body_text="Complete the form:",
|
|
266
|
+
parameters=FlowParameters(
|
|
267
|
+
flow_id="FLOW_ID",
|
|
268
|
+
flow_cta="Open Form",
|
|
269
|
+
flow_action="navigate",
|
|
270
|
+
),
|
|
271
|
+
))
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Interactive CTA URL
|
|
275
|
+
|
|
276
|
+
```python
|
|
277
|
+
from whatsapp_cloud_api import InteractiveCtaUrlMessage
|
|
278
|
+
from whatsapp_cloud_api.resources.messages import CtaUrlParameters
|
|
279
|
+
|
|
280
|
+
await client.messages.send_interactive_cta_url(InteractiveCtaUrlMessage(
|
|
281
|
+
phone_number_id="PHONE_ID",
|
|
282
|
+
to="5511999999999",
|
|
283
|
+
body_text="Visit our website",
|
|
284
|
+
parameters=CtaUrlParameters(display_text="Open", url="https://example.com"),
|
|
285
|
+
))
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### Mark as Read
|
|
289
|
+
|
|
290
|
+
```python
|
|
291
|
+
from whatsapp_cloud_api import MarkReadInput
|
|
292
|
+
|
|
293
|
+
await client.messages.mark_read(MarkReadInput(
|
|
294
|
+
phone_number_id="PHONE_ID",
|
|
295
|
+
message_id="wamid.xxx",
|
|
296
|
+
))
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
## Media
|
|
300
|
+
|
|
301
|
+
```python
|
|
302
|
+
from whatsapp_cloud_api.resources.media import MediaUploadInput
|
|
303
|
+
|
|
304
|
+
# Upload
|
|
305
|
+
result = await client.media.upload(MediaUploadInput(
|
|
306
|
+
phone_number_id="PHONE_ID",
|
|
307
|
+
type="image",
|
|
308
|
+
file=open("photo.jpg", "rb").read(),
|
|
309
|
+
filename="photo.jpg",
|
|
310
|
+
mime_type="image/jpeg",
|
|
311
|
+
))
|
|
312
|
+
print(result.id) # media ID to use in messages
|
|
313
|
+
|
|
314
|
+
# Get metadata
|
|
315
|
+
meta = await client.media.get("MEDIA_ID")
|
|
316
|
+
print(meta.url, meta.mime_type)
|
|
317
|
+
|
|
318
|
+
# Download
|
|
319
|
+
data = await client.media.download("MEDIA_ID")
|
|
320
|
+
|
|
321
|
+
# Delete
|
|
322
|
+
await client.media.delete("MEDIA_ID")
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
## Templates
|
|
326
|
+
|
|
327
|
+
```python
|
|
328
|
+
from whatsapp_cloud_api.resources.templates import (
|
|
329
|
+
TemplateListInput, TemplateCreateInput, TemplateDeleteInput,
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
# List
|
|
333
|
+
templates = await client.templates.list(TemplateListInput(
|
|
334
|
+
business_account_id="WABA_ID",
|
|
335
|
+
))
|
|
336
|
+
|
|
337
|
+
# Create
|
|
338
|
+
result = await client.templates.create(TemplateCreateInput(
|
|
339
|
+
business_account_id="WABA_ID",
|
|
340
|
+
name="order_confirmation",
|
|
341
|
+
language="pt_BR",
|
|
342
|
+
category="UTILITY",
|
|
343
|
+
components=[
|
|
344
|
+
{"type": "BODY", "text": "Pedido {{1}} confirmado!"},
|
|
345
|
+
],
|
|
346
|
+
))
|
|
347
|
+
|
|
348
|
+
# Delete
|
|
349
|
+
await client.templates.delete(TemplateDeleteInput(
|
|
350
|
+
business_account_id="WABA_ID",
|
|
351
|
+
name="order_confirmation",
|
|
352
|
+
))
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
## Phone Numbers
|
|
356
|
+
|
|
357
|
+
```python
|
|
358
|
+
from whatsapp_cloud_api.resources.phone_numbers import (
|
|
359
|
+
RequestCodeInput, VerifyCodeInput, RegisterInput, UpdateBusinessProfileInput,
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
# Request verification code
|
|
363
|
+
await client.phone_numbers.request_code(RequestCodeInput(
|
|
364
|
+
phone_number_id="PHONE_ID", code_method="SMS", language="pt_BR",
|
|
365
|
+
))
|
|
366
|
+
|
|
367
|
+
# Verify
|
|
368
|
+
await client.phone_numbers.verify_code(VerifyCodeInput(
|
|
369
|
+
phone_number_id="PHONE_ID", code="123456",
|
|
370
|
+
))
|
|
371
|
+
|
|
372
|
+
# Register
|
|
373
|
+
await client.phone_numbers.register(RegisterInput(
|
|
374
|
+
phone_number_id="PHONE_ID", pin="123456",
|
|
375
|
+
))
|
|
376
|
+
|
|
377
|
+
# Business profile
|
|
378
|
+
profile = await client.phone_numbers.business_profile.get("PHONE_ID")
|
|
379
|
+
|
|
380
|
+
await client.phone_numbers.business_profile.update(UpdateBusinessProfileInput(
|
|
381
|
+
phone_number_id="PHONE_ID",
|
|
382
|
+
about="We sell things",
|
|
383
|
+
description="Best store in town",
|
|
384
|
+
websites=["https://example.com"],
|
|
385
|
+
))
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
## Webhooks
|
|
389
|
+
|
|
390
|
+
### Signature Verification
|
|
391
|
+
|
|
392
|
+
```python
|
|
393
|
+
from whatsapp_cloud_api import verify_signature
|
|
394
|
+
|
|
395
|
+
is_valid = verify_signature(
|
|
396
|
+
app_secret="YOUR_META_APP_SECRET",
|
|
397
|
+
raw_body=request_body_bytes,
|
|
398
|
+
signature_header=request.headers.get("x-hub-signature-256"),
|
|
399
|
+
)
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
### Payload Normalization
|
|
403
|
+
|
|
404
|
+
```python
|
|
405
|
+
from whatsapp_cloud_api import normalize_webhook
|
|
406
|
+
|
|
407
|
+
webhook = normalize_webhook(payload)
|
|
408
|
+
|
|
409
|
+
print(webhook.phone_number_id)
|
|
410
|
+
print(webhook.messages) # list[WebhookMessage]
|
|
411
|
+
print(webhook.statuses) # list[MessageStatusUpdate]
|
|
412
|
+
print(webhook.contacts) # list[dict]
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
## Event-Driven Webhooks (pyventus)
|
|
416
|
+
|
|
417
|
+
Install with `uv add "whatsapp-cloud-api-py[events]"`.
|
|
418
|
+
|
|
419
|
+
Instead of manually parsing webhook payloads with `if/elif` chains, use typed event handlers:
|
|
420
|
+
|
|
421
|
+
```python
|
|
422
|
+
from whatsapp_cloud_api import normalize_webhook, verify_signature
|
|
423
|
+
from whatsapp_cloud_api.events import (
|
|
424
|
+
dispatch_webhook,
|
|
425
|
+
TextReceived,
|
|
426
|
+
ImageReceived,
|
|
427
|
+
ButtonReply,
|
|
428
|
+
ListReply,
|
|
429
|
+
FlowResponse,
|
|
430
|
+
LocationReceived,
|
|
431
|
+
ReactionReceived,
|
|
432
|
+
OrderReceived,
|
|
433
|
+
MessageDelivered,
|
|
434
|
+
MessageRead,
|
|
435
|
+
MessageFailed,
|
|
436
|
+
)
|
|
437
|
+
from pyventus.events import EventLinker, AsyncIOEventEmitter
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
@EventLinker.on(TextReceived)
|
|
441
|
+
async def handle_text(event: TextReceived):
|
|
442
|
+
print(f"Text from {event.from_number}: {event.body}")
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
@EventLinker.on(ImageReceived)
|
|
446
|
+
async def handle_image(event: ImageReceived):
|
|
447
|
+
media_bytes = await client.media.download(event.image_id)
|
|
448
|
+
# process image...
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
@EventLinker.on(ButtonReply)
|
|
452
|
+
async def handle_button(event: ButtonReply):
|
|
453
|
+
print(f"Button pressed: {event.button_id} ({event.button_title})")
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
@EventLinker.on(MessageFailed)
|
|
457
|
+
async def handle_failure(event: MessageFailed):
|
|
458
|
+
logger.error(f"Message {event.message_id} failed: {event.errors}")
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
# Dispatch
|
|
462
|
+
webhook = normalize_webhook(raw_payload)
|
|
463
|
+
emitter = AsyncIOEventEmitter()
|
|
464
|
+
dispatch_webhook(webhook, emitter)
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
### FastAPI Integration
|
|
468
|
+
|
|
469
|
+
```python
|
|
470
|
+
from fastapi import FastAPI, Request, Depends, HTTPException
|
|
471
|
+
from pyventus.events import EventLinker, FastAPIEventEmitter
|
|
472
|
+
from whatsapp_cloud_api import WhatsAppClient, normalize_webhook, verify_signature
|
|
473
|
+
from whatsapp_cloud_api.events import dispatch_webhook, TextReceived
|
|
474
|
+
|
|
475
|
+
app = FastAPI()
|
|
476
|
+
client = WhatsAppClient(access_token="YOUR_TOKEN")
|
|
477
|
+
APP_SECRET = "YOUR_META_APP_SECRET"
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
@EventLinker.on(TextReceived)
|
|
481
|
+
async def echo(event: TextReceived):
|
|
482
|
+
from whatsapp_cloud_api import TextMessage
|
|
483
|
+
await client.messages.send_text(TextMessage(
|
|
484
|
+
phone_number_id=event.phone_number_id,
|
|
485
|
+
to=event.from_number,
|
|
486
|
+
body=f"You said: {event.body}",
|
|
487
|
+
))
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
@app.post("/webhook")
|
|
491
|
+
async def webhook(request: Request, emitter=Depends(FastAPIEventEmitter())):
|
|
492
|
+
body = await request.body()
|
|
493
|
+
if not verify_signature(
|
|
494
|
+
app_secret=APP_SECRET,
|
|
495
|
+
raw_body=body,
|
|
496
|
+
signature_header=request.headers.get("x-hub-signature-256"),
|
|
497
|
+
):
|
|
498
|
+
raise HTTPException(status_code=403)
|
|
499
|
+
|
|
500
|
+
data = normalize_webhook(await request.json())
|
|
501
|
+
dispatch_webhook(data, emitter)
|
|
502
|
+
return {"status": "ok"}
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
@app.get("/webhook")
|
|
506
|
+
async def verify_webhook(mode: str = "", token: str = "", challenge: str = ""):
|
|
507
|
+
if mode == "subscribe" and token == "YOUR_VERIFY_TOKEN":
|
|
508
|
+
return int(challenge)
|
|
509
|
+
raise HTTPException(status_code=403)
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
The `FastAPIEventEmitter` runs handlers via Starlette's `BackgroundTasks`, so the endpoint returns immediately while events are processed in the background.
|
|
513
|
+
|
|
514
|
+
### Available Events
|
|
515
|
+
|
|
516
|
+
| Event | Trigger | Key Fields |
|
|
517
|
+
|---|---|---|
|
|
518
|
+
| `TextReceived` | Text message | `body`, `from_number` |
|
|
519
|
+
| `ImageReceived` | Image message | `image_id`, `mime_type`, `caption` |
|
|
520
|
+
| `VideoReceived` | Video message | `video_id`, `mime_type`, `caption` |
|
|
521
|
+
| `AudioReceived` | Audio/voice note | `audio_id`, `mime_type`, `voice` |
|
|
522
|
+
| `DocumentReceived` | Document | `document_id`, `filename`, `caption` |
|
|
523
|
+
| `StickerReceived` | Sticker | `sticker_id`, `animated` |
|
|
524
|
+
| `LocationReceived` | Location | `latitude`, `longitude`, `name` |
|
|
525
|
+
| `ContactsReceived` | Contact card(s) | `contacts` |
|
|
526
|
+
| `ReactionReceived` | Reaction emoji | `emoji`, `reacted_message_id` |
|
|
527
|
+
| `ButtonReply` | Interactive button | `button_id`, `button_title` |
|
|
528
|
+
| `ListReply` | Interactive list | `list_id`, `list_title` |
|
|
529
|
+
| `FlowResponse` | WhatsApp Flow | `response_json`, `flow_token` |
|
|
530
|
+
| `OrderReceived` | Product order | `catalog_id`, `product_items` |
|
|
531
|
+
| `MessageSent` | Status: sent | `message_id`, `recipient_id` |
|
|
532
|
+
| `MessageDelivered` | Status: delivered | `message_id`, `recipient_id` |
|
|
533
|
+
| `MessageRead` | Status: read | `message_id`, `recipient_id` |
|
|
534
|
+
| `MessageFailed` | Status: failed | `message_id`, `errors` |
|
|
535
|
+
| `UnknownMessageReceived` | Unmapped type | `raw_type`, `raw_data` |
|
|
536
|
+
|
|
537
|
+
All events inherit from `WhatsAppEvent` and include `phone_number_id`. Message events also include `message_id`, `timestamp`, `from_number`, and `context`.
|
|
538
|
+
|
|
539
|
+
## Error Handling
|
|
540
|
+
|
|
541
|
+
```python
|
|
542
|
+
from whatsapp_cloud_api import GraphApiError
|
|
543
|
+
|
|
544
|
+
try:
|
|
545
|
+
await client.messages.send_text(msg)
|
|
546
|
+
except GraphApiError as e:
|
|
547
|
+
print(e.category) # "throttling", "authorization", "parameter", ...
|
|
548
|
+
print(e.retry.action) # "retry", "retry_after", "fix_and_retry", "do_not_retry", "refresh_token"
|
|
549
|
+
print(e.retry.retry_after_ms) # milliseconds to wait (for rate limits)
|
|
550
|
+
|
|
551
|
+
if e.is_rate_limit():
|
|
552
|
+
await asyncio.sleep(e.retry.retry_after_ms / 1000)
|
|
553
|
+
# retry...
|
|
554
|
+
|
|
555
|
+
if e.requires_token_refresh():
|
|
556
|
+
# refresh your access token
|
|
557
|
+
pass
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
## Client Configuration
|
|
561
|
+
|
|
562
|
+
```python
|
|
563
|
+
from whatsapp_cloud_api import WhatsAppClient
|
|
564
|
+
|
|
565
|
+
# Default: graph.facebook.com, v23.0, HTTP/2, 30s timeout
|
|
566
|
+
client = WhatsAppClient(access_token="TOKEN")
|
|
567
|
+
|
|
568
|
+
# Custom configuration
|
|
569
|
+
client = WhatsAppClient(
|
|
570
|
+
access_token="TOKEN",
|
|
571
|
+
base_url="https://graph.facebook.com",
|
|
572
|
+
graph_version="v23.0",
|
|
573
|
+
timeout=60.0,
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
# Bring your own httpx client
|
|
577
|
+
import httpx
|
|
578
|
+
custom_http = httpx.AsyncClient(http2=True, timeout=60.0)
|
|
579
|
+
client = WhatsAppClient(access_token="TOKEN", http_client=custom_http)
|
|
580
|
+
|
|
581
|
+
# Always use as async context manager
|
|
582
|
+
async with WhatsAppClient(access_token="TOKEN") as client:
|
|
583
|
+
await client.messages.send_text(...)
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
## Project Structure
|
|
587
|
+
|
|
588
|
+
```
|
|
589
|
+
src/whatsapp_cloud_api/
|
|
590
|
+
__init__.py # Public API
|
|
591
|
+
client.py # Async HTTP client (httpx, HTTP/2)
|
|
592
|
+
types.py # Pydantic response models
|
|
593
|
+
errors/
|
|
594
|
+
graph_api_error.py # GraphApiError + from_response()
|
|
595
|
+
categorize.py # Error code -> category mapping
|
|
596
|
+
retry.py # RetryHint (action + delay)
|
|
597
|
+
resources/
|
|
598
|
+
messages/
|
|
599
|
+
models.py # Pydantic models for all message types
|
|
600
|
+
resource.py # MessagesResource (20+ send methods)
|
|
601
|
+
templates/
|
|
602
|
+
models.py # Template CRUD input models
|
|
603
|
+
resource.py # TemplatesResource
|
|
604
|
+
media.py # Upload, download, get, delete
|
|
605
|
+
phone_numbers.py # Registration, verification, profile
|
|
606
|
+
flows.py # Flow management + deploy
|
|
607
|
+
webhooks/
|
|
608
|
+
verify.py # HMAC-SHA256 signature verification
|
|
609
|
+
normalize.py # Webhook payload normalization
|
|
610
|
+
events/
|
|
611
|
+
events.py # Dataclass events (18 types)
|
|
612
|
+
dispatcher.py # NormalizedWebhook -> pyventus events
|
|
613
|
+
utils/
|
|
614
|
+
case.py # snake_case <-> camelCase (cached)
|
|
615
|
+
```
|
|
616
|
+
|
|
617
|
+
## Acknowledgments
|
|
618
|
+
|
|
619
|
+
This project was inspired by [`@kapso/whatsapp-cloud-api`](https://github.com/gokapso/whatsapp-cloud-api-js), a TypeScript client for the same API. While the two projects cover similar ground, this Python SDK was written independently with its own architecture and design decisions.
|
|
620
|
+
|
|
621
|
+
## License
|
|
622
|
+
|
|
623
|
+
MIT
|
|
@@ -23,7 +23,7 @@ whatsapp_cloud_api/utils/case.py,sha256=Vu2LH15ZuCKqfXZdRn1fPk-ctmQgSaqC3_wz4kwD
|
|
|
23
23
|
whatsapp_cloud_api/webhooks/__init__.py,sha256=wKf4IE-exZXQ_xUwhkf4J54e7-EGOkvoASmp2Uv5sM0,131
|
|
24
24
|
whatsapp_cloud_api/webhooks/normalize.py,sha256=8tTAmGRvzZ15AmuWXBphOlmoomM86e3SfVpKJKqATmY,2633
|
|
25
25
|
whatsapp_cloud_api/webhooks/verify.py,sha256=DAA2vHtm2vB9AthDZrER6b3jZTU31CaDVUnnvRvOKg0,1033
|
|
26
|
-
whatsapp_cloud_api_py-0.1.
|
|
27
|
-
whatsapp_cloud_api_py-0.1.
|
|
28
|
-
whatsapp_cloud_api_py-0.1.
|
|
29
|
-
whatsapp_cloud_api_py-0.1.
|
|
26
|
+
whatsapp_cloud_api_py-0.1.1.dist-info/METADATA,sha256=O6IM8STdUUtF5NHSXzYpj36A5hczOrs6sCXNqd15DbA,18857
|
|
27
|
+
whatsapp_cloud_api_py-0.1.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
28
|
+
whatsapp_cloud_api_py-0.1.1.dist-info/licenses/LICENSE,sha256=x8Z_8RMIgoi1dz7Wl4v_0OdmLI-EHdc3kJtE4PbaFT4,1062
|
|
29
|
+
whatsapp_cloud_api_py-0.1.1.dist-info/RECORD,,
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: whatsapp-cloud-api-py
|
|
3
|
-
Version: 0.1.0
|
|
4
|
-
Summary: Async Python SDK for WhatsApp Business Cloud API with Pydantic V2
|
|
5
|
-
Project-URL: Homepage, https://github.com/HeiCg/whatsapp-cloud-api-py
|
|
6
|
-
Project-URL: Repository, https://github.com/HeiCg/whatsapp-cloud-api-py
|
|
7
|
-
Project-URL: Issues, https://github.com/HeiCg/whatsapp-cloud-api-py/issues
|
|
8
|
-
License-Expression: MIT
|
|
9
|
-
License-File: LICENSE
|
|
10
|
-
Requires-Python: >=3.11
|
|
11
|
-
Requires-Dist: httpx[http2]>=0.27
|
|
12
|
-
Requires-Dist: pydantic>=2.7
|
|
13
|
-
Provides-Extra: dev
|
|
14
|
-
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
15
|
-
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
16
|
-
Requires-Dist: pyventus>=0.7.2; extra == 'dev'
|
|
17
|
-
Requires-Dist: respx>=0.22; extra == 'dev'
|
|
18
|
-
Requires-Dist: ruff>=0.8; extra == 'dev'
|
|
19
|
-
Provides-Extra: events
|
|
20
|
-
Requires-Dist: pyventus>=0.7.2; extra == 'events'
|
|
21
|
-
Provides-Extra: server
|
|
22
|
-
Requires-Dist: cryptography>=43.0; extra == 'server'
|
|
23
|
-
Provides-Extra: webhooks
|
|
24
|
-
Requires-Dist: starlette>=0.37; extra == 'webhooks'
|
|
File without changes
|
{whatsapp_cloud_api_py-0.1.0.dist-info → whatsapp_cloud_api_py-0.1.1.dist-info}/licenses/LICENSE
RENAMED
|
File without changes
|