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

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.1.tar.gz (14.5 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.1-py3-none-any.whl (16.5 kB view details)

Uploaded Python 3

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

Hashes for zindua_sdk-1.0.1.tar.gz
Algorithm Hash digest
SHA256 75b898e98c949bc3ddb56710b44b0cfa6f4737ee6eb7e431c55b632ba568f392
MD5 806820e0079d5281015b9c83f8b39ad5
BLAKE2b-256 54b5ee6043e566e5c9f5bd959e0f2cf270548b720896b297a0d5cdc1498cb6a0

See more details on using hashes here.

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

Hashes for zindua_sdk-1.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 73b9bfd3d76c2ee6d114d81716a3661b2baceaf4d9edb7899165c4909aa39cc5
MD5 dba827808d7f684a37f2610e9cb72136
BLAKE2b-256 e20109a736ee12387c1a15e3747e2426b34e11688f3a344e75fdba90e177e961

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