Zindua

Instant communication platform for businesses, optimized for developers.

Product
  • WhatsApp
  • Campaigns
  • PushMirror
  • MailGuard
  • Pricing
  • Compare
Developers
  • Documentation
  • Node.js
  • Next.js
  • PHP
  • FastAPI
  • MCP / Cursor
Solutions
  • Public sector
  • Health & care
  • Banking & fintech
  • NGOs & associations
  • SaaS & product teams
  • All solutions
Company
  • About
  • Blog
  • Contact
  • Status
  • Terms
  • Privacy

Product

  • WhatsApp
  • Campaigns
  • PushMirror
  • MailGuard
  • Pricing
  • Compare

Developers

  • Documentation
  • Node.js
  • Next.js
  • PHP
  • FastAPI
  • MCP / Cursor

Solutions

  • Public sector
  • Health & care
  • Banking & fintech
  • NGOs & associations
  • SaaS & product teams
  • All solutions

Company

  • About
  • Blog
  • Contact
  • Status
  • Terms
  • Privacy

© 2026 Zindua, Inc. All rights reserved.

System status
Zindua
How it WorksPricingCompare
Sign in
Get started
Start Free
Documentation

Overview

Developer documentation

Use HTTP / cURL from any stack, or an official SDK. Set channel to email or whatsapp. Zindua orchestrates delivery through your connected providers — not a hosted ESP.

Domain & custom sender setup →

HTTP / cURL·Next.js·Node.js·MCP·PushMirror
5 min

Connect channels

Email service + WhatsApp QR

2 min

Create templates

One slug, two channels

10s

Send via HTTP

curl · fetch · any client

Start by product

Start by product

Choose the integration path that fits your stack. All options use the same API and templates.

Universal

HTTP API

No SDK required. Use POST /api/v1/send from any backend.

Open guide
ModelContextProtocolAI editors

MCP / Cursor

Connect Cursor in a few steps: doctor, real templates, then generate your OTP integration.

Open guide
Official

Node.js SDK

Official @zindua/sdk — Express, Fastify, Nest, Next. Email OTP, WhatsApp, verifyEmail.

Open guide
Official

Next.js starter

Scaffold a full auth app with @zindua/create-app.

Open guide
SupabaseEdge Functions

Supabase

Auth Hooks + custom OTP Edge Functions. Email & WhatsApp from your providers.

Open guide
Official

Fastify

Typed Node backends with @zindua/sdk and Fastify plugins.

Open guide
Official

FastAPI

Async Python with zindua-sdk and FastAPI Depends().

Open guide
Guide

Nodemailer

Keep your SMTP. Swap transporter.sendMail for zindua.send — templates, logs, WhatsApp.

Open guide
Bridge

React Email

Migrate from Nodemailer, SendGrid, or SES. Keep your provider — host templates on Zindua.

Open guide
Official

PHP SDK

Laravel, Symfony, and WordPress custom integrations.

Open guide
.NETOfficial

.NET SDK

ASP.NET Core with Zindua.Sdk NuGet package.

Open guide
Official

Java SDK

One Maven Central package for Spring Boot, Quarkus, and any Java backend.

Open guide
Official

WordPress plugin

No-code OTP flows for WordPress and WooCommerce.

Open guide

PushMirror

< 5 min

PushMirror

1-tap approve, emoji, or digit challenges via WhatsApp, Web Push, or BYO FCM/APNs. Same API key as email and WhatsApp OTP. New machine sign-in — not team invites.

Full PushMirror guide + live schematic

Dual-device demo, Firebase setup, default PushMirror icon, and SDK samples on the product page.

Mobile / Firebase guideLive demo

API key → .env

Copy znd_live_… from your project. Put it in ZINDUA_API_KEY on the server only (create challenges).

Webhook secret → .env

Push → Config → rotate secret (znd_sec_…). Put in ZINDUA_WEBHOOK_SECRET to verify x-zindua-signature.

FCM key → dashboard

Firebase Console → Cloud Messaging → Server key. Paste under Push → Config. Not in the download zip.

1. Backend challenge

Create a push challenge from Node.js, Python, PHP, or .NET using your znd_live_ key.

server.ts
const challenge = await zindua.pushMirror.create({
  to: "+243832499559",
  type: "emoji",
  purpose: "login"
});

2. Realtime frontend stream

Listen to the SSE event stream for instant approval without polling HTTP.

client.js
const es = new EventSource("/api/v1/challenges/" + challenge.id + "/stream");
es.onmessage = (e) => {
  if (JSON.parse(e.data).status === "approved") {
    window.location.href = "/dashboard";
  }
};

Android / iOS via Firebase

Step-by-step Firebase Console path, google-services.json / GoogleService-Info.plist placement, device registration, SSE vs webhooks, and Flutter / Kotlin / Swift snippets on /pushmirror/mobile. Empty Logo URL uses the PushMirror default mark; prompt footer shows Powered by Zindua.

Read this first

Domain (custom sender)

If you want to send from your own domain (noreply@yourcompany.com), this is the checklist. Zindua is an orchestration layer: you connect a provider in Service, and Domain helps you align DNS records.

Zindua does not become your SMTP provider when you add a domain here. Outbound email still goes through the provider you connect under Service.

Use Service to choose who sends mail. Use Domains & DNS to paste records at Cloudflare, OVH, or Route 53.

How the pieces connect

