Skip to main content

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
React Email → Zindua zindua.run/react-email
Full API reference zindua.run/developers
Dashboard zindua.run/login
pip install zindua-sdk==1.2.0

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

  1. Sign in at zindua.run/login.
  2. Go to ProjectsCreate project (e.g. My SaaS).
  3. Copy the API key:
    • znd_test_… — sandbox (safe for local dev).
    • znd_live_… — production.
  4. Set the default language for the project (Dashboard → project settings). Example: fr. This language is used when you omit lang in send().

Step 2 — Connect a delivery service

Channel Dashboard path What you configure
Email Project → Service Gmail OAuth, Outlook, or custom SMTP. Zindua sends through your provider.
WhatsApp 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

  1. Open TemplatesNew template.
  2. Set a slug (e.g. otp-verification, welcome, password-reset). You pass this slug in send(template=...).
  3. Add variables used in the body, e.g. {{code}}, {{appName}}, {{name}}.
  4. Add language versions for each locale you support:
    • French (fr) — subject + body with {{code}}.
    • English (en) — same variables, translated copy.
    • Swahili (sw), etc.
  5. Pick one language as the template default (used when the requested lang does 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())

WhatsApp anti-ban robot (required)

Zindua’s anti-ban Guardian paces WhatsApp OTP. Rapid or parallel sends look like spam and can ban your linked number.

  • Official Python SDK cooperates with the Guardian automatically.
  • Prefer a job queue / resend cooldown in production apps.

Guide: WhatsApp anti-ban Guardian

Upgrade (already installed?)

pip install -U zindua-sdk
zindua = Zindua(api_key=os.environ["ZINDUA_API_KEY"])

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
upsert_template(slug=…, lang=…, subject=…, body_html=…) PUT /templates/{slug}/versions/{lang} Create/update a locale (React Email HTML import)
render_template(template=…, lang=…, variables=…) POST /templates/render Interpolate without sending (prefer send)
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

Release files for zindua-sdk 1.2.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 zindua-sdk 1.2.0
File Size Uploaded
zindua_sdk-1.2.0.tar.gz 17.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for zindua-sdk 1.2.0
File Interpreter ABI Platform
zindua_sdk-1.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 36.0 kB

Release files / zindua_sdk-1.2.0.tar.gz

Download URL zindua_sdk-1.2.0.tar.gz
Size 17.0 kB
Tags Source
SHA-256 checksum
How to use checksums
f690920fa69add4310df6af86b8ee4594b985de94d9681f3ee89d85f819b5bba
BLAKE2b-256 checksum
How to use checksums
27929fd5e325a74bb7e7c41d0c8ff868dabe532397045e2965b4a9d405256cfc
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.9.6

Release files / zindua_sdk-1.2.0-py3-none-any.whl

Download URL zindua_sdk-1.2.0-py3-none-any.whl
Size 19.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b46a56e266a8a7544013129fe26934877c182d3f5919979a4c82be722d016071
BLAKE2b-256 checksum
How to use checksums
a314718c9ede65e514291da2ae3dd26bfb5ee7f3cabed876721e46ec1f3d7cd2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.9.6

Release history Release notifications | RSS feed

1.2.1

2 release files

This release

1.2.0 This release

2 release files

1.1.0

2 release files

1.0.3

2 release files

1.0.2

2 release files

1.0.1

2 release files

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