Reusable Python SDK for M-PESA Daraja, SasaPay, and Paystack payments.
Project description
noriapay
Example-first Python SDK for M-PESA Daraja, SasaPay, and Paystack, built on httpx with sync and async clients.
Install
pip install noriapay
What It Covers
- M-PESA Daraja: STK push, STK query, C2B URL registration, B2C, B2B, reversal, transaction status, account balance, QR generation
- SasaPay: request payment, OTP completion, B2C, B2B
- Paystack: transaction initialize and verify, bank listing, account resolution, transfer recipient creation, transfer initiation, transfer finalization, transfer verification
- Sync and async clients for every supported provider
- Custom
httpxclients, retry policy, hooks, env-based configuration, webhook verification helpers
Quick Setup
Environment Variables
# M-PESA
export MPESA_CONSUMER_KEY=your_consumer_key
export MPESA_CONSUMER_SECRET=your_consumer_secret
export MPESA_ENVIRONMENT=sandbox
# optional
export MPESA_BASE_URL=
export MPESA_TIMEOUT_SECONDS=30
export MPESA_TOKEN_CACHE_SKEW_SECONDS=60
# SasaPay
export SASAPAY_CLIENT_ID=your_client_id
export SASAPAY_CLIENT_SECRET=your_client_secret
export SASAPAY_ENVIRONMENT=sandbox
export SASAPAY_BASE_URL=https://sandbox.sasapay.app/api/v1
# optional
export SASAPAY_TIMEOUT_SECONDS=30
export SASAPAY_TOKEN_CACHE_SKEW_SECONDS=60
# Paystack
export PAYSTACK_SECRET_KEY=sk_test_xxx
# optional
export PAYSTACK_BASE_URL=https://api.paystack.co
export PAYSTACK_TIMEOUT_SECONDS=30
SasaPay note:
SASAPAY_BASE_URLdefaults to the sandbox host- for live SasaPay, set
SASAPAY_ENVIRONMENT=productionand provide the liveSASAPAY_BASE_URLissued for your account or environment
Build Clients From Env
from noriapay import (
AsyncMpesaClient,
AsyncPaystackClient,
AsyncSasaPayClient,
MpesaClient,
PaystackClient,
SasaPayClient,
)
mpesa = MpesaClient.from_env()
sasapay = SasaPayClient.from_env()
paystack = PaystackClient.from_env()
async def build_async_clients() -> tuple[
AsyncMpesaClient,
AsyncSasaPayClient,
AsyncPaystackClient,
]:
return (
AsyncMpesaClient.from_env(),
AsyncSasaPayClient.from_env(),
AsyncPaystackClient.from_env(),
)
Direct Construction
from noriapay import MpesaClient, PaystackClient, SasaPayClient
mpesa = MpesaClient(
consumer_key="consumer-key",
consumer_secret="consumer-secret",
environment="sandbox",
)
sasapay = SasaPayClient(
client_id="client-id",
client_secret="client-secret",
environment="sandbox",
)
paystack = PaystackClient(secret_key="sk_test_xxx")
M-PESA Recipes
Create A Client
from noriapay import MpesaClient
mpesa = MpesaClient.from_env()
# or direct construction
mpesa = MpesaClient(
consumer_key="consumer-key",
consumer_secret="consumer-secret",
environment="sandbox",
)
STK Push
from noriapay import MpesaClient, build_mpesa_stk_password, build_mpesa_timestamp
mpesa = MpesaClient.from_env()
timestamp = build_mpesa_timestamp()
response = mpesa.stk_push(
{
"BusinessShortCode": "174379",
"Password": build_mpesa_stk_password(
business_short_code="174379",
passkey="your-passkey",
timestamp=timestamp,
),
"Timestamp": timestamp,
"TransactionType": "CustomerPayBillOnline",
"Amount": 1,
"PartyA": "254700000000",
"PartyB": "174379",
"PhoneNumber": "254700000000",
"CallBackURL": "https://example.com/mpesa/callback",
"AccountReference": "INV-001",
"TransactionDesc": "Payment for invoice INV-001",
}
)
checkout_request_id = response["CheckoutRequestID"]
Query An STK Push
from noriapay import build_mpesa_stk_password, build_mpesa_timestamp
timestamp = build_mpesa_timestamp()
status = mpesa.stk_push_query(
{
"BusinessShortCode": "174379",
"Password": build_mpesa_stk_password(
business_short_code="174379",
passkey="your-passkey",
timestamp=timestamp,
),
"Timestamp": timestamp,
"CheckoutRequestID": "ws_CO_123456789",
}
)
Register C2B URLs
response = mpesa.register_c2b_urls(
{
"ShortCode": "600000",
"ResponseType": "Completed",
"ConfirmationURL": "https://example.com/mpesa/confirmation",
"ValidationURL": "https://example.com/mpesa/validation",
},
version="v2",
)
Send A B2C Payment
response = mpesa.b2c_payment(
{
"InitiatorName": "apiuser",
"SecurityCredential": "EncryptedPassword",
"CommandID": "BusinessPayment",
"Amount": 10,
"PartyA": "600000",
"PartyB": "254700000000",
"Remarks": "Customer payout",
"QueueTimeOutURL": "https://example.com/mpesa/timeout",
"ResultURL": "https://example.com/mpesa/result",
}
)
Send A B2B Payment
response = mpesa.b2b_payment(
{
"Initiator": "apiuser",
"SecurityCredential": "EncryptedPassword",
"CommandID": "BusinessPayBill",
"Amount": 20,
"PartyA": "600000",
"PartyB": "600001",
"Remarks": "Merchant settlement",
"AccountReference": "SETTLEMENT-001",
"QueueTimeOutURL": "https://example.com/mpesa/timeout",
"ResultURL": "https://example.com/mpesa/result",
}
)
Reverse A Transaction
response = mpesa.reversal(
{
"Initiator": "apiuser",
"SecurityCredential": "EncryptedPassword",
"CommandID": "TransactionReversal",
"TransactionID": "LKXXXX1234",
"Amount": 30,
"ReceiverParty": "600000",
"RecieverIdentifierType": "11",
"ResultURL": "https://example.com/mpesa/result",
"QueueTimeOutURL": "https://example.com/mpesa/timeout",
"Remarks": "Reverse duplicate charge",
}
)
Check Transaction Status
response = mpesa.transaction_status(
{
"Initiator": "apiuser",
"SecurityCredential": "EncryptedPassword",
"CommandID": "TransactionStatusQuery",
"TransactionID": "LKXXXX1234",
"PartyA": "600000",
"IdentifierType": "4",
"ResultURL": "https://example.com/mpesa/result",
"QueueTimeOutURL": "https://example.com/mpesa/timeout",
"Remarks": "Status check",
}
)
Check Account Balance
response = mpesa.account_balance(
{
"Initiator": "apiuser",
"SecurityCredential": "EncryptedPassword",
"CommandID": "AccountBalance",
"PartyA": "600000",
"IdentifierType": "4",
"ResultURL": "https://example.com/mpesa/result",
"QueueTimeOutURL": "https://example.com/mpesa/timeout",
"Remarks": "Balance check",
}
)
Generate A QR Code
response = mpesa.generate_qr_code(
{
"MerchantName": "Noria",
"MerchantShortCode": "174379",
"Amount": 40,
"QRType": "Dynamic",
}
)
Async M-PESA
from noriapay import AsyncMpesaClient, build_mpesa_stk_password, build_mpesa_timestamp
async def push() -> None:
async with AsyncMpesaClient.from_env() as mpesa:
timestamp = build_mpesa_timestamp()
response = await mpesa.stk_push(
{
"BusinessShortCode": "174379",
"Password": build_mpesa_stk_password(
business_short_code="174379",
passkey="your-passkey",
timestamp=timestamp,
),
"Timestamp": timestamp,
"TransactionType": "CustomerPayBillOnline",
"Amount": 1,
"PartyA": "254700000000",
"PartyB": "174379",
"PhoneNumber": "254700000000",
"CallBackURL": "https://example.com/mpesa/callback",
"AccountReference": "INV-001",
"TransactionDesc": "Payment",
}
)
print(response["CheckoutRequestID"])
M-PESA Notes
MPESA_BASE_URLS["sandbox"]ishttps://sandbox.safaricom.co.keMPESA_BASE_URLS["production"]ishttps://api.safaricom.co.kebuild_mpesa_timestamp()returnsYYYYMMDDHHMMSSbuild_mpesa_stk_password()base64-encodesshortcode + passkey + timestamp- amount fields accept
str,int, orfloat; the SDK serializes them to provider-compatible strings - you can provide
token_provider=instead ofconsumer_keyandconsumer_secret
M-PESA API Map
MpesaClient(
*,
consumer_key=None,
consumer_secret=None,
token_provider=None,
environment="sandbox",
base_url=None,
client=None,
session=None,
timeout_seconds=None,
token_cache_skew_seconds=60.0,
default_headers=None,
retry=None,
hooks=None,
)
MpesaClient.from_env(
*,
prefix="MPESA_",
environ=None,
token_provider=None,
client=None,
session=None,
default_headers=None,
retry=None,
hooks=None,
)
mpesa.get_access_token(force_refresh=False)
mpesa.stk_push(payload, options=None)
mpesa.stk_push_query(payload, options=None)
mpesa.register_c2b_urls(payload, version="v2", options=None)
mpesa.b2c_payment(payload, options=None)
mpesa.b2b_payment(payload, options=None)
mpesa.reversal(payload, options=None)
mpesa.transaction_status(payload, options=None)
mpesa.account_balance(payload, options=None)
mpesa.generate_qr_code(payload, options=None)
mpesa.close()
AsyncMpesaClient(
*,
consumer_key=None,
consumer_secret=None,
token_provider=None,
environment="sandbox",
base_url=None,
client=None,
timeout_seconds=None,
token_cache_skew_seconds=60.0,
default_headers=None,
retry=None,
hooks=None,
)
AsyncMpesaClient.from_env(
*,
prefix="MPESA_",
environ=None,
token_provider=None,
client=None,
default_headers=None,
retry=None,
hooks=None,
)
await mpesa.get_access_token(force_refresh=False)
await mpesa.stk_push(payload, options=None)
await mpesa.stk_push_query(payload, options=None)
await mpesa.register_c2b_urls(payload, version="v2", options=None)
await mpesa.b2c_payment(payload, options=None)
await mpesa.b2b_payment(payload, options=None)
await mpesa.reversal(payload, options=None)
await mpesa.transaction_status(payload, options=None)
await mpesa.account_balance(payload, options=None)
await mpesa.generate_qr_code(payload, options=None)
await mpesa.aclose()
M-PESA Exported Models
MpesaApiResponseMpesaStkPushRequestMpesaStkPushResponseMpesaStkQueryRequestMpesaRegisterC2BUrlsRequestMpesaB2CRequestMpesaB2BRequestMpesaReversalRequestMpesaTransactionStatusRequestMpesaAccountBalanceRequestMpesaQrCodeRequest
SasaPay Recipes
Create A Client
from noriapay import SasaPayClient
sasapay = SasaPayClient.from_env()
# or direct construction
sasapay = SasaPayClient(
client_id="client-id",
client_secret="client-secret",
environment="sandbox",
)
For production SasaPay:
sasapay = SasaPayClient(
client_id="client-id",
client_secret="client-secret",
environment="production",
base_url="https://your-live-sasapay-host/api/v1",
)
Request A Mobile Money Payment
response = sasapay.request_payment(
{
"MerchantCode": "600980",
"NetworkCode": "63902",
"Currency": "KES",
"Amount": 1,
"PhoneNumber": "254700000080",
"AccountReference": "INV-001",
"TransactionDesc": "Invoice payment",
"CallBackURL": "https://example.com/sasapay/callback",
}
)
checkout_request_id = response["CheckoutRequestID"]
Request A Wallet Payment And Complete OTP
NetworkCode="0" is the documented SasaPay wallet flow and typically requires process_payment().
request = sasapay.request_payment(
{
"MerchantCode": "600980",
"NetworkCode": "0",
"Currency": "KES",
"Amount": "1.00",
"PhoneNumber": "254700000080",
"AccountReference": "WALLET-001",
"TransactionDesc": "Wallet debit",
"CallBackURL": "https://example.com/sasapay/callback",
}
)
otp_result = sasapay.process_payment(
{
"MerchantCode": "600980",
"CheckoutRequestID": request["CheckoutRequestID"],
"VerificationCode": "123456",
}
)
Send A B2C Payment
response = sasapay.b2c_payment(
{
"MerchantCode": "600980",
"Amount": 10,
"Currency": "KES",
"MerchantTransactionReference": "B2C-001",
"ReceiverNumber": "254700000080",
"Channel": "63902",
"Reason": "Customer payout",
"CallBackURL": "https://example.com/sasapay/callback",
}
)
Send A B2B Payment
response = sasapay.b2b_payment(
{
"MerchantCode": "600980",
"MerchantTransactionReference": "B2B-001",
"Currency": "KES",
"Amount": 12,
"ReceiverMerchantCode": "600981",
"AccountReference": "SETTLEMENT-001",
"ReceiverAccountType": "merchant",
"NetworkCode": "63902",
"Reason": "Merchant settlement",
"CallBackURL": "https://example.com/sasapay/callback",
}
)
Type Your Callback Handlers
from noriapay import SasaPayC2BCallback, SasaPayC2BIpn, SasaPayTransferCallback
def handle_c2b_callback(payload: SasaPayC2BCallback) -> None:
print(payload["CheckoutRequestID"], payload["ResultCode"])
def handle_ipn(payload: SasaPayC2BIpn) -> None:
print(payload["TransID"], payload["TransAmount"])
def handle_transfer_callback(payload: SasaPayTransferCallback) -> None:
print(payload.get("MerchantTransactionReference"))
Async SasaPay
from noriapay import AsyncSasaPayClient
async def request_payment() -> None:
async with AsyncSasaPayClient.from_env() as sasapay:
response = await sasapay.request_payment(
{
"MerchantCode": "600980",
"NetworkCode": "63902",
"Currency": "KES",
"Amount": 1,
"PhoneNumber": "254700000080",
"AccountReference": "INV-001",
"TransactionDesc": "Invoice payment",
"CallBackURL": "https://example.com/sasapay/callback",
}
)
print(response["CheckoutRequestID"])
SasaPay Notes
SASAPAY_BASE_URLis the sandbox default:https://sandbox.sasapay.app/api/v1environment="production"is supported, but you must provide the livebase_url- amount fields accept
str,int, orfloat; the SDK serializes them to provider-compatible strings - you can provide
token_provider=instead ofclient_idandclient_secret - documented network behavior reviewed for this package:
NetworkCode="0"is SasaPay wallet- values such as
63902represent external payment channels like M-PESA
SasaPay API Map
SasaPayClient(
*,
client_id=None,
client_secret=None,
token_provider=None,
environment="sandbox",
base_url=None,
client=None,
session=None,
timeout_seconds=None,
token_cache_skew_seconds=60.0,
default_headers=None,
retry=None,
hooks=None,
)
SasaPayClient.from_env(
*,
prefix="SASAPAY_",
environ=None,
token_provider=None,
client=None,
session=None,
default_headers=None,
retry=None,
hooks=None,
)
sasapay.get_access_token(force_refresh=False)
sasapay.request_payment(payload, options=None)
sasapay.process_payment(payload, options=None)
sasapay.b2c_payment(payload, options=None)
sasapay.b2b_payment(payload, options=None)
sasapay.close()
AsyncSasaPayClient(
*,
client_id=None,
client_secret=None,
token_provider=None,
environment="sandbox",
base_url=None,
client=None,
timeout_seconds=None,
token_cache_skew_seconds=60.0,
default_headers=None,
retry=None,
hooks=None,
)
AsyncSasaPayClient.from_env(
*,
prefix="SASAPAY_",
environ=None,
token_provider=None,
client=None,
default_headers=None,
retry=None,
hooks=None,
)
await sasapay.get_access_token(force_refresh=False)
await sasapay.request_payment(payload, options=None)
await sasapay.process_payment(payload, options=None)
await sasapay.b2c_payment(payload, options=None)
await sasapay.b2b_payment(payload, options=None)
await sasapay.aclose()
SasaPay Exported Models
SasaPayAuthResponseSasaPayRequestPaymentRequestSasaPayRequestPaymentResponseSasaPayProcessPaymentRequestSasaPayProcessPaymentResponseSasaPayB2CRequestSasaPayB2CResponseSasaPayB2BRequestSasaPayB2BResponseSasaPayC2BCallbackSasaPayC2BIpnSasaPayTransferCallback
Paystack Recipes
Create A Client
from noriapay import PaystackClient
paystack = PaystackClient.from_env()
# or direct construction
paystack = PaystackClient(secret_key="sk_test_xxx")
Initialize A Checkout Transaction
response = paystack.initialize_transaction(
{
"email": "customer@example.com",
"amount": 5000,
"currency": "KES",
"reference": "order-123",
"callback_url": "https://example.com/paystack/callback",
"metadata": {"order_id": "order-123"},
}
)
authorization_url = response["data"]["authorization_url"]
reference = response["data"]["reference"]
Paystack amounts are lowest-unit integers. 5000 means 50.00 in a 2-decimal currency such as KES.
Verify A Transaction
verification = paystack.verify_transaction("order-123")
if verification["data"]["status"] == "success":
print("Payment confirmed")
List Supported Banks
banks = paystack.list_banks(
{
"country": "kenya",
"currency": "KES",
"type": "mobile_money",
}
)
Resolve An Account
account = paystack.resolve_account(
account_number="247247",
bank_code="MPTILL",
)
print(account["data"]["account_name"])
Create A Transfer Recipient
recipient = paystack.create_transfer_recipient(
{
"type": "mobile_money_business",
"name": "Till Transfer Example",
"account_number": "247247",
"bank_code": "MPTILL",
"currency": "KES",
"description": "Settlement till",
}
)
recipient_code = recipient["data"]["recipient_code"]
Initiate A Transfer
transfer = paystack.initiate_transfer(
{
"source": "balance",
"amount": 5000,
"recipient": recipient_code,
"reference": "transfer-123",
"currency": "KES",
"reason": "Merchant settlement",
"account_reference": "ACC-123",
}
)
Finalize A Transfer
result = paystack.finalize_transfer(
{
"transfer_code": "TRF_queued",
"otp": "123456",
}
)
Verify A Transfer
verification = paystack.verify_transfer("transfer-123")
Async Paystack
from noriapay import AsyncPaystackClient
async def initialize_checkout() -> None:
async with AsyncPaystackClient.from_env() as paystack:
response = await paystack.initialize_transaction(
{
"email": "customer@example.com",
"amount": 5000,
"reference": "order-123",
"currency": "KES",
}
)
print(response["data"]["authorization_url"])
Verify Paystack Webhooks
from noriapay import PAYSTACK_WEBHOOK_IPS, require_paystack_signature, require_source_ip
def verify_paystack_webhook(raw_body: bytes, signature: str | None, source_ip: str | None) -> None:
require_paystack_signature(raw_body, signature, "sk_test_xxx")
require_source_ip(source_ip, PAYSTACK_WEBHOOK_IPS)
Paystack Notes
PAYSTACK_BASE_URLishttps://api.paystack.co- environment selection comes from the secret key you use
RequestOptions.access_tokencan override the bearer token on a single requestPaystackClientandAsyncPaystackClientdo not use OAuth token lookup- this package currently covers the initial transaction and transfer subset described here
Paystack API Map
PaystackClient(
*,
secret_key=None,
base_url=None,
client=None,
session=None,
timeout_seconds=None,
default_headers=None,
retry=None,
hooks=None,
)
PaystackClient.from_env(
*,
prefix="PAYSTACK_",
environ=None,
client=None,
session=None,
default_headers=None,
retry=None,
hooks=None,
)
paystack.initialize_transaction(payload, options=None)
paystack.verify_transaction(reference, options=None)
paystack.list_banks(query=None, options=None)
paystack.resolve_account(account_number, bank_code, options=None)
paystack.create_transfer_recipient(payload, options=None)
paystack.initiate_transfer(payload, options=None)
paystack.finalize_transfer(payload, options=None)
paystack.verify_transfer(reference, options=None)
paystack.close()
AsyncPaystackClient(
*,
secret_key=None,
base_url=None,
client=None,
timeout_seconds=None,
default_headers=None,
retry=None,
hooks=None,
)
AsyncPaystackClient.from_env(
*,
prefix="PAYSTACK_",
environ=None,
client=None,
default_headers=None,
retry=None,
hooks=None,
)
await paystack.initialize_transaction(payload, options=None)
await paystack.verify_transaction(reference, options=None)
await paystack.list_banks(query=None, options=None)
await paystack.resolve_account(account_number, bank_code, options=None)
await paystack.create_transfer_recipient(payload, options=None)
await paystack.initiate_transfer(payload, options=None)
await paystack.finalize_transfer(payload, options=None)
await paystack.verify_transfer(reference, options=None)
await paystack.aclose()
Paystack Exported Models
PaystackApiResponsePaystackInitializeTransactionRequestPaystackInitializeTransactionDataPaystackInitializeTransactionResponsePaystackTransactionPaystackVerifyTransactionResponsePaystackBankPaystackListBanksQueryPaystackListBanksResponsePaystackResolveAccountDataPaystackResolveAccountResponsePaystackTransferRecipientDetailsPaystackTransferRecipientPaystackCreateTransferRecipientRequestPaystackCreateTransferRecipientResponsePaystackTransferPaystackInitiateTransferRequestPaystackInitiateTransferResponsePaystackFinalizeTransferRequestPaystackFinalizeTransferResponsePaystackVerifyTransferResponse
Returned Paystack payloads also include nested objects such as authorization, customer, recipient details, and cursor metadata. Those nested shapes are part of the provider JSON returned by the SDK even when they are not exported as top-level public names.
Customization Recipes
Per-Request Overrides With RequestOptions
from noriapay import RequestOptions
result = mpesa.generate_qr_code(
{
"MerchantName": "Noria",
"MerchantShortCode": "174379",
"Amount": 40,
"QRType": "Dynamic",
},
options=RequestOptions(
headers={"x-request-id": "req-123"},
timeout_seconds=10,
),
)
For Paystack, access_token overrides the bearer token for one request:
result = paystack.finalize_transfer(
{
"transfer_code": "TRF_queued",
"otp": "123456",
},
options=RequestOptions(
access_token="sk_test_override",
headers={"x-request-id": "req-123"},
),
)
For M-PESA and SasaPay, force_token_refresh=True forces a fresh OAuth lookup on that request:
result = mpesa.stk_push(
payload,
options=RequestOptions(force_token_refresh=True),
)
Retries
from noriapay import RetryPolicy
retry = RetryPolicy(
max_attempts=3,
retry_methods=("GET", "POST"),
retry_on_statuses=(500, 502, 503, 504),
retry_on_network_error=True,
base_delay_seconds=0.25,
max_delay_seconds=2.0,
backoff_multiplier=2.0,
)
paystack = PaystackClient.from_env(retry=retry)
You can also attach retry rules per request:
response = sasapay.request_payment(
payload,
options=RequestOptions(
retry=RetryPolicy(
max_attempts=2,
retry_methods=("POST",),
retry_on_statuses=(500,),
)
),
)
Hooks
from noriapay import Hooks, MpesaClient
def before_request(context) -> None:
context.headers["x-trace-id"] = "trace-123"
def after_response(context) -> None:
print(context.method, context.url, context.response_body)
def on_error(context) -> None:
print("request failed", context.error)
mpesa = MpesaClient.from_env(
hooks=Hooks(
before_request=before_request,
after_response=after_response,
on_error=on_error,
)
)
Hook context objects are:
BeforeRequestContextAfterResponseContextErrorContext
Inject Your Own httpx Client
import httpx
from noriapay import MpesaClient
http_client = httpx.Client(
headers={"x-app-name": "billing-service"},
verify=True,
)
mpesa = MpesaClient.from_env(client=http_client)
Sync constructors accept client= and keep session= as a backward-compatible alias.
Async example:
import httpx
from noriapay import AsyncPaystackClient
http_client = httpx.AsyncClient(headers={"x-app-name": "billing-service"})
paystack = AsyncPaystackClient.from_env(client=http_client)
Use A Custom OAuth Token Provider
MpesaClient and SasaPayClient can use any object that implements get_access_token(force_refresh=False). The async clients accept an async version of the same contract.
Using the built-in token providers directly:
from noriapay import ClientCredentialsTokenProvider, MpesaClient
provider = ClientCredentialsTokenProvider(
token_url="https://sandbox.safaricom.co.ke/oauth/v1/generate",
client_id="consumer-key",
client_secret="consumer-secret",
query={"grant_type": "client_credentials"},
)
mpesa = MpesaClient(token_provider=provider, environment="sandbox")
from noriapay import AsyncClientCredentialsTokenProvider, AsyncSasaPayClient
provider = AsyncClientCredentialsTokenProvider(
token_url="https://sandbox.sasapay.app/api/v1/auth/token/",
client_id="client-id",
client_secret="client-secret",
query={"grant_type": "client_credentials"},
)
sasapay = AsyncSasaPayClient(
token_provider=provider,
environment="sandbox",
)
Error Handling
from noriapay import (
ApiError,
AuthenticationError,
ConfigurationError,
NetworkError,
TimeoutError,
WebhookVerificationError,
)
try:
response = paystack.verify_transaction("order-123")
except ConfigurationError:
...
except AuthenticationError:
...
except TimeoutError:
...
except NetworkError:
...
except ApiError as error:
print(error.status_code, error.response_body)
except WebhookVerificationError:
...
Shared API Reference
Core Shared Types
Environment = Literal["sandbox", "production"]
class AccessTokenProvider(Protocol):
def get_access_token(self, force_refresh: bool = False) -> str: ...
class AsyncAccessTokenProvider(Protocol):
async def get_access_token(self, force_refresh: bool = False) -> str: ...
AccessToken(
access_token: str,
expires_in: int,
token_type: str | None = None,
scope: str | None = None,
raw: dict[str, Any] = {},
)
Request And Retry Types
RequestOptions(
headers=None,
timeout_seconds=None,
retry=None,
access_token=None,
force_token_refresh=False,
)
RetryDecisionContext(
attempt: int,
max_attempts: int,
method: str,
url: str,
status: int | None = None,
error: object = None,
)
RetryPolicy(
max_attempts=1,
retry_methods=(),
retry_on_statuses=(),
retry_on_network_error=False,
base_delay_seconds=0.0,
max_delay_seconds=60.0,
backoff_multiplier=2.0,
should_retry=None,
)
Hooks
Hooks(
before_request=None,
after_response=None,
on_error=None,
)
before_request, after_response, and on_error each accept a single callable or a sequence of callables.
Built-In Token Providers
ClientCredentialsTokenProvider(
token_url,
client_id,
client_secret,
client=None,
session=None,
timeout_seconds=None,
query=None,
cache_skew_seconds=60.0,
map_response=None,
)
provider.get_token(force_refresh=False)
provider.get_access_token(force_refresh=False)
provider.clear_cache()
provider.close()
AsyncClientCredentialsTokenProvider(
token_url,
client_id,
client_secret,
client=None,
timeout_seconds=None,
query=None,
cache_skew_seconds=60.0,
map_response=None,
)
await provider.get_token(force_refresh=False)
await provider.get_access_token(force_refresh=False)
provider.clear_cache()
await provider.aclose()
Webhook Helpers
PAYSTACK_WEBHOOK_IPS
compute_paystack_signature(raw_body, secret_key)
verify_paystack_signature(raw_body, signature, secret_key)
require_paystack_signature(raw_body, signature, secret_key)
verify_source_ip(source_ip, allowed_ips)
require_source_ip(source_ip, allowed_ips)
Exceptions
NoriapayErrorConfigurationErrorAuthenticationErrorTimeoutErrorNetworkErrorApiErrorWebhookVerificationError
Public Export Index
Shared Exports
AccessTokenAccessTokenProviderAfterResponseContextApiErrorAsyncAccessTokenProviderAsyncClientCredentialsTokenProviderAuthenticationErrorBeforeRequestContextClientCredentialsTokenProviderConfigurationErrorEnvironmentErrorContextHooksNetworkErrorNoriapayErrorRequestOptionsRetryDecisionContextRetryPolicyTimeoutErrorWebhookVerificationError
M-PESA Exports
MPESA_BASE_URLSAsyncMpesaClientMpesaAccountBalanceRequestMpesaApiResponseMpesaB2BRequestMpesaB2CRequestMpesaClientMpesaQrCodeRequestMpesaRegisterC2BUrlsRequestMpesaReversalRequestMpesaStkPushRequestMpesaStkPushResponseMpesaStkQueryRequestMpesaTransactionStatusRequestbuild_mpesa_stk_passwordbuild_mpesa_timestamp
SasaPay Exports
SASAPAY_BASE_URLAsyncSasaPayClientSasaPayAuthResponseSasaPayB2BRequestSasaPayB2BResponseSasaPayB2CRequestSasaPayB2CResponseSasaPayC2BCallbackSasaPayC2BIpnSasaPayClientSasaPayProcessPaymentRequestSasaPayProcessPaymentResponseSasaPayRequestPaymentRequestSasaPayRequestPaymentResponseSasaPayTransferCallback
Paystack Exports
PAYSTACK_BASE_URLAsyncPaystackClientPaystackApiResponsePaystackBankPaystackClientPaystackCreateTransferRecipientRequestPaystackCreateTransferRecipientResponsePaystackFinalizeTransferRequestPaystackFinalizeTransferResponsePaystackInitializeTransactionDataPaystackInitializeTransactionRequestPaystackInitializeTransactionResponsePaystackInitiateTransferRequestPaystackInitiateTransferResponsePaystackListBanksQueryPaystackListBanksResponsePaystackResolveAccountDataPaystackResolveAccountResponsePaystackTransactionPaystackTransferPaystackTransferRecipientPaystackTransferRecipientDetailsPaystackVerifyTransactionResponsePaystackVerifyTransferResponse
Webhook Helper Exports
PAYSTACK_WEBHOOK_IPScompute_paystack_signaturerequire_paystack_signaturerequire_source_ipverify_paystack_signatureverify_source_ip
Live Sandbox Checks
The package includes opt-in integration tests against live sandbox or test credentials.
export RUN_LIVE_SANDBOX_TESTS=1
uv run pytest -m integration
Credentials expected by the integration suite:
- M-PESA:
MPESA_CONSUMER_KEY,MPESA_CONSUMER_SECRET - SasaPay:
SASAPAY_CLIENT_ID,SASAPAY_CLIENT_SECRET - Paystack:
PAYSTACK_SECRET_KEY
Use test or sandbox credentials only.
Provider Docs
- SasaPay getting started: https://developer.sasapay.app/docs/getting-started
- SasaPay authentication: https://developer.sasapay.app/docs/apis/authentication
- SasaPay C2B: https://developer.sasapay.app/docs/apis/c2b
- SasaPay B2C: https://developer.sasapay.app/docs/apis/b2c
- SasaPay B2B: https://developer.sasapay.app/docs/apis/b2b
- Paystack API reference: https://paystack.com/docs/api/
- Paystack transfer recipient guide: https://paystack.com/docs/transfers/creating-transfer-recipients/
- Paystack webhooks: https://paystack.com/docs/payments/webhooks/
Notes
- choose
MpesaClient,SasaPayClient, andPaystackClientfor blocking code paths - choose
AsyncMpesaClient,AsyncSasaPayClient, andAsyncPaystackClientforasyncioapplications - sync clients use
httpx.Client; async clients usehttpx.AsyncClient - sync constructors accept
client=and also keepsession=as a backward-compatible alias - provider JSON is returned as parsed Python data structures
- the package uses
TypedDictpayload and response models for editor guidance, not full runtime schema validation
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file noriapay-0.1.1.tar.gz.
File metadata
- Download URL: noriapay-0.1.1.tar.gz
- Upload date:
- Size: 43.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4c2d306f88a94f5b633a6792c4dc666e3be206ac07c13b9e10cffd7f6c120d3f
|
|
| MD5 |
fde3039ff95af18605f99d2fba91141a
|
|
| BLAKE2b-256 |
706c371369f55d88875565a265250b6640b43b8edaeb082f25a645a07307cb0f
|
Provenance
The following attestation bundles were made for noriapay-0.1.1.tar.gz:
Publisher:
ci.yml on thekiharani/py-packages
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
noriapay-0.1.1.tar.gz -
Subject digest:
4c2d306f88a94f5b633a6792c4dc666e3be206ac07c13b9e10cffd7f6c120d3f - Sigstore transparency entry: 1261954782
- Sigstore integration time:
-
Permalink:
thekiharani/py-packages@fd9d465daabfc1a75236ff11019daba1af084bf9 -
Branch / Tag:
refs/heads/release - Owner: https://github.com/thekiharani
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@fd9d465daabfc1a75236ff11019daba1af084bf9 -
Trigger Event:
push
-
Statement type:
File details
Details for the file noriapay-0.1.1-py3-none-any.whl.
File metadata
- Download URL: noriapay-0.1.1-py3-none-any.whl
- Upload date:
- Size: 26.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
36e36f166aa72ae09a1121ccd8fe686d90662b04695df2b4c1c5e0ced166f302
|
|
| MD5 |
c041e8eecdaf047d6424f45b0e3caa9e
|
|
| BLAKE2b-256 |
255adc28da108c50b23d485df4d06eb6c0883642a6e3ac9b746979805bcd52b2
|
Provenance
The following attestation bundles were made for noriapay-0.1.1-py3-none-any.whl:
Publisher:
ci.yml on thekiharani/py-packages
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
noriapay-0.1.1-py3-none-any.whl -
Subject digest:
36e36f166aa72ae09a1121ccd8fe686d90662b04695df2b4c1c5e0ced166f302 - Sigstore transparency entry: 1261954811
- Sigstore integration time:
-
Permalink:
thekiharani/py-packages@fd9d465daabfc1a75236ff11019daba1af084bf9 -
Branch / Tag:
refs/heads/release - Owner: https://github.com/thekiharani
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
ci.yml@fd9d465daabfc1a75236ff11019daba1af084bf9 -
Trigger Event:
push
-
Statement type: