Skip to main content

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)

Source distribution for norialabs-send 0.1.1
File Size Uploaded
norialabs_send-0.1.1.tar.gz 7.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for norialabs-send 0.1.1
File Interpreter ABI Platform
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 log

Release 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

Release history Release notifications | RSS feed

0.1.2

2 release files

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