wazzapi 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- wazzapi-0.1.0/.gitignore +10 -0
- wazzapi-0.1.0/PKG-INFO +201 -0
- wazzapi-0.1.0/README.md +179 -0
- wazzapi-0.1.0/examples/create_template.py +18 -0
- wazzapi-0.1.0/examples/list_contacts.py +15 -0
- wazzapi-0.1.0/examples/preview_template.py +17 -0
- wazzapi-0.1.0/examples/send_message.py +18 -0
- wazzapi-0.1.0/examples/verify_webhook.py +48 -0
- wazzapi-0.1.0/pyproject.toml +53 -0
- wazzapi-0.1.0/tests/smoke_test.py +38 -0
- wazzapi-0.1.0/tests/test_client.py +90 -0
- wazzapi-0.1.0/tests/test_resources.py +159 -0
- wazzapi-0.1.0/tests/test_webhooks.py +157 -0
- wazzapi-0.1.0/wazzapi/__init__.py +36 -0
- wazzapi-0.1.0/wazzapi/client.py +120 -0
- wazzapi-0.1.0/wazzapi/errors.py +60 -0
- wazzapi-0.1.0/wazzapi/models/__init__.py +11 -0
- wazzapi-0.1.0/wazzapi/models/base.py +10 -0
- wazzapi-0.1.0/wazzapi/models/contacts.py +204 -0
- wazzapi-0.1.0/wazzapi/models/messages.py +173 -0
- wazzapi-0.1.0/wazzapi/models/templates.py +100 -0
- wazzapi-0.1.0/wazzapi/models/webhooks.py +87 -0
- wazzapi-0.1.0/wazzapi/py.typed +0 -0
- wazzapi-0.1.0/wazzapi/resources/__init__.py +5 -0
- wazzapi-0.1.0/wazzapi/resources/base.py +14 -0
- wazzapi-0.1.0/wazzapi/resources/contacts.py +222 -0
- wazzapi-0.1.0/wazzapi/resources/messages.py +174 -0
- wazzapi-0.1.0/wazzapi/resources/templates.py +87 -0
- wazzapi-0.1.0/wazzapi/webhooks.py +131 -0
wazzapi-0.1.0/.gitignore
ADDED
wazzapi-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wazzapi
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Typed Python SDK for WazzAPI
|
|
5
|
+
Project-URL: API, https://api.wazzapi.com
|
|
6
|
+
Keywords: api,sdk,wazzapi,webhook,whatsapp
|
|
7
|
+
Classifier: Development Status :: 4 - Beta
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: Operating System :: OS Independent
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Requires-Python: >=3.10
|
|
19
|
+
Requires-Dist: httpx<1.0,>=0.28
|
|
20
|
+
Requires-Dist: pydantic<3.0,>=2.11
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# WazzAPI Python SDK
|
|
24
|
+
|
|
25
|
+
Official Python SDK for WazzAPI.
|
|
26
|
+
|
|
27
|
+
Use it to send messages, manage contacts and templates, and verify incoming webhooks with a simple typed client.
|
|
28
|
+
|
|
29
|
+
## What you can do with this SDK
|
|
30
|
+
|
|
31
|
+
- send direct WhatsApp messages
|
|
32
|
+
- list, create, update, and delete contacts
|
|
33
|
+
- manage message templates and preview rendered content
|
|
34
|
+
- verify and parse incoming WazzAPI webhooks
|
|
35
|
+
- work with typed request and response models
|
|
36
|
+
|
|
37
|
+
## Requirements
|
|
38
|
+
|
|
39
|
+
- Python 3.10+
|
|
40
|
+
- a WazzAPI account
|
|
41
|
+
- a WazzAPI API key
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
From PyPI:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install wazzapi-py
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
With uv:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
uv add wazzapi-py
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Need a WazzAPI account first?
|
|
58
|
+
|
|
59
|
+
Sign up at `https://app.wazzapi.com` using your Google organization account. On first sign-in, WazzAPI creates a new workspace for you automatically.
|
|
60
|
+
|
|
61
|
+
## Configuration
|
|
62
|
+
|
|
63
|
+
For most integrations, you only need your API key:
|
|
64
|
+
|
|
65
|
+
- `WAZZAPI_API_KEY`
|
|
66
|
+
|
|
67
|
+
If you plan to receive webhooks, also configure:
|
|
68
|
+
|
|
69
|
+
- `WAZZAPI_WEBHOOK_SECRET`
|
|
70
|
+
|
|
71
|
+
The SDK uses `https://api.wazzapi.com` by default, so you do not need to set a base URL.
|
|
72
|
+
|
|
73
|
+
## Quick start
|
|
74
|
+
|
|
75
|
+
### Send a message
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from wazzapi import SendMessageRequest, WazzapiClient
|
|
79
|
+
|
|
80
|
+
with WazzapiClient(api_key="your-api-key") as client:
|
|
81
|
+
response = client.messages.send(
|
|
82
|
+
SendMessageRequest(
|
|
83
|
+
phone_number="+6281234567890",
|
|
84
|
+
whatsapp_account_id="your-whatsapp-account-id",
|
|
85
|
+
content="Hello from WazzAPI!",
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
print(response.model_dump())
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Error handling
|
|
93
|
+
|
|
94
|
+
When the API returns a non-success response, the SDK raises `WazzapiAPIError`.
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
from wazzapi import WazzapiAPIError, WazzapiClient
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
with WazzapiClient(api_key="your-api-key") as client:
|
|
101
|
+
client.messages.get("missing-message-id")
|
|
102
|
+
except WazzapiAPIError as exc:
|
|
103
|
+
print(exc.status_code)
|
|
104
|
+
print(exc.message)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## More usage examples
|
|
108
|
+
|
|
109
|
+
### List contacts
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from wazzapi import WazzapiClient
|
|
113
|
+
|
|
114
|
+
with WazzapiClient(api_key="your-api-key") as client:
|
|
115
|
+
response = client.contacts.list(limit=20, search="alice")
|
|
116
|
+
|
|
117
|
+
for contact in response.contacts:
|
|
118
|
+
print(contact.model_dump())
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Create a template
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
from wazzapi import TemplateCreateRequest, WazzapiClient
|
|
125
|
+
|
|
126
|
+
with WazzapiClient(api_key="your-api-key") as client:
|
|
127
|
+
template = client.templates.create(
|
|
128
|
+
TemplateCreateRequest(
|
|
129
|
+
name="welcome-message",
|
|
130
|
+
category="marketing",
|
|
131
|
+
content="Hi {{name}}, welcome to WazzAPI!",
|
|
132
|
+
)
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
print(template.model_dump())
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Preview a template
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
from wazzapi import TemplatePreviewRequest, WazzapiClient
|
|
142
|
+
|
|
143
|
+
with WazzapiClient(api_key="your-api-key") as client:
|
|
144
|
+
preview = client.templates.preview(
|
|
145
|
+
TemplatePreviewRequest(
|
|
146
|
+
content="Hi {{name}}, your code is {{code}}.",
|
|
147
|
+
custom_variables={"name": "Alice", "code": "WZ-1234"},
|
|
148
|
+
)
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
print(preview.model_dump())
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Webhook verification
|
|
155
|
+
|
|
156
|
+
Use `WebhookHandler` to verify the raw request body against `X-Wazzapi-Signature` before parsing JSON.
|
|
157
|
+
|
|
158
|
+
```python
|
|
159
|
+
from wazzapi import WebhookHandler
|
|
160
|
+
|
|
161
|
+
handler = WebhookHandler("your-webhook-secret")
|
|
162
|
+
webhook = handler.verify_and_parse(raw_body, request.headers)
|
|
163
|
+
|
|
164
|
+
print(webhook.event_type)
|
|
165
|
+
print(webhook.data.model_dump())
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
WazzAPI webhook headers:
|
|
169
|
+
|
|
170
|
+
- `X-Wazzapi-Signature`
|
|
171
|
+
- `X-Wazzapi-Event`
|
|
172
|
+
- `X-Wazzapi-Event-ID`
|
|
173
|
+
|
|
174
|
+
Supported webhook event families:
|
|
175
|
+
|
|
176
|
+
- message events: `message.received`, `message.sent`, `message.delivered`, `message.read`, `message.failed`
|
|
177
|
+
- device events: `device.connected`, `device.disconnected`
|
|
178
|
+
|
|
179
|
+
## Examples
|
|
180
|
+
|
|
181
|
+
Ready-to-run examples live in `examples/`:
|
|
182
|
+
|
|
183
|
+
- `examples/list_contacts.py`
|
|
184
|
+
- `examples/send_message.py`
|
|
185
|
+
- `examples/create_template.py`
|
|
186
|
+
- `examples/preview_template.py`
|
|
187
|
+
- `examples/verify_webhook.py`
|
|
188
|
+
|
|
189
|
+
Run them with:
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
uv run python examples/list_contacts.py
|
|
193
|
+
uv run python examples/send_message.py
|
|
194
|
+
uv run python examples/create_template.py
|
|
195
|
+
uv run python examples/preview_template.py
|
|
196
|
+
uv run python examples/verify_webhook.py
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Release automation
|
|
200
|
+
|
|
201
|
+
This repository includes a GitHub Actions workflow that publishes to PyPI automatically when a tag matching `v*` is pushed.
|
wazzapi-0.1.0/README.md
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# WazzAPI Python SDK
|
|
2
|
+
|
|
3
|
+
Official Python SDK for WazzAPI.
|
|
4
|
+
|
|
5
|
+
Use it to send messages, manage contacts and templates, and verify incoming webhooks with a simple typed client.
|
|
6
|
+
|
|
7
|
+
## What you can do with this SDK
|
|
8
|
+
|
|
9
|
+
- send direct WhatsApp messages
|
|
10
|
+
- list, create, update, and delete contacts
|
|
11
|
+
- manage message templates and preview rendered content
|
|
12
|
+
- verify and parse incoming WazzAPI webhooks
|
|
13
|
+
- work with typed request and response models
|
|
14
|
+
|
|
15
|
+
## Requirements
|
|
16
|
+
|
|
17
|
+
- Python 3.10+
|
|
18
|
+
- a WazzAPI account
|
|
19
|
+
- a WazzAPI API key
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
From PyPI:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install wazzapi-py
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
With uv:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
uv add wazzapi-py
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Need a WazzAPI account first?
|
|
36
|
+
|
|
37
|
+
Sign up at `https://app.wazzapi.com` using your Google organization account. On first sign-in, WazzAPI creates a new workspace for you automatically.
|
|
38
|
+
|
|
39
|
+
## Configuration
|
|
40
|
+
|
|
41
|
+
For most integrations, you only need your API key:
|
|
42
|
+
|
|
43
|
+
- `WAZZAPI_API_KEY`
|
|
44
|
+
|
|
45
|
+
If you plan to receive webhooks, also configure:
|
|
46
|
+
|
|
47
|
+
- `WAZZAPI_WEBHOOK_SECRET`
|
|
48
|
+
|
|
49
|
+
The SDK uses `https://api.wazzapi.com` by default, so you do not need to set a base URL.
|
|
50
|
+
|
|
51
|
+
## Quick start
|
|
52
|
+
|
|
53
|
+
### Send a message
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from wazzapi import SendMessageRequest, WazzapiClient
|
|
57
|
+
|
|
58
|
+
with WazzapiClient(api_key="your-api-key") as client:
|
|
59
|
+
response = client.messages.send(
|
|
60
|
+
SendMessageRequest(
|
|
61
|
+
phone_number="+6281234567890",
|
|
62
|
+
whatsapp_account_id="your-whatsapp-account-id",
|
|
63
|
+
content="Hello from WazzAPI!",
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
print(response.model_dump())
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Error handling
|
|
71
|
+
|
|
72
|
+
When the API returns a non-success response, the SDK raises `WazzapiAPIError`.
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from wazzapi import WazzapiAPIError, WazzapiClient
|
|
76
|
+
|
|
77
|
+
try:
|
|
78
|
+
with WazzapiClient(api_key="your-api-key") as client:
|
|
79
|
+
client.messages.get("missing-message-id")
|
|
80
|
+
except WazzapiAPIError as exc:
|
|
81
|
+
print(exc.status_code)
|
|
82
|
+
print(exc.message)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## More usage examples
|
|
86
|
+
|
|
87
|
+
### List contacts
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from wazzapi import WazzapiClient
|
|
91
|
+
|
|
92
|
+
with WazzapiClient(api_key="your-api-key") as client:
|
|
93
|
+
response = client.contacts.list(limit=20, search="alice")
|
|
94
|
+
|
|
95
|
+
for contact in response.contacts:
|
|
96
|
+
print(contact.model_dump())
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Create a template
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from wazzapi import TemplateCreateRequest, WazzapiClient
|
|
103
|
+
|
|
104
|
+
with WazzapiClient(api_key="your-api-key") as client:
|
|
105
|
+
template = client.templates.create(
|
|
106
|
+
TemplateCreateRequest(
|
|
107
|
+
name="welcome-message",
|
|
108
|
+
category="marketing",
|
|
109
|
+
content="Hi {{name}}, welcome to WazzAPI!",
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
print(template.model_dump())
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Preview a template
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from wazzapi import TemplatePreviewRequest, WazzapiClient
|
|
120
|
+
|
|
121
|
+
with WazzapiClient(api_key="your-api-key") as client:
|
|
122
|
+
preview = client.templates.preview(
|
|
123
|
+
TemplatePreviewRequest(
|
|
124
|
+
content="Hi {{name}}, your code is {{code}}.",
|
|
125
|
+
custom_variables={"name": "Alice", "code": "WZ-1234"},
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
print(preview.model_dump())
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Webhook verification
|
|
133
|
+
|
|
134
|
+
Use `WebhookHandler` to verify the raw request body against `X-Wazzapi-Signature` before parsing JSON.
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
from wazzapi import WebhookHandler
|
|
138
|
+
|
|
139
|
+
handler = WebhookHandler("your-webhook-secret")
|
|
140
|
+
webhook = handler.verify_and_parse(raw_body, request.headers)
|
|
141
|
+
|
|
142
|
+
print(webhook.event_type)
|
|
143
|
+
print(webhook.data.model_dump())
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
WazzAPI webhook headers:
|
|
147
|
+
|
|
148
|
+
- `X-Wazzapi-Signature`
|
|
149
|
+
- `X-Wazzapi-Event`
|
|
150
|
+
- `X-Wazzapi-Event-ID`
|
|
151
|
+
|
|
152
|
+
Supported webhook event families:
|
|
153
|
+
|
|
154
|
+
- message events: `message.received`, `message.sent`, `message.delivered`, `message.read`, `message.failed`
|
|
155
|
+
- device events: `device.connected`, `device.disconnected`
|
|
156
|
+
|
|
157
|
+
## Examples
|
|
158
|
+
|
|
159
|
+
Ready-to-run examples live in `examples/`:
|
|
160
|
+
|
|
161
|
+
- `examples/list_contacts.py`
|
|
162
|
+
- `examples/send_message.py`
|
|
163
|
+
- `examples/create_template.py`
|
|
164
|
+
- `examples/preview_template.py`
|
|
165
|
+
- `examples/verify_webhook.py`
|
|
166
|
+
|
|
167
|
+
Run them with:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
uv run python examples/list_contacts.py
|
|
171
|
+
uv run python examples/send_message.py
|
|
172
|
+
uv run python examples/create_template.py
|
|
173
|
+
uv run python examples/preview_template.py
|
|
174
|
+
uv run python examples/verify_webhook.py
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Release automation
|
|
178
|
+
|
|
179
|
+
This repository includes a GitHub Actions workflow that publishes to PyPI automatically when a tag matching `v*` is pushed.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from wazzapi import TemplateCreateRequest, WazzapiClient
|
|
6
|
+
|
|
7
|
+
api_key = os.environ["WAZZAPI_API_KEY"]
|
|
8
|
+
|
|
9
|
+
with WazzapiClient(api_key=api_key) as client:
|
|
10
|
+
response = client.templates.create(
|
|
11
|
+
TemplateCreateRequest(
|
|
12
|
+
name="welcome-message",
|
|
13
|
+
category="marketing",
|
|
14
|
+
content="Hi {{name}}, welcome to WazzAPI!",
|
|
15
|
+
)
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
print(response.model_dump())
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from wazzapi import WazzapiClient
|
|
6
|
+
|
|
7
|
+
api_key = os.environ["WAZZAPI_API_KEY"]
|
|
8
|
+
|
|
9
|
+
with WazzapiClient(api_key=api_key) as client:
|
|
10
|
+
response = client.contacts.list(limit=20, search="alice")
|
|
11
|
+
|
|
12
|
+
for contact in response.contacts:
|
|
13
|
+
print(contact.model_dump())
|
|
14
|
+
|
|
15
|
+
print({"total": response.total, "limit": response.limit, "offset": response.offset})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from wazzapi import TemplatePreviewRequest, WazzapiClient
|
|
6
|
+
|
|
7
|
+
api_key = os.environ["WAZZAPI_API_KEY"]
|
|
8
|
+
|
|
9
|
+
with WazzapiClient(api_key=api_key) as client:
|
|
10
|
+
preview = client.templates.preview(
|
|
11
|
+
TemplatePreviewRequest(
|
|
12
|
+
content="Hi {{name}}, your code is {{code}}.",
|
|
13
|
+
custom_variables={"name": "Alice", "code": "WZ-1234"},
|
|
14
|
+
)
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
print(preview.model_dump())
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from wazzapi import SendMessageRequest, WazzapiClient
|
|
6
|
+
|
|
7
|
+
api_key = os.environ["WAZZAPI_API_KEY"]
|
|
8
|
+
|
|
9
|
+
with WazzapiClient(api_key=api_key) as client:
|
|
10
|
+
response = client.messages.send(
|
|
11
|
+
SendMessageRequest(
|
|
12
|
+
phone_number="+6281234567890",
|
|
13
|
+
whatsapp_account_id="your-whatsapp-account-id",
|
|
14
|
+
content="Hello from the WazzAPI Python SDK",
|
|
15
|
+
)
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
print(response.model_dump())
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
from wazzapi import WebhookHandler
|
|
7
|
+
|
|
8
|
+
secret = os.environ["WAZZAPI_WEBHOOK_SECRET"]
|
|
9
|
+
handler = WebhookHandler(secret)
|
|
10
|
+
|
|
11
|
+
raw_body = json.dumps(
|
|
12
|
+
{
|
|
13
|
+
"id": "97e1d724-f38d-46f6-bc4c-d9e0f4cf9285",
|
|
14
|
+
"event_type": "message.received",
|
|
15
|
+
"timestamp": "2026-04-26T09:15:30Z",
|
|
16
|
+
"organization_id": "0f1d6e38-d6bc-49be-9c39-1fcf2f946d7e",
|
|
17
|
+
"webhook_id": "a17d6351-d8f6-4cd8-ae0e-fce090afdb8f",
|
|
18
|
+
"data": {
|
|
19
|
+
"message_id": "f4fd147d-e2a8-4d52-9eb5-98a72f5b90ab",
|
|
20
|
+
"whatsapp_message_id": "wamid.123",
|
|
21
|
+
"phone_number": "6281234567890",
|
|
22
|
+
"account_name": "Support Device",
|
|
23
|
+
"status": "delivered",
|
|
24
|
+
"direction": "inbound",
|
|
25
|
+
"message_type": "text",
|
|
26
|
+
"failure_reason": None,
|
|
27
|
+
"reason": None,
|
|
28
|
+
"sent_at": None,
|
|
29
|
+
"delivered_at": "2026-04-26T09:15:29Z",
|
|
30
|
+
"read_at": None,
|
|
31
|
+
"failed_at": None,
|
|
32
|
+
"whatsapp_account_id": "c053d8ef-6c19-4ecb-9cc5-a4a64be79d92",
|
|
33
|
+
"campaign_id": None,
|
|
34
|
+
"batch_id": None,
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
separators=(",", ":"),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
headers = {
|
|
41
|
+
"X-Wazzapi-Signature": handler.generate_signature(raw_body),
|
|
42
|
+
"X-Wazzapi-Event": "message.received",
|
|
43
|
+
"X-Wazzapi-Event-ID": "2e0b76fe-f5b4-4e50-80cb-ec7cce2c8fd5",
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
webhook = handler.verify_and_parse(raw_body, headers)
|
|
47
|
+
print(webhook.event_type)
|
|
48
|
+
print(webhook.data.model_dump())
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "wazzapi"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Typed Python SDK for WazzAPI"
|
|
5
|
+
readme = { file = "README.md", content-type = "text/markdown" }
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
keywords = ["api", "sdk", "wazzapi", "webhook", "whatsapp"]
|
|
8
|
+
classifiers = [
|
|
9
|
+
"Development Status :: 4 - Beta",
|
|
10
|
+
"Intended Audience :: Developers",
|
|
11
|
+
"Operating System :: OS Independent",
|
|
12
|
+
"Programming Language :: Python :: 3",
|
|
13
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
14
|
+
"Programming Language :: Python :: 3.10",
|
|
15
|
+
"Programming Language :: Python :: 3.11",
|
|
16
|
+
"Programming Language :: Python :: 3.12",
|
|
17
|
+
"Programming Language :: Python :: 3.13",
|
|
18
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
19
|
+
"Typing :: Typed",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"httpx>=0.28,<1.0",
|
|
23
|
+
"pydantic>=2.11,<3.0",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
API = "https://api.wazzapi.com"
|
|
28
|
+
|
|
29
|
+
[dependency-groups]
|
|
30
|
+
dev = [
|
|
31
|
+
"pytest>=8.3,<9.0",
|
|
32
|
+
"twine>=6.1,<7.0",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
[build-system]
|
|
36
|
+
requires = ["hatchling>=1.27.0"]
|
|
37
|
+
build-backend = "hatchling.build"
|
|
38
|
+
|
|
39
|
+
[tool.hatch.build.targets.wheel]
|
|
40
|
+
packages = ["wazzapi"]
|
|
41
|
+
|
|
42
|
+
[tool.hatch.build.targets.sdist]
|
|
43
|
+
include = [
|
|
44
|
+
"/README.md",
|
|
45
|
+
"/examples",
|
|
46
|
+
"/pyproject.toml",
|
|
47
|
+
"/tests",
|
|
48
|
+
"/wazzapi",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
[tool.pytest]
|
|
52
|
+
python_files = ["test_*.py"]
|
|
53
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from wazzapi import (
|
|
4
|
+
SendMessageRequest,
|
|
5
|
+
WazzapiClient,
|
|
6
|
+
WebhookHandler,
|
|
7
|
+
__version__,
|
|
8
|
+
generate_webhook_signature,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def main() -> None:
|
|
13
|
+
assert __version__ == "0.1.0"
|
|
14
|
+
|
|
15
|
+
client = WazzapiClient(api_key="smoke-test-token")
|
|
16
|
+
try:
|
|
17
|
+
assert str(client.http.base_url) == "https://api.wazzapi.com"
|
|
18
|
+
assert client.http.headers["Authorization"] == "Bearer smoke-test-token"
|
|
19
|
+
finally:
|
|
20
|
+
client.close()
|
|
21
|
+
|
|
22
|
+
request = SendMessageRequest(
|
|
23
|
+
phone_number="+6281234567890",
|
|
24
|
+
whatsapp_account_id="wa_123",
|
|
25
|
+
content="smoke test",
|
|
26
|
+
)
|
|
27
|
+
assert request.phone_number == "+6281234567890"
|
|
28
|
+
assert request.content == "smoke test"
|
|
29
|
+
|
|
30
|
+
handler = WebhookHandler("secret")
|
|
31
|
+
signature = generate_webhook_signature(b"{}", "secret")
|
|
32
|
+
assert handler.verify_signature(b"{}", signature)
|
|
33
|
+
|
|
34
|
+
print("smoke-ok")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
if __name__ == "__main__":
|
|
38
|
+
main()
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from wazzapi import WazzapiAPIError, WazzapiClient
|
|
9
|
+
from wazzapi.models import ContactCreateRequest
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_client_formats_bearer_auth_header() -> None:
|
|
13
|
+
client = WazzapiClient(base_url="https://api.example.com", api_key="plain-token")
|
|
14
|
+
try:
|
|
15
|
+
assert client.http.headers["Authorization"] == "Bearer plain-token"
|
|
16
|
+
assert client.http.headers["Accept"] == "application/json"
|
|
17
|
+
assert client.http.headers["Content-Type"] == "application/json"
|
|
18
|
+
finally:
|
|
19
|
+
client.close()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_client_uses_default_wazzapi_base_url() -> None:
|
|
23
|
+
client = WazzapiClient(api_key="plain-token")
|
|
24
|
+
try:
|
|
25
|
+
assert str(client.http.base_url) == "https://api.wazzapi.com"
|
|
26
|
+
finally:
|
|
27
|
+
client.close()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_client_preserves_existing_bearer_prefix() -> None:
|
|
31
|
+
client = WazzapiClient(base_url="https://api.example.com", api_key="Bearer already-set")
|
|
32
|
+
try:
|
|
33
|
+
assert client.http.headers["Authorization"] == "Bearer already-set"
|
|
34
|
+
finally:
|
|
35
|
+
client.close()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_client_serializes_models_and_filters_none_query_params() -> None:
|
|
39
|
+
seen: dict[str, object] = {}
|
|
40
|
+
|
|
41
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
42
|
+
seen["query"] = dict(request.url.params)
|
|
43
|
+
seen["body"] = json.loads(request.content.decode("utf-8"))
|
|
44
|
+
return httpx.Response(
|
|
45
|
+
201,
|
|
46
|
+
json={
|
|
47
|
+
"id": "contact_1",
|
|
48
|
+
"phone_number": "+6281234567890",
|
|
49
|
+
"source": "manual",
|
|
50
|
+
"is_opted_out": False,
|
|
51
|
+
"tags": [],
|
|
52
|
+
"created_at": "2026-05-01T00:00:00Z",
|
|
53
|
+
"updated_at": "2026-05-01T00:00:00Z",
|
|
54
|
+
},
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
transport = httpx.MockTransport(handler)
|
|
58
|
+
with WazzapiClient(base_url="https://api.example.com", transport=transport) as client:
|
|
59
|
+
response = client._request(
|
|
60
|
+
"POST",
|
|
61
|
+
"/api/v1/contacts",
|
|
62
|
+
params={"limit": 10, "search": None},
|
|
63
|
+
json_body=ContactCreateRequest(phone_number="+6281234567890", name=None),
|
|
64
|
+
response_model=None,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
assert seen["query"] == {"limit": "10"}
|
|
68
|
+
assert seen["body"] == {"phone_number": "+6281234567890"}
|
|
69
|
+
assert response["id"] == "contact_1"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@pytest.mark.parametrize(
|
|
73
|
+
("payload", "expected_message"),
|
|
74
|
+
[
|
|
75
|
+
({"detail": "Message not found"}, "Message not found"),
|
|
76
|
+
({"detail": [{"msg": "Invalid input"}]}, "Invalid input"),
|
|
77
|
+
({"message": "Rate limited"}, "Rate limited"),
|
|
78
|
+
],
|
|
79
|
+
)
|
|
80
|
+
def test_client_raises_api_error_with_parsed_message(payload: dict[str, object], expected_message: str) -> None:
|
|
81
|
+
def handler(_: httpx.Request) -> httpx.Response:
|
|
82
|
+
return httpx.Response(400, json=payload)
|
|
83
|
+
|
|
84
|
+
transport = httpx.MockTransport(handler)
|
|
85
|
+
with WazzapiClient(base_url="https://api.example.com", transport=transport) as client:
|
|
86
|
+
with pytest.raises(WazzapiAPIError) as exc_info:
|
|
87
|
+
client._request("GET", "/api/v1/test")
|
|
88
|
+
|
|
89
|
+
assert exc_info.value.message == expected_message
|
|
90
|
+
assert exc_info.value.status_code == 400
|