Your app

POST /api/v1/send

API key + template slug

Zindua

Queue → email worker

Templates, logs, routing

Service (dashboard)

SendGrid / Gmail / SMTP

fromEmail = noreply@acme.com

Domains & DNS (dashboard)

SPF / DKIM / DMARC / Zindua TXT at your DNS host. Helps deliverability; does not replace Service.

What you see in the dashboard

Service

Provider

SendGrid, Gmail, Outlook, Mailgun, custom SMTP

From name

Acme

From email

noreply@acme.com

Credentials

API key or OAuth (depends on provider)

Domains & DNS
acme.com
Add domain
DomainStatusDNSActions
acme.comVerified4/5Configure DNS

DNS checklist only. Does not replace provider verification or SMTP credentials.

Where to open these screens

  • Sign inUse your Zindua account.
  • DashboardOpen the developer dashboard after login.
  • Your projectPick the project in the left sidebar.
  • ServiceConnect Gmail, SendGrid, SMTP, etc. Set From name and From email.
  • Domains & DNSAdd your hostname and copy DNS records (Pro/Me plans).

Steps 4 and 5 are separate menus: use Service first, then Domains & DNS when you need a custom domain on Pro/Team.

Configure DNS modal (example for acme.com)

Zindua verificationTXT

_zindua.acme.com

Proves you control the zone. Required for Verified status.

DKIMTXT

zindua._domainkey.acme.com

Public key for future signing; align with your ESP today.

SPFTXT

acme.com

Hint based on your Service provider (e.g. include:sendgrid.net).

DMARCTXT

_dmarc.acme.com

Policy record for receivers.

Return path (MX)MX

bounce.acme.com

Optional; shown when ZINDUA_BOUNCE_MX_HOST is set.

Domains & DNS (dashboard)

Pro/Team projects can add a sending hostname and get DNS records to paste at their DNS host. This does not activate sending by itself.

  • “Verified” means we found the Zindua ownership TXT on your DNS. It does not mean Zindua is now your SMTP provider.
  • DKIM keys generated here are stored for future signing. Today, outbound mail is signed by your connected provider, not by Zindua’s worker.

Recommended workflow

01Connect Service (Gmail, SendGrid, SMTP, …).
02Verify the domain with that provider if required.
03Add the same domain here and align SPF/DKIM/DMARC.
04Set fromEmail in Service to an address on that domain.
05Call POST /api/v1/send.

Compare pricing and ESP features on /compare.

Example: send from noreply@acme.com with SendGrid

01

Dashboard → Service

Connect SendGrid with your API key.

  • From name: Acme
  • From email: noreply@acme.com
02

SendGrid dashboard

Authenticate domain acme.com (Sender Authentication).

  • Complete their DNS steps before going live.
03

Dashboard → Domains & DNS

Add domain acme.com, open Configure DNS.

  • Copy each TXT/MX into Cloudflare (or OVH, Route 53, …).
  • Click Verify all until Status = Verified.
04

Your backend

Send with the project API key.

  • From header in the delivered email = noreply@acme.com (from Service).

Step 4: API request

curl -X POST https://zindua.run/api/v1/send \
  -H "Authorization: Bearer znd_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "user@example.com",
    "channel": "email",
    "template": "welcome",
    "variables": { "name": "Alex" }
  }'

Single route

POST /api/v1/send

All messages use this endpoint. Change channel per request without changing URL or API key.

POSThttps://zindua.run/api/v1/send

Authorization: Bearer znd_live_xxx

Copy your project key from Dashboard → Projects → your project (starts with znd_live_ or znd_test_).

FieldRequiredDescription
toYesRecipient. Email if channel is email (default). E.164 phone with + for WhatsApp (e.g. +243812345678).
templateYesTemplate slug from your dashboard (same slug for email and WhatsApp).
channelNo"email" (default) or "whatsapp". One route, switch channel per request.
langNoISO 639-1 code (fr, en, sw…). Falls back to project default if missing.
variablesNoKey/value map for {{placeholders}} in the template.
cc, bcc, replyTo, attachmentsNoEmail only. Ignored when channel is whatsapp.
Email body
body-email.json
{
  "to": "user@example.com",
  "channel": "email",
  "template": "welcome",
  "variables": { "name": "Alex" }
}
WhatsApp body
body-whatsapp.json
{
  "to": "+243812345678",
  "channel": "whatsapp",
  "template": "otp-verification",
  "variables": { "code": "4592" }
}
Response (202)
response.json
{
  "success": true,
  "status": "queued",
  "logId": "uuid",
  "channel": "email",
  "langUsed": "fr",
  "langFallback": false,
  "testMode": false,
  "project": "Overlook",
  "context": {
    "project": { "id": "uuid", "name": "Overlook", "slug": "overlook", "teamId": "uuid" },
    "apiKey": { "mode": "live", "prefix": "znd_live_", "suffix": "ejho" },
    "plan": {
      "slug": "free",
      "name": "Free",
      "status": "active",
      "emailApiEnabled": false,
      "whatsappEnabled": true,
      "emailQuota": 25000,
      "emailsUsed": 0
    },
    "channels": { "email": true, "whatsapp": false }
  }
}

Full HTTP / cURL guide . Copy-paste examples for email, WhatsApp, fetch, and error handling.

No SDK required

HTTP / cURL

