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}")

An interrupted send is retried by the client, and the client attaches an idempotency key of its own so the retry is a replay rather than a second message. Pass your own idempotency_key when you want that guarantee to span your retries too, not only the SDK's.

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.

Delivery is at-least-once: a retry or a provider redelivery can bring the same event twice. event["id"] — the Noria-Event-Id header — is stable across both, so record it and ignore an id you have already handled.

Tests

uv sync
uv run pytest
uv run ruff check src tests

Release files for norialabs-send 0.1.2

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.2
File Size Uploaded
norialabs_send-0.1.2.tar.gz 8.1 kB Details

Built distribution (wheel)

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

Total release size: 17.3 kB

Release files / norialabs_send-0.1.2.tar.gz

Download URL norialabs_send-0.1.2.tar.gz
Size 8.1 kB
Tags Source
SHA-256 checksum
How to use checksums
bdb776b80e9af8c565f7b6d2197f8082bc7342a2294cbf92f9915b1bfc2dfaa2
BLAKE2b-256 checksum
How to use checksums
c1989e8507ee01862e2a0a14914dc001fe18a60a631d920dc51ba6f463457567
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

Release files / norialabs_send-0.1.2-py3-none-any.whl

Download URL norialabs_send-0.1.2-py3-none-any.whl
Size 9.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
3b7582b56781679eafabdeac5546dca6c9988a68218ea6bc723b77046b495643
BLAKE2b-256 checksum
How to use checksums
e3ded8a5ed624bbfc5fdf2ef26012457b5b4dd298088019b2d36f36bc0ce5e30
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

Release history Release notifications | RSS feed

This release

0.1.2 This release

2 release files

0.1.1

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