nylonpay-py 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.
- nylonpay_py-0.1.0/.env.example +7 -0
- nylonpay_py-0.1.0/.gitignore +32 -0
- nylonpay_py-0.1.0/.trash/result.py +77 -0
- nylonpay_py-0.1.0/LICENSE +21 -0
- nylonpay_py-0.1.0/PKG-INFO +416 -0
- nylonpay_py-0.1.0/README.md +372 -0
- nylonpay_py-0.1.0/py.typed +0 -0
- nylonpay_py-0.1.0/pyproject.toml +72 -0
- nylonpay_py-0.1.0/src/nylonpay/__init__.py +116 -0
- nylonpay_py-0.1.0/src/nylonpay/coerce.py +104 -0
- nylonpay_py-0.1.0/src/nylonpay/config.py +51 -0
- nylonpay_py-0.1.0/src/nylonpay/factory.py +78 -0
- nylonpay_py-0.1.0/src/nylonpay/fingerprint.py +41 -0
- nylonpay_py-0.1.0/src/nylonpay/nonce.py +21 -0
- nylonpay_py-0.1.0/src/nylonpay/payment.py +281 -0
- nylonpay_py-0.1.0/src/nylonpay/phone.py +39 -0
- nylonpay_py-0.1.0/src/nylonpay/pubsub.py +114 -0
- nylonpay_py-0.1.0/src/nylonpay/sdk.py +499 -0
- nylonpay_py-0.1.0/src/nylonpay/signature.py +96 -0
- nylonpay_py-0.1.0/src/nylonpay/slang.py +116 -0
- nylonpay_py-0.1.0/src/nylonpay/transport.py +449 -0
- nylonpay_py-0.1.0/src/nylonpay/types.py +572 -0
- nylonpay_py-0.1.0/src/nylonpay/verify_response.py +35 -0
- nylonpay_py-0.1.0/src/nylonpay/verify_webhook.py +116 -0
- nylonpay_py-0.1.0/src/nylonpay/wire.py +88 -0
- nylonpay_py-0.1.0/task.md +94 -0
- nylonpay_py-0.1.0/tests/__init__.py +0 -0
- nylonpay_py-0.1.0/tests/integration/__init__.py +0 -0
- nylonpay_py-0.1.0/tests/integration/conftest.py +26 -0
- nylonpay_py-0.1.0/tests/integration/test_integration.py +349 -0
- nylonpay_py-0.1.0/tests/security/__init__.py +0 -0
- nylonpay_py-0.1.0/tests/security/test_security.py +473 -0
- nylonpay_py-0.1.0/tests/unit/__init__.py +0 -0
- nylonpay_py-0.1.0/tests/unit/test_coerce.py +309 -0
- nylonpay_py-0.1.0/tests/unit/test_factory.py +65 -0
- nylonpay_py-0.1.0/tests/unit/test_fingerprint.py +19 -0
- nylonpay_py-0.1.0/tests/unit/test_nonce.py +27 -0
- nylonpay_py-0.1.0/tests/unit/test_phone.py +51 -0
- nylonpay_py-0.1.0/tests/unit/test_pubsub.py +88 -0
- nylonpay_py-0.1.0/tests/unit/test_sdk.py +383 -0
- nylonpay_py-0.1.0/tests/unit/test_signature.py +125 -0
- nylonpay_py-0.1.0/tests/unit/test_signing_conformance.py +132 -0
- nylonpay_py-0.1.0/tests/unit/test_slang.py +85 -0
- nylonpay_py-0.1.0/tests/unit/test_transport.py +271 -0
- nylonpay_py-0.1.0/tests/unit/test_verify_response.py +48 -0
- nylonpay_py-0.1.0/tests/unit/test_verify_webhook.py +120 -0
- nylonpay_py-0.1.0/tests/unit/test_wire.py +140 -0
- nylonpay_py-0.1.0/uv.lock +347 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Nylon Pay SDK — integration test credentials
|
|
2
|
+
# Copy to .env and fill with your credentials.
|
|
3
|
+
NYLONPAY_API_KEY=npk_your_key_here
|
|
4
|
+
NYLONPAY_API_SECRET=nps_your_secret_here
|
|
5
|
+
NYLONPAY_TEST_PHONE=076XXXXXXX
|
|
6
|
+
# Set to "live" for live-only tests (I15)
|
|
7
|
+
NYLONPAY_TEST_MODE=sandbox
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
*.egg-info/
|
|
7
|
+
*.egg
|
|
8
|
+
dist/
|
|
9
|
+
build/
|
|
10
|
+
.eggs/
|
|
11
|
+
|
|
12
|
+
# Virtual environments
|
|
13
|
+
.venv/
|
|
14
|
+
venv/
|
|
15
|
+
env/
|
|
16
|
+
|
|
17
|
+
# Environment
|
|
18
|
+
.env
|
|
19
|
+
|
|
20
|
+
# IDE
|
|
21
|
+
.idea/
|
|
22
|
+
.vscode/
|
|
23
|
+
*.swp
|
|
24
|
+
*.swo
|
|
25
|
+
|
|
26
|
+
# OS
|
|
27
|
+
.DS_Store
|
|
28
|
+
Thumbs.db
|
|
29
|
+
|
|
30
|
+
# Test
|
|
31
|
+
.pytest_cache/
|
|
32
|
+
.ruff_cache/
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Result type for operations that can fail. Mirrors slang-ts Result.
|
|
2
|
+
|
|
3
|
+
Use ``Ok(value)`` for success, ``Err(error)`` for failure.
|
|
4
|
+
Check ``.is_ok`` / ``.is_err`` before accessing ``.value`` / ``.error``.
|
|
5
|
+
|
|
6
|
+
WHY a custom Result type: the SDK distinguishes programmer errors (invalid
|
|
7
|
+
config, missing required fields — thrown) from operational errors (network
|
|
8
|
+
failures, provider rejections, timeouts — returned as results). This mirrors
|
|
9
|
+
the TypeScript SDK's use of ``slang-ts`` Result and keeps the error boundary
|
|
10
|
+
explicit at every call site.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from typing import Generic, TypeVar, cast
|
|
17
|
+
|
|
18
|
+
T = TypeVar("T")
|
|
19
|
+
E = TypeVar("E")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class Result(Generic[T, E]):
|
|
24
|
+
"""A value that is either ok (success) or err (failure).
|
|
25
|
+
|
|
26
|
+
Construct with ``Result.ok(value)`` or ``Result.err(error)``.
|
|
27
|
+
Never construct directly — the class methods enforce the invariant
|
|
28
|
+
that exactly one of value/error is set.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
_is_ok: bool
|
|
32
|
+
_value: T | None = None
|
|
33
|
+
_error: E | None = None
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def ok(cls, value: T) -> Result[T, E]:
|
|
37
|
+
"""Create a successful result carrying ``value``."""
|
|
38
|
+
return cls(_is_ok=True, _value=value)
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def err(cls, error: E) -> Result[T, E]:
|
|
42
|
+
"""Create an error result carrying ``error``."""
|
|
43
|
+
return cls(_is_ok=False, _error=error)
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def is_ok(self) -> bool:
|
|
47
|
+
"""True if this is a success result."""
|
|
48
|
+
return self._is_ok
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def is_err(self) -> bool:
|
|
52
|
+
"""True if this is an error result."""
|
|
53
|
+
return not self._is_ok
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def value(self) -> T:
|
|
57
|
+
"""The success value. Raises ``ValueError`` if accessed on an error result."""
|
|
58
|
+
if not self._is_ok:
|
|
59
|
+
raise ValueError("Cannot access .value on an error result")
|
|
60
|
+
return cast("T", self._value)
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def error(self) -> E:
|
|
64
|
+
"""The error value. Raises ``ValueError`` if accessed on a success result."""
|
|
65
|
+
if self._is_ok:
|
|
66
|
+
raise ValueError("Cannot access .error on a success result")
|
|
67
|
+
return cast("E", self._error)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def Ok(value: T) -> Result[T, E]:
|
|
71
|
+
"""Create a successful result. Convenience alias for ``Result.ok(value)``."""
|
|
72
|
+
return Result(_is_ok=True, _value=value)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def Err(error: E) -> Result[T, E]:
|
|
76
|
+
"""Create an error result. Convenience alias for ``Result.err(error)``."""
|
|
77
|
+
return Result(_is_ok=False, _error=error)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nile Squad
|
|
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,416 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nylonpay-py
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Nylon Pay SDK for merchant integrations
|
|
5
|
+
Project-URL: Homepage, https://github.com/nile-squad/nylonpay-py#readme
|
|
6
|
+
Project-URL: Repository, https://github.com/nile-squad/nylonpay-py
|
|
7
|
+
Project-URL: Issues, https://github.com/nile-squad/nylonpay-py/issues
|
|
8
|
+
Author-email: Nile Squad <contact@nilesquad.com>
|
|
9
|
+
License: MIT License
|
|
10
|
+
|
|
11
|
+
Copyright (c) 2026 Nile Squad
|
|
12
|
+
|
|
13
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
14
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
15
|
+
in the Software without restriction, including without limitation the rights
|
|
16
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
17
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
18
|
+
furnished to do so, subject to the following conditions:
|
|
19
|
+
|
|
20
|
+
The above copyright notice and this permission notice shall be included in all
|
|
21
|
+
copies or substantial portions of the Software.
|
|
22
|
+
|
|
23
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
24
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
25
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
26
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
27
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
28
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
29
|
+
SOFTWARE.
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Keywords: fintech,mobile-money,nylon-pay,nylonpay,payments,payouts,python,sdk
|
|
32
|
+
Classifier: Development Status :: 3 - Alpha
|
|
33
|
+
Classifier: Intended Audience :: Developers
|
|
34
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
35
|
+
Classifier: Programming Language :: Python :: 3
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
40
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
41
|
+
Requires-Python: >=3.10
|
|
42
|
+
Requires-Dist: httpx>=0.27
|
|
43
|
+
Description-Content-Type: text/markdown
|
|
44
|
+
|
|
45
|
+
# nylonpay-py
|
|
46
|
+
|
|
47
|
+
Server-side SDK for integrating Nylon Pay payment operations into your Python application.
|
|
48
|
+
|
|
49
|
+
[Full documentation](https://docs.nylonpay.nilesquad.com/docs)
|
|
50
|
+
|
|
51
|
+
## Install
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install nylonpay-py
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Requires Python 3.10+.
|
|
58
|
+
|
|
59
|
+
## Quick Start
|
|
60
|
+
|
|
61
|
+
Create a client, initiate a payment, subscribe to events, and wait for completion.
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
from nylonpay import create_nylon_pay
|
|
65
|
+
import secrets
|
|
66
|
+
|
|
67
|
+
nylonpay = create_nylon_pay(
|
|
68
|
+
api_key="npk_test_...",
|
|
69
|
+
api_secret="nps_test_...",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
payment = nylonpay.collect_payment(
|
|
73
|
+
amount=10000,
|
|
74
|
+
currency="UGX",
|
|
75
|
+
customer={"name": "Jane", "phone_number": "+256700000000"},
|
|
76
|
+
description="Order #1234",
|
|
77
|
+
reference=secrets.token_hex(7),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
def on_success(data):
|
|
81
|
+
print(f"Paid: {data.transaction.reference}")
|
|
82
|
+
|
|
83
|
+
def on_failed(data):
|
|
84
|
+
print(f"Failed: {data.error}")
|
|
85
|
+
|
|
86
|
+
payment.on("success", on_success)
|
|
87
|
+
payment.on("failed", on_failed)
|
|
88
|
+
|
|
89
|
+
tx = payment.wait()
|
|
90
|
+
if tx is not None:
|
|
91
|
+
print(f"Transaction {tx.reference} completed")
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Configuration
|
|
95
|
+
|
|
96
|
+
Create the SDK instance with `create_nylon_pay()`. All options are keyword arguments.
|
|
97
|
+
|
|
98
|
+
Your API key determines the mode — `npk_sandbox_...` for test mode, `npk_live_...` for production. There is no separate `mode` option.
|
|
99
|
+
|
|
100
|
+
| Field | Required | Default | Description |
|
|
101
|
+
|---|---|---|---|
|
|
102
|
+
| `api_key` | Yes | | Must start with `npk_` |
|
|
103
|
+
| `api_secret` | Yes | | Must start with `nps_` |
|
|
104
|
+
| `base_url` | No | `https://api.nylonpay.nilesquad.com/api/services` | Override for a custom endpoint |
|
|
105
|
+
| `timeout_ms` | No | `30000` | Request timeout in milliseconds |
|
|
106
|
+
| `max_retries` | No | `3` | Retry count for failed requests |
|
|
107
|
+
| `max_poll_interval_ms` | No | `2000` | Interval between status checks |
|
|
108
|
+
| `max_poll_duration_ms` | No | `300000` | Maximum wait time for `wait()` (about 5 minutes) |
|
|
109
|
+
| `max_poll_attempts` | No | `150` | Maximum status check attempts |
|
|
110
|
+
| `force` | No | `False` | Bypass instance cache and create a fresh instance |
|
|
111
|
+
| `hooks` | No | `None` | Lifecycle hooks (`SdkHooks`) for cross-cutting concerns |
|
|
112
|
+
| `http_client` | No | `None` | `httpx.Client` for testing injection |
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
nylonpay = create_nylon_pay(
|
|
116
|
+
api_key="npk_test_...",
|
|
117
|
+
api_secret="nps_test_...",
|
|
118
|
+
timeout_ms=15000,
|
|
119
|
+
max_retries=5,
|
|
120
|
+
)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The factory caches instances by `api_key + base_url + sha256(api_secret)`. Rotating the secret produces a different cache key and a fresh instance. Pass `force=True` to bypass caching.
|
|
124
|
+
|
|
125
|
+
## Operations
|
|
126
|
+
|
|
127
|
+
All operations accept keyword arguments. Nested types (`customer`, `destination`, `items`) accept plain dicts — no need to import dataclasses.
|
|
128
|
+
|
|
129
|
+
### collect_payment
|
|
130
|
+
|
|
131
|
+
Initiate a payment collection. Returns a `PaymentInstance` with event-driven updates.
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
payment = nylonpay.collect_payment(
|
|
135
|
+
amount=10000,
|
|
136
|
+
currency="UGX",
|
|
137
|
+
customer={"name": "Jane", "phone_number": "+256700000000"},
|
|
138
|
+
description="Order #1234",
|
|
139
|
+
method="mobileMoney",
|
|
140
|
+
reference="ORDER-2026-001",
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
def on_success(data):
|
|
144
|
+
print(f"Paid: {data.transaction.reference}")
|
|
145
|
+
|
|
146
|
+
def on_failed(data):
|
|
147
|
+
print(f"Failed: {data.error}")
|
|
148
|
+
|
|
149
|
+
payment.on("success", on_success)
|
|
150
|
+
payment.on("failed", on_failed)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`reference` is optional and auto-generated if omitted. A supplied reference must be **13 to 15 characters**; the SDK raises `SdkException` with category `validation` otherwise. A raw UUID is 36 characters and will be rejected — use a short id of your own or omit the field.
|
|
154
|
+
|
|
155
|
+
### collect_payment_and_resolve
|
|
156
|
+
|
|
157
|
+
Block until the collection reaches a terminal state. Single request/response — the server checks status internally, no client-side waiting.
|
|
158
|
+
|
|
159
|
+
```python
|
|
160
|
+
result = nylonpay.collect_payment_and_resolve(
|
|
161
|
+
amount=5000,
|
|
162
|
+
currency="UGX",
|
|
163
|
+
customer={"name": "Jane", "phone_number": "+256700000000"},
|
|
164
|
+
description="Quick payment",
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
if result.is_ok:
|
|
168
|
+
print("Paid:", result.value.reference)
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### make_payout
|
|
172
|
+
|
|
173
|
+
Disburse funds to a destination account. Returns a `PaymentInstance` with event-driven updates.
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
payout = nylonpay.make_payout(
|
|
177
|
+
amount=50000,
|
|
178
|
+
currency="UGX",
|
|
179
|
+
customer={"name": "Jane", "phone_number": "+256700000000"},
|
|
180
|
+
destination={
|
|
181
|
+
"account_holder_name": "Jane Doe",
|
|
182
|
+
"account_number": "123456",
|
|
183
|
+
},
|
|
184
|
+
description="Refund for order #1234",
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
tx = payout.wait()
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### make_payout_and_resolve
|
|
191
|
+
|
|
192
|
+
Block until the payout reaches a terminal state. Single request/response.
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
result = nylonpay.make_payout_and_resolve(
|
|
196
|
+
amount=50000,
|
|
197
|
+
currency="UGX",
|
|
198
|
+
customer={"name": "Jane", "phone_number": "+256700000000"},
|
|
199
|
+
destination={
|
|
200
|
+
"account_holder_name": "Jane Doe",
|
|
201
|
+
"account_number": "123456",
|
|
202
|
+
},
|
|
203
|
+
description="Refund",
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
if result.is_ok:
|
|
207
|
+
print("Payout completed:", result.value.reference)
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### get_status
|
|
211
|
+
|
|
212
|
+
One-shot status check for a transaction. Does not wait — returns the current server-side state.
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
result = nylonpay.get_status(reference="ORDER-2026-001")
|
|
216
|
+
if result.is_ok:
|
|
217
|
+
print(result.value.status)
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### get_transaction
|
|
221
|
+
|
|
222
|
+
Look up a full transaction record by `id` or `reference`. At least one must be provided.
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
result = nylonpay.get_transaction(reference="ORDER-2026-001")
|
|
226
|
+
if result.is_ok:
|
|
227
|
+
print(result.value.failure_reason)
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### verify_phone
|
|
231
|
+
|
|
232
|
+
Pre-validate a phone number and get the registered name.
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
result = nylonpay.verify_phone(phone_number="+256700000000")
|
|
236
|
+
if result.is_ok and result.value.verified:
|
|
237
|
+
print("Registered to:", result.value.customer_name)
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Phone numbers are normalized automatically — any common format works: `+256 700 000 000`, `0700000000`, `256700000000` are all accepted.
|
|
241
|
+
|
|
242
|
+
### create_invoice
|
|
243
|
+
|
|
244
|
+
Generate a hosted payment link. Card payments are only supported via this hosted flow — card details never reach your servers.
|
|
245
|
+
|
|
246
|
+
```python
|
|
247
|
+
result = nylonpay.create_invoice(
|
|
248
|
+
amount=25000,
|
|
249
|
+
currency="UGX",
|
|
250
|
+
description="Monthly subscription",
|
|
251
|
+
items=[{"name": "Pro Plan", "quantity": 1, "unit_price": 25000}],
|
|
252
|
+
redirect_url="https://myapp.com/thank-you",
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
if result.is_ok:
|
|
256
|
+
print("Invoice URL:", result.value.url)
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### verify_webhook_signature
|
|
260
|
+
|
|
261
|
+
Verify incoming webhook payloads before processing. Operates on raw payload bytes or string — never re-serialize parsed JSON, which would alter the signed content.
|
|
262
|
+
|
|
263
|
+
```python
|
|
264
|
+
from nylonpay import verify_webhook_signature
|
|
265
|
+
|
|
266
|
+
is_valid = verify_webhook_signature(
|
|
267
|
+
payload=raw_payload_bytes,
|
|
268
|
+
signature=signature_header,
|
|
269
|
+
secret="nps_...",
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
if not is_valid:
|
|
273
|
+
# Reject — payload did not originate from Nylon Pay
|
|
274
|
+
...
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
The verification checks authenticity and freshness. Pass `tolerance_seconds=0` to disable the freshness check. Returns `True` only when both checks pass. Never raises.
|
|
278
|
+
|
|
279
|
+
## PaymentInstance Events
|
|
280
|
+
|
|
281
|
+
`collect_payment` and `make_payout` return a `PaymentInstance` with event-driven updates.
|
|
282
|
+
|
|
283
|
+
| Event | Description |
|
|
284
|
+
|---|---|
|
|
285
|
+
| `processing` | Transaction is being processed |
|
|
286
|
+
| `success` | Transaction completed successfully |
|
|
287
|
+
| `failed` | Transaction failed |
|
|
288
|
+
| `cancelled` | Transaction was cancelled |
|
|
289
|
+
| `error` | Network or server error |
|
|
290
|
+
|
|
291
|
+
```python
|
|
292
|
+
def on_success(data):
|
|
293
|
+
print(f"Paid: {data.transaction.reference}")
|
|
294
|
+
|
|
295
|
+
payment.on("success", on_success)
|
|
296
|
+
payment.once("success", on_success) # fires at most once
|
|
297
|
+
payment.off("success", on_success) # remove handler
|
|
298
|
+
|
|
299
|
+
tx = payment.wait()
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### wait()
|
|
303
|
+
|
|
304
|
+
Block until terminal state. Returns `Transaction` on success, `None` on failure, cancellation, or error. Never raises.
|
|
305
|
+
|
|
306
|
+
```python
|
|
307
|
+
tx = payment.wait()
|
|
308
|
+
if tx is not None:
|
|
309
|
+
print("paid:", tx.reference)
|
|
310
|
+
else:
|
|
311
|
+
print("failed or timed out")
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
## Error Handling
|
|
315
|
+
|
|
316
|
+
Operations that return `Result[T, str]` use the SDK's `Result` type. Check `.is_ok` / `.is_err` before accessing `.value` / `.error`.
|
|
317
|
+
|
|
318
|
+
```python
|
|
319
|
+
from nylonpay import parse_error
|
|
320
|
+
|
|
321
|
+
result = nylonpay.get_status(reference="ORDER-2026-001")
|
|
322
|
+
if result.is_err:
|
|
323
|
+
error = parse_error(result.error)
|
|
324
|
+
if error.retryable:
|
|
325
|
+
# Retry the operation
|
|
326
|
+
...
|
|
327
|
+
print(f"Category: {error.category}, Message: {error.message}")
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### SdkException
|
|
331
|
+
|
|
332
|
+
Operations that throw on initiation failure (invalid input, missing fields) raise `SdkException`. It carries structured error information.
|
|
333
|
+
|
|
334
|
+
```python
|
|
335
|
+
from nylonpay import SdkException
|
|
336
|
+
|
|
337
|
+
try:
|
|
338
|
+
payment = nylonpay.collect_payment(
|
|
339
|
+
amount=100,
|
|
340
|
+
currency="UGX",
|
|
341
|
+
customer={"name": "Jane", "phone_number": "+256700000000"},
|
|
342
|
+
description="Test",
|
|
343
|
+
)
|
|
344
|
+
except SdkException as e:
|
|
345
|
+
print(f"Category: {e.category}")
|
|
346
|
+
print(f"Retryable: {e.retryable}")
|
|
347
|
+
print(f"Message: {e}")
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### Error Categories
|
|
351
|
+
|
|
352
|
+
| Category | Description | Retryable |
|
|
353
|
+
|---|---|---|
|
|
354
|
+
| `auth` | Invalid credentials | No |
|
|
355
|
+
| `validation` | Invalid input | No |
|
|
356
|
+
| `limit` | Account limit exceeded | No |
|
|
357
|
+
| `rate_limit` | Rate limited | Yes |
|
|
358
|
+
| `account` | Account issue (suspended, etc.) | No |
|
|
359
|
+
| `provider` | Provider rejected the transaction | No |
|
|
360
|
+
| `duplicate` | Reference already used | No |
|
|
361
|
+
| `not_found` | Resource not found | No |
|
|
362
|
+
| `internal` | Server error | Varies |
|
|
363
|
+
| `network` | Network error | Yes |
|
|
364
|
+
| `timeout` | Request timed out | Yes |
|
|
365
|
+
|
|
366
|
+
## Hooks
|
|
367
|
+
|
|
368
|
+
Lifecycle hooks fire on every matching operation. Use them for cross-cutting concerns like logging, audit trails, and payload enrichment.
|
|
369
|
+
|
|
370
|
+
Each hook is wrapped in `SdkHook` which provides a safe boundary — if the hook's `fn` raises an exception, it is routed to `on_error` instead of crashing the payment flow.
|
|
371
|
+
|
|
372
|
+
```python
|
|
373
|
+
from nylonpay import create_nylon_pay, SdkHook, SdkHooks
|
|
374
|
+
|
|
375
|
+
def log_before_collect(input):
|
|
376
|
+
print(f"About to collect: {input.reference}")
|
|
377
|
+
return input # can mutate and return, or return None to skip
|
|
378
|
+
|
|
379
|
+
def log_after_collect(result, after_input):
|
|
380
|
+
if result.is_ok:
|
|
381
|
+
print(f"Collected: {result.value.reference}")
|
|
382
|
+
else:
|
|
383
|
+
print(f"Failed: {result.error}")
|
|
384
|
+
|
|
385
|
+
def on_hook_error(exc):
|
|
386
|
+
print(f"Hook failed: {exc}")
|
|
387
|
+
|
|
388
|
+
hooks = SdkHooks(
|
|
389
|
+
before_collect=SdkHook(fn=log_before_collect, on_error=on_hook_error),
|
|
390
|
+
after_collect=SdkHook(fn=log_after_collect, on_error=on_hook_error),
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
nylonpay = create_nylon_pay(
|
|
394
|
+
api_key="npk_test_...",
|
|
395
|
+
api_secret="nps_test_...",
|
|
396
|
+
hooks=hooks,
|
|
397
|
+
)
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
Available hooks: `before_collect`, `after_collect`, `before_payout`, `after_payout`. Each can be `None` (no-op).
|
|
401
|
+
|
|
402
|
+
## Supported Currencies
|
|
403
|
+
|
|
404
|
+
`USD`, `EUR`, `GBP`, `KES`, `UGX`, `TZS`, `RWF`
|
|
405
|
+
|
|
406
|
+
## Links
|
|
407
|
+
|
|
408
|
+
- [Documentation](https://docs.nylonpay.nilesquad.com/docs)
|
|
409
|
+
- [SDK Spec](https://github.com/nile-squad/specs/blob/main/nylonpay-sdk-spec/spec.md)
|
|
410
|
+
- [GitHub Repository](https://github.com/nile-squad/nylonpay-py)
|
|
411
|
+
- [TypeScript SDK](https://github.com/nile-squad/nylonpay-ts)
|
|
412
|
+
- [Nylon Pay](https://nylonpay.nilesquad.com)
|
|
413
|
+
|
|
414
|
+
## License
|
|
415
|
+
|
|
416
|
+
MIT
|