norialabs-pay
Python client for Noria Pay. Collect, pay out and refund across M-PESA, SasaPay and Paystack through one internal service instead of wiring a provider into every product.
Sync and async, typed, httpx the only dependency.
pip install norialabs-pay
Collecting
An idempotency key is a required argument, not an option you can forget. Use something your own system already owns — the invoice id plus the attempt — not a fresh uuid per call, or the key protects nothing.
from noria_pay import Pay
pay = Pay(api_key=os.environ["PAY_API_KEY"], base_url="https://pay.noria.co.ke")
charge = pay.charges.create(
{
"amount_minor": 150_000, # KES 1,500.00
"channel": "mpesa",
"reference": invoice.number, # yours; the service never interprets it
"description": "April rent",
"payer_phone": customer.phone,
"metadata": {"invoice_id": str(invoice.id)},
},
f"invoice:{invoice.id}:{attempt}",
)
# charge["next_action"]["type"] is "await_payer" (the STK prompt is already sent)
# or "redirect" (send them to ["url"]).
AsyncPay has the same surface with await, and both work as context managers.
The one error that is not like the others
A 504 carrying outcome_unknown means the provider never answered. The payer may already
have been debited. The client never retries it, and neither should you.
from noria_pay import PayError
try:
pay.charges.create(charge, key)
except PayError as error:
if error.outcome_unknown:
settled = pay.wait_for_settlement(error.transaction_id)
# resolve against settled["status"], never by charging again
raise
Receiving webhooks
Verify against the raw body. A framework that hands you a parsed dict has already lost the byte order, and re-serialising it will not match the signature.
from noria_pay import verify_webhook
@app.post("/hooks/pay")
async def hook(request: Request):
event = verify_webhook(
await request.body(),
request.headers["pay-signature"],
os.environ["PAY_WEBHOOK_SECRET"],
)
if event["type"] == "succeeded":
# settled_minor is what actually moved, which is not always amount_minor: a payer can
# underpay an STK prompt, and Paystack deducts its fee before settlement.
await settle(event["data"]["reference"], event["data"]["settled_minor"], event["data"]["fee_minor"])
return Response(status_code=204)
It raises on a bad signature, a wrong secret, or a timestamp outside the five-minute tolerance, so a replay cannot post twice.
Amounts
Always minor units. The service rejects a KES amount that is not a whole number of shillings rather than rounding it, because the rails settle whole shillings and a silent round only surfaces in reconciliation.
Surfaces
pay.charges · payouts · refunds · transactions · payment_methods · payment_links ·
webhooks
This is not noriapay
That package wraps the providers directly and this service uses it internally. This one talks
to Noria Pay, which owns the persistence, the callbacks and the reconciliation that noriapay
deliberately leaves to you.
Events are not delivered in order. Delivery runs in parallel and retries, so a later event can
arrive first; order on created_at, which is when the event happened rather than when the
attempt went out. Every payload also carries the transaction's status as it stood at delivery,
so acting on that is safe whatever order they arrive in.
Release files for norialabs-pay 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| norialabs_pay-0.1.0.tar.gz | 8.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| norialabs_pay-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 17.5 kB
Release files / norialabs_pay-0.1.0.tar.gz
| Download URL | norialabs_pay-0.1.0.tar.gz |
|---|---|
| Size | 8.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b25ee78999668cc399708462b6867282aa0c7361a64930a4d6520b3564a5010c
|
|
BLAKE2b-256 checksum How to use checksums |
d42db2fd59ff3ea328e1f180ede8f1745bb93e95d599ba64c6502c799d7c7cb8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 22, 2026.
Transparency logRelease files / norialabs_pay-0.1.0-py3-none-any.whl
| Download URL | norialabs_pay-0.1.0-py3-none-any.whl |
|---|---|
| Size | 9.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
377d0cd746c3c95831d11d48e65e364919bb95b2277979512748a7894f9740d4
|
|
BLAKE2b-256 checksum How to use checksums |
30f271e09feb64ebfb5aa295a13676d466ccddcb0b5e55da3f5bc26092bc3979
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 22, 2026.
Transparency log