Copy-paste examples that work without installing a package. Replace ZINDUA_API_KEY with your project key.

Every integration can use plain HTTP; no SDK required. Send JSON to the endpoint below with your project API key (znd_live_… or znd_test_…). Works from curl, Postman, Go, PHP, Ruby, Java, or any HTTP client.

Endpoint

POSThttps://zindua.run/api/v1/send

Required headers

HeaderValueNotes
AuthorizationBearer znd_live_xxxxxxxxYour project API key from the dashboard.
Content-Typeapplication/jsonRequest body must be JSON.

cURL (email)

curl-email.sh
curl -X POST https://zindua.run/api/v1/send \
  -H "Authorization: Bearer $ZINDUA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "user@example.com",
    "channel": "email",
    "template": "welcome",
    "variables": { "name": "Alex" }
  }'

cURL (WhatsApp OTP)

curl-whatsapp.sh
curl -X POST https://zindua.run/api/v1/send \
  -H "Authorization: Bearer $ZINDUA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+243812345678",
    "channel": "whatsapp",
    "template": "otp-verification",
    "variables": { "code": "4592" }
  }'

JavaScript fetch (Node 18+, Deno, Bun)

fetch-send.ts
const res = await fetch("https://zindua.run/api/v1/send", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.ZINDUA_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    to: "+243812345678",
    channel: "whatsapp",
    template: "otp-verification",
    variables: { code: "4592" },
  }),
});

if (!res.ok) {
  const err = await res.json();
  throw new Error(err.error ?? `HTTP ${res.status}`);
}

const data = await res.json();
console.log(data);

HTTP status codes

202Accepted. Message queued (or sent in test mode).
400Invalid body (missing to/template, bad email or phone format).
401Missing or invalid API key.
403Plan limit, origin not allowed (browser), or subscription issue.
404Template slug not found for this project.
422WhatsApp not connected, paused, or template missing WhatsApp body.
429Rate limit. Retry after retryAfterSec (WhatsApp).

Error response (JSON)

error-401.json
# Example error (401)
{
  "error": "Missing or invalid API key. Use: Authorization: Bearer znd_live_xxx"
}

Real projects

Examples

Default template language is English. Developers can send with lang fr or es after creating those template versions in the dashboard.

Navigation

Explorer

examples/login-otp-next

3 files match

Quickstart

terminal
npx @zindua/create-app@latest my-app
cd my-app
cp .env.example .env.local
# edit .env.local with your test key from dashboard
npm run dev

# Or use the monorepo example:
cd examples/login-otp-next
cp .env.example .env.local
npm install
npm run dev

Create .env.local from .env.example and keep it local only. Never commit any real znd_test_ or znd_live_ API key.

Download ZIP

Next.js OTP demo

Client pages call backend route handlers. The SDK stays server-side while users test email and WhatsApp OTP from a realistic login flow. Or scaffold with: npx @zindua/create-app@latest

Next.js App Router + Route Handlersexamples/login-otp-nexthttp://localhost:3010

Try in 60 seconds

  1. 01Open http://localhost:3010 and enter email or phone (+243...).
  2. 02Click Send OTP by email or Send OTP by WhatsApp.
  3. 03Keep lang as en by default, or switch to fr/es after creating those template versions.
examples/login-otp-next/.env.example
ZINDUA_API_KEY=znd_test_xxxxxxxxxxxxxxxxxxxxxxxx
ZINDUA_APP_NAME=LoginDemo
# Slug from Dashboard -> Templates (e.g. otp-verification)
ZINDUA_TEMPLATE_SLUG=otp-verification
Terminal
examples/login-otp-next

Install

$ npm install

added 180 packages in 4s

Run app

$ npm run dev

ready - started server on http://localhost:3010

Quick OTP test

$ curl -s -X POST http://localhost:3010/api/auth/send-otp -H "Content-Type: application/json" -d '{"channel":"whatsapp","to":"+243812345678","lang":"en"}'

{"ok":true,"channel":"whatsapp","to":"+243812345678"}

Dashboard prerequisites

  • ·Create a project and copy API key from Dashboard -> Projects.
  • ·Create template slug otp-verification with default language set to en and token {{code}}.
  • ·Add optional template versions for fr and es if you want localized OTP copy.
  • ·Connect WhatsApp line (QR) to test WhatsApp OTP.
  • ·Connect an email service to test email OTP.

Step by step

  1. 01Open http://localhost:3010 and enter email or phone (+243...).
  2. 02Click Send OTP by email or Send OTP by WhatsApp.
  3. 03Keep lang as en by default, or switch to fr/es after creating those template versions.
  4. 04Enter the 6-digit OTP on /verify and submit.
  5. 05Check /success page and dashboard logs for delivery status.

How Zindua Works

How Zindua Works

Four steps from dashboard to delivery on email or WhatsApp.

01

Connect channels

Email: Dashboard → Services (Gmail, Outlook, SMTP). WhatsApp: scan QR under Dashboard → WhatsApp. Messages go out from your accounts.

02

Create templates

One slug per template. Email body is HTML; WhatsApp body is short text. Use {{variables}} on both channels.

03

Send from code

POST /api/v1/send with channel email or whatsapp. Same API key, same logs. Free plan starts with WhatsApp OTP only.

04

Track delivery

View logs in the dashboard. Webhooks fire for email.delivered and email.failed when configured.

Quickstart

< 5 min

Quickstart

