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.
- whatsapp_cloud_api_py-0.2.0/PKG-INFO +631 -0
- {whatsapp_cloud_api_py-0.1.0 → whatsapp_cloud_api_py-0.2.0}/README.md +18 -10
- {whatsapp_cloud_api_py-0.1.0 → whatsapp_cloud_api_py-0.2.0}/pyproject.toml +2 -1
- {whatsapp_cloud_api_py-0.1.0 → whatsapp_cloud_api_py-0.2.0}/src/whatsapp_cloud_api/client.py +3 -3
- {whatsapp_cloud_api_py-0.1.0 → whatsapp_cloud_api_py-0.2.0}/tests/test_client.py +5 -5
- {whatsapp_cloud_api_py-0.1.0 → whatsapp_cloud_api_py-0.2.0}/tests/test_messages_resource.py +1 -1
- {whatsapp_cloud_api_py-0.1.0 → whatsapp_cloud_api_py-0.2.0}/tests/test_resources_flows.py +1 -1
- {whatsapp_cloud_api_py-0.1.0 → whatsapp_cloud_api_py-0.2.0}/tests/test_resources_media.py +2 -2
- {whatsapp_cloud_api_py-0.1.0 → whatsapp_cloud_api_py-0.2.0}/tests/test_resources_phone_numbers.py +1 -1
- {whatsapp_cloud_api_py-0.1.0 → whatsapp_cloud_api_py-0.2.0}/tests/test_resources_templates.py +1 -1
- whatsapp_cloud_api_py-0.1.0/PKG-INFO +0 -24
- {whatsapp_cloud_api_py-0.1.0 → whatsapp_cloud_api_py-0.2.0}/.github/workflows/publish.yml +0 -0
- {whatsapp_cloud_api_py-0.1.0 → whatsapp_cloud_api_py-0.2.0}/.gitignore +0 -0
- {whatsapp_cloud_api_py-0.1.0 → whatsapp_cloud_api_py-0.2.0}/LICENSE +0 -0
- {whatsapp_cloud_api_py-0.1.0 → whatsapp_cloud_api_py-0.2.0}/src/whatsapp_cloud_api/__init__.py +0 -0
- {whatsapp_cloud_api_py-0.1.0 → whatsapp_cloud_api_py-0.2.0}/src/whatsapp_cloud_api/errors/__init__.py +0 -0
- {whatsapp_cloud_api_py-0.1.0 → whatsapp_cloud_api_py-0.2.0}/src/whatsapp_cloud_api/errors/categorize.py +0 -0
- {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
- {whatsapp_cloud_api_py-0.1.0 → whatsapp_cloud_api_py-0.2.0}/src/whatsapp_cloud_api/errors/retry.py +0 -0
- {whatsapp_cloud_api_py-0.1.0 → whatsapp_cloud_api_py-0.2.0}/src/whatsapp_cloud_api/events/__init__.py +0 -0
- {whatsapp_cloud_api_py-0.1.0 → whatsapp_cloud_api_py-0.2.0}/src/whatsapp_cloud_api/events/dispatcher.py +0 -0
- {whatsapp_cloud_api_py-0.1.0 → whatsapp_cloud_api_py-0.2.0}/src/whatsapp_cloud_api/events/events.py +0 -0
- {whatsapp_cloud_api_py-0.1.0 → whatsapp_cloud_api_py-0.2.0}/src/whatsapp_cloud_api/resources/__init__.py +0 -0
- {whatsapp_cloud_api_py-0.1.0 → whatsapp_cloud_api_py-0.2.0}/src/whatsapp_cloud_api/resources/flows.py +0 -0
- {whatsapp_cloud_api_py-0.1.0 → whatsapp_cloud_api_py-0.2.0}/src/whatsapp_cloud_api/resources/media.py +0 -0
- {whatsapp_cloud_api_py-0.1.0 → whatsapp_cloud_api_py-0.2.0}/src/whatsapp_cloud_api/resources/messages/__init__.py +0 -0
- {whatsapp_cloud_api_py-0.1.0 → whatsapp_cloud_api_py-0.2.0}/src/whatsapp_cloud_api/resources/messages/models.py +0 -0
- {whatsapp_cloud_api_py-0.1.0 → whatsapp_cloud_api_py-0.2.0}/src/whatsapp_cloud_api/resources/messages/resource.py +0 -0
- {whatsapp_cloud_api_py-0.1.0 → whatsapp_cloud_api_py-0.2.0}/src/whatsapp_cloud_api/resources/phone_numbers.py +0 -0
- {whatsapp_cloud_api_py-0.1.0 → whatsapp_cloud_api_py-0.2.0}/src/whatsapp_cloud_api/resources/templates/__init__.py +0 -0
- {whatsapp_cloud_api_py-0.1.0 → whatsapp_cloud_api_py-0.2.0}/src/whatsapp_cloud_api/resources/templates/models.py +0 -0
- {whatsapp_cloud_api_py-0.1.0 → whatsapp_cloud_api_py-0.2.0}/src/whatsapp_cloud_api/resources/templates/resource.py +0 -0
- {whatsapp_cloud_api_py-0.1.0 → whatsapp_cloud_api_py-0.2.0}/src/whatsapp_cloud_api/types.py +0 -0
- {whatsapp_cloud_api_py-0.1.0 → whatsapp_cloud_api_py-0.2.0}/src/whatsapp_cloud_api/utils/__init__.py +0 -0
- {whatsapp_cloud_api_py-0.1.0 → whatsapp_cloud_api_py-0.2.0}/src/whatsapp_cloud_api/utils/case.py +0 -0
- {whatsapp_cloud_api_py-0.1.0 → whatsapp_cloud_api_py-0.2.0}/src/whatsapp_cloud_api/webhooks/__init__.py +0 -0
- {whatsapp_cloud_api_py-0.1.0 → whatsapp_cloud_api_py-0.2.0}/src/whatsapp_cloud_api/webhooks/normalize.py +0 -0
- {whatsapp_cloud_api_py-0.1.0 → whatsapp_cloud_api_py-0.2.0}/src/whatsapp_cloud_api/webhooks/verify.py +0 -0
- {whatsapp_cloud_api_py-0.1.0 → whatsapp_cloud_api_py-0.2.0}/tests/__init__.py +0 -0
- {whatsapp_cloud_api_py-0.1.0 → whatsapp_cloud_api_py-0.2.0}/tests/conftest.py +0 -0
- {whatsapp_cloud_api_py-0.1.0 → whatsapp_cloud_api_py-0.2.0}/tests/test_errors_categorize.py +0 -0
- {whatsapp_cloud_api_py-0.1.0 → whatsapp_cloud_api_py-0.2.0}/tests/test_errors_graph_api_error.py +0 -0
- {whatsapp_cloud_api_py-0.1.0 → whatsapp_cloud_api_py-0.2.0}/tests/test_errors_retry.py +0 -0
- {whatsapp_cloud_api_py-0.1.0 → whatsapp_cloud_api_py-0.2.0}/tests/test_events.py +0 -0
- {whatsapp_cloud_api_py-0.1.0 → whatsapp_cloud_api_py-0.2.0}/tests/test_events_dispatcher.py +0 -0
- {whatsapp_cloud_api_py-0.1.0 → whatsapp_cloud_api_py-0.2.0}/tests/test_messages_models.py +0 -0
- {whatsapp_cloud_api_py-0.1.0 → whatsapp_cloud_api_py-0.2.0}/tests/test_types.py +0 -0
- {whatsapp_cloud_api_py-0.1.0 → whatsapp_cloud_api_py-0.2.0}/tests/test_utils_case.py +0 -0
- {whatsapp_cloud_api_py-0.1.0 → whatsapp_cloud_api_py-0.2.0}/tests/test_webhooks_normalize.py +0 -0
- {whatsapp_cloud_api_py-0.1.0 → whatsapp_cloud_api_py-0.2.0}/tests/test_webhooks_verify.py +0 -0
- {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="
|
|
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:
|
|
540
|
-
client = WhatsAppClient(access_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
|
|
552
|
+
# Custom timeout
|
|
543
553
|
client = WhatsAppClient(
|
|
544
|
-
access_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="
|
|
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="
|
|
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.
|
|
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",
|
{whatsapp_cloud_api_py-0.1.0 → whatsapp_cloud_api_py-0.2.0}/src/whatsapp_cloud_api/client.py
RENAMED
|
@@ -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://
|
|
19
|
-
_DEFAULT_VERSION = "
|
|
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 {"
|
|
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://
|
|
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/
|
|
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://
|
|
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["
|
|
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["
|
|
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://
|
|
25
|
+
BASE = "https://api.kapso.ai/meta/whatsapp/v24.0"
|
|
26
26
|
PHONE = "1234567890"
|
|
27
27
|
MSG_URL = f"{BASE}/{PHONE}/messages"
|
|
28
28
|
|
|
@@ -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://
|
|
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
|
|
158
|
+
assert req.headers.get("x-api-key") == "tok"
|
{whatsapp_cloud_api_py-0.1.0 → whatsapp_cloud_api_py-0.2.0}/tests/test_resources_templates.py
RENAMED
|
@@ -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://
|
|
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'
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{whatsapp_cloud_api_py-0.1.0 → whatsapp_cloud_api_py-0.2.0}/src/whatsapp_cloud_api/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{whatsapp_cloud_api_py-0.1.0 → whatsapp_cloud_api_py-0.2.0}/src/whatsapp_cloud_api/errors/retry.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{whatsapp_cloud_api_py-0.1.0 → whatsapp_cloud_api_py-0.2.0}/src/whatsapp_cloud_api/events/events.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{whatsapp_cloud_api_py-0.1.0 → whatsapp_cloud_api_py-0.2.0}/src/whatsapp_cloud_api/utils/__init__.py
RENAMED
|
File without changes
|
{whatsapp_cloud_api_py-0.1.0 → whatsapp_cloud_api_py-0.2.0}/src/whatsapp_cloud_api/utils/case.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{whatsapp_cloud_api_py-0.1.0 → whatsapp_cloud_api_py-0.2.0}/tests/test_errors_graph_api_error.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{whatsapp_cloud_api_py-0.1.0 → whatsapp_cloud_api_py-0.2.0}/tests/test_webhooks_normalize.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|