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.
Files changed (48) hide show
  1. nylonpay_py-0.1.0/.env.example +7 -0
  2. nylonpay_py-0.1.0/.gitignore +32 -0
  3. nylonpay_py-0.1.0/.trash/result.py +77 -0
  4. nylonpay_py-0.1.0/LICENSE +21 -0
  5. nylonpay_py-0.1.0/PKG-INFO +416 -0
  6. nylonpay_py-0.1.0/README.md +372 -0
  7. nylonpay_py-0.1.0/py.typed +0 -0
  8. nylonpay_py-0.1.0/pyproject.toml +72 -0
  9. nylonpay_py-0.1.0/src/nylonpay/__init__.py +116 -0
  10. nylonpay_py-0.1.0/src/nylonpay/coerce.py +104 -0
  11. nylonpay_py-0.1.0/src/nylonpay/config.py +51 -0
  12. nylonpay_py-0.1.0/src/nylonpay/factory.py +78 -0
  13. nylonpay_py-0.1.0/src/nylonpay/fingerprint.py +41 -0
  14. nylonpay_py-0.1.0/src/nylonpay/nonce.py +21 -0
  15. nylonpay_py-0.1.0/src/nylonpay/payment.py +281 -0
  16. nylonpay_py-0.1.0/src/nylonpay/phone.py +39 -0
  17. nylonpay_py-0.1.0/src/nylonpay/pubsub.py +114 -0
  18. nylonpay_py-0.1.0/src/nylonpay/sdk.py +499 -0
  19. nylonpay_py-0.1.0/src/nylonpay/signature.py +96 -0
  20. nylonpay_py-0.1.0/src/nylonpay/slang.py +116 -0
  21. nylonpay_py-0.1.0/src/nylonpay/transport.py +449 -0
  22. nylonpay_py-0.1.0/src/nylonpay/types.py +572 -0
  23. nylonpay_py-0.1.0/src/nylonpay/verify_response.py +35 -0
  24. nylonpay_py-0.1.0/src/nylonpay/verify_webhook.py +116 -0
  25. nylonpay_py-0.1.0/src/nylonpay/wire.py +88 -0
  26. nylonpay_py-0.1.0/task.md +94 -0
  27. nylonpay_py-0.1.0/tests/__init__.py +0 -0
  28. nylonpay_py-0.1.0/tests/integration/__init__.py +0 -0
  29. nylonpay_py-0.1.0/tests/integration/conftest.py +26 -0
  30. nylonpay_py-0.1.0/tests/integration/test_integration.py +349 -0
  31. nylonpay_py-0.1.0/tests/security/__init__.py +0 -0
  32. nylonpay_py-0.1.0/tests/security/test_security.py +473 -0
  33. nylonpay_py-0.1.0/tests/unit/__init__.py +0 -0
  34. nylonpay_py-0.1.0/tests/unit/test_coerce.py +309 -0
  35. nylonpay_py-0.1.0/tests/unit/test_factory.py +65 -0
  36. nylonpay_py-0.1.0/tests/unit/test_fingerprint.py +19 -0
  37. nylonpay_py-0.1.0/tests/unit/test_nonce.py +27 -0
  38. nylonpay_py-0.1.0/tests/unit/test_phone.py +51 -0
  39. nylonpay_py-0.1.0/tests/unit/test_pubsub.py +88 -0
  40. nylonpay_py-0.1.0/tests/unit/test_sdk.py +383 -0
  41. nylonpay_py-0.1.0/tests/unit/test_signature.py +125 -0
  42. nylonpay_py-0.1.0/tests/unit/test_signing_conformance.py +132 -0
  43. nylonpay_py-0.1.0/tests/unit/test_slang.py +85 -0
  44. nylonpay_py-0.1.0/tests/unit/test_transport.py +271 -0
  45. nylonpay_py-0.1.0/tests/unit/test_verify_response.py +48 -0
  46. nylonpay_py-0.1.0/tests/unit/test_verify_webhook.py +120 -0
  47. nylonpay_py-0.1.0/tests/unit/test_wire.py +140 -0
  48. 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