Start with HTTP / cURL (works everywhere), then pick a framework. Click the top bar to jump.

Next.js OTP starter

Scaffold a full login flow with email + WhatsApp OTP. API key stays server-side.

npx @zindua/create-app@latest my-app
View starter

Zindua CLI

Send OTP tests and diagnose your API key from the terminal. Works with any stack.

npx @zindua/cli@latest doctor
CLI commands

CLI

< 1 min

Send and diagnose from the terminal

Send OTP tests, push/render templates (React Email HTML), inspect your project, and diagnose your API key from the terminal.

@zindua/cliv1.2.0
terminal
npx @zindua/cli@latest doctor

Reads ZINDUA_API_KEY from env or .env.local. Never pass the key as a positional argument.

Diagnose API key

Checks key format, GET /project, and email/WhatsApp channel readiness.

terminal
npx @zindua/cli@latest doctor

Send OTP test

Queue a message via your connected channels. Add --json for CI scripts.

terminal
npx @zindua/cli@latest send --to user@example.com --template otp-verification --var code=482910

Inspect project

Project name, plan, API key suffix, channel status.

terminal
npx @zindua/cli@latest project

List templates

Synced template slugs, languages, and variables.

terminal
npx @zindua/cli@latest templates list

Push HTML template

Import React Email HTML into a hosted locale. See zindua.run/react-email.

terminal
npx @zindua/cli@latest templates push --slug welcome --lang en --subject "Welcome {{name}}" --html ./welcome.html --default

Render template (no send)

Escape hatch: interpolate {{vars}} without delivery. Prefer send for production.

terminal
npx @zindua/cli@latest templates render --template welcome --var name=Ada --out preview.html
cheat-sheet.sh
# Set key once
export ZINDUA_API_KEY=znd_test_your_key

npx @zindua/cli@latest doctor
npx @zindua/cli@latest send --to user@example.com --template otp-verification --var code=482910
npx @zindua/cli@latest project
npx @zindua/cli@latest templates list
npx @zindua/cli@latest templates push --slug welcome --lang en --subject "Hi {{name}}" --html ./welcome.html --default

ModelContextProtocolMCP for Cursor & Claude

< 5 min

MCP for Cursor & Claude

Local MCP server for Cursor and Claude Desktop. Closed-loop OTP: diagnose your live project, scaffold a certified snippet, then send a test only after you confirm a recipient. Same config for Node, Python, ASP.NET, PHP, or WordPress.

Full MCP guide

Why not npm i, Python/.NET/PHP with Cursor, email + WhatsApp test prompts.

Open /mcp

npm i @zindua/mcp on the npm page is optional. For Cursor, create .cursor/mcp.json with npx. Same for Python, ASP.NET, and PHP projects.

@zindua/mcpv1.2.0zindua.run/mcp

Before you start

  • ·Node.js 18+ on the machine running Cursor or Claude Desktop (needed so npx can start the MCP process). Your app language can still be Python, C#, PHP, etc.
  • ·Create a Zindua project and copy an API key (prefer a test key while integrating).
  • ·Connect Gmail/SMTP in the dashboard for email; connect WhatsApp in the dashboard for WhatsApp OTP.
  • ·Create at least one template (slug + variables). The same slug can be used for email and WhatsApp.

Where to put mcp.json

This project (recommended)

.cursor/mcp.json

At the root of your app repo (FastAPI, ASP.NET, Laravel, Next.js, …). Teammates who open the folder in Cursor get the same tools.

All Cursor projects

~/.cursor/mcp.json

In your user home folder. Available in every workspace on your machine.

project layout
my-otp-app/                 ← any stack (Python, .NET, PHP, Node…)
├── .cursor/
│   └── mcp.json            ← ONLY file required for Cursor MCP
├── .env                    ← ZINDUA_API_KEY for YOUR app SDK (server-side)
├── .gitignore
└── … your app code …
01

Open Cursor Settings → Tools & MCP

macOS: Cmd+Shift+J · Windows/Linux: Ctrl+Shift+J. Or create the JSON file manually (see tree below).

02

Create .cursor/mcp.json

In the project root: mkdir -p .cursor then add mcp.json. First run downloads @zindua/mcp via npx. You do not run npm i @zindua/mcp in your app.

03

Reload and verify

Save, reload the Cursor window, confirm the zindua server is connected under Tools & MCP.

04

Ask the assistant

Prefer zindua_workflow_otp or /zindua-ship. Fix channels if needed, then scaffold and send only with an explicit recipient.

Cursor · .cursor/mcp.json

Replace YOUR_ZINDUA_API_KEY, or use ${env:ZINDUA_API_KEY} and export the key in your shell so you can commit mcp.json without secrets.

.cursor/mcp.json
{
  "mcpServers": {
    "zindua": {
      "command": "npx",
      "args": ["-y", "@zindua/mcp@latest"],
      "env": {
        "ZINDUA_API_KEY": "YOUR_ZINDUA_API_KEY"
      }
    }
  }
}

Safer for git: keep the key in your environment and reference it.

.cursor/mcp.json
{
  "mcpServers": {
    "zindua": {
      "command": "npx",
      "args": ["-y", "@zindua/mcp@latest"],
      "env": {
        "ZINDUA_API_KEY": "${env:ZINDUA_API_KEY}"
      }
    }
  }
}

Claude Desktop

