Skip to main content

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

  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 robot paces WhatsApp OTP. Rapid or parallel sends look like spam and can ban your linked number.

  • Wait ≥ 3 seconds between WhatsApp send() calls (same project).
  • SDK default auto_pace_whatsapp=True waits on this client and logs a warning.
  • Prefer a job queue / resend cooldown in production apps.

Guide: WhatsApp OTP anti-ban pacing

zindua = Zindua(api_key=os.environ["ZINDUA_API_KEY"])
# zindua = Zindua(..., auto_pace_whatsapp=False)  # only if you already queue OTP

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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

zindua_sdk-1.0.2.tar.gz (15.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

zindua_sdk-1.0.2-py3-none-any.whl (18.0 kB view details)

Uploaded Python 3

File details

Details for the file zindua_sdk-1.0.2.tar.gz.

File metadata

  • Download URL: zindua_sdk-1.0.2.tar.gz
  • Upload date:
  • Size: 15.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.9.6

File hashes

Hashes for zindua_sdk-1.0.2.tar.gz
Algorithm Hash digest
SHA256 36c285580a4b34d2d3e276dac1b79b19f6fb771a8fad86314de575186086b552
MD5 a51ebe7537a7ea40b65527e549bc7fdd
BLAKE2b-256 a3473dce499663647e70a503723483ae02ff87393e6e94cbdbed7cd4cc1f573d

See more details on using hashes here.

File details

Details for the file zindua_sdk-1.0.2-py3-none-any.whl.

File metadata

  • Download URL: zindua_sdk-1.0.2-py3-none-any.whl
  • Upload date:
  • Size: 18.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.9.6

File hashes

Hashes for zindua_sdk-1.0.2-py3-none-any.whl
Algorithm Hash digest
SHA256 a70ca1c7e065afdee572222d6845bb22b7c50914c98526ff53a56b9334be472b
MD5 38f7e1eb2c595542c5c9d59f5244f05a
BLAKE2b-256 df269287caba8c7c78a4949781fe2a906c7c687e4e73d2a418b9def10630466e

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page