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

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.0
File Size Uploaded
norialabs_send-0.1.0.tar.gz 7.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for norialabs-send 0.1.0
File Interpreter ABI Platform
norialabs_send-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 15.3 kB

Release files / norialabs_send-0.1.0.tar.gz

Download URL norialabs_send-0.1.0.tar.gz
Size 7.1 kB
Tags Source
SHA-256 checksum
How to use checksums
2b81f3dc31a67ebe56b41bf92a8a90fab013cc1621cf8d611b2ceec0c644413b
BLAKE2b-256 checksum
How to use checksums
1783e9401dad3fff182410ada2ef983683f3eabc38bd820376861bc93884c321
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.0-py3-none-any.whl

Download URL norialabs_send-0.1.0-py3-none-any.whl
Size 8.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
3092a000d168cfa0bc37d9b23fd6bfb3a358da67a2b21348b3056d4c69b4bc8f
BLAKE2b-256 checksum
How to use checksums
3737bd752fd3e589b1c52f11634fed2fd934037815f45ffa877cfda4124650bd
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

0.1.1

2 release files

This release

0.1.0 This release

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