clickpesa-python-sdk 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.
- clickpesa_python_sdk-0.1.0/LICENSE +21 -0
- clickpesa_python_sdk-0.1.0/PKG-INFO +512 -0
- clickpesa_python_sdk-0.1.0/README.md +477 -0
- clickpesa_python_sdk-0.1.0/pyproject.toml +65 -0
- clickpesa_python_sdk-0.1.0/setup.cfg +4 -0
- clickpesa_python_sdk-0.1.0/src/clickpesa/__init__.py +146 -0
- clickpesa_python_sdk-0.1.0/src/clickpesa/_version.py +1 -0
- clickpesa_python_sdk-0.1.0/src/clickpesa/async_client.py +307 -0
- clickpesa_python_sdk-0.1.0/src/clickpesa/client.py +302 -0
- clickpesa_python_sdk-0.1.0/src/clickpesa/exceptions.py +100 -0
- clickpesa_python_sdk-0.1.0/src/clickpesa/py.typed +0 -0
- clickpesa_python_sdk-0.1.0/src/clickpesa/security.py +74 -0
- clickpesa_python_sdk-0.1.0/src/clickpesa/services/__init__.py +21 -0
- clickpesa_python_sdk-0.1.0/src/clickpesa/services/account.py +87 -0
- clickpesa_python_sdk-0.1.0/src/clickpesa/services/billpay.py +340 -0
- clickpesa_python_sdk-0.1.0/src/clickpesa/services/exchange.py +74 -0
- clickpesa_python_sdk-0.1.0/src/clickpesa/services/links.py +169 -0
- clickpesa_python_sdk-0.1.0/src/clickpesa/services/payments.py +248 -0
- clickpesa_python_sdk-0.1.0/src/clickpesa/services/payouts.py +299 -0
- clickpesa_python_sdk-0.1.0/src/clickpesa/webhooks.py +42 -0
- clickpesa_python_sdk-0.1.0/src/clickpesa_python_sdk.egg-info/PKG-INFO +512 -0
- clickpesa_python_sdk-0.1.0/src/clickpesa_python_sdk.egg-info/SOURCES.txt +30 -0
- clickpesa_python_sdk-0.1.0/src/clickpesa_python_sdk.egg-info/dependency_links.txt +1 -0
- clickpesa_python_sdk-0.1.0/src/clickpesa_python_sdk.egg-info/requires.txt +10 -0
- clickpesa_python_sdk-0.1.0/src/clickpesa_python_sdk.egg-info/top_level.txt +1 -0
- clickpesa_python_sdk-0.1.0/tests/test_async_client.py +124 -0
- clickpesa_python_sdk-0.1.0/tests/test_billpay.py +114 -0
- clickpesa_python_sdk-0.1.0/tests/test_client.py +221 -0
- clickpesa_python_sdk-0.1.0/tests/test_links.py +86 -0
- clickpesa_python_sdk-0.1.0/tests/test_payments.py +122 -0
- clickpesa_python_sdk-0.1.0/tests/test_payouts.py +249 -0
- clickpesa_python_sdk-0.1.0/tests/test_webhook.py +80 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Jackson Linus
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: clickpesa-python-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Production-grade Python SDK for the ClickPesa API — sync & async, collections, payouts, BillPay and more
|
|
5
|
+
Author-email: Jackson Linus <jacksonlinus95@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/JAXPARROW/clickpesa-python-sdk
|
|
8
|
+
Project-URL: Documentation, https://docs.clickpesa.com
|
|
9
|
+
Project-URL: Repository, https://github.com/JAXPARROW/clickpesa-python-sdk
|
|
10
|
+
Project-URL: Bug Tracker, https://github.com/JAXPARROW/clickpesa-python-sdk/issues
|
|
11
|
+
Project-URL: Changelog, https://github.com/JAXPARROW/clickpesa-python-sdk/blob/main/CHANGELOG.md
|
|
12
|
+
Keywords: clickpesa,payments,fintech,tanzania,sdk,api
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Intended Audience :: Developers
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: httpx>=0.24.0
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
29
|
+
Requires-Dist: respx>=0.20; extra == "dev"
|
|
30
|
+
Requires-Dist: build; extra == "dev"
|
|
31
|
+
Requires-Dist: twine; extra == "dev"
|
|
32
|
+
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
33
|
+
Requires-Dist: ruff>=0.1; extra == "dev"
|
|
34
|
+
Dynamic: license-file
|
|
35
|
+
|
|
36
|
+
# ClickPesa Python SDK
|
|
37
|
+
|
|
38
|
+
[](https://pypi.org/project/clickpesa-python-sdk/)
|
|
39
|
+
[](https://pypi.org/project/clickpesa-python-sdk/)
|
|
40
|
+
[](https://opensource.org/licenses/MIT)
|
|
41
|
+
|
|
42
|
+
Production-grade Python SDK for the [ClickPesa API](https://docs.clickpesa.com) — supports both **sync** and **async** usage, with automatic token management, checksum injection, retry logic, and a full exception hierarchy.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Features
|
|
47
|
+
|
|
48
|
+
- **Sync & Async** — `ClickPesa` for blocking code, `AsyncClickPesa` for `asyncio` / FastAPI / async frameworks
|
|
49
|
+
- **Auto Auth** — JWT tokens are fetched and cached automatically (55-minute window, 1-hour API TTL)
|
|
50
|
+
- **Checksum injection** — HMAC-SHA256 checksums added to every mutating request when a `checksum_key` is configured
|
|
51
|
+
- **Retries** — exponential backoff on transient 5xx errors (configurable)
|
|
52
|
+
- **Thread-safe** — safe to share a single client across threads or async tasks
|
|
53
|
+
- **Context manager** — `with` / `async with` support for automatic cleanup
|
|
54
|
+
- **Typed exceptions** — structured error hierarchy with `status_code` and `response` attributes
|
|
55
|
+
- **PEP 561 compliant** — ships with `py.typed` for mypy / pyright support
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Installation
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install clickpesa-python-sdk
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Requires **Python 3.10+**.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Quick Start
|
|
70
|
+
|
|
71
|
+
### Sync
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from clickpesa import ClickPesa
|
|
75
|
+
|
|
76
|
+
with ClickPesa(
|
|
77
|
+
client_id="YOUR_CLIENT_ID",
|
|
78
|
+
api_key="YOUR_API_KEY",
|
|
79
|
+
checksum_key="YOUR_CHECKSUM_KEY", # optional but recommended
|
|
80
|
+
sandbox=True, # set False for production
|
|
81
|
+
) as client:
|
|
82
|
+
balance = client.account.get_balance()
|
|
83
|
+
print(balance)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Async
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
import asyncio
|
|
90
|
+
from clickpesa import AsyncClickPesa
|
|
91
|
+
|
|
92
|
+
async def main():
|
|
93
|
+
async with AsyncClickPesa(
|
|
94
|
+
client_id="YOUR_CLIENT_ID",
|
|
95
|
+
api_key="YOUR_API_KEY",
|
|
96
|
+
checksum_key="YOUR_CHECKSUM_KEY",
|
|
97
|
+
sandbox=True,
|
|
98
|
+
) as client:
|
|
99
|
+
balance = await client.account.get_balance()
|
|
100
|
+
print(balance)
|
|
101
|
+
|
|
102
|
+
asyncio.run(main())
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Configuration
|
|
108
|
+
|
|
109
|
+
| Parameter | Type | Default | Description |
|
|
110
|
+
| --- | --- | --- | --- |
|
|
111
|
+
| `client_id` | `str` | required | Your ClickPesa application Client ID |
|
|
112
|
+
| `api_key` | `str` | required | Your ClickPesa application API key |
|
|
113
|
+
| `checksum_key` | `str \| None` | `None` | Enables HMAC-SHA256 checksum on every mutating request |
|
|
114
|
+
| `sandbox` | `bool` | `False` | Target sandbox (`api-sandbox.clickpesa.com`) instead of production |
|
|
115
|
+
| `timeout` | `float` | `30.0` | Per-request timeout in seconds |
|
|
116
|
+
| `max_retries` | `int` | `3` | Max retry attempts on transient server errors |
|
|
117
|
+
|
|
118
|
+
> **Note:** `order_id` / `orderReference` values must be **alphanumeric only** (no hyphens, underscores, or special characters). The API will reject any order reference containing non-alphanumeric characters.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Collections
|
|
123
|
+
|
|
124
|
+
### USSD Push
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
# 1. Preview — check available methods and fees before charging
|
|
128
|
+
preview = client.payments.preview_ussd_push(
|
|
129
|
+
amount="5000",
|
|
130
|
+
order_id="ORD20240001",
|
|
131
|
+
phone="255712345678", # optional: include to get sender details
|
|
132
|
+
fetch_sender_details=True, # optional: returns accountName / accountProvider
|
|
133
|
+
)
|
|
134
|
+
print(preview["activeMethods"]) # [{"name": "TIGO-PESA", "status": "AVAILABLE", "fee": 580, ...}]
|
|
135
|
+
|
|
136
|
+
# 2. Initiate — triggers PIN prompt on the customer's phone
|
|
137
|
+
transaction = client.payments.initiate_ussd_push(
|
|
138
|
+
amount="5000",
|
|
139
|
+
phone="255712345678",
|
|
140
|
+
order_id="ORD20240001",
|
|
141
|
+
currency="TZS", # only TZS supported
|
|
142
|
+
)
|
|
143
|
+
print(transaction["id"], transaction["status"])
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Card Payment
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
# 1. Preview — check card method availability
|
|
150
|
+
preview = client.payments.preview_card(amount="50", order_id="CARD001")
|
|
151
|
+
|
|
152
|
+
# 2. Initiate — generate a hosted payment link
|
|
153
|
+
result = client.payments.initiate_card(
|
|
154
|
+
amount="50",
|
|
155
|
+
order_id="CARD001",
|
|
156
|
+
currency="USD", # only USD supported
|
|
157
|
+
customer={
|
|
158
|
+
"fullName": "John Doe",
|
|
159
|
+
"email": "john@example.com",
|
|
160
|
+
"phoneNumber": "255712345678",
|
|
161
|
+
},
|
|
162
|
+
# or use an existing customer ID:
|
|
163
|
+
# customer={"id": "CUST_123"}
|
|
164
|
+
)
|
|
165
|
+
print(result["cardPaymentLink"]) # redirect customer here
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Query Payments
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
# Single payment by order reference
|
|
172
|
+
payments = client.payments.get_status("ORD20240001")
|
|
173
|
+
|
|
174
|
+
# Paginated list with filters
|
|
175
|
+
page = client.payments.list_all(
|
|
176
|
+
status="SUCCESS",
|
|
177
|
+
collectedCurrency="TZS",
|
|
178
|
+
startDate="2024-01-01",
|
|
179
|
+
endDate="2024-12-31",
|
|
180
|
+
limit=20,
|
|
181
|
+
skip=0,
|
|
182
|
+
)
|
|
183
|
+
print(page["totalCount"], page["data"])
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Disbursements
|
|
189
|
+
|
|
190
|
+
### Mobile Money Payout
|
|
191
|
+
|
|
192
|
+
```python
|
|
193
|
+
# 1. Preview — verify fees and recipient before sending
|
|
194
|
+
preview = client.payouts.preview_mobile_money(
|
|
195
|
+
amount=10000,
|
|
196
|
+
phone="255712345678",
|
|
197
|
+
order_id="PAY20240001",
|
|
198
|
+
currency="TZS", # TZS or USD; recipient always receives TZS
|
|
199
|
+
)
|
|
200
|
+
print(preview["fee"], preview["receiver"]["accountName"])
|
|
201
|
+
|
|
202
|
+
# 2. Create — disburse funds
|
|
203
|
+
payout = client.payouts.create_mobile_money(
|
|
204
|
+
amount=10000,
|
|
205
|
+
phone="255712345678",
|
|
206
|
+
order_id="PAY20240001",
|
|
207
|
+
)
|
|
208
|
+
print(payout["id"], payout["status"]) # status: AUTHORIZED → PROCESSING → SUCCESS
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Bank Payout (ACH / RTGS)
|
|
212
|
+
|
|
213
|
+
```python
|
|
214
|
+
# Get list of supported banks and their BIC codes
|
|
215
|
+
banks = client.payouts.get_banks()
|
|
216
|
+
# [{"name": "EQUITY BANK TANZANIA LIMITED", "value": "equity_bank_tanzania_limited", "bic": "EQBLTZTZ"}, ...]
|
|
217
|
+
|
|
218
|
+
# 1. Preview
|
|
219
|
+
preview = client.payouts.preview_bank(
|
|
220
|
+
amount=500000,
|
|
221
|
+
account_number="1234567890",
|
|
222
|
+
bic="EQBLTZTZ",
|
|
223
|
+
order_id="BANK20240001",
|
|
224
|
+
transfer_type="ACH", # "ACH" or "RTGS"
|
|
225
|
+
currency="TZS",
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
# 2. Create
|
|
229
|
+
payout = client.payouts.create_bank(
|
|
230
|
+
amount=500000,
|
|
231
|
+
account_number="1234567890",
|
|
232
|
+
account_name="Jane Doe",
|
|
233
|
+
bic="EQBLTZTZ",
|
|
234
|
+
order_id="BANK20240001",
|
|
235
|
+
transfer_type="RTGS",
|
|
236
|
+
currency="TZS",
|
|
237
|
+
)
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Query Payouts
|
|
241
|
+
|
|
242
|
+
```python
|
|
243
|
+
# Single payout by order reference
|
|
244
|
+
payouts = client.payouts.get_status("PAY20240001")
|
|
245
|
+
|
|
246
|
+
# All payouts with filters
|
|
247
|
+
page = client.payouts.list_all(
|
|
248
|
+
channel="MOBILE MONEY",
|
|
249
|
+
status="SUCCESS",
|
|
250
|
+
limit=50,
|
|
251
|
+
)
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## BillPay
|
|
257
|
+
|
|
258
|
+
ClickPesa BillPay lets customers pay using a numeric control number through mobile money, SIM banking, and CRDB Wakalas. There are two types of control numbers:
|
|
259
|
+
|
|
260
|
+
- **Order** — one-time, closes after payment. Ideal for invoices and e-commerce orders.
|
|
261
|
+
- **Customer** — static and reusable per customer. Ideal for subscriptions and recurring payments.
|
|
262
|
+
|
|
263
|
+
> **Note:** Every ClickPesa merchant has a 4-digit **Merchant BillPay-Namba** visible on the dashboard. Order control numbers can also be generated *offline* (no API call) by concatenating your Merchant BillPay-Namba with any internal order reference (e.g. `1122` + `231256` = `1122231256`). The SDK only covers API-based generation.
|
|
264
|
+
|
|
265
|
+
### Create Control Numbers
|
|
266
|
+
|
|
267
|
+
```python
|
|
268
|
+
# Order control number (one-time)
|
|
269
|
+
cn = client.billpay.create_order_control_number(
|
|
270
|
+
bill_reference="INVOICE001", # optional — becomes the control number; auto-generated if omitted
|
|
271
|
+
amount=90900,
|
|
272
|
+
description="Water Bill - July 2024",
|
|
273
|
+
payment_mode="EXACT", # "EXACT" or "ALLOW_PARTIAL_AND_OVER_PAYMENT"
|
|
274
|
+
)
|
|
275
|
+
print(cn["billPayNumber"]) # share this with your customer
|
|
276
|
+
|
|
277
|
+
# Customer control number (persistent / recurring)
|
|
278
|
+
cn = client.billpay.create_customer_control_number(
|
|
279
|
+
customer_name="John Doe",
|
|
280
|
+
phone="255712345678", # phone or email required
|
|
281
|
+
email="john@example.com",
|
|
282
|
+
amount=50000,
|
|
283
|
+
payment_mode="ALLOW_PARTIAL_AND_OVER_PAYMENT",
|
|
284
|
+
)
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Bulk Create (up to 50 per request)
|
|
288
|
+
|
|
289
|
+
```python
|
|
290
|
+
# Bulk order control numbers
|
|
291
|
+
result = client.billpay.bulk_create_order_numbers([
|
|
292
|
+
{"billAmount": 10000, "billDescription": "Invoice #001", "billPaymentMode": "EXACT"},
|
|
293
|
+
{"billAmount": 20000, "billDescription": "Invoice #002"},
|
|
294
|
+
{"billReference": "MYREF003", "billAmount": 5000},
|
|
295
|
+
])
|
|
296
|
+
print(result["created"], result["failed"])
|
|
297
|
+
print(result["billPayNumbers"])
|
|
298
|
+
|
|
299
|
+
# Bulk customer control numbers
|
|
300
|
+
result = client.billpay.bulk_create_customer_numbers([
|
|
301
|
+
{"customerName": "Alice", "customerPhone": "255712345678", "billAmount": 15000},
|
|
302
|
+
{"customerName": "Bob", "customerEmail": "bob@example.com"},
|
|
303
|
+
])
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### Manage Existing Numbers
|
|
307
|
+
|
|
308
|
+
```python
|
|
309
|
+
# Query details
|
|
310
|
+
details = client.billpay.get_details("55042914871931")
|
|
311
|
+
|
|
312
|
+
# Update amount, description or payment mode
|
|
313
|
+
client.billpay.update_reference(
|
|
314
|
+
"55042914871931",
|
|
315
|
+
amount=120000,
|
|
316
|
+
description="Updated Water Bill",
|
|
317
|
+
payment_mode="EXACT",
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
# Activate / deactivate
|
|
321
|
+
client.billpay.update_status("55042914871931", "INACTIVE")
|
|
322
|
+
client.billpay.update_status("55042914871931", "ACTIVE")
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
---
|
|
326
|
+
|
|
327
|
+
## Hosted Links
|
|
328
|
+
|
|
329
|
+
```python
|
|
330
|
+
# Checkout link — customer chooses their payment method
|
|
331
|
+
result = client.links.generate_checkout(
|
|
332
|
+
order_id="LINK001",
|
|
333
|
+
order_currency="TZS",
|
|
334
|
+
total_price="15000",
|
|
335
|
+
customer_name="Jane Doe",
|
|
336
|
+
customer_email="jane@example.com",
|
|
337
|
+
customer_phone="255712345678",
|
|
338
|
+
description="Order LINK001",
|
|
339
|
+
)
|
|
340
|
+
print(result["checkoutLink"]) # redirect customer here
|
|
341
|
+
|
|
342
|
+
# With itemised order instead of a flat total
|
|
343
|
+
result = client.links.generate_checkout(
|
|
344
|
+
order_id="LINK002",
|
|
345
|
+
order_currency="USD",
|
|
346
|
+
order_items=[
|
|
347
|
+
{"name": "Widget A", "price": "25.00", "quantity": 2},
|
|
348
|
+
{"name": "Widget B", "price": "10.00", "quantity": 1},
|
|
349
|
+
],
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
# Payout link — recipient enters their own bank / mobile details
|
|
353
|
+
result = client.links.generate_payout(amount="50000", order_id="POUT001")
|
|
354
|
+
print(result["payoutLink"])
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
---
|
|
358
|
+
|
|
359
|
+
## Account & Exchange
|
|
360
|
+
|
|
361
|
+
```python
|
|
362
|
+
# Account balances
|
|
363
|
+
result = client.account.get_balance()
|
|
364
|
+
# {"balances": [{"currency": "TZS", "balance": 39700}, {"currency": "USD", "balance": 0}]}
|
|
365
|
+
print(result["balances"])
|
|
366
|
+
|
|
367
|
+
# Transaction statement
|
|
368
|
+
statement = client.account.get_statement(
|
|
369
|
+
currency="TZS",
|
|
370
|
+
start_date="2024-01-01",
|
|
371
|
+
end_date="2024-12-31",
|
|
372
|
+
)
|
|
373
|
+
print(statement["accountDetails"])
|
|
374
|
+
print(statement["transactions"])
|
|
375
|
+
|
|
376
|
+
# Exchange rates
|
|
377
|
+
rates = client.exchange.get_rates() # all pairs
|
|
378
|
+
rates = client.exchange.get_rates(source="USD", target="TZS") # specific pair
|
|
379
|
+
# [{"source": "USD", "target": "TZS", "rate": 2510, "date": "..."}]
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
---
|
|
383
|
+
|
|
384
|
+
## Async Usage
|
|
385
|
+
|
|
386
|
+
Every method on `AsyncClickPesa` is the `await`-able equivalent:
|
|
387
|
+
|
|
388
|
+
```python
|
|
389
|
+
import asyncio
|
|
390
|
+
from clickpesa import AsyncClickPesa
|
|
391
|
+
|
|
392
|
+
async def run_payments():
|
|
393
|
+
async with AsyncClickPesa(
|
|
394
|
+
client_id="YOUR_CLIENT_ID",
|
|
395
|
+
api_key="YOUR_API_KEY",
|
|
396
|
+
sandbox=True,
|
|
397
|
+
) as client:
|
|
398
|
+
# Run multiple API calls concurrently
|
|
399
|
+
balance, rates = await asyncio.gather(
|
|
400
|
+
client.account.get_balance(),
|
|
401
|
+
client.exchange.get_rates(source="USD"),
|
|
402
|
+
)
|
|
403
|
+
print(balance, rates)
|
|
404
|
+
|
|
405
|
+
# Collections
|
|
406
|
+
tx = await client.payments.initiate_ussd_push(
|
|
407
|
+
amount="3000",
|
|
408
|
+
phone="255712345678",
|
|
409
|
+
order_id="ASYNC001",
|
|
410
|
+
)
|
|
411
|
+
|
|
412
|
+
# Disbursements
|
|
413
|
+
payout = await client.payouts.create_mobile_money(
|
|
414
|
+
amount=3000,
|
|
415
|
+
phone="255712345678",
|
|
416
|
+
order_id="ASYNC002",
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
asyncio.run(run_payments())
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
## Webhook Verification
|
|
425
|
+
|
|
426
|
+
```python
|
|
427
|
+
from clickpesa import WebhookValidator
|
|
428
|
+
|
|
429
|
+
# In your webhook endpoint (Flask / FastAPI / Django etc.)
|
|
430
|
+
def webhook_handler(request):
|
|
431
|
+
is_valid = WebhookValidator.verify(
|
|
432
|
+
payload=request.json,
|
|
433
|
+
signature=request.headers["X-ClickPesa-Signature"],
|
|
434
|
+
checksum_key="YOUR_CHECKSUM_KEY",
|
|
435
|
+
)
|
|
436
|
+
if not is_valid:
|
|
437
|
+
return {"error": "Invalid signature"}, 401
|
|
438
|
+
|
|
439
|
+
# Process event ...
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
---
|
|
443
|
+
|
|
444
|
+
## Error Handling
|
|
445
|
+
|
|
446
|
+
All errors are subclasses of `ClickPesaError` and carry `.status_code` and `.response`:
|
|
447
|
+
|
|
448
|
+
```python
|
|
449
|
+
from clickpesa.exceptions import (
|
|
450
|
+
AuthenticationError, # 401 — invalid credentials / expired token
|
|
451
|
+
ForbiddenError, # 403 — feature not enabled on your account
|
|
452
|
+
ValidationError, # 400 — bad payload
|
|
453
|
+
InsufficientFundsError, # 400 — not enough balance (subclass of ValidationError)
|
|
454
|
+
NotFoundError, # 404 — resource not found
|
|
455
|
+
ConflictError, # 409 — duplicate orderReference / billReference
|
|
456
|
+
RateLimitError, # 429 — payout request already in progress
|
|
457
|
+
ServerError, # 5xx — ClickPesa server error
|
|
458
|
+
ClickPesaError, # base class — catches all of the above
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
try:
|
|
462
|
+
client.payments.initiate_ussd_push("5000", "255712345678", "ORD001")
|
|
463
|
+
|
|
464
|
+
except InsufficientFundsError as e:
|
|
465
|
+
print(f"Not enough balance: {e}")
|
|
466
|
+
|
|
467
|
+
except ConflictError as e:
|
|
468
|
+
print(f"Order reference already used: {e}")
|
|
469
|
+
print(f"HTTP {e.status_code} — {e.response}")
|
|
470
|
+
|
|
471
|
+
except ClickPesaError as e:
|
|
472
|
+
print(f"Unexpected API error [{e.status_code}]: {e}")
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
---
|
|
476
|
+
|
|
477
|
+
## Health Check
|
|
478
|
+
|
|
479
|
+
```python
|
|
480
|
+
# Returns True if the API is reachable and credentials are valid
|
|
481
|
+
if client.is_healthy():
|
|
482
|
+
print("Connected to ClickPesa")
|
|
483
|
+
else:
|
|
484
|
+
print("API unreachable or credentials invalid")
|
|
485
|
+
|
|
486
|
+
# Async equivalent
|
|
487
|
+
healthy = await client.is_healthy()
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
---
|
|
491
|
+
|
|
492
|
+
## Development
|
|
493
|
+
|
|
494
|
+
```bash
|
|
495
|
+
git clone https://github.com/JAXPARROW/clickpesa-python-sdk
|
|
496
|
+
cd clickpesa-python-sdk
|
|
497
|
+
|
|
498
|
+
# Install with dev dependencies
|
|
499
|
+
pip install -e ".[dev]"
|
|
500
|
+
|
|
501
|
+
# Run tests
|
|
502
|
+
pytest
|
|
503
|
+
|
|
504
|
+
# Run tests with coverage
|
|
505
|
+
pytest --cov=clickpesa --cov-report=term-missing
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
---
|
|
509
|
+
|
|
510
|
+
## License
|
|
511
|
+
|
|
512
|
+
MIT — see [LICENSE](LICENSE) for details.
|