macOS: ~/Library/Application Support/Claude/claude_desktop_config.json · Windows: %APPDATA%\Claude\claude_desktop_config.json

claude_desktop_config.json
{
  "mcpServers": {
    "zindua": {
      "command": "npx",
      "args": ["-y", "@zindua/mcp@latest"],
      "env": {
        "ZINDUA_API_KEY": "YOUR_ZINDUA_API_KEY"
      }
    }
  }
}

What your agent can do

“Run zindua_workflow_otp for my FastAPI WhatsApp OTP.”

“Call zindua_doctor and tell me if email and WhatsApp are ready.”

“Recommend an OTP template, then zindua_scaffold for nextjs.”

“I confirm: validate then send a test OTP email to me@example.com using template otp-verification.”

“Watch the logId from that send until it is delivered or failed.”

“List my recent Zindua logs and summarize failures.”

zindua_workflow_otp

Start here. Doctor + templates + nextSteps. Sets stopCodegen if channels are not ready.

zindua_doctor

API key, project reachability, email ready, WhatsApp ready.

zindua_scaffold

Certified snippets for nextjs, fastapi, aspnet, php, nodejs from a real template slug.

zindua_validate_payload

Dry-run to/channel/slug/variables. Does not send.

zindua_send_test

Send a test only after you confirm to + template. Blocks znd_live_ unless forceLive.

zindua_watch_log

Poll delivery until terminal status + fix hints.

zindua_list_templates

Exact template slugs, languages, and variables.

zindua_recommend_template

Score OTP-like templates for an intent (login_otp, …).

zindua_list_recent_logs

Recent deliveries for this project API key.

zindua_email_path / zindua_whatsapp_status

Channel readiness + dashboard setupUrl (no QR in chat).

zindua_wordpress_connect

Bind a site URL to this API key (WordPress).

/zindua-ship (Cursor plugin)

Slash command that runs the closed loop. Cursor only — Claude: ask in natural language.

HTTP / cURL

Full integration guide

01Install
terminal
# Any HTTP client
02Set API Key
.env
export ZINDUA_API_KEY="znd_live_xxxxxxxxxxxxxxxxxxxx"
03Initialize
terminal
# https://zindua.run/api/v1/send
# Authorization: Bearer znd_live_xxx
04Send email
send-email-terminal
curl -X POST https://zindua.run/api/v1/send \
  -H "Authorization: Bearer $ZINDUA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "user@example.com",
    "channel": "email",
    "template": "welcome",
    "variables": { "name": "Alex" }
  }'
05Send WhatsApp
send-whatsapp-terminal
curl -X POST https://zindua.run/api/v1/send \
  -H "Authorization: Bearer $ZINDUA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+243812345678",
    "channel": "whatsapp",
    "template": "otp-verification",
    "variables": { "code": "4592" }
  }'
06Language (optional)
i18n-terminal
curl -X POST https://zindua.run/api/v1/send \
  -H "Authorization: Bearer $ZINDUA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+243812345678",
    "channel": "whatsapp",
    "template": "otp-verification",
    "lang": "fr",
    "variables": { "code": "4592" }
  }'

Next.js

Full integration guide

01Install
terminal
npm install @zindua/sdk
02Set API Key
.env
# .env.local — one key per Zindua project (server only)
ZINDUA_KEY_MELLIA=znd_live_xxxxxxxxxxxxxxxxxxxxxxxx
ZINDUA_KEY_OVERLOOK=znd_live_yyyyyyyyyyyyyyyyyyyyyyyy
03Initialize
route.ts
// lib/zindua.ts
import { Zindua } from '@zindua/sdk';

export const zinduaByTenant = {
  mellia: new Zindua({ apiKey: process.env.ZINDUA_KEY_MELLIA! }),
  overlook: new Zindua({ apiKey: process.env.ZINDUA_KEY_OVERLOOK! }),
};
04Send email
send-email-route.ts
// Pick tenant → project key (mellia | overlook)
await zinduaByTenant.mellia.send({
  to: 'user@example.com',
  template: 'welcome',
  variables: { name: 'Alex' },
});
05Send WhatsApp
send-whatsapp-route.ts
await zinduaByTenant.overlook.send({
  to: '+243812345678',
  channel: 'whatsapp',
  template: 'otp-verification',
  variables: { code: '4592', app: 'MonApp' },
});
06Language (optional)
i18n-route.ts
await zinduaByTenant.overlook.send({
  to: '+243812345678',
  channel: 'whatsapp',
  template: 'otp-verification',
  lang: 'fr',
  variables: { code: '4592' },
});

// Errors: import { ZinduaError } from '@zindua/sdk'
// catch (e) { if (e instanceof ZinduaError) console.log(e.code, e.status) }

React

Full integration guide

01Install
terminal
npm install @zindua/sdk
02Set API Key
.env
ZINDUA_API_KEY=znd_live_xxxxxxxxxxxxxxxxxxxx
03Initialize
server.js
// server/zindua.ts (backend only)
import { Zindua } from '@zindua/sdk';

export const zindua = new Zindua({
  apiKey: process.env.ZINDUA_API_KEY!,
});
04Send email
send-email-server.js
// pages/api/notify.ts
import { zindua } from '@/server/zindua';

