Skip to main content

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

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for norialabs-pay 0.1.1
File Size Uploaded
norialabs_pay-0.1.1.tar.gz 9.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for norialabs-pay 0.1.1
File Interpreter ABI Platform
norialabs_pay-0.1.1-py3-none-any.whl Python 3 none any Details

Total release size: 19.3 kB

Release files / norialabs_pay-0.1.1.tar.gz

Download URL norialabs_pay-0.1.1.tar.gz
Size 9.1 kB
Tags Source
SHA-256 checksum
How to use checksums
b11eb87371c391cb96e9c9c6d03d0ba8f15516be78bba367da9f425d1a931fce
BLAKE2b-256 checksum
How to use checksums
b85c3060da73457dd487c1f877aa87645f41d46f2f4ada986cf701ca1339992d
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 23, 2026.

Transparency log

Release files / norialabs_pay-0.1.1-py3-none-any.whl

Download URL norialabs_pay-0.1.1-py3-none-any.whl
Size 10.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d7fbf1ece88b6cab4a872ce95512d950d3d8e9b5b3bec114c2ecf52fbbad7f96
BLAKE2b-256 checksum
How to use checksums
6abd9b4125dcde27eba9def348e62a3e811c3c4317b5344840fd3ad47e052bcf
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 23, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.1 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page