One API.
Email + WhatsApp.
A single route: POST /v1/send. Set channel to email or whatsapp. Same templates, same API key, same logs.
POST /v1/send
All messages use this endpoint. Change channel per request without changing URL or API key.
Authorization: Bearer znd_live_xxx
| Field | Required | Description |
|---|---|---|
| to | Yes | Recipient. Email address if channel is email (default). E.164 phone if channel is whatsapp (e.g. 243812345678). |
| template | Yes | Template slug from your dashboard (same slug for email and WhatsApp). |
| channel | No | "email" (default) or "whatsapp". One route, switch channel per request. |
| lang | No | ISO 639-1 code (fr, en, sw…). Falls back to project default if missing. |
| variables | No | Key/value map for {{placeholders}} in the template. |
| cc, bcc, replyTo, attachments | No | Email only. Ignored when channel is whatsapp. |
{
"to": "user@example.com",
"channel": "email",
"template": "welcome",
"variables": { "name": "Alex" }
}{
"to": "243812345678",
"channel": "whatsapp",
"template": "otp-verification",
"variables": { "code": "4592" }
}{
"success": true,
"status": "queued",
"logId": "uuid",
"channel": "whatsapp",
"langUsed": "fr",
"langFallback": false,
"testMode": false
}curl -X POST https://api.zindua.dev/v1/v1/send \
-H "Authorization: Bearer $ZINDUA_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "243812345678",
"channel": "whatsapp",
"template": "otp-verification",
"variables": { "code": "4592" }
}'How Zindua Works
Four steps from dashboard to delivery on email or WhatsApp.
Connect channels
Email: Dashboard → Services (Gmail, Outlook, SMTP). WhatsApp: scan QR under Dashboard → WhatsApp. Messages go out from your accounts.
Create templates
One slug per template. Email body is HTML; WhatsApp body is short text. Use {{variables}} on both channels.
Send from code
POST /v1/send with channel email or whatsapp. Same API key, same logs. Free plan starts with WhatsApp OTP only.
Track delivery
View logs in the dashboard. Webhooks for email events. WhatsApp delivery status appears in logs.
Quickstart
Scroll through each framework or click one in the top bar to jump directly.
Next.js
Full integration guide
npm install @zindua/sdk# .env.local
ZINDUA_API_KEY=znd_live_xxxxxxxxxxxxxxxxxxxx// lib/zindua.ts
import { Zindua } from '@zindua/sdk';
export const zindua = new Zindua({
apiKey: process.env.ZINDUA_API_KEY!,
});// Email (default channel)
await zindua.send({
to: 'user@example.com',
template: 'welcome',
variables: { name: 'Alex' },
});
// Same route, explicit channel:
await zindua.send({
to: 'user@example.com',
channel: 'email',
template: 'welcome',
variables: { name: 'Alex' },
});// WhatsApp OTP (E.164 phone, no + prefix)
await zindua.send({
to: '243812345678',
channel: 'whatsapp',
template: 'otp-verification',
variables: { code: '4592', app: 'MonApp' },
});await zindua.send({
to: '243812345678',
channel: 'whatsapp',
template: 'otp-verification',
lang: 'fr',
variables: { code: '4592' },
});React
Full integration guide
npm install @zindua/sdkZINDUA_API_KEY=znd_live_xxxxxxxxxxxxxxxxxxxx// Backend only. Never call Zindua from the browser.
const { Zindua } = require('@zindua/sdk');
const zindua = new Zindua({ apiKey: process.env.ZINDUA_API_KEY });app.post('/api/notify', async (req, res) => {
const { email, channel = 'email', phone, template, variables } = req.body;
const to = channel === 'whatsapp' ? phone : email;
const result = await zindua.send({ to, channel, template, variables });
res.json(result);
});// Client calls YOUR backend, not Zindua:
await fetch('/api/notify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
channel: 'whatsapp',
phone: '243812345678',
template: 'otp-verification',
variables: { code: '482910' },
}),
});// Pass lang from your app locale
body: JSON.stringify({
channel: 'whatsapp',
phone: '243812345678',
template: 'otp-verification',
lang: 'fr',
variables: { code: '482910' },
})Python
Full integration guide
pip install zinduaZINDUA_API_KEY=znd_live_xxxxxxxxxxxxxxxxxxxximport os
from zindua import Zindua
zindua = Zindua(api_key=os.environ["ZINDUA_API_KEY"])# Email
zindua.send(
to="user@example.com",
template="reset-password",
variables={"name": "Sarah", "link": "https://..."},
)# WhatsApp
zindua.send(
to="243812345678",
channel="whatsapp",
template="otp-verification",
variables={"code": "4592"},
)zindua.send(
to="243812345678",
channel="whatsapp",
template="otp-verification",
lang="fr",
variables={"code": "4592"},
)Flutter
Full integration guide
# pubspec.yaml
dependencies:
zindua: ^1.0.0// flutter run --dart-define=ZINDUA_KEY=znd_live_...import 'package:zindua/zindua.dart';
final zindua = Zindua(apiKey: const String.fromEnvironment('ZINDUA_KEY'));await zindua.send(
to: 'customer@example.com',
template: 'order-shipped',
variables: {'trackingUrl': 'https://...'},
);await zindua.send(
to: '243812345678',
channel: 'whatsapp',
template: 'otp-verification',
variables: {'code': '4592'},
);await zindua.send(
to: '243812345678',
channel: 'whatsapp',
template: 'otp-verification',
lang: 'ar',
variables: {'code': '4592'},
);React Native
Full integration guide
npm install @zindua/react-nativeZINDUA_API_KEY=znd_live_xxxxxxxxxxxxxxxxxxxx// Call your backend; do not embed Zindua keys in the app binary.// Your Node backend:
await zindua.send({
to: email,
channel: 'email',
template: 'verify-email',
variables: { link },
});// Your Node backend:
await zindua.send({
to: phone,
channel: 'whatsapp',
template: 'otp-verification',
variables: { code },
});await fetch('https://api.yourapp.com/send-otp', {
method: 'POST',
body: JSON.stringify({ phone, code, lang: 'fr' }),
});REST / cURL
Full integration guide
# Any HTTP clientexport ZINDUA_KEY="znd_live_xxxxxxxxxxxxxxxxxxxx"# https://api.zindua.dev/v1/v1/send
# Authorization: Bearer znd_live_xxxcurl -X POST https://api.zindua.dev/v1/v1/send \
-H "Authorization: Bearer $ZINDUA_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "user@example.com",
"channel": "email",
"template": "welcome",
"variables": { "name": "Alex" }
}'curl -X POST https://api.zindua.dev/v1/v1/send \
-H "Authorization: Bearer $ZINDUA_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "243812345678",
"channel": "whatsapp",
"template": "otp-verification",
"variables": { "code": "4592" }
}'curl -X POST https://api.zindua.dev/v1/v1/send \
-H "Authorization: Bearer $ZINDUA_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "243812345678",
"channel": "whatsapp",
"template": "otp-verification",
"lang": "fr",
"variables": { "code": "4592" }
}'Templates
One slug for email and WhatsApp. Update copy in the dashboard without redeploying your app.
How templates work
Create a template in Dashboard → Templates and set a slug (e.g. otp-verification).
Email: paste HTML with {{variables}}. WhatsApp: short plain text for OTP and alerts.
Add language versions if needed (fr, en, sw…).
Call send() with template slug + channel. Zindua renders variables per channel.
<!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>{{app}} — your verification code: {{code}}
This code expires in 10 minutes. Do not share it.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)
Create a project and copy your API key (znd_live_… or znd_test_…).
Open Dashboard → your project → WhatsApp → Connect.
Scan the QR code with the phone you use for OTP (dedicated business line recommended).
Wait until status shows Connected. You can Pause sending or Unlink the number anytime.
Call POST /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.
curl -X POST https://api.zindua.dev/v1/v1/send \
-H "Authorization: Bearer $ZINDUA_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "243812345678",
"channel": "whatsapp",
"template": "otp-verification",
"variables": { "code": "4592" }
}'Plans & channels
Free starts with WhatsApp OTP. Upgrade for email API and higher WhatsApp quotas. Same POST /v1/send on every plan.
| Plan | Email API | WhatsApp quota | |
|---|---|---|---|
| Free | — | Yes | 200 / month |
| Pro | Yes | Yes | 20,000 / month |
| Team | Yes | Yes | Unlimited |
Free: WhatsApp OTP only. Connect your number in the dashboard.
Pro: Email API + WhatsApp. Connect Gmail/SMTP and your WhatsApp line.
Team: Full channels for production scale.
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.
await zindua.send({
to: '243812345678',
channel: 'whatsapp',
template: 'otp-verification',
lang: 'fr',
variables: { code: '4592' },
});Webhooks
Get real-time notifications when emails are delivered, bounced, opened, or clicked. Set up in Dashboard → Settings → Webhooks.
{
"type": "email.delivered",
"timestamp": "2026-04-12T12:00:00Z",
"data": {
"messageId": "msg_482910",
"to": "user@example.com",
"template": "welcome",
"lang": "fr",
"deliveredAt": "2026-04-12T12:00:01Z"
}
}Official SDKs
We build and maintain SDKs for major platforms. All SDKs wrap the same REST API with native ergonomics.
Full TypeScript types. Works with Next.js, Express, Remix, Hono, and edge runtimes.
Sync and async support. Compatible with Django, FastAPI, Flask, and serverless.
Native Dart SDK. Works on iOS, Android, Web, and Desktop Flutter apps.
Optimized for mobile. Supports Expo and bare React Native workflows.
Language-agnostic HTTP API. Use from Go, Ruby, PHP, Java, or anything with HTTP.
Use the Node SDK in your backend. Never expose API keys in client-side React.
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.
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
Keys are scoped to a single project. Rotate instantly from Dashboard if leaked.
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)
If you call from a browser, add allowed origins under Settings → Integrations. Server-to-server calls without Origin are unaffected.
Verify webhook signatures
Validate X-Zindua-Signature with your webhook secret before trusting delivery events.