export default async function handler(req, res) {
  const { email, template, variables } = req.body;
  const result = await zindua.send({ to: email, template, variables });
  res.status(202).json(result);
}
05Send WhatsApp
send-whatsapp-server.js
// Client calls YOUR backend, not Zindua:
await fetch('/api/notify', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    phone: '+243812345678',
    template: 'otp-verification',
    variables: { code: '482910' },
  }),
});
06Language (optional)
i18n-server.js
// Backend maps phone → Zindua send:
await zindua.send({
  to: phone,
  channel: 'whatsapp',
  template: 'otp-verification',
  lang: 'fr',
  variables: { code },
});

Python

Full integration guide

01Install
terminal
pip install zindua-sdk
02Set API Key
.env
export ZINDUA_API_KEY=znd_live_xxxxxxxxxxxxxxxxxxxx
03Initialize
app.py
import os
from zindua import Zindua

zindua = Zindua(api_key=os.environ["ZINDUA_API_KEY"])
04Send email
send-email-app.py
result = await zindua.send(
    to="user@example.com",
    channel="email",
    template="reset-password",
    variables={"name": "Sarah", "link": "https://..."},
)
print(result.log_id, result.status)
05Send WhatsApp
send-whatsapp-app.py
result = await zindua.send(
    to="+243812345678",
    channel="whatsapp",
    template="otp-verification",
    variables={"code": "4592"},
)
print(result.log_id)
06Language (optional)
i18n-app.py
result = await zindua.send(
    to="+243812345678",
    channel="whatsapp",
    template="otp-verification",
    lang="fr",
    variables={"code": "4592"},
)
print(result.lang_fallback)

Flutter

Full integration guide

01Install
pubspec.yaml
# pubspec.yaml
dependencies:
  http: ^1.2.0
02Set API Key
.env
// Pass key from your backend. Never ship znd_live_ in the app
03Initialize
main.dart
// Call YOUR backend; it holds the Zindua API key.
// Direct Zindua calls from mobile are not recommended.
04Send email
send-email-main.dart
// Your backend POST https://zindua.run/api/v1/send
// Body: { "to": "customer@example.com", "template": "order-shipped", ... }
05Send WhatsApp
send-whatsapp-main.dart
// Your backend:
// { "to": "+243812345678", "channel": "whatsapp", "template": "otp-verification", ... }
06Language (optional)
i18n-main.dart
await http.post(
  Uri.parse('https://api.yourapp.com/v1/notify'),
  headers: {'Content-Type': 'application/json'},
  body: jsonEncode({
    'phone': '+243812345678',
    'template': 'otp-verification',
    'lang': 'fr',
    'code': '4592',
  }),
);

React Native

Full integration guide

01Install
terminal
# Use your backend. No Zindua key in the app
02Set API Key
.env
ZINDUA_API_KEY=znd_live_... # server .env only
03Initialize
VerifyScreen.tsx
// Node/Express backend with @zindua/sdk or HTTP POST https://zindua.run/api/v1/send
04Send email
send-email-VerifyScreen.tsx
// Backend:
await zindua.send({
  to: email,
  channel: 'email',
  template: 'verify-email',
  variables: { link },
});
05Send WhatsApp
send-whatsapp-VerifyScreen.tsx
await zindua.send({
  to: phone,
  channel: 'whatsapp',
  template: 'otp-verification',
  variables: { code },
});
06Language (optional)
i18n-VerifyScreen.tsx
await fetch('https://api.yourapp.com/send-otp', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ phone: '+243812345678', code, lang: 'fr' }),
});

Templates

Templates

One slug for email and WhatsApp. Update copy in the dashboard without redeploying your app.

How templates work

01

Create a template in Dashboard → Templates and set a slug (e.g. otp-verification).

02

Email: paste HTML with {{variables}}. WhatsApp: short plain text for OTP and alerts.

03

Add language versions if needed (fr, en, sw…).

04

Call send() with template slug + channel. Zindua renders variables per channel.

Email (HTML)
welcome-template.html
<!DOCTYPE html>
<html>
<body style="font-family: sans-serif; padding: 20px;">
  <h1>Welcome, {{name}}!</h1>
  <p>Thanks for joining {{appName}}.</p>
  <a href="{{verifyUrl}}" 
     style="background: #f97316; color: white; 
            padding: 12px 24px; border-radius: 8px;
            text-decoration: none; display: inline-block;">
    Verify Your Email
  </a>
</body>
</html>
WhatsApp (plain text)
otp-verification-whatsapp.txt
{{app}}: your verification code: {{code}}

This code expires in 10 minutes. Do not share it.

WhatsApp channel

Send OTP & codes on WhatsApp

Use the same API and templates as email. Set channel: "whatsapp" and a phone number in E.164 format. Your message is delivered from the number you connect in the dashboard.

Dashboard setup (before your first send)

01

Create a project and copy your API key (znd_live_… or znd_test_…).

02

Open Dashboard → your project → WhatsApp → Connect.

03

Scan the QR code with the phone you use for OTP (dedicated business line recommended).

04

Wait until status shows Connected. You can Pause sending or Unlink the number anytime.

05

Call POST /api/v1/send from your backend only. Never expose the API key in mobile or web clients.

Free plan: WhatsApp OTP only (200 messages/month). Pro and Team add the email API plus higher or unlimited WhatsApp OTP quotas.

Pause

Temporarily stop outbound WhatsApp from your number. API returns a clear error until you resume.

Unlink

