Server-side SDK for integrating Nylon Pay payment operations into your Python application.
pip install nylonpay-pyRequires Python 3.10+.
from nylonpay import create_nylon_pay
def on_error(error):
if error.reason == "SERVICES_DOWN":
pause_payment_attempts(error.message)
nylonpay = create_nylon_pay(
api_key="npk_test_...",
api_secret="nps_test_...",
on_error=on_error,
)
payment = nylonpay.collect_payment(
amount=10000,
currency="UGX",
customer={"name": "Jane", "phone_number": "+256700000000"},
description="Order #1234",
reference="550e8400-e29b-41d4-a716-446655440000", # optional; omit to auto-generate a UUID v4
)
def on_success(data):
print(f"Paid: {data.transaction.reference}")
def on_failed(data):
print(f"Failed: {data.error}")
payment.on("success", on_success)
payment.on("failed", on_failed)
tx = payment.wait()
if tx is not None:
print(f"Transaction {tx.reference} completed")Create the SDK instance with create_nylon_pay(). All options are keyword arguments.
Your API key determines the mode: use npk_test_... for test mode and
npk_live_... for production.
| Field | Required | Default | Description |
|---|---|---|---|
api_key |
Yes | Must start with npk_ |
|
api_secret |
Yes | Must start with nps_ |
|
base_url |
No | https://api.nylonpay.com/api/services |
Override for a custom endpoint. LEGACY_BASE_URL is the original address, still served |
timeout_ms |
No | 90000 |
Request timeout in milliseconds |
max_retries |
No | 3 |
Retry count for failed requests |
max_poll_interval_ms |
No | 2000 |
Interval between status checks |
max_poll_duration_ms |
No | (none) | Optional cap on total wait time. Omit to wait until terminal. |
max_poll_attempts |
No | (none) | Optional cap on status checks. Omit to wait until terminal. |
on_delayed |
No | "wait" |
"return" hands back a delayed still-pending payment; "wait" keeps polling |
force |
No | False |
Bypass instance cache and create a fresh instance |
hooks |
No | None |
Lifecycle hooks (SdkHooks) for cross-cutting concerns |
http_client |
No | None |
httpx.Client for testing injection |
on_error |
No | None |
Global handler for structured operation errors on this SDK instance |
nylonpay = create_nylon_pay(
api_key="npk_test_...",
api_secret="nps_test_...",
timeout_ms=15000,
max_retries=5,
)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.
All operations accept keyword arguments. Nested types (customer, destination, items) accept plain dicts.
Initiate a payment collection. Returns a PaymentInstance with event-driven updates.
payment = nylonpay.collect_payment(
amount=10000,
currency="UGX",
customer={"name": "Jane", "phone_number": "+256700000000"},
description="Order #1234",
method="mobileMoney",
reference="550e8400-e29b-41d4-a716-446655440000",
)
def on_success(data):
print(f"Paid: {data.transaction.reference}")
def on_failed(data):
print(f"Failed: {data.error}")
payment.on("success", on_success)
payment.on("failed", on_failed)reference is optional and auto-generated if omitted. A supplied reference must be a valid UUID (any version); the SDK raises SdkException with category validation otherwise. Omit the field to auto-generate a UUID v4.
Block until the collection reaches a terminal state. Single request and response; the server resolves the payment and returns the final state.
result = nylonpay.collect_payment_and_resolve(
amount=5000,
currency="UGX",
customer={"name": "Jane", "phone_number": "+256700000000"},
description="Quick payment",
)
if result.is_ok:
print("Paid:", result.value.reference)Send funds to a destination mobile money wallet or bank account. destination["phone"] is required on every payout. Returns a PaymentInstance with event-driven updates.
payout = nylonpay.make_payout(
amount=50000,
currency="UGX",
customer={"name": "Jane", "phone_number": "+256700000000"},
destination={
"account_holder_name": "Jane Doe",
"account_number": "123456",
"phone": "+256700000000",
},
description="Refund for order #1234",
)
tx = payout.wait()Block until the payout reaches a terminal state. Single request and response.
result = nylonpay.make_payout_and_resolve(
amount=50000,
currency="UGX",
customer={"name": "Jane", "phone_number": "+256700000000"},
destination={
"account_holder_name": "Jane Doe",
"account_number": "123456",
"phone": "+256700000000",
},
description="Refund",
)
if result.is_ok:
print("Payout completed:", result.value.reference)With a sandbox key, collect_payment, collect_payment_and_resolve, make_payout and make_payout_and_resolve accept two test-only options:
test_outcome="success"or"fail"(or a failure code) forces the result.prompt="pin"holds the payment until the merchant types the customer's 5-digit PIN, shown on the dashboard phone simulator. The wait is up to about 2 minutes with 3 tries. The PIN is one per merchant and works with every sandbox key of that merchant.
If both are set, test_outcome wins. With neither, the sandbox decides on its own. A live key rejects both. The *_and_resolve variants can wait about 2 minutes with prompt, so raise max_poll_duration_ms above that if you cap polling.
make_payout returns immediately with a reference for tracking and idempotent retries. The payout status flows through several stages:
pending, Payout accepted and queued for processingprocessing, Payout is being processedon_hold, Payout waiting on review. Polling continues and it finishes on its own assuccessful,failed, orcancelled. From aKES,TZS,RWF, orCDFwallet it also waits for you to confirm the exchange rate on the dashboard; with no answer within 24 hours it endscancelledand the money returns to your wallet.successful, Payout completed; funds sent to destinationfailed, Payout failed; funds refunded to merchant accountcancelled, Payout was cancelled by the merchant
Polling and webhooks: Monitor payout progress by:
- Subscribing to
"processing"events (coverspending,processing, andon_holdstates) - Listening for terminal events:
"success","failed","cancelled" - Receiving webhook notifications at your configured endpoint
The SDK keeps polling while a payout is on_hold and stops when it reaches a final state. status_text carries the review sentence, for example "This payout is being reviewed and will complete shortly."
payout = nylonpay.make_payout(
amount=50000,
currency="UGX",
customer={"name": "Jane", "phone_number": "+256700000000"},
destination={
"account_holder_name": "Jane Doe",
"account_number": "123456",
"phone": "+256700000000",
},
description="Refund for order #1234",
)
def on_processing(data):
# Payout is in flight (pending, processing, or on_hold)
if data.transaction and data.transaction.status == "on_hold":
print("Payout under review:", data.transaction.status_text)
# "This payout is being reviewed and will complete shortly"
payout.on("processing", on_processing)
def on_success(data):
print("Payout complete:", data.transaction.reference)
payout.on("success", on_success)
# Or wait for terminal state
tx = payout.wait()
if tx is not None:
print("Final status:", tx.status)One-shot status check for a transaction. Returns the current server-side state in a single call.
result = nylonpay.get_status(reference="550e8400-e29b-41d4-a716-446655440000")
if result.is_ok:
print(result.value.status)Look up a full transaction record by id or reference. At least one must be provided.
result = nylonpay.get_transaction(reference="550e8400-e29b-41d4-a716-446655440000")
if result.is_ok:
print(result.value.failure_reason)Pre-validate a phone number and get the registered name.
result = nylonpay.verify_phone(phone_number="+256700000000")
if result.is_ok and result.value.verified:
print("Registered to:", result.value.customer_name)Phone numbers are normalized automatically. Uganda local 0700000000 becomes
256700000000. Kenya +254 710 000 000 stays 254710000000. Local 0…
numbers take the payment currency's dial code, and so does a 9-digit local
number typed without its 0 (712345678 with TZS becomes 255712345678).
verify_phone has no currency, so a local number is treated as Uganda.
Generate a hosted payment link. Card payments run through this hosted flow so card details never reach your servers. Invoices require an approved Level 2 account in every currency.
result = nylonpay.create_invoice(
amount=25000,
currency="UGX",
customer_email="customer@example.com",
description="Monthly subscription",
items=[{"name": "Pro Plan", "quantity": 1, "unit_price": 25000}],
)
if result.is_ok:
print("Invoice link:", result.value.payment_link)Verify incoming webhook payloads before processing. Operates on raw payload bytes or string, never re-serialize parsed JSON, which would alter the signed content.
from nylonpay import VerifyWebhookInput, verify_webhook_signature
is_valid = verify_webhook_signature(
VerifyWebhookInput(
payload=raw_payload_bytes,
signature=signature_header,
secret="nps_...",
)
)
if not is_valid:
# Reject: payload did not originate from Nylon Pay
...The standalone function takes a single VerifyWebhookInput. The client method is the keyword-argument variant of the same check:
is_valid = nylonpay.verify_webhook_signature(
payload=raw_payload_bytes,
signature=signature_header,
secret="nps_...",
)The verification checks authenticity and freshness. Pass tolerance_seconds=0 to disable the freshness check. Returns True only when both checks pass. Never raises.
collect_payment and make_payout return a PaymentInstance with event-driven updates.
| Event | Description |
|---|---|
processing |
Transaction is being processed (covers pending, processing, and on_hold states) |
success |
Transaction completed successfully |
failed |
Transaction failed |
cancelled |
Transaction was cancelled |
error |
Network or server error |
A payout on on_hold is waiting on review and completes on its own. From a KES, TZS, RWF, or CDF wallet it also waits for you to confirm the exchange rate on the dashboard; with no answer within 24 hours it ends cancelled and the money returns to your wallet. Polling continues automatically; read transaction?.status_text for the review sentence.
def on_success(data):
print(f"Paid: {data.transaction.reference}")
payment.on("success", on_success)
payment.once("success", on_success) # fires at most once
payment.off("success", on_success) # remove handler
tx = payment.wait()Block until terminal state. Returns Transaction on success, None on failure, cancellation, or error. Never raises. Default: waits until the payment finishes.
tx = payment.wait()
if tx is not None:
print("paid:", tx.reference)
else:
print("failed or timed out")Delayed payments (v0.4+): After about three minutes in flight, responses may include delayed=True. Use on_delayed="return" to get the still-pending payment back and rely on webhooks:
nylonpay = create_nylon_pay(
api_key="npk_test_...",
api_secret="nps_test_...",
on_delayed="return",
)
result = nylonpay.collect_payment_and_resolve(...)
if result.is_ok and result.value.delayed and result.value.status == "pending":
# Still in flight, handle via webhooks
...Set max_poll_duration_ms when you want to bound the total wait time.
Operations that return Result[T, str] use the SDK's Result type. Check .is_ok / .is_err before accessing .value / .error.
from nylonpay import parse_error
result = nylonpay.get_status(reference="550e8400-e29b-41d4-a716-446655440000")
if result.is_err:
error = parse_error(result.error)
if error.retryable:
# Retry the operation
...
print(f"Reason: {error.reason}, Message: {error.message}")Operations that throw on initiation failure (invalid input, missing fields) raise SdkException. It carries structured error information.
from nylonpay import SdkException
try:
payment = nylonpay.collect_payment(
amount=100,
currency="UGX",
customer={"name": "Jane", "phone_number": "+256700000000"},
description="Test",
)
except SdkException as e:
print(f"Reason: {e.reason}")
print(f"Code: {e.code}")
print(f"Retryable: {e.retryable}")
print(f"Message: {e}")| Category | Description | Retryable |
|---|---|---|
auth |
Invalid credentials | No |
validation |
Invalid input | No |
limit |
Account limit exceeded | No |
rate_limit |
Rate limited | Yes |
account |
Account issue (suspended, etc.) | No |
provider |
Provider rejected the transaction | No |
duplicate |
Reference already used | No |
not_found |
Resource not found | No |
internal |
Server error | Varies |
network |
Network error | Yes |
timeout |
Request timed out | Yes |
Lifecycle hooks fire on every matching operation. Use them for cross-cutting concerns like logging, audit trails, and payload enrichment.
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.
from nylonpay import create_nylon_pay, SdkHook, SdkHooks
def log_before_collect(input):
print(f"About to collect: {input.reference}")
return input # can mutate and return, or return None to skip
def log_after_collect(result, after_input):
if result.is_ok:
print(f"Collected: {result.value.reference}")
else:
print(f"Failed: {result.error}")
def on_hook_error(exc):
print(f"Hook failed: {exc}")
hooks = SdkHooks(
before_collect=SdkHook(fn=log_before_collect, on_error=on_hook_error),
after_collect=SdkHook(fn=log_after_collect, on_error=on_hook_error),
)
nylonpay = create_nylon_pay(
api_key="npk_test_...",
api_secret="nps_test_...",
hooks=hooks,
)Available hooks: before_collect, after_collect, before_payout, after_payout. Each can be None (no-op).
USD, EUR, GBP, KES, UGX, TZS, RWF, CDF
MIT