Zindua

Zindua for FastAPI

Async Python backends with the official zindua-sdk v1.0.2. send(), get_log(), multilingual templates, attachments, and FastAPI Depends().

PyPIpip install zindua-sdk
FastAPIpip install "zindua-sdk[fastapi]" uvicorn python-dotenv

Python 3.10+. PyPI package: pip install zindua-sdk. FastAPI extras are optional for this guide. pip install zindua-sdk matches PyPI exactly. The FastAPI line adds uvicorn and Depends() helpers for local dev.

Async-first

Use httpx.AsyncClient inside FastAPI routes for non-blocking sends.

Server-side keys

ZINDUA_API_KEY stays in .env. Never expose it to Swagger from a public browser without auth.

Email + WhatsApp

Same REST payload as Node and PHP. Switch channel in JSON.

Official SDK

zindua-sdk validates payloads locally, async send(), get_log(), and FastAPI Depends().

Dependency injection

Wrap the send helper in FastAPI Depends() for clean route handlers.

CLI + Swagger

python -m zindua doctor or npx @zindua/cli (Node). Then exercise routes in /docs.

Quick start

Install and send

Pick a file in the explorer. Same layout as your IDE.

install.sh
# PyPI (SDK only, same as pypi.org/project/zindua-sdk)
pip install zindua-sdk

# This guide: FastAPI + uvicorn + dotenv (optional)
pip install "zindua-sdk[fastapi]" uvicorn python-dotenv
Dashboard

Before pip install

Four steps in the dashboard, then verify below.

01

Project + default language

Dashboard → Projects → create project. Copy znd_test_… or znd_live_…. Set default language (fr, en, …), used when you omit lang in send().

02

Connect Service (email)

Project → Service → Gmail, Outlook, or SMTP. Without this, send() returns EMAIL_SERVICE_NOT_CONFIGURED.

03

Create template + languages

Templates → New. Slug e.g. otp-verification. Add variables {{code}}, {{appName}}. Add fr/en (or more) versions. Pick a template default lang.

04

Install from PyPI and verify

pip install zindua-sdk on Python 3.10+. Run python -m zindua doctor, then the verify script below with your key.

Verify

Confirm pip install works

CLI or script. Click a file to preview. Runs against zindua.run with your API key.

verify.sh
# After: pip install zindua-sdk  (Python 3.10+)
export ZINDUA_API_KEY=znd_test_your_key_here

# 1) Key format + GET /project + channel readiness
python -m zindua doctor

# 2) Queue a test message (use your template slug from Dashboard → Templates)
python -m zindua send \
  --to you@example.com \
  --template otp-verification \
  --var code=482910 \
  --var appName=MyApp

# WhatsApp (E.164 with +)
python -m zindua send \
  --to +243812345678 \
  --channel whatsapp \
  --template otp-verification \
  --var code=482910

Official package on PyPI

Install with pip install zindua-sdk. Full README mirrors this guide. Requires Python 3.10+.

https://pypi.org/project/zindua-sdk/
View on PyPI
Swagger

Test with /docs

FastAPI generates OpenAPI automatically. Use Swagger UI to exercise your routes before wiring a frontend.

from pydantic import BaseModel, EmailStr, Field
from fastapi import Depends
from zindua.integrations.fastapi import get_zindua
from zindua import Zindua

class SendOtpRequest(BaseModel):
    to: EmailStr
    code: str = Field(min_length=4, max_length=8)
    lang: str | None = Field(default=None, examples=["fr", "en"])

@app.post("/send-otp", tags=["Zindua"])
async def send_otp(body: SendOtpRequest, zindua: Zindua = Depends(get_zindua)):
    return await zindua.send(
        to=str(body.to),
        template="otp-verification",
        lang=body.lang,
        variables={"code": body.code},
    )

# uvicorn main:app --reload --port 8000
# Swagger UI: http://127.0.0.1:8000/docs
POST /send

Request body fields

Same JSON as @zindua/sdk on Node. Validate with Pydantic before calling httpx.

FieldRequiredDescription
toYesEmail if channel is email (default). E.164 phone with + for WhatsApp (e.g. +243812345678).
templateYesTemplate slug from Dashboard → Templates.
channelNo"email" (default) or "whatsapp". Must match the format of to.
langNoISO 639-1 (fr, en, sw…). If omitted, Zindua uses the project default. If the lang is missing on the template, the template default is used and langFallback: true is returned.
variablesNoKey/value map for {{placeholders}}. Max 50 keys, 4096 chars per value.
cc, bcc, replyToNoEmail only. Ignored when channel is whatsapp.
attachmentsNoEmail only. Array (max 5) of { url: string, filename?: string }. HTTPS URLs fetched server-side and attached to the email.
Languages

Multilingual templates

Each template can have several language versions in the dashboard. One slug, multiple langs.

What you sendWhat Zindua uses
No lang in JSONProject default language (Dashboard → project settings)
lang: "en" and English exists on templateEnglish version rendered, langUsed: en
lang: "de" but only fr/en existTemplate default language + langFallback: true in response
# Omit lang → project default language
# Unknown lang on template → template default + lang_fallback=True
result = await zindua.send(
    to=to,
    template="otp-verification",
    lang=user_locale,  # optional: "fr", "en", …
    variables={"code": code, "appName": "MyApp"},
)
print(result.lang_used, result.lang_fallback)
Email