Disconnect the session completely. Scan again to reconnect. Unlink does not delete your templates or logs.

Full examples: and Quickstart step 5 per framework.

send-whatsapp-otp.sh
curl -X POST https://zindua.run/api/v1/send \
  -H "Authorization: Bearer $ZINDUA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+243812345678",
    "channel": "whatsapp",
    "template": "otp-verification",
    "variables": { "code": "4592" }
  }'

Plans & channels

Plans & channels

Free starts with WhatsApp OTP. Upgrade for email API and higher WhatsApp quotas. Same POST /api/v1/send on every plan.

PlanEmail APIWhatsAppWhatsApp quota
Free Yes Yes100 / month
Pro Yes Yes15,000 / month
Me Yes YesUnlimited

Free: Email API when a Service is connected on the project (Gmail/SMTP). WhatsApp via QR. PushMirror: 10/mo.

Pro: Email API + WhatsApp + PushMirror (8k/mo). Connect Gmail/SMTP and your WhatsApp line.

Me: Full channels for production scale. PushMirror: 100k/mo.

Compare plans

Multilingual messages

Multilingual messages

Each template can have multiple language versions for email and WhatsApp. Zindua picks the right one from your send() call.

Default Language

Every project has a default language (Dashboard → Project Settings). When you call send() with optional lang, Zindua uses that template version for email or WhatsApp. If the version is missing, it falls back to the project default.

lang: "sw"
✓ Swahili version found
lang: "de"
↩ Not found → Falls back to default (fr)
No lang param
→ Uses project default (fr)
Send with language (HTTP / cURL)
i18n-rest.ts
curl -X POST https://zindua.run/api/v1/send \
  -H "Authorization: Bearer $ZINDUA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+243812345678",
    "channel": "whatsapp",
    "template": "otp-verification",
    "lang": "fr",
    "variables": { "code": "4592" }
  }'

Webhooks

Webhooks

HTTPS callbacks for delivery events. Configure URL and events under Dashboard → Settings → Integrations. Payloads are signed when WEBHOOK_SIGNING_SECRET is set on the server.

Webhook Payload
webhook-payload.json
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "type": "email.delivered",
  "version": "2026-04-13",
  "created": "2026-04-12T12:00:00.000Z",
  "data": {
    "logId": "uuid",
    "projectId": "uuid",
    "recipient": "user@example.com",
    "messageId": "msg_482910"
  }
}

Maven Central

v1.0.0

Java SDK

Official Java 17 SDK on Maven Central. One JAR for Spring Boot, Quarkus, Play, Micronaut, and a plain Main. No extra Spring starter. Server-side only.

run.zindua:zindua-sdkv1.0.0

Gradle (Kotlin DSL)

build.gradle.kts
implementation("run.zindua:zindua-sdk:1.0.0")

Maven

pom.xml
<dependency>
  <groupId>run.zindua</groupId>
  <artifactId>zindua-sdk</artifactId>
  <version>1.0.0</version>
</dependency>

Same JAR for Spring Boot, Play Framework, Quarkus, or a plain Main. No extra starter. Keep ZINDUA_API_KEY on the server.

Full Java guide · Maven Central

Send OTP
Main.java
import io.zindua.sdk.ZinduaClient;
import io.zindua.sdk.ZinduaSendOptions;

ZinduaClient zindua = new ZinduaClient(System.getenv("ZINDUA_API_KEY"));

zindua.send(ZinduaSendOptions.builder()
    .to("user@example.com")
    .template("otp-verification")
    .variable("code", "482910")
    .build());

SDKs & HTTP API

SDKs & HTTP API

Node/TypeScript SDK matches POST /api/v1/send. Other stacks use the REST endpoint from your backend.

Official Node.js SDK on npm

@zindua/sdkv1.4.0
install.sh
npm install @zindua/sdk

Already installed? Upgrade for Guardian

upgrade.sh
npm install @zindua/sdk@1.4.0

One template slug for email and WhatsApp. Add up to 3 languages per template on Free (5 templates per project); Pro and Team raise both limits. Pass lang in send() to pick the locale.

Plan limits · npm

PHP SDK

Official PHP SDK for Laravel, Symfony, WordPress custom, and cron scripts.

composer require zindua/sdkcomposer update zindua/sdkPackagist
View PHP SDK

Python SDK

Official Python SDK for FastAPI, Django, Flask, and async scripts. send(), get_log(), attachments.

pip install zindua-sdkpip install zindua-sdk==1.2.0PyPI
View FastAPI guide

Java SDK

Official Java 17 SDK on Maven Central. One JAR for Spring Boot, Quarkus, Play, Micronaut, and a plain Main. No extra Spring starter. Server-side only.

implementation("run.zindua:zindua-sdk:1.0.0")Maven Central
View Java SDK
.NET

.NET SDK

Official .NET SDK for ASP.NET Core. AddZindua DI, SendAsync for email and WhatsApp OTP.

dotnet add package Zindua.SdkNuGet
View .NET SDK

WhatsApp anti-ban Guardian

Upgrade all official SDKs

Already on Zindua? Upgrade your SDK so WhatsApp sends cooperate with the Guardian automatically. No config change. Update the package, then redeploy. Read the Guardian guide. Also see Email verification.

