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.
Files changed (32) hide show
  1. clickpesa_python_sdk-0.1.0/LICENSE +21 -0
  2. clickpesa_python_sdk-0.1.0/PKG-INFO +512 -0
  3. clickpesa_python_sdk-0.1.0/README.md +477 -0
  4. clickpesa_python_sdk-0.1.0/pyproject.toml +65 -0
  5. clickpesa_python_sdk-0.1.0/setup.cfg +4 -0
  6. clickpesa_python_sdk-0.1.0/src/clickpesa/__init__.py +146 -0
  7. clickpesa_python_sdk-0.1.0/src/clickpesa/_version.py +1 -0
  8. clickpesa_python_sdk-0.1.0/src/clickpesa/async_client.py +307 -0
  9. clickpesa_python_sdk-0.1.0/src/clickpesa/client.py +302 -0
  10. clickpesa_python_sdk-0.1.0/src/clickpesa/exceptions.py +100 -0
  11. clickpesa_python_sdk-0.1.0/src/clickpesa/py.typed +0 -0
  12. clickpesa_python_sdk-0.1.0/src/clickpesa/security.py +74 -0
  13. clickpesa_python_sdk-0.1.0/src/clickpesa/services/__init__.py +21 -0
  14. clickpesa_python_sdk-0.1.0/src/clickpesa/services/account.py +87 -0
  15. clickpesa_python_sdk-0.1.0/src/clickpesa/services/billpay.py +340 -0
  16. clickpesa_python_sdk-0.1.0/src/clickpesa/services/exchange.py +74 -0
  17. clickpesa_python_sdk-0.1.0/src/clickpesa/services/links.py +169 -0
  18. clickpesa_python_sdk-0.1.0/src/clickpesa/services/payments.py +248 -0
  19. clickpesa_python_sdk-0.1.0/src/clickpesa/services/payouts.py +299 -0
  20. clickpesa_python_sdk-0.1.0/src/clickpesa/webhooks.py +42 -0
  21. clickpesa_python_sdk-0.1.0/src/clickpesa_python_sdk.egg-info/PKG-INFO +512 -0
  22. clickpesa_python_sdk-0.1.0/src/clickpesa_python_sdk.egg-info/SOURCES.txt +30 -0
  23. clickpesa_python_sdk-0.1.0/src/clickpesa_python_sdk.egg-info/dependency_links.txt +1 -0
  24. clickpesa_python_sdk-0.1.0/src/clickpesa_python_sdk.egg-info/requires.txt +10 -0
  25. clickpesa_python_sdk-0.1.0/src/clickpesa_python_sdk.egg-info/top_level.txt +1 -0
  26. clickpesa_python_sdk-0.1.0/tests/test_async_client.py +124 -0
  27. clickpesa_python_sdk-0.1.0/tests/test_billpay.py +114 -0
  28. clickpesa_python_sdk-0.1.0/tests/test_client.py +221 -0
  29. clickpesa_python_sdk-0.1.0/tests/test_links.py +86 -0
  30. clickpesa_python_sdk-0.1.0/tests/test_payments.py +122 -0
  31. clickpesa_python_sdk-0.1.0/tests/test_payouts.py +249 -0
  32. 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
+ [![PyPI version](https://badge.fury.io/py/clickpesa-python-sdk.svg)](https://pypi.org/project/clickpesa-python-sdk/)
39
+ [![Python](https://img.shields.io/pypi/pyversions/clickpesa-python-sdk)](https://pypi.org/project/clickpesa-python-sdk/)
40
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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.