cc, bcc, replyTo, attachments

Email-only fields. WhatsApp ignores them.

# Email only, max 5 HTTPS urls
result = await zindua.send(
    to="user@example.com",
    template="invoice-ready",
    variables={"name": "Alex", "amount": "49.00"},
    cc="billing@myapp.com",
    reply_to="support@myapp.com",
    attachments=[{"url": "https://cdn.myapp.com/invoice.pdf", "filename": "invoice.pdf"}],
)
print(result.log_id)
Architecture

Your database, Zindua delivery

Zindua is not an auth database. Store OTP codes in Redis or Postgres; use POST /send only to deliver the message.

# Zindua delivers the message. Your app owns OTP state.
import os
import random
import redis.asyncio as redis
from zindua import Zindua

zindua = Zindua(api_key=os.environ["ZINDUA_API_KEY"])
r = redis.from_url(os.environ["REDIS_URL"])

@app.post("/otp/request")
async def otp_request(email: str):
    code = f"{random.randint(100000, 999999)}"
    await r.setex(f"otp:{email}", 600, code)
    result = await zindua.send(
        to=email,
        template="otp-verification",
        variables={"code": code},
    )
    return {"ok": True, "logId": result.log_id}
Patterns

Reusable send helper

Extract httpx logic once, inject in routes.

from zindua.integrations.fastapi import get_zindua, ZinduaDep

@app.get("/health/zindua")
async def zindua_health(zindua: ZinduaDep):
    project = await zindua.get_project()
    return {"project": project.get("project"), "channels": project.get("channels")}
Response

logId and langFallback

status: queued means accepted for delivery. Save logId for tracing.

{
  "success": true,
  "status": "queued",
  "logId": "550e8400-e29b-41d4-a716-446655440000",
  "channel": "email",
  "langUsed": "fr",
  "langFallback": false,
  "testMode": false,
  "context": {
    "plan": {"slug": "free", "emailQuota": 25000, "emailsUsed": 3},
    "channels": {"email": true, "whatsapp": false}
  }
}
API

Endpoints

Same REST surface as every Zindua integration.

MethodEndpointDescription
send()POST /sendQueue email or WhatsApp. Returns logId and context.
get_log()GET /logs/{logId}Delivery status for one message in your project.
get_project()GET /projectProject name, plan, channel readiness.
get_templates()GET /templatesSynced slugs, langs, variables per template.
connect()POST /connectBind API key to site URL (WordPress-style connect).
is_test_mode()(local)True for znd_test_ keys (sandbox).
Logs & analytics

After send()

Every send() returns logId. Poll GET /logs/{logId} from your backend, persist logId in your database, or use Dashboard → Logs and webhooks (email.delivered / email.failed).

Errors

HTTP status codes

Map Zindua API errors to HTTPException in FastAPI routes.

CodeHTTPMeaningAction
INVALID_EMAIL400Bad email or phone used with channel email.Validate with Pydantic EmailStr or match channel.
INVALID_PHONE400Bad phone or email used with channel whatsapp.Use +243… E.164 format.
TEMPLATE_NOT_FOUND404Unknown template slug.Fix slug or create template in dashboard.
EMAIL_SERVICE_NOT_CONFIGURED422No Gmail/SMTP connected.Dashboard → project → Service.
WHATSAPP_NOT_CONNECTED422WhatsApp not linked.Dashboard → WhatsApp → scan QR.
QUOTA_EXCEEDED429Monthly limit reached.Upgrade plan or wait for reset.
Security

Production checklist

Server-only API key

Load ZINDUA_API_KEY from .env or secrets manager. Never commit real keys. Never expose in frontend SPAs.

Authorization: Bearer only

Send Bearer znd_live_… in httpx headers. Query params and X-Api-Key are rejected by the API.

Protect Swagger in production

Disable /docs in production or protect it with auth. Swagger must not let anonymous users trigger sends with your key.

Trace with logId

Every successful send returns logId. Correlate with Dashboard → Logs or inbound webhooks.

Environment

Variables

VariableRequiredExampleNotes
ZINDUA_API_KEYYesznd_test_xxxxxxxxxxxxxxxxxxxxxxxxFrom Dashboard → Projects. Use znd_test_ in staging, znd_live_ in production.
ZINDUA_API_BASE_URLNohttps://zindua.run/api/v1Optional API override (e.g. local API during development).

Python CLI and optional Node CLI

Prefer python -m zindua (ships with zindua-sdk). npx @zindua/cli is the same API checks but requires Node 18+ on your machine. Neither is imported from Python code.

# Official Python CLI (recommended)
python -m zindua doctor
python -m zindua send --to user@example.com --template otp-verification --var code=482910

# Optional Node CLI (dev machine with Node installed)
npx @zindua/cli@latest doctor

# Raw HTTP without CLI
curl -H "Authorization: Bearer $ZINDUA_API_KEY" https://zindua.run/api/v1/project

Quick check without Node: python -m zindua doctor

Node backend instead?

Use @zindua/sdk with Fastify for typed send() on Node 18+.