Node.js / TypeScriptv1.4.0
npm install @zindua/sdk@1.4.0
PHP / Composerv1.1.0
composer update zindua/sdk
Python / PyPIv1.2.0
pip install zindua-sdk==1.2.0
.NET / NuGetv1.1.0
dotnet add package Zindua.Sdk --version 1.1.0
Java / Mavenv1.0.0
<dependency> <groupId>run.zindua</groupId> <artifactId>zindua-sdk</artifactId> <version>1.0.0</version> </dependency>

Configuration des Clés (API Key & Webhook Secret)

Où placer vos identifiants révocables lors du téléchargement d'un exemple ou SDK

.env.local / Server Environment
1. Clé d'API émettriceDashboard > Projets
ZINDUA_API_KEY=znd_live_xxxxxxxxxxxxxxxx

Sert à authentifier vos requêtes backend vers Zindua (envoi d'OTP, création de push challenge).

2. Secret Webhook révocableDashboard > Webhooks
ZINDUA_WEBHOOK_SECRET=znd_sec_e08ca8c9...

Sert à valider la signature HMAC x-zindua-signature sur votre serveur d'événement Push.

Official
Next.js

Starter officiel Next.js App Router avec support OTP et PushMirror.

npx @zindua/create-app@latest
Official
Fastify

Plugin Fastify ultra-rapide pour Node.js avec vérification HMAC.

npm install @zindua/sdk@1.4.0
Official
FastAPI

Routes asynchrones Python avec httpx et vérification de signature webhook.

pip install zindua-sdk==1.2.0
Official
PHP

zindua/sdk pour Laravel, Symfony, WordPress custom avec validation d'événements.

composer require zindua/sdk
.NETOfficial
.NET / C#

Zindua.Sdk NuGet pour ASP.NET Core et services Webhook arrière-plan.

dotnet add package Zindua.Sdk --version 1.1.0
Official
Java / Spring

Un JAR Maven Central pour Spring Boot, Quarkus, Maven et Gradle. Pas de starter Spring séparé. Clé côté serveur.

implementation("run.zindua:zindua-sdk:1.0.0")
Plugin
WordPress

Extension officielle pour la connexion OTP et validation Push WooCommerce.

Plugin Zindua OTP
Official
Node.js / TS

Guide @zindua/sdk — Express, NestJS, Remix, Hono. Voir zindua.run/nodejs.

npm install @zindua/sdk@1.4.0
Official
CLI

Outil terminal : envoi d'OTP, test de push, diagnostic de clé API.

npx @zindua/cli@latest doctor
ModelContextProtocolOfficial
MCP / Cursor

Connectez Cursor ou Claude avec @zindua/mcp pour scaffold et tests fermés.

zindua.run/mcp
Official
Python

zindua-sdk pour FastAPI, Django et scripts asynchrones. send() & webhooks.

pip install zindua-sdk==1.2.0
Mobile
Flutter / Dart

L'app mobile communique avec votre serveur backend qui sécurise la clé API.

Backend proxy API
Mobile
React Native

Applications iOS/Android n'intégrant jamais la clé API dans les binaires.

Backend proxy API

Several clients, several API keys

Several clients, several API keys

Create one Zindua project per client (or per brand). Your application chooses the right key server-side. Never ship multiple keys to the browser.

Zindua projectAPI keyUsage
Client A (e.g. mellia)znd_live_…Templates + Gmail/SMTP for client A
Client B (e.g. Overlook)znd_live_…Templates + Gmail/SMTP for client B
Separate quotas and logs per client
Different Gmail/SMTP per project
Revoke one key without affecting the other

One project + one key is fine when every customer shares the same sender, templates, and quota. For separate clients, prefer two projects.

.env
# .env — server only, never in the front-end
ZINDUA_KEY_MELLIA=znd_live_xxxxxxxxxxxxxxxxxxxxxxxx
ZINDUA_KEY_OVERLOOK=znd_live_yyyyyyyyyyyyyyyyyyyyyyyy
lib/zindua.ts
import { Zindua } from "@zindua/sdk";

const zinduaByTenant: Record<string, Zindua> = {
  mellia: new Zindua({ apiKey: process.env.ZINDUA_KEY_MELLIA! }),
  overlook: new Zindua({ apiKey: process.env.ZINDUA_KEY_OVERLOOK! }),
};

export async function sendOtp(tenantId: "mellia" | "overlook", to: string, code: string) {
  const client = zinduaByTenant[tenantId];
  return client.send({
    to,
    template: "otp",
    variables: { code },
  });
}

Security

Security

Keep your integration safe. Here's what matters.

Never expose your API key

Use znd_live_ keys only on the server (env vars, secrets manager). Never in mobile apps, browsers, or public repos.

Authorization: Bearer only

Send znd_live_… in Authorization: Bearer. Never in URL (?api_key=), JSON body, or X-Api-Key — blocked by the API.

Backend-only send()

Your app calls your API route; your API route calls Zindua. The end user never sees Zindua credentials.

One key per project

Each Zindua project has its own key. A request only accesses that project's templates, service, and logs.

WhatsApp session stays on Zindua

After QR scan, the linked session is stored encrypted on our side. You only use your API key; you never receive session tokens.

CORS allowlist (browser)

Browser calls must match allowed origins under Dashboard → Settings → Integrations. Server-to-server calls without Origin are unaffected.

Verify webhook signatures

When WEBHOOK_SIGNING_SECRET is set on the server, validate X-Zindua-Signature (sha256=…) before trusting events.