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