achek-sdk 1.0.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.
- achek_sdk-1.0.0/.gitignore +5 -0
- achek_sdk-1.0.0/PKG-INFO +91 -0
- achek_sdk-1.0.0/README.md +65 -0
- achek_sdk-1.0.0/achekverify/__init__.py +43 -0
- achek_sdk-1.0.0/achekverify/client.py +195 -0
- achek_sdk-1.0.0/achekverify/errors.py +55 -0
- achek_sdk-1.0.0/achekverify/webhooks.py +56 -0
- achek_sdk-1.0.0/pyproject.toml +36 -0
achek_sdk-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: achek-sdk
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Official Python SDK for Achek — WhatsApp OTP, AI chatbots & business messaging for Nigeria
|
|
5
|
+
Project-URL: Homepage, https://achek.com.ng
|
|
6
|
+
Project-URL: Documentation, https://docs.achek.com.ng
|
|
7
|
+
Project-URL: Repository, https://github.com/CalebDevX/AchekVerify
|
|
8
|
+
Project-URL: Issues, https://github.com/CalebDevX/AchekVerify/issues
|
|
9
|
+
Author-email: Achek <dev@achek.com.ng>
|
|
10
|
+
License: MIT
|
|
11
|
+
Keywords: 2fa,achek,chatbot,nigeria,otp,verification,whatsapp
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Communications
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
23
|
+
Requires-Python: >=3.8
|
|
24
|
+
Requires-Dist: requests>=2.28.0
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# achek-sdk
|
|
28
|
+
|
|
29
|
+
Official Python SDK for [Achek](https://achek.com.ng) — WhatsApp OTP, AI chatbots & business messaging for Nigerian developers.
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install achek-sdk
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Quick Start
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from achekverify import AchekConnect
|
|
41
|
+
|
|
42
|
+
client = AchekConnect("your_api_key")
|
|
43
|
+
|
|
44
|
+
# Send a WhatsApp OTP
|
|
45
|
+
result = client.send_otp("+2348XXXXXXXXX")
|
|
46
|
+
print(result["sessionId"])
|
|
47
|
+
|
|
48
|
+
# Verify the OTP the user enters
|
|
49
|
+
check = client.verify_otp("+2348XXXXXXXXX", otp="482910")
|
|
50
|
+
print(check["valid"]) # True / False
|
|
51
|
+
|
|
52
|
+
# Send a plain WhatsApp message
|
|
53
|
+
client.send_message("+2348XXXXXXXXX", "Hello from Achek!")
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Authentication
|
|
57
|
+
|
|
58
|
+
Get your API key from the [Achek dashboard](https://console.achek.com.ng).
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
client = AchekConnect("ak_live_your_key_here")
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Features
|
|
65
|
+
|
|
66
|
+
- **WhatsApp OTP** — Send and verify one-time passwords via WhatsApp
|
|
67
|
+
- **Messaging** — Send plain and templated WhatsApp messages
|
|
68
|
+
- **AI Bot** — Create and manage AI chatbot agents
|
|
69
|
+
- **Webhooks** — Verify and parse incoming webhook events
|
|
70
|
+
- **Broadcasts** — Send bulk messages to multiple numbers
|
|
71
|
+
|
|
72
|
+
## Webhook Verification
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from achekverify import Webhooks
|
|
76
|
+
|
|
77
|
+
wh = Webhooks("your_webhook_secret")
|
|
78
|
+
payload = wh.verify(raw_body, signature_header)
|
|
79
|
+
print(payload["event"]) # e.g. "otp.verified"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Links
|
|
83
|
+
|
|
84
|
+
- [Documentation](https://docs.achek.com.ng)
|
|
85
|
+
- [Dashboard](https://console.achek.com.ng)
|
|
86
|
+
- [GitHub](https://github.com/CalebDevX/AchekVerify)
|
|
87
|
+
- [Support](https://achek.com.ng/support)
|
|
88
|
+
|
|
89
|
+
## License
|
|
90
|
+
|
|
91
|
+
MIT
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# achek-sdk
|
|
2
|
+
|
|
3
|
+
Official Python SDK for [Achek](https://achek.com.ng) — WhatsApp OTP, AI chatbots & business messaging for Nigerian developers.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install achek-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from achekverify import AchekConnect
|
|
15
|
+
|
|
16
|
+
client = AchekConnect("your_api_key")
|
|
17
|
+
|
|
18
|
+
# Send a WhatsApp OTP
|
|
19
|
+
result = client.send_otp("+2348XXXXXXXXX")
|
|
20
|
+
print(result["sessionId"])
|
|
21
|
+
|
|
22
|
+
# Verify the OTP the user enters
|
|
23
|
+
check = client.verify_otp("+2348XXXXXXXXX", otp="482910")
|
|
24
|
+
print(check["valid"]) # True / False
|
|
25
|
+
|
|
26
|
+
# Send a plain WhatsApp message
|
|
27
|
+
client.send_message("+2348XXXXXXXXX", "Hello from Achek!")
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Authentication
|
|
31
|
+
|
|
32
|
+
Get your API key from the [Achek dashboard](https://console.achek.com.ng).
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
client = AchekConnect("ak_live_your_key_here")
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Features
|
|
39
|
+
|
|
40
|
+
- **WhatsApp OTP** — Send and verify one-time passwords via WhatsApp
|
|
41
|
+
- **Messaging** — Send plain and templated WhatsApp messages
|
|
42
|
+
- **AI Bot** — Create and manage AI chatbot agents
|
|
43
|
+
- **Webhooks** — Verify and parse incoming webhook events
|
|
44
|
+
- **Broadcasts** — Send bulk messages to multiple numbers
|
|
45
|
+
|
|
46
|
+
## Webhook Verification
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from achekverify import Webhooks
|
|
50
|
+
|
|
51
|
+
wh = Webhooks("your_webhook_secret")
|
|
52
|
+
payload = wh.verify(raw_body, signature_header)
|
|
53
|
+
print(payload["event"]) # e.g. "otp.verified"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Links
|
|
57
|
+
|
|
58
|
+
- [Documentation](https://docs.achek.com.ng)
|
|
59
|
+
- [Dashboard](https://console.achek.com.ng)
|
|
60
|
+
- [GitHub](https://github.com/CalebDevX/AchekVerify)
|
|
61
|
+
- [Support](https://achek.com.ng/support)
|
|
62
|
+
|
|
63
|
+
## License
|
|
64
|
+
|
|
65
|
+
MIT
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AchekConnect Python SDK
|
|
3
|
+
======================
|
|
4
|
+
Official client for the AchekConnect API — WhatsApp OTP, messaging, and AI bots.
|
|
5
|
+
|
|
6
|
+
Quick start::
|
|
7
|
+
|
|
8
|
+
from achekconnect import AchekConnect
|
|
9
|
+
|
|
10
|
+
client = AchekConnect("your-api-key")
|
|
11
|
+
|
|
12
|
+
# Send OTP
|
|
13
|
+
result = client.send_otp("+2348012345678")
|
|
14
|
+
print(result["sessionId"])
|
|
15
|
+
|
|
16
|
+
# Verify OTP
|
|
17
|
+
check = client.verify_otp("+2348012345678", otp="482910")
|
|
18
|
+
print(check["valid"]) # True / False
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from .client import AchekConnect
|
|
22
|
+
from .errors import (
|
|
23
|
+
AchekError,
|
|
24
|
+
AuthenticationError,
|
|
25
|
+
NetworkError,
|
|
26
|
+
NotFoundError,
|
|
27
|
+
RateLimitError,
|
|
28
|
+
ValidationError,
|
|
29
|
+
)
|
|
30
|
+
from .webhooks import Webhooks
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"AchekConnect",
|
|
34
|
+
"AchekError",
|
|
35
|
+
"AuthenticationError",
|
|
36
|
+
"RateLimitError",
|
|
37
|
+
"ValidationError",
|
|
38
|
+
"NotFoundError",
|
|
39
|
+
"NetworkError",
|
|
40
|
+
"Webhooks",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
__version__ = "1.0.0"
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import time
|
|
2
|
+
from typing import Any, Dict, Optional
|
|
3
|
+
|
|
4
|
+
import requests
|
|
5
|
+
|
|
6
|
+
from .errors import (
|
|
7
|
+
AchekError,
|
|
8
|
+
AuthenticationError,
|
|
9
|
+
NetworkError,
|
|
10
|
+
NotFoundError,
|
|
11
|
+
RateLimitError,
|
|
12
|
+
ValidationError,
|
|
13
|
+
)
|
|
14
|
+
from .webhooks import Webhooks
|
|
15
|
+
|
|
16
|
+
DEFAULT_BASE_URL = "https://api.achek.com.ng/v1"
|
|
17
|
+
DEFAULT_TIMEOUT = 30
|
|
18
|
+
DEFAULT_RETRIES = 3
|
|
19
|
+
SDK_VERSION = "achekconnect-python/1.0.0"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AchekConnect:
|
|
23
|
+
"""
|
|
24
|
+
Official AchekConnect Python client.
|
|
25
|
+
|
|
26
|
+
Usage::
|
|
27
|
+
|
|
28
|
+
from achekconnect import AchekConnect
|
|
29
|
+
|
|
30
|
+
client = AchekConnect("your-api-key")
|
|
31
|
+
result = client.send_otp("+2348012345678")
|
|
32
|
+
print(result["sessionId"])
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
api_key: str,
|
|
38
|
+
*,
|
|
39
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
40
|
+
timeout: int = DEFAULT_TIMEOUT,
|
|
41
|
+
retries: int = DEFAULT_RETRIES,
|
|
42
|
+
webhook_secret: str = "",
|
|
43
|
+
):
|
|
44
|
+
if not api_key:
|
|
45
|
+
raise AuthenticationError("api_key is required")
|
|
46
|
+
self._base_url = base_url.rstrip("/")
|
|
47
|
+
self._timeout = timeout
|
|
48
|
+
self._retries = retries
|
|
49
|
+
self._session = requests.Session()
|
|
50
|
+
self._session.headers.update(
|
|
51
|
+
{
|
|
52
|
+
"Authorization": f"Bearer {api_key}",
|
|
53
|
+
"Content-Type": "application/json",
|
|
54
|
+
"X-SDK": SDK_VERSION,
|
|
55
|
+
}
|
|
56
|
+
)
|
|
57
|
+
self.webhooks = Webhooks(webhook_secret)
|
|
58
|
+
|
|
59
|
+
# ──────────────────────────────── Transport ────────────────────────────────
|
|
60
|
+
|
|
61
|
+
def _request(self, method: str, path: str, **kwargs) -> Dict[str, Any]:
|
|
62
|
+
url = f"{self._base_url}{path}"
|
|
63
|
+
attempt = 0
|
|
64
|
+
while True:
|
|
65
|
+
try:
|
|
66
|
+
res = self._session.request(method, url, timeout=self._timeout, **kwargs)
|
|
67
|
+
except requests.exceptions.Timeout:
|
|
68
|
+
if attempt < self._retries:
|
|
69
|
+
attempt += 1
|
|
70
|
+
time.sleep(min(0.5 * 2 ** attempt, 10))
|
|
71
|
+
continue
|
|
72
|
+
raise NetworkError("Request timed out")
|
|
73
|
+
except requests.exceptions.RequestException as exc:
|
|
74
|
+
if attempt < self._retries:
|
|
75
|
+
attempt += 1
|
|
76
|
+
time.sleep(min(0.5 * 2 ** attempt, 10))
|
|
77
|
+
continue
|
|
78
|
+
raise NetworkError(str(exc)) from exc
|
|
79
|
+
|
|
80
|
+
if res.status_code == 401:
|
|
81
|
+
raise AuthenticationError()
|
|
82
|
+
|
|
83
|
+
if res.status_code == 429:
|
|
84
|
+
retry_after = int(res.headers.get("Retry-After", 60))
|
|
85
|
+
if attempt < self._retries:
|
|
86
|
+
time.sleep(retry_after)
|
|
87
|
+
attempt += 1
|
|
88
|
+
continue
|
|
89
|
+
raise RateLimitError(retry_after)
|
|
90
|
+
|
|
91
|
+
data = res.json()
|
|
92
|
+
|
|
93
|
+
if not data.get("success"):
|
|
94
|
+
err = data.get("error", {})
|
|
95
|
+
msg = err.get("message", "Request failed")
|
|
96
|
+
code = err.get("code", "UNKNOWN")
|
|
97
|
+
if res.status_code == 422:
|
|
98
|
+
raise ValidationError(msg)
|
|
99
|
+
if res.status_code == 404:
|
|
100
|
+
raise NotFoundError(msg)
|
|
101
|
+
raise AchekError(msg, code, res.status_code)
|
|
102
|
+
|
|
103
|
+
return data
|
|
104
|
+
|
|
105
|
+
# ──────────────────────────────── OTP ─────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
def send_otp(
|
|
108
|
+
self,
|
|
109
|
+
phone: str,
|
|
110
|
+
*,
|
|
111
|
+
length: int = 6,
|
|
112
|
+
expires_in: int = 600,
|
|
113
|
+
template: Optional[str] = None,
|
|
114
|
+
sender_number: Optional[str] = None,
|
|
115
|
+
) -> Dict[str, Any]:
|
|
116
|
+
"""
|
|
117
|
+
Send a WhatsApp OTP to a phone number.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
phone: E.164 number e.g. ``"+2348012345678"``.
|
|
121
|
+
length: OTP digit length — 4 or 6 (default 6).
|
|
122
|
+
expires_in: Seconds until expiry (default 600).
|
|
123
|
+
template: Custom message with ``{{otp}}`` placeholder.
|
|
124
|
+
sender_number: Override the default sender number.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
dict with ``sessionId``, ``phone``, ``expiresAt``, ``remaining``.
|
|
128
|
+
"""
|
|
129
|
+
body: Dict[str, Any] = {"phone": phone, "length": length, "expiresIn": expires_in}
|
|
130
|
+
if template is not None:
|
|
131
|
+
body["template"] = template
|
|
132
|
+
if sender_number is not None:
|
|
133
|
+
body["senderNumber"] = sender_number
|
|
134
|
+
return self._request("POST", "/otp/send", json=body)["data"]
|
|
135
|
+
|
|
136
|
+
def verify_otp(self, phone: str, otp: str) -> Dict[str, Any]:
|
|
137
|
+
"""
|
|
138
|
+
Verify a code entered by the user.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
dict with ``valid`` (bool), ``phone``, ``sessionId``.
|
|
142
|
+
"""
|
|
143
|
+
return self._request("POST", "/otp/verify", json={"phone": phone, "otp": otp})["data"]
|
|
144
|
+
|
|
145
|
+
# ──────────────────────────────── Messaging ────────────────────────────────
|
|
146
|
+
|
|
147
|
+
def send_message(
|
|
148
|
+
self,
|
|
149
|
+
phone: str,
|
|
150
|
+
message: str,
|
|
151
|
+
*,
|
|
152
|
+
sender_number: Optional[str] = None,
|
|
153
|
+
) -> Dict[str, Any]:
|
|
154
|
+
"""Send a plain WhatsApp message."""
|
|
155
|
+
body: Dict[str, Any] = {"phone": phone, "message": message}
|
|
156
|
+
if sender_number is not None:
|
|
157
|
+
body["senderNumber"] = sender_number
|
|
158
|
+
return self._request("POST", "/messages/send", json=body)["data"]
|
|
159
|
+
|
|
160
|
+
# ──────────────────────────────── AI Agents ────────────────────────────────
|
|
161
|
+
|
|
162
|
+
def create_agent(
|
|
163
|
+
self,
|
|
164
|
+
name: str,
|
|
165
|
+
system_prompt: str,
|
|
166
|
+
*,
|
|
167
|
+
provider: str = "openai",
|
|
168
|
+
model: Optional[str] = None,
|
|
169
|
+
welcome_message: Optional[str] = None,
|
|
170
|
+
webhook_url: Optional[str] = None,
|
|
171
|
+
phone_number: Optional[str] = None,
|
|
172
|
+
) -> Dict[str, Any]:
|
|
173
|
+
"""Create a new AI WhatsApp agent."""
|
|
174
|
+
body: Dict[str, Any] = {
|
|
175
|
+
"name": name,
|
|
176
|
+
"systemPrompt": system_prompt,
|
|
177
|
+
"provider": provider,
|
|
178
|
+
}
|
|
179
|
+
if model is not None:
|
|
180
|
+
body["model"] = model
|
|
181
|
+
if welcome_message is not None:
|
|
182
|
+
body["welcomeMessage"] = welcome_message
|
|
183
|
+
if webhook_url is not None:
|
|
184
|
+
body["webhookUrl"] = webhook_url
|
|
185
|
+
if phone_number is not None:
|
|
186
|
+
body["phoneNumber"] = phone_number
|
|
187
|
+
return self._request("POST", "/agents", json=body)["data"]
|
|
188
|
+
|
|
189
|
+
def get_agent(self, agent_id: str) -> Dict[str, Any]:
|
|
190
|
+
"""Retrieve an agent by ID."""
|
|
191
|
+
return self._request("GET", f"/agents/{agent_id}")["data"]
|
|
192
|
+
|
|
193
|
+
def delete_agent(self, agent_id: str) -> Dict[str, Any]:
|
|
194
|
+
"""Delete an agent permanently."""
|
|
195
|
+
return self._request("DELETE", f"/agents/{agent_id}")["data"]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
class AchekError(Exception):
|
|
2
|
+
"""Base exception for all AchekConnect SDK errors."""
|
|
3
|
+
|
|
4
|
+
def __init__(
|
|
5
|
+
self,
|
|
6
|
+
message: str,
|
|
7
|
+
code: str = "UNKNOWN",
|
|
8
|
+
status_code: int = None,
|
|
9
|
+
request_id: str = None,
|
|
10
|
+
):
|
|
11
|
+
super().__init__(message)
|
|
12
|
+
self.message = message
|
|
13
|
+
self.code = code
|
|
14
|
+
self.status_code = status_code
|
|
15
|
+
self.request_id = request_id
|
|
16
|
+
|
|
17
|
+
def __repr__(self) -> str:
|
|
18
|
+
return f"{self.__class__.__name__}(code={self.code!r}, message={self.message!r})"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AuthenticationError(AchekError):
|
|
22
|
+
"""Raised when the API key is missing or invalid (HTTP 401)."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, message: str = "Invalid or missing API key"):
|
|
25
|
+
super().__init__(message, "AUTHENTICATION_ERROR", 401)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class RateLimitError(AchekError):
|
|
29
|
+
"""Raised when the rate limit is exceeded (HTTP 429)."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, retry_after: int = 60, message: str = "Rate limit exceeded"):
|
|
32
|
+
super().__init__(message, "RATE_LIMIT_ERROR", 429)
|
|
33
|
+
self.retry_after = retry_after
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ValidationError(AchekError):
|
|
37
|
+
"""Raised when request validation fails (HTTP 422)."""
|
|
38
|
+
|
|
39
|
+
def __init__(self, message: str, field: str = None):
|
|
40
|
+
super().__init__(message, "VALIDATION_ERROR", 422)
|
|
41
|
+
self.field = field
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class NotFoundError(AchekError):
|
|
45
|
+
"""Raised when a resource is not found (HTTP 404)."""
|
|
46
|
+
|
|
47
|
+
def __init__(self, resource: str = "Resource"):
|
|
48
|
+
super().__init__(f"{resource} not found", "NOT_FOUND", 404)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class NetworkError(AchekError):
|
|
52
|
+
"""Raised for connection failures, timeouts, and other transport errors."""
|
|
53
|
+
|
|
54
|
+
def __init__(self, message: str):
|
|
55
|
+
super().__init__(message, "NETWORK_ERROR")
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import hmac
|
|
2
|
+
import hashlib
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any, Dict
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Webhooks:
|
|
8
|
+
"""Helpers for verifying and parsing incoming AchekConnect webhook events."""
|
|
9
|
+
|
|
10
|
+
def __init__(self, secret: str):
|
|
11
|
+
self._secret = secret
|
|
12
|
+
|
|
13
|
+
def verify(self, payload: bytes, signature: str) -> bool:
|
|
14
|
+
"""
|
|
15
|
+
Return True if the HMAC-SHA256 signature matches the payload.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
payload: Raw request body bytes.
|
|
19
|
+
signature: Value of the X-AchekConnect-Signature header.
|
|
20
|
+
"""
|
|
21
|
+
if not self._secret:
|
|
22
|
+
return False
|
|
23
|
+
expected = hmac.new(
|
|
24
|
+
self._secret.encode("utf-8"), payload, hashlib.sha256
|
|
25
|
+
).hexdigest()
|
|
26
|
+
return hmac.compare_digest(expected, signature)
|
|
27
|
+
|
|
28
|
+
def parse(self, payload: str) -> Dict[str, Any]:
|
|
29
|
+
"""Parse a raw JSON payload string into a dict."""
|
|
30
|
+
return json.loads(payload)
|
|
31
|
+
|
|
32
|
+
def construct_event(self, payload: bytes, signature: str) -> Dict[str, Any]:
|
|
33
|
+
"""
|
|
34
|
+
Verify the signature then parse the payload.
|
|
35
|
+
|
|
36
|
+
Use this in your webhook route handler::
|
|
37
|
+
|
|
38
|
+
@app.route("/webhook", methods=["POST"])
|
|
39
|
+
def webhook():
|
|
40
|
+
event = client.webhooks.construct_event(
|
|
41
|
+
request.get_data(),
|
|
42
|
+
request.headers.get("X-AchekConnect-Signature", "")
|
|
43
|
+
)
|
|
44
|
+
if event["type"] == "otp.verified":
|
|
45
|
+
...
|
|
46
|
+
return "", 200
|
|
47
|
+
|
|
48
|
+
Raises:
|
|
49
|
+
ValueError: If the signature does not match.
|
|
50
|
+
"""
|
|
51
|
+
if not self.verify(payload, signature):
|
|
52
|
+
raise ValueError(
|
|
53
|
+
"Webhook signature verification failed. "
|
|
54
|
+
"Ensure you are passing the raw request body and the correct secret."
|
|
55
|
+
)
|
|
56
|
+
return self.parse(payload.decode("utf-8"))
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "achek-sdk"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Official Python SDK for Achek — WhatsApp OTP, AI chatbots & business messaging for Nigeria"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Achek", email = "dev@achek.com.ng" }]
|
|
13
|
+
keywords = ["achek", "otp", "whatsapp", "nigeria", "verification", "2fa", "chatbot"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 5 - Production/Stable",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.8",
|
|
20
|
+
"Programming Language :: Python :: 3.9",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Topic :: Communications",
|
|
25
|
+
"Topic :: Software Development :: Libraries",
|
|
26
|
+
]
|
|
27
|
+
dependencies = ["requests>=2.28.0"]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://achek.com.ng"
|
|
31
|
+
Documentation = "https://docs.achek.com.ng"
|
|
32
|
+
Repository = "https://github.com/CalebDevX/AchekVerify"
|
|
33
|
+
Issues = "https://github.com/CalebDevX/AchekVerify/issues"
|
|
34
|
+
|
|
35
|
+
[tool.hatch.build.targets.wheel]
|
|
36
|
+
packages = ["achekverify"]
|