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

  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
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.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 zindua-sdk 1.1.0
File Size Uploaded
zindua_sdk-1.1.0.tar.gz 16.3 kB Details

Built distribution (wheel)

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

Total release size: 34.8 kB

Release files / zindua_sdk-1.1.0.tar.gz

Download URL zindua_sdk-1.1.0.tar.gz
Size 16.3 kB
Tags Source
SHA-256 checksum
How to use checksums
525b1906a4d1b1326668bd7c0b20419ac078b034a32c7d3fc7e916a6411862d6
BLAKE2b-256 checksum
How to use checksums
e226bd88773a720b129f89d40d9eb5d4561dd90b86083e9217f3af3598fdf473
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.1.0-py3-none-any.whl

Download URL zindua_sdk-1.1.0-py3-none-any.whl
Size 18.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
0f9e0471a1934cc3d57903f7f04503d34a0d70d61e2ae64491a582a2cdd658a3
BLAKE2b-256 checksum
How to use checksums
0eb0b9bd6abb423ec9fe257d27c10474d09f0b0b605e148dcaeba52ca23820f2
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

1.2.0

2 release files

This release

1.1.0 This release

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