Zindua for FastAPI
Async Python backends with the official zindua-sdk v1.0.2. send(), get_log(), multilingual templates, attachments, and FastAPI Depends().
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.
Install and send
Pick a file in the explorer. Same layout as your IDE.
# 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-dotenvBefore pip install
Four steps in the dashboard, then verify below.
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().
Connect Service (email)
Project → Service → Gmail, Outlook, or SMTP. Without this, send() returns EMAIL_SERVICE_NOT_CONFIGURED.
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.
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.
Confirm pip install works
CLI or script. Click a file to preview. Runs against zindua.run with your API key.
# 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=482910Official package on PyPI
Install with pip install zindua-sdk. Full README mirrors this guide. Requires Python 3.10+.
https://pypi.org/project/zindua-sdk/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/docsRequest body fields
Same JSON as @zindua/sdk on Node. Validate with Pydantic before calling httpx.
| Field | Required | Description |
|---|---|---|
| to | Yes | Email if channel is email (default). E.164 phone with + for WhatsApp (e.g. +243812345678). |
| template | Yes | Template slug from Dashboard → Templates. |
| channel | No | "email" (default) or "whatsapp". Must match the format of to. |
| lang | No | ISO 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. |
| variables | No | Key/value map for {{placeholders}}. Max 50 keys, 4096 chars per value. |
| cc, bcc, replyTo | No | Email only. Ignored when channel is whatsapp. |
| attachments | No | Email only. Array (max 5) of { url: string, filename?: string }. HTTPS URLs fetched server-side and attached to the email. |
Multilingual templates
Each template can have several language versions in the dashboard. One slug, multiple langs.
| What you send | What Zindua uses |
|---|---|
| No lang in JSON | Project default language (Dashboard → project settings) |
| lang: "en" and English exists on template | English version rendered, langUsed: en |
| lang: "de" but only fr/en exist | Template 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)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)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}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")}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}
}
}Endpoints
Same REST surface as every Zindua integration.
| Method | Endpoint | Description |
|---|---|---|
| send() | POST /send | Queue email or WhatsApp. Returns logId and context. |
| get_log() | GET /logs/{logId} | Delivery status for one message in your project. |
| get_project() | GET /project | Project name, plan, channel readiness. |
| get_templates() | GET /templates | Synced slugs, langs, variables per template. |
| connect() | POST /connect | Bind API key to site URL (WordPress-style connect). |
| is_test_mode() | (local) | True for znd_test_ keys (sandbox). |
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).
HTTP status codes
Map Zindua API errors to HTTPException in FastAPI routes.
| Code | HTTP | Meaning | Action |
|---|---|---|---|
| INVALID_EMAIL | 400 | Bad email or phone used with channel email. | Validate with Pydantic EmailStr or match channel. |
| INVALID_PHONE | 400 | Bad phone or email used with channel whatsapp. | Use +243… E.164 format. |
| TEMPLATE_NOT_FOUND | 404 | Unknown template slug. | Fix slug or create template in dashboard. |
| EMAIL_SERVICE_NOT_CONFIGURED | 422 | No Gmail/SMTP connected. | Dashboard → project → Service. |
| WHATSAPP_NOT_CONNECTED | 422 | WhatsApp not linked. | Dashboard → WhatsApp → scan QR. |
| QUOTA_EXCEEDED | 429 | Monthly limit reached. | Upgrade plan or wait for reset. |
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.
Variables
| Variable | Required | Example | Notes |
|---|---|---|---|
| ZINDUA_API_KEY | Yes | znd_test_xxxxxxxxxxxxxxxxxxxxxxxx | From Dashboard → Projects. Use znd_test_ in staging, znd_live_ in production. |
| ZINDUA_API_BASE_URL | No | https://zindua.run/api/v1 | Optional 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/projectQuick check without Node: python -m zindua doctor
Node backend instead?
Use @zindua/sdk with Fastify for typed send() on Node 18+.