nomba 0.1.1__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.
- nomba-0.1.1/.gitignore +25 -0
- nomba-0.1.1/CHANGELOG.md +19 -0
- nomba-0.1.1/LICENSE +21 -0
- nomba-0.1.1/PKG-INFO +391 -0
- nomba-0.1.1/README.md +361 -0
- nomba-0.1.1/examples/async_quickstart.py +17 -0
- nomba-0.1.1/examples/checkout_order.py +38 -0
- nomba-0.1.1/examples/pagination.py +23 -0
- nomba-0.1.1/examples/quickstart.py +22 -0
- nomba-0.1.1/examples/transfer_with_requery.py +57 -0
- nomba-0.1.1/examples/webhook_fastapi.py +30 -0
- nomba-0.1.1/examples/webhook_flask.py +33 -0
- nomba-0.1.1/pyproject.toml +106 -0
- nomba-0.1.1/src/nomba/__init__.py +55 -0
- nomba-0.1.1/src/nomba/_client.py +359 -0
- nomba-0.1.1/src/nomba/_config.py +107 -0
- nomba-0.1.1/src/nomba/_core.py +550 -0
- nomba-0.1.1/src/nomba/_enums.py +45 -0
- nomba-0.1.1/src/nomba/_version.py +1 -0
- nomba-0.1.1/src/nomba/errors.py +154 -0
- nomba-0.1.1/src/nomba/py.typed +0 -0
- nomba-0.1.1/src/nomba/resources/__init__.py +1 -0
- nomba-0.1.1/src/nomba/resources/_base.py +78 -0
- nomba-0.1.1/src/nomba/resources/accounts.py +149 -0
- nomba-0.1.1/src/nomba/resources/auth.py +21 -0
- nomba-0.1.1/src/nomba/resources/bills.py +148 -0
- nomba-0.1.1/src/nomba/resources/checkout.py +285 -0
- nomba-0.1.1/src/nomba/resources/direct_debits.py +98 -0
- nomba-0.1.1/src/nomba/resources/global_collection.py +45 -0
- nomba-0.1.1/src/nomba/resources/global_payout.py +171 -0
- nomba-0.1.1/src/nomba/resources/terminals.py +43 -0
- nomba-0.1.1/src/nomba/resources/transactions.py +282 -0
- nomba-0.1.1/src/nomba/resources/transfers.py +65 -0
- nomba-0.1.1/src/nomba/resources/virtual_accounts.py +106 -0
- nomba-0.1.1/src/nomba/tokens.py +106 -0
- nomba-0.1.1/src/nomba/types/__init__.py +410 -0
- nomba-0.1.1/src/nomba/types/accounts.py +48 -0
- nomba-0.1.1/src/nomba/types/auth.py +10 -0
- nomba-0.1.1/src/nomba/types/bills.py +116 -0
- nomba-0.1.1/src/nomba/types/checkout.py +258 -0
- nomba-0.1.1/src/nomba/types/direct_debits.py +121 -0
- nomba-0.1.1/src/nomba/types/global_collection.py +41 -0
- nomba-0.1.1/src/nomba/types/global_payout.py +155 -0
- nomba-0.1.1/src/nomba/types/shared.py +47 -0
- nomba-0.1.1/src/nomba/types/terminals.py +29 -0
- nomba-0.1.1/src/nomba/types/transactions.py +66 -0
- nomba-0.1.1/src/nomba/types/transfers.py +65 -0
- nomba-0.1.1/src/nomba/types/virtual_accounts.py +77 -0
- nomba-0.1.1/src/nomba/types/webhooks.py +16 -0
- nomba-0.1.1/src/nomba/webhooks.py +197 -0
- nomba-0.1.1/tests/__init__.py +0 -0
- nomba-0.1.1/tests/conftest.py +142 -0
- nomba-0.1.1/tests/fixtures/webhook-fixture.json +1 -0
- nomba-0.1.1/tests/test_auth.py +391 -0
- nomba-0.1.1/tests/test_config.py +163 -0
- nomba-0.1.1/tests/test_errors.py +150 -0
- nomba-0.1.1/tests/test_idempotency.py +139 -0
- nomba-0.1.1/tests/test_pagination.py +75 -0
- nomba-0.1.1/tests/test_policies.py +136 -0
- nomba-0.1.1/tests/test_references.py +114 -0
- nomba-0.1.1/tests/test_resources.py +782 -0
- nomba-0.1.1/tests/test_retries.py +153 -0
- nomba-0.1.1/tests/test_sandbox_smoke.py +31 -0
- nomba-0.1.1/tests/test_total_timeout.py +161 -0
- nomba-0.1.1/tests/test_validation.py +98 -0
- nomba-0.1.1/tests/test_version.py +24 -0
- nomba-0.1.1/tests/test_webhooks.py +284 -0
nomba-0.1.1/.gitignore
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# virtualenvs
|
|
2
|
+
.venv/
|
|
3
|
+
venv/
|
|
4
|
+
|
|
5
|
+
# build output
|
|
6
|
+
dist/
|
|
7
|
+
build/
|
|
8
|
+
*.egg-info/
|
|
9
|
+
|
|
10
|
+
# caches
|
|
11
|
+
__pycache__/
|
|
12
|
+
*.py[cod]
|
|
13
|
+
.pytest_cache/
|
|
14
|
+
.mypy_cache/
|
|
15
|
+
.ruff_cache/
|
|
16
|
+
|
|
17
|
+
# coverage
|
|
18
|
+
.coverage
|
|
19
|
+
.coverage.*
|
|
20
|
+
coverage.xml
|
|
21
|
+
htmlcov/
|
|
22
|
+
|
|
23
|
+
# local
|
|
24
|
+
.env
|
|
25
|
+
.DS_Store
|
nomba-0.1.1/CHANGELOG.md
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.1] - 2026-11-03
|
|
4
|
+
|
|
5
|
+
First release on PyPI. Version 0.1.0 was never published: PyPI had already used that filename for an earlier, deleted `nomba` project, and filenames can't be reused.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `Nomba` (sync) and `AsyncNomba` (async) clients with the same resources: `auth`, `accounts`, `virtual_accounts`, `checkout`, `transfers`, `transactions`, `terminals`, `direct_debits`, `bills`, `global_payout`, `global_collection`, `webhooks`.
|
|
10
|
+
- Configuration from arguments or `NOMBA_*` environment variables, sandbox by default.
|
|
11
|
+
- Automatic access tokens: issued on first use, refreshed before expiry, shared between concurrent calls, retried once on a 401. Pluggable token store.
|
|
12
|
+
- Error classes for each HTTP status, plus connection and timeout errors.
|
|
13
|
+
- Automatic retries for GET requests and token calls only.
|
|
14
|
+
- `timeout` bounds the whole attempt, including reading the response body, not just each connection phase.
|
|
15
|
+
- Generated `merchantTxRef` (and `orderReference` for saved-card charges) when you don't pass one, returned in the result and attached to any error the call raises as `err.reference`.
|
|
16
|
+
- Path parameter validation and encoding.
|
|
17
|
+
- Cursor pagination helpers (`*_all`).
|
|
18
|
+
- Webhook signature verification with an exact constant-time compare, usable without a client (`nomba.webhooks.verify_signature`), with optional timestamp tolerance. Rejects non-RFC 3339 timestamps and `responseCode` values containing `:`, which would make the signed fields ambiguous.
|
|
19
|
+
- `TypedDict` types for params and responses; `TransactionStatus` and `WebhookEventType` enums.
|
nomba-0.1.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nomba
|
|
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.
|
nomba-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: nomba
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Official Python SDK for the Nomba API: transfers, checkout, virtual accounts, bills and webhooks.
|
|
5
|
+
Project-URL: Homepage, https://github.com/NombaOSS/nomba-python
|
|
6
|
+
Project-URL: Documentation, https://developer.nomba.com
|
|
7
|
+
Project-URL: Source, https://github.com/NombaOSS/nomba-python
|
|
8
|
+
Project-URL: Changelog, https://github.com/NombaOSS/nomba-python/blob/main/CHANGELOG.md
|
|
9
|
+
Project-URL: Issues, https://github.com/NombaOSS/nomba-python/issues
|
|
10
|
+
Author-email: Nomba <security@nomba.com>
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: api,fintech,nigeria,nomba,payments,sdk
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
24
|
+
Classifier: Topic :: Office/Business :: Financial
|
|
25
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
26
|
+
Classifier: Typing :: Typed
|
|
27
|
+
Requires-Python: >=3.10
|
|
28
|
+
Requires-Dist: httpx>=0.27
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# nomba-python
|
|
32
|
+
|
|
33
|
+
Official Python SDK for the [Nomba API](https://developer.nomba.com): transfers, checkout, virtual accounts, bills, terminals, direct debits, global payouts and webhooks.
|
|
34
|
+
|
|
35
|
+
[](https://github.com/NombaOSS/nomba-python/actions/workflows/ci.yml)
|
|
36
|
+
[](https://pypi.org/project/nomba/)
|
|
37
|
+
[](LICENSE)
|
|
38
|
+
|
|
39
|
+
Sync and async clients, typed params and responses (`TypedDict`), automatic token handling, safe retries, and webhook verification. Python 3.10 or newer. The only dependency is [httpx](https://www.python-httpx.org/).
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install nomba
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Quickstart
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
from nomba import Nomba
|
|
51
|
+
|
|
52
|
+
nomba = Nomba(
|
|
53
|
+
client_id="...", # or NOMBA_CLIENT_ID
|
|
54
|
+
client_secret="...", # or NOMBA_CLIENT_SECRET
|
|
55
|
+
account_id="...", # or NOMBA_ACCOUNT_ID (your parent account ID)
|
|
56
|
+
) # sandbox by default
|
|
57
|
+
|
|
58
|
+
balance = nomba.accounts.get_balance()
|
|
59
|
+
print(balance["amount"], balance["currency"])
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Async is the same surface with `await`:
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from nomba import AsyncNomba
|
|
66
|
+
|
|
67
|
+
async with AsyncNomba() as nomba:
|
|
68
|
+
balance = await nomba.accounts.get_balance()
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Both clients work as context managers; otherwise call `close()` (sync) or `await aclose()` (async) when you're done. Every method returns the `data` field of Nomba's response envelope.
|
|
72
|
+
|
|
73
|
+
## Configuration
|
|
74
|
+
|
|
75
|
+
| Option | Default | Notes |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| `client_id` | env `NOMBA_CLIENT_ID` | Required. |
|
|
78
|
+
| `client_secret` | env `NOMBA_CLIENT_SECRET` | Required. Never logged or shown in `repr()`. |
|
|
79
|
+
| `account_id` | env `NOMBA_ACCOUNT_ID` | Required. Your parent account ID, sent as the `accountId` header on every request. |
|
|
80
|
+
| `environment` | env `NOMBA_ENVIRONMENT`, else `"sandbox"` | `"sandbox"` (`https://sandbox.nomba.com`) or `"production"` (`https://api.nomba.com`). Anything else is rejected. |
|
|
81
|
+
| `base_url` | from `environment` | Override for proxies or tests. Must be `https://` unless the host is `localhost` or `127.0.0.1`. |
|
|
82
|
+
| `timeout` | `30` | Seconds. The total time one attempt may take, from connecting to reading the last byte of the body. |
|
|
83
|
+
| `max_retries` | `2` | Automatic retries for GET requests and token calls. See [Retries](#retries-and-idempotency). |
|
|
84
|
+
| `token_store` | in-memory | Where the access token is cached. See [Token store](#token-store). |
|
|
85
|
+
| `http_client` | a new `httpx.Client` / `httpx.AsyncClient` | Bring your own for proxies, custom TLS or connection limits. The SDK won't close a client you pass in. |
|
|
86
|
+
|
|
87
|
+
A missing credential raises `ConfigurationError` at construction, naming the option and environment variable. No network call happens until your first API call.
|
|
88
|
+
|
|
89
|
+
## Resources
|
|
90
|
+
|
|
91
|
+
Body parameters are dicts with Nomba's field names (`accountNumber`, `merchantTxRef`, ...), typed as `TypedDict`s in `nomba.types`. Query and path options are snake_case keyword arguments. Methods that can act for a sub-account take `sub_account_id=`.
|
|
92
|
+
|
|
93
|
+
### Accounts
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
parent = nomba.accounts.get_parent()
|
|
97
|
+
balance = nomba.accounts.get_balance(sub_account_id="...") # omit for the parent account
|
|
98
|
+
sub = nomba.accounts.get_sub_account(account_ref="merchant-42") # or account_id=
|
|
99
|
+
page = nomba.accounts.list_terminals(limit=50)
|
|
100
|
+
for terminal in nomba.accounts.list_all_terminals():
|
|
101
|
+
print(terminal["terminalId"])
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Virtual accounts
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
va = nomba.virtual_accounts.create({"accountRef": "invoice-1042", "accountName": "Ada Lovelace"})
|
|
108
|
+
nomba.virtual_accounts.get("invoice-1042")
|
|
109
|
+
nomba.virtual_accounts.update("invoice-1042", {"expectedAmount": "5000.00"})
|
|
110
|
+
nomba.virtual_accounts.expire("invoice-1042")
|
|
111
|
+
for account in nomba.virtual_accounts.list_all({"expired": False}):
|
|
112
|
+
print(account["bankAccountNumber"])
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### Transfers
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
banks = nomba.transfers.list_banks()
|
|
119
|
+
who = nomba.transfers.lookup_account(account_number="0123456789", bank_code="058")
|
|
120
|
+
transfer = nomba.transfers.to_bank(
|
|
121
|
+
{
|
|
122
|
+
"amount": 3500,
|
|
123
|
+
"accountNumber": "0123456789",
|
|
124
|
+
"accountName": who["accountName"],
|
|
125
|
+
"bankCode": "058",
|
|
126
|
+
"merchantTxRef": "payout-1042", # optional, generated if omitted
|
|
127
|
+
"narration": "Invoice 1042",
|
|
128
|
+
}
|
|
129
|
+
)
|
|
130
|
+
nomba.transfers.to_wallet({"amount": 500, "receiverAccountId": "..."})
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Transactions
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
nomba.transactions.get(merchant_tx_ref="payout-1042") # or transaction_ref=, order_reference=, order_id=
|
|
137
|
+
nomba.transactions.requery(session_id) # by NIP session ID
|
|
138
|
+
page = nomba.transactions.list(limit=50, date_from="2026-01-01T00:00:00Z")
|
|
139
|
+
for txn in nomba.transactions.filter_all({"status": "SUCCESS", "source": "api"}):
|
|
140
|
+
...
|
|
141
|
+
for entry in nomba.transactions.list_all_bank_transactions():
|
|
142
|
+
...
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Checkout
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
order = nomba.checkout.create_order(
|
|
149
|
+
{
|
|
150
|
+
"order": {
|
|
151
|
+
"callbackUrl": "https://merchant.example.com/done",
|
|
152
|
+
"customerEmail": "ada@example.com",
|
|
153
|
+
"amount": "5000.00",
|
|
154
|
+
"currency": "NGN",
|
|
155
|
+
},
|
|
156
|
+
"tokenizeCard": True,
|
|
157
|
+
}
|
|
158
|
+
)
|
|
159
|
+
redirect_to(order["checkoutLink"])
|
|
160
|
+
|
|
161
|
+
nomba.checkout.get_transaction(id_type="ORDER_REFERENCE", id=order["orderReference"])
|
|
162
|
+
nomba.checkout.refund({"transactionId": "..."})
|
|
163
|
+
|
|
164
|
+
charge = nomba.checkout.charge_saved_card(
|
|
165
|
+
{
|
|
166
|
+
"tokenKey": "...",
|
|
167
|
+
"order": {"customerEmail": "ada@example.com", "amount": "5000.00", "currency": "NGN"},
|
|
168
|
+
}
|
|
169
|
+
)
|
|
170
|
+
if charge.get("otpRequired"):
|
|
171
|
+
nomba.checkout.submit_card_otp(
|
|
172
|
+
{"otp": "...", "orderReference": charge["orderReference"], "transactionId": charge["transactionId"]}
|
|
173
|
+
)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Also: `get_order`, `cancel_order`, `list_saved_cards`, `update_saved_card_email`, `delete_saved_card`, `get_transfer_account`, `submit_card_details`, `resend_otp`, `confirm_transaction_receipt`, `cancel_transaction`, `request_save_card_otp`, `request_saved_cards_otp`, `submit_user_otp`, `get_saved_cards`.
|
|
177
|
+
|
|
178
|
+
### Terminals
|
|
179
|
+
|
|
180
|
+
```python
|
|
181
|
+
nomba.terminals.assign({"serialNumber": "91230309116826", "terminalLabel": "Till 1"})
|
|
182
|
+
nomba.terminals.unassign({"serialNumber": "91230309116826"}, sub_account_id="...")
|
|
183
|
+
nomba.terminals.send_payment_request("2KUD4AKB", {"merchantTxRef": "pos-1", "amount": 1000, "currency": "NGN"})
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### Direct debits
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
mandate = nomba.direct_debits.create_mandate({...})
|
|
190
|
+
nomba.direct_debits.get_mandate_status(mandate["mandateId"])
|
|
191
|
+
nomba.direct_debits.debit_mandate({"mandateId": mandate["mandateId"], "amount": "110.00"})
|
|
192
|
+
nomba.direct_debits.list_mandates(page=1, page_size=20, from_="2026-01-01", to="2026-02-01")
|
|
193
|
+
nomba.direct_debits.update_mandate_status({"mandateId": "...", "status": "SUSPEND"})
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
`from` is a Python keyword, so the date filter is `from_=`; it is sent as `from`.
|
|
197
|
+
|
|
198
|
+
### Bills
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
nomba.bills.buy_airtime({"amount": 200, "phoneNumber": "08055441122", "network": "MTN"})
|
|
202
|
+
plans = nomba.bills.list_data_plans("mtn")
|
|
203
|
+
nomba.bills.buy_data({"productId": plans[0]["productId"], "phoneNumber": "08055441122", "network": "MTN"})
|
|
204
|
+
customer = nomba.bills.lookup_electricity_customer(disco="jed", customer_id="12345678902")
|
|
205
|
+
nomba.bills.buy_electricity(
|
|
206
|
+
{"disco": "jed", "customerId": "12345678902", "amount": 1000, "meterType": "prepaid", "payerName": customer["name"]}
|
|
207
|
+
)
|
|
208
|
+
nomba.bills.pay_cable_tv({"cableTvType": "dstv", "customerId": "...", "amount": 19000})
|
|
209
|
+
nomba.bills.fund_betting({"bettingProvider": "bet9ja", "customerId": "...", "amount": 1000})
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Also: `list_electricity_providers`, `list_cable_tv_plans`, `lookup_cable_tv_customer`, `list_betting_providers`, `lookup_betting_customer`. Purchases use the v2 endpoints.
|
|
213
|
+
|
|
214
|
+
### Global payout and collection
|
|
215
|
+
|
|
216
|
+
```python
|
|
217
|
+
accounts = nomba.global_payout.list_accounts() # sandbox uses Nomba's sandbox account paths
|
|
218
|
+
rates = nomba.global_payout.get_exchange_rates(from_="USD", to="CDF")
|
|
219
|
+
quote = nomba.global_payout.convert_money(
|
|
220
|
+
{"amount": 15, "currency": "USD", "destinationCurrency": "EUR", "transactionType": "EXCHANGE"}
|
|
221
|
+
)
|
|
222
|
+
nomba.global_payout.authorize_transfer({...})
|
|
223
|
+
nomba.global_collection.initiate_inflow(
|
|
224
|
+
{"phoneNumber": "...", "callbackUrl": "...", "amount": 10, "currency": "CDF", "topupVendor": "AIRTEL"}
|
|
225
|
+
)
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Also: `get_account`, `list_bank_providers`, `list_payment_methods`, `get_transaction`, `authorize_exchange`, `global_collection.list_drc_providers`, `global_collection.get_transaction`.
|
|
229
|
+
|
|
230
|
+
### Auth
|
|
231
|
+
|
|
232
|
+
Tokens are handled for you. `nomba.auth.revoke()` revokes the cached access token and clears the token store.
|
|
233
|
+
|
|
234
|
+
## Errors
|
|
235
|
+
|
|
236
|
+
All errors inherit from `nomba.NombaError`.
|
|
237
|
+
|
|
238
|
+
| Exception | When |
|
|
239
|
+
|---|---|
|
|
240
|
+
| `ConfigurationError` | Missing or invalid client options. |
|
|
241
|
+
| `AuthenticationError` | 401 after the SDK refreshed the token and retried once, or the token could not be issued. |
|
|
242
|
+
| `PermissionDeniedError` | 403 |
|
|
243
|
+
| `NotFoundError` | 404 |
|
|
244
|
+
| `ValidationError` | 400 or 422, or a client-side check failed before sending (bad path parameter, missing argument, `limit` over 50). Client-side ones have `http_status=None`. |
|
|
245
|
+
| `RateLimitError` | 429. `retry_after` (seconds) and `rate_limit_limit`, `rate_limit_remaining`, `rate_limit_window` from the `X-Rate-Limit-*` headers. |
|
|
246
|
+
| `ServerError` | 5xx |
|
|
247
|
+
| `ApiError` | HTTP 2xx but a non-success `code`. Also the parent of the five classes above. |
|
|
248
|
+
| `APIConnectionError` | Network failure. |
|
|
249
|
+
| `APITimeoutError` | Timeout (a subclass of `APIConnectionError`). |
|
|
250
|
+
| `WebhookVerificationError` | See [Webhooks](#webhooks). |
|
|
251
|
+
|
|
252
|
+
`ApiError` carries `http_status`, `code`, `description` (Nomba's message), `body` (parsed response), `method` and `path`. Every error from a money-moving call also carries `reference` (see [Retries and idempotency](#retries-and-idempotency)):
|
|
253
|
+
|
|
254
|
+
```python
|
|
255
|
+
from nomba import ApiError, RateLimitError
|
|
256
|
+
|
|
257
|
+
try:
|
|
258
|
+
nomba.transfers.to_bank({...})
|
|
259
|
+
except RateLimitError as err:
|
|
260
|
+
wait(err.retry_after)
|
|
261
|
+
except ApiError as err:
|
|
262
|
+
print(err) # Nomba API error 400 (code "400"): Invalid account number [POST /v2/transfers/bank]
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Messages never include your client secret, tokens, headers or query strings.
|
|
266
|
+
|
|
267
|
+
## Retries and idempotency
|
|
268
|
+
|
|
269
|
+
GET requests and token calls are retried automatically on connection errors, timeouts, 429 and 5xx, up to `max_retries` times, with exponential backoff and jitter (about 0.5 s, 1 s, 2 s, up to 8 s). On a 429 the SDK waits for `Retry-After` (capped at 60 s).
|
|
270
|
+
|
|
271
|
+
POST, PUT and DELETE are **never** retried automatically. A timeout or a 502 doesn't tell you whether Nomba processed the request, and retrying a transfer could send the money twice. The one exception is a 401: Nomba rejected the token, so the request wasn't processed, and the SDK gets a new token and retries once.
|
|
272
|
+
|
|
273
|
+
For money-moving calls (`transfers.to_bank`, `transfers.to_wallet`, every `bills` purchase), the SDK sends a `merchantTxRef`. Pass your own, store it before you call, and reuse it if you retry: Nomba uses it to reject duplicates. If you leave it out, the SDK generates a random UUID v4 and returns it in the result as `result["merchantTxRef"]`. `checkout.charge_saved_card` has no `merchantTxRef`; its idempotency key is `order.orderReference`, which the SDK generates the same way and returns as `result["orderReference"]`.
|
|
274
|
+
|
|
275
|
+
Every error raised by a money-moving call has `err.reference`, a `Reference` with `field` (`"merchantTxRef"`, or `"orderReference"` for saved-card charges) and `value` (yours or the generated one), so you never lose it. Other calls leave it `None`.
|
|
276
|
+
|
|
277
|
+
On `APITimeoutError`, `APIConnectionError` or `ServerError` the outcome is unknown. Don't send a new request with a new reference. Requery with `err.reference.value` first, and if Nomba has no record of it, retry with the **same** reference:
|
|
278
|
+
|
|
279
|
+
```python
|
|
280
|
+
from nomba import APIConnectionError, NotFoundError, ServerError
|
|
281
|
+
|
|
282
|
+
try:
|
|
283
|
+
result = nomba.transfers.to_bank(params)
|
|
284
|
+
except (APIConnectionError, ServerError) as err: # APITimeoutError is an APIConnectionError
|
|
285
|
+
ref = err.reference
|
|
286
|
+
try:
|
|
287
|
+
result = nomba.transactions.get(merchant_tx_ref=ref.value)
|
|
288
|
+
except NotFoundError:
|
|
289
|
+
result = nomba.transfers.to_bank({**params, ref.field: ref.value})
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
See [examples/transfer_with_requery.py](examples/transfer_with_requery.py).
|
|
293
|
+
|
|
294
|
+
## Transaction statuses
|
|
295
|
+
|
|
296
|
+
The SDK never raises because of `data.status`. Check it yourself; the values are in `nomba.TransactionStatus`.
|
|
297
|
+
|
|
298
|
+
| `data.status` | What happened | Refund your customer? |
|
|
299
|
+
|---|---|---|
|
|
300
|
+
| `SUCCESS` | Delivered to the destination. | No |
|
|
301
|
+
| `REFUND` | Funds returned to your Nomba wallet. | Yes |
|
|
302
|
+
| `CANCELLED` | The transaction won't proceed. | Contact Nomba support |
|
|
303
|
+
| `PAYMENT_FAILED` | The debit failed; Nomba may retry. | Wait for the webhook |
|
|
304
|
+
| `REVERSED_BY_VENDOR` | Nomba may retry through another vendor. | Wait for the webhook |
|
|
305
|
+
| `PENDING_BILLING` | Still processing. | Wait for the webhook |
|
|
306
|
+
| `PROCESSING` | Still processing (v1 endpoints). | Wait for the webhook |
|
|
307
|
+
|
|
308
|
+
If you get a status that isn't listed, requery or wait for the webhook. Only refund on a `payout_refund` webhook or a requery that returns `REFUND`. Source: [Response codes](https://developer.nomba.com/docs/api-basics/response-codes).
|
|
309
|
+
|
|
310
|
+
## Pagination
|
|
311
|
+
|
|
312
|
+
List endpoints return one page: `{"results": [...], "cursor": "..."}`. Pass the cursor back to get the next page; an empty cursor means the last page. `limit` is at most 50.
|
|
313
|
+
|
|
314
|
+
The `*_all` helpers follow the cursors for you and yield items:
|
|
315
|
+
|
|
316
|
+
```python
|
|
317
|
+
for txn in nomba.transactions.list_all(limit=50, date_from="2026-01-01T00:00:00Z"):
|
|
318
|
+
...
|
|
319
|
+
|
|
320
|
+
async for txn in async_nomba.transactions.list_all():
|
|
321
|
+
...
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
Helpers: `transactions.list_all`, `transactions.filter_all`, `transactions.list_all_bank_transactions`, `accounts.list_all_terminals`, `virtual_accounts.list_all`. `direct_debits.list_mandates` and `checkout.list_saved_cards` use page numbers instead.
|
|
325
|
+
|
|
326
|
+
## Webhooks
|
|
327
|
+
|
|
328
|
+
Verify every webhook before you trust it. You don't need API credentials for this:
|
|
329
|
+
|
|
330
|
+
```python
|
|
331
|
+
from flask import Flask, abort, request
|
|
332
|
+
from nomba.webhooks import verify_signature
|
|
333
|
+
from nomba import WebhookEventType, WebhookVerificationError
|
|
334
|
+
|
|
335
|
+
app = Flask(__name__)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
@app.post("/webhooks/nomba")
|
|
339
|
+
def nomba_webhook():
|
|
340
|
+
try:
|
|
341
|
+
event = verify_signature(request.get_data(), request.headers, SIGNING_KEY, tolerance_seconds=300)
|
|
342
|
+
except WebhookVerificationError:
|
|
343
|
+
abort(401)
|
|
344
|
+
if event["event_type"] == WebhookEventType.PAYMENT_SUCCESS:
|
|
345
|
+
...
|
|
346
|
+
return "", 200
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
**Pass the raw request body** (`request.get_data()` in Flask, `await request.body()` in FastAPI), as bytes or str. Don't parse it and re-encode it.
|
|
350
|
+
|
|
351
|
+
The signature is a base64 HMAC-SHA256 of fields from the body and the `nomba-timestamp` header, keyed with the signing key from your Nomba dashboard. The SDK reads `nomba-signature` (or `nomba-sig-value`) and `nomba-timestamp` case-insensitively, rejects any `nomba-signature-algorithm` other than `HmacSHA256`, and compares the signature exactly, in constant time. Base64 is case-sensitive, so a signature that differs only in letter case is rejected.
|
|
352
|
+
|
|
353
|
+
Because the signed fields are joined with `:` and the timestamp itself contains colons, the SDK always requires `nomba-timestamp` to be a strict RFC 3339 timestamp (for example `2025-09-29T10:51:44Z`) and rejects a webhook whose `data.transaction.responseCode` contains `:`. Otherwise part of the timestamp could be moved into the body without changing the signature.
|
|
354
|
+
|
|
355
|
+
`tolerance_seconds` (off by default) rejects webhooks whose timestamp is further than that from now, which limits replays. Nomba retries failed deliveries, so make your handler idempotent on `event["requestId"]` either way.
|
|
356
|
+
|
|
357
|
+
From a client you can also call `nomba.webhooks.verify(raw_body, headers, signing_key)`. Event types are in `nomba.WebhookEventType`: `payment_success`, `payment_failed`, `payment_reversal`, `payout_success`, `payout_failed`, `payout_refund`. See [examples/webhook_flask.py](examples/webhook_flask.py) and [examples/webhook_fastapi.py](examples/webhook_fastapi.py).
|
|
358
|
+
|
|
359
|
+
## Token store
|
|
360
|
+
|
|
361
|
+
The client gets an access token on the first call, refreshes it within 5 minutes of expiry, and shares one in-flight token request between concurrent calls. Tokens live in memory by default.
|
|
362
|
+
|
|
363
|
+
If you run several processes, Nomba recommends a single authenticating instance. Implement a store to share one token:
|
|
364
|
+
|
|
365
|
+
```python
|
|
366
|
+
from nomba import Nomba, Token
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
class RedisTokenStore:
|
|
370
|
+
def get(self) -> Token | None: ...
|
|
371
|
+
def set(self, token: Token | None) -> None: ...
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
nomba = Nomba(token_store=RedisTokenStore())
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
`AsyncNomba` accepts the same sync interface or an async one (`async def get` / `async def set`). `Token` is a frozen dataclass with `access_token`, `refresh_token` and `expires_at` (an aware `datetime`); store and restore those three fields.
|
|
378
|
+
|
|
379
|
+
## Sandbox and production
|
|
380
|
+
|
|
381
|
+
The client uses the sandbox unless you pass `environment="production"` or set `NOMBA_ENVIRONMENT=production`. Sandbox and production use different credentials. Global Payout account endpoints use Nomba's separate sandbox paths in the sandbox; the SDK picks the right one.
|
|
382
|
+
|
|
383
|
+
## Links
|
|
384
|
+
|
|
385
|
+
- API docs: https://developer.nomba.com
|
|
386
|
+
- OpenAPI spec: https://github.com/NombaOSS/openapi
|
|
387
|
+
- [Changelog](CHANGELOG.md) · [Security](SECURITY.md) · [Releasing](RELEASING.md)
|
|
388
|
+
|
|
389
|
+
## License
|
|
390
|
+
|
|
391
|
+
MIT. See [LICENSE](LICENSE).
|