norialabs-send
Python client for Noria Send: transactional email and
SMS through one internal service. Sync and async, typed, no dependency beyond httpx.
pip install norialabs-send # or: uv add norialabs-send
from noria_send import Send
send = Send(api_key=os.environ["NORIA_SEND_KEY"])
send.emails.send({
"from_": "Noria <hello@norialabs.com>",
"to": "founder@example.com",
"subject": "Welcome",
"html": "<p>Hi there</p>",
})
send.sms.send({"from_": "NORIA", "to": "0712345678", "text": "Your code is 482913"})
base_url defaults to https://send.noria.co.ke; pass it to reach a local service or another
instance.
from_ carries the trailing underscore because from is a keyword; it is sent as from.
Every other field is exactly what goes on the wire.
Async
The same surface, for FastAPI and anything else on asyncio.
from noria_send import AsyncSend
async with AsyncSend(api_key=key, base_url=url) as send:
await send.emails.send({"to": "a@example.com", "subject": "Hi", "text": "there"})
await send.sms.send({"to": "0712345678", "text": "Your code is 482913"})
Sending
send.emails.send(email, idempotency_key=f"signin-{token.id}")
send.emails.send_batch([email, email])
send.sms.send(message, idempotency_key=f"otp-{token.id}")
send.sms.send_batch([message, message])
Both also take template and variables instead of a body, scheduled_at for a future send
and tags. Email adds attachments as base64, plus cc, bcc, reply_to and headers.
An SMS takes one recipient, because one row carries one provider message id and that is what a
delivery receipt is matched against. Use send_batch for many.
Reading what was sent
One ledger covers every channel:
send.messages.list(channel="sms", status="failed", limit=50)
send.messages.get(message_id)
send.messages.events(message_id)
send.messages.cancel(message_id)
send.messages.requeue(message_id)
Domains, senders, templates, suppressions, webhooks
send.domains.create("norialabs.com") # returns the DNS records to publish
send.senders.create("NORIA") # registered pending approval
send.templates.upsert("welcome", subject="Hi {{name}}", html=html)
send.templates.upsert("otp", channel="sms", text="Code {{code}}")
send.suppressions.add("0712345678", channel="sms", reason="unsubscribe")
send.is_suppressed("0712345678", "sms")
send.webhooks.create("https://app.example.com/hooks/send", ["delivered", "bounced"])
A slug is unique per channel, so an otp email template and an otp SMS template can coexist.
Errors
Failures raise SendError with code, status, details and request_id, plus
suppressed, over_quota and retryable for the common branches. The client retries 408,
429 and 5xx with backoff and never retries quota_exceeded, suppressed_recipient,
domain_not_verified, sender_not_approved or message_too_long.
try:
send.sms.send(message)
except SendError as error:
if error.suppressed:
return
raise
Webhooks
from noria_send import verify_webhook
event = verify_webhook(
payload=await request.body(),
signature=request.headers["noria-signature"],
secret=os.environ["NORIA_SEND_WEBHOOK_SECRET"],
)
HMAC-SHA256 over <timestamp>.<raw body>, compared in constant time, with a five minute
tolerance against replay. event["data"]["channel"] says which channel the event came from.
Tests
uv sync
uv run pytest
uv run ruff check src tests
Release files for norialabs-send 0.1.1
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_send-0.1.1.tar.gz | 7.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| norialabs_send-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 15.3 kB
Release files / norialabs_send-0.1.1.tar.gz
| Download URL | norialabs_send-0.1.1.tar.gz |
|---|---|
| Size | 7.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
eef541fd9d1748cc5e11b7e02474b7c7151d3489549337a3d0747c76a715ba59
|
|
BLAKE2b-256 checksum How to use checksums |
862e1fe72afaaa80cb5af234a5f43be201af1c240af0988a89954cb4411cb1e8
|
| 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 21, 2026.
Transparency logRelease files / norialabs_send-0.1.1-py3-none-any.whl
| Download URL | norialabs_send-0.1.1-py3-none-any.whl |
|---|---|
| Size | 8.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
4d8fe8f5c3dbd102e43214d3c3314e639af9bb20221a537f1fec1452b754a6cb
|
|
BLAKE2b-256 checksum How to use checksums |
f158f24405c2ff417f00c388732f4df0aa5ffc8dba9a664e037ddd1a757116e2
|
| 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 21, 2026.
Transparency log