Official Zindua SDK for Python — transactional email and WhatsApp via POST /api/v1/send.
Project description
zindua-sdk
Official server-side Python SDK for Zindua: send transactional email and WhatsApp from FastAPI, Django, Flask, or any Python 3.10+ backend.
| FastAPI guide | zindua.run/fastapi |
| Full API reference | zindua.run/developers |
| Dashboard | zindua.run/login |
pip install zindua-sdk
With FastAPI helpers:
pip install "zindua-sdk[fastapi]"
Requirements: Python 3.10+. Run only on your backend — never ship ZINDUA_API_KEY to browsers or mobile apps.
Before you write code (Dashboard setup)
Zindua is an orchestration layer: you configure delivery and templates in the dashboard, then call send() from your API.
Step 1 — Create a project
- Sign in at zindua.run/login.
- Go to Projects → Create project (e.g.
My SaaS). - Copy the API key:
znd_test_…— sandbox (safe for local dev).znd_live_…— production.
- Set the default language for the project (Dashboard → project settings). Example:
fr. This language is used when you omitlanginsend().
Step 2 — Connect a delivery service
| Channel | Dashboard path | What you configure |
|---|---|---|
| Project → Service | Gmail OAuth, Outlook, or custom SMTP. Zindua sends through your provider. | |
| Project → WhatsApp | Scan QR code to link the number. Required for OTP on WhatsApp. |
Without a connected service, send() returns EMAIL_SERVICE_NOT_CONFIGURED or WHATSAPP_NOT_CONNECTED.
Step 3 — Create templates
- Open Templates → New template.
- Set a slug (e.g.
otp-verification,welcome,password-reset). You pass this slug insend(template=...). - Add variables used in the body, e.g.
{{code}},{{appName}},{{name}}. - Add language versions for each locale you support:
- French (
fr) — subject + body with{{code}}. - English (
en) — same variables, translated copy. - Swahili (
sw), etc.
- French (
- Pick one language as the template default (used when the requested
langdoes not exist).
Example template slug otp-verification:
| Lang | Subject | Body snippet |
|---|---|---|
fr (default) |
Votre code {{appName}} |
Votre code est {{code}}. Il expire dans 10 minutes. |
en |
Your {{appName}} code |
Your code is {{code}}. It expires in 10 minutes. |
Step 4 — Verify setup from your terminal
export ZINDUA_API_KEY=znd_test_your_key_here
python -m zindua doctor
python -m zindua send --to you@example.com --template otp-verification --var code=482910 --var appName=MyApp
Install and configure
pip install zindua-sdk python-dotenv
# FastAPI stack:
pip install "zindua-sdk[fastapi]" uvicorn
# .env — never commit; load via python-dotenv or your host (Railway, Fly.io, etc.)
ZINDUA_API_KEY=znd_test_xxxxxxxxxxxxxxxxxxxxxxxx
# Optional overrides
# ZINDUA_API_BASE_URL=https://zindua.run/api/v1
# ZINDUA_SITE_URL=https://yourapp.com
import os
from dotenv import load_dotenv
from zindua import Zindua
load_dotenv()
zindua = Zindua(api_key=os.environ["ZINDUA_API_KEY"])
Send a message
import asyncio
from zindua import Zindua
async def main():
client = Zindua(api_key="znd_test_xxxxxxxxxxxxxxxxxxxxxxxx")
# Email (default channel)
result = await client.send(
to="user@example.com",
template="welcome",
variables={"name": "Alex"},
)
# WhatsApp — E.164 phone with +
result = await client.send(
to="+243812345678",
channel="whatsapp",
template="otp-verification",
variables={"code": "482910", "appName": "MyApp"},
)
print(result.log_id, result.status, result.lang_used)
asyncio.run(main())
Sync helpers exist for scripts and WSGI apps: client.send_sync(...), client.get_log_sync(...).
Channel and to must match
The SDK validates locally before calling the API.
channel |
Valid to |
Rejected |
|---|---|---|
email (default) |
user@example.com |
+243812345678 |
whatsapp |
+243812345678 |
user@example.com |
Template language (lang)
When you create a project, you choose a default language. Each template can have several language versions.
What you pass to send() |
What Zindua renders |
|---|---|
No lang |
Project default language |
lang="en" and English exists on the template |
English version, result.lang_used == "en" |
lang="de" but only fr / en exist |
Template default + result.lang_fallback is True |
# Use the user's locale from your database or Accept-Language header
user_locale = "fr" # optional
result = await zindua.send(
to=contact,
channel=channel,
template="otp-verification",
lang=user_locale, # omit to use project default
variables={"code": code, "appName": "MyApp"},
)
if result.lang_fallback:
# Requested lang was missing; template default was used
pass
List available langs per template:
data = await zindua.get_templates()
for tpl in data["templates"]:
print(tpl.slug, tpl.langs, tpl.default_lang, tpl.variables)
End-to-end: FastAPI backend + Next.js frontend
Rule: the browser never sees ZINDUA_API_KEY. The Next.js app calls your FastAPI API; FastAPI calls Zindua.
[Next.js browser] → POST /api/auth/request-otp → [FastAPI]
↓
store OTP in Redis
zindua.send(...)
↓
[Zindua → Gmail / WhatsApp]
FastAPI — generate OTP, send, verify
# main.py
import os
import secrets
from contextlib import asynccontextmanager
import redis.asyncio as redis
from dotenv import load_dotenv
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, EmailStr
from zindua import Zindua, ZinduaError
load_dotenv()
redis_client: redis.Redis | None = None
@asynccontextmanager
async def lifespan(app: FastAPI):
global redis_client
redis_client = redis.from_url(os.environ["REDIS_URL"])
yield
await redis_client.aclose()
app = FastAPI(lifespan=lifespan)
zindua = Zindua(api_key=os.environ["ZINDUA_API_KEY"])
class RequestOtpBody(BaseModel):
email: EmailStr
lang: str | None = None # e.g. "fr", "en" — optional
class VerifyOtpBody(BaseModel):
email: EmailStr
code: str
@app.post("/auth/request-otp")
async def request_otp(body: RequestOtpBody):
code = f"{secrets.randbelow(1_000_000):06d}"
await redis_client.setex(f"otp:{body.email}", 600, code)
try:
result = await zindua.send(
to=body.email,
channel="email",
template="otp-verification",
lang=body.lang,
variables={"code": code, "appName": "MyApp"},
)
except ZinduaError as exc:
raise HTTPException(
status_code=exc.status or 502,
detail={"code": exc.code, "error": str(exc)},
) from exc
return {"ok": True, "logId": result.log_id, "status": result.status}
@app.post("/auth/verify-otp")
async def verify_otp(body: VerifyOtpBody):
stored = await redis_client.get(f"otp:{body.email}")
if not stored or stored.decode() != body.code:
raise HTTPException(status_code=401, detail="Invalid or expired code")
await redis_client.delete(f"otp:{body.email}")
return {"ok": True}
Run locally:
uvicorn main:app --reload
# Open http://127.0.0.1:8000/docs to test routes
FastAPI — dependency injection
from fastapi import Depends
from zindua.integrations.fastapi import ZinduaDep
@app.post("/send-otp")
async def send_otp(to: str, code: str, zindua: ZinduaDep):
return await zindua.send(
to=to,
template="otp-verification",
variables={"code": code, "appName": "MyApp"},
)
Install: pip install "zindua-sdk[fastapi]".
Next.js — call your FastAPI backend
// app/login/page.tsx — client component (no Zindua key here)
"use client";
export default function LoginPage() {
async function requestOtp(email: string, lang?: string) {
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/auth/request-otp`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, lang }),
});
if (!res.ok) throw new Error("Failed to send OTP");
return res.json();
}
// ... UI that collects email, calls requestOtp(), then verify endpoint
}
# .env.local (Next.js — public URL of your FastAPI only)
NEXT_PUBLIC_API_URL=http://127.0.0.1:8000
Alternatively, proxy through a Next.js Route Handler (same pattern as @zindua/sdk on Node):
// app/api/auth/request-otp/route.ts
import { NextResponse } from "next/server";
export async function POST(req: Request) {
const body = await req.json();
const upstream = await fetch(`${process.env.FASTAPI_URL}/auth/request-otp`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
return NextResponse.json(await upstream.json(), { status: upstream.status });
}
Use one place for the Zindua key: either FastAPI or a Node BFF — not both unless you run separate projects.
Email attachments
Email only. Pass up to 5 HTTPS URLs; Zindua fetches them server-side and attaches the files.
result = await zindua.send(
to="user@example.com",
template="invoice",
variables={"name": "Sarah", "invoiceNumber": "INV-1042"},
attachments=[
{"url": "https://cdn.example.com/invoices/inv-1042.pdf", "filename": "invoice.pdf"},
],
)
Track delivery (get_log)
Every successful send() returns a log_id. Poll status from your backend:
result = await zindua.send(
to="user@example.com",
template="welcome",
variables={"name": "Alex"},
)
log = await zindua.get_log(result.log_id)
print(log.status, log.error, log.opened_at)
Statuses include queued, sent, delivered, failed, etc. For full history, use Dashboard → Logs or configure webhooks.
Error handling
from zindua import ZinduaError
try:
result = await zindua.send(
to="user@example.com",
template="otp-verification",
variables={"code": "482910"},
)
except ZinduaError as exc:
# exc.status — HTTP status (400, 422, 429, …)
# exc.code — platform code (TEMPLATE_NOT_FOUND, QUOTA_EXCEEDED, …)
print(exc.code, exc.status, exc.details)
| Code | Typical cause | Fix |
|---|---|---|
TEMPLATE_NOT_FOUND |
Wrong slug | Match Dashboard → Templates slug |
EMAIL_SERVICE_NOT_CONFIGURED |
No Gmail/SMTP | Project → Service |
WHATSAPP_NOT_CONNECTED |
QR not scanned | Project → WhatsApp |
QUOTA_EXCEEDED |
Plan limit | Upgrade or wait for reset |
INVALID_EMAIL / INVALID_PHONE |
to does not match channel |
Fix payload or let SDK validate early |
CLI (smoke tests)
Ships with the package — no separate install.
export ZINDUA_API_KEY=znd_test_...
python -m zindua doctor
python -m zindua send --to user@example.com --template otp-verification --var code=482910
python -m zindua send --to +243812345678 --channel whatsapp --template otp-verification --var code=482910
API methods
| Method | HTTP | Description |
|---|---|---|
send() |
POST /send |
Queue email or WhatsApp |
get_log() |
GET /logs/{logId} |
Delivery status for one message |
get_project() |
GET /project |
Plan, channels, project metadata |
get_templates() |
GET /templates |
Slugs, langs, variables |
connect() |
POST /connect |
Bind key to site URL (WordPress-style) |
is_test_mode() |
(local) | True for znd_test_ keys |
Security
| Do | Don't |
|---|---|
Store ZINDUA_API_KEY in env / secrets manager |
Expose the key in React, Flutter, or Swagger on a public URL |
| Call Zindua from FastAPI, Django, Celery, cron | Import zindua in a public Jupyter notebook with a live key |
| Store OTPs in Redis/Postgres with TTL | Rely on Zindua to store verification codes |
Use znd_test_ in staging |
Share znd_live_ keys in chat or git |
Client options
Zindua(
api_key="znd_live_xxxxxxxxxxxxxxxxxxxxxxxx",
base_url="http://localhost:3000/api/v1", # optional — local Zindua dev only
timeout=30.0, # seconds, max 120
site_url="https://yourapp.com", # optional — WordPress / connect flows
)
Environment variables read by get_zindua():
| Variable | Required | Description |
|---|---|---|
ZINDUA_API_KEY |
Yes | Project API key |
ZINDUA_API_BASE_URL |
No | Override API base (default https://zindua.run/api/v1) |
ZINDUA_SITE_URL |
No | Sent as X-Zindua-Site-Url when set |
License
MIT © Zindua
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file zindua_sdk-1.0.1.tar.gz.
File metadata
- Download URL: zindua_sdk-1.0.1.tar.gz
- Upload date:
- Size: 14.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.9.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
75b898e98c949bc3ddb56710b44b0cfa6f4737ee6eb7e431c55b632ba568f392
|
|
| MD5 |
806820e0079d5281015b9c83f8b39ad5
|
|
| BLAKE2b-256 |
54b5ee6043e566e5c9f5bd959e0f2cf270548b720896b297a0d5cdc1498cb6a0
|
File details
Details for the file zindua_sdk-1.0.1-py3-none-any.whl.
File metadata
- Download URL: zindua_sdk-1.0.1-py3-none-any.whl
- Upload date:
- Size: 16.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.9.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
73b9bfd3d76c2ee6d114d81716a3661b2baceaf4d9edb7899165c4909aa39cc5
|
|
| MD5 |
dba827808d7f684a37f2610e9cb72136
|
|
| BLAKE2b-256 |
e20109a736ee12387c1a15e3747e2426b34e11688f3a344e75fdba90e177e961
|