Skip to content

About

Nylon Pay Python SDK

Resources

Stars

4 stars

Watchers

0 watching

Forks

Latest commit

 

History

59 Commits

Folders and files

Repository files navigation

nylonpay-py

Server-side SDK for integrating Nylon Pay payment operations into your Python application.

Full documentation

Install

pip install nylonpay-py

Requires Python 3.10+.

Quick Start

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")

Configuration

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.

Operations

All operations accept keyword arguments. Nested types (customer, destination, items) accept plain dicts.

collect_payment

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.

collect_payment_and_resolve

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)

make_payout

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()

make_payout_and_resolve

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)

Sandbox options

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.

Payout Lifecycle

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 processing
  • processing, Payout is being processed
  • on_hold, Payout waiting on review. Polling continues and it finishes on its own as successful, failed, or cancelled. 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.
  • successful, Payout completed; funds sent to destination
  • failed, Payout failed; funds refunded to merchant account
  • cancelled, Payout was cancelled by the merchant

Polling and webhooks: Monitor payout progress by:

  1. Subscribing to "processing" events (covers pending, processing, and on_hold states)
  2. Listening for terminal events: "success", "failed", "cancelled"
  3. 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)

get_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)

get_transaction

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)

verify_phone

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.

create_invoice

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_webhook_signature

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.

PaymentInstance Events

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()

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.

Error Handling

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}")

SdkException

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}")

Error Categories

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

Hooks

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).

Supported Currencies

USD, EUR, GBP, KES, UGX, TZS, RWF, CDF

Links

License

MIT

About

Nylon Pay Python SDK

Resources

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages