Overview
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.
Connect channels
Email service + WhatsApp QR
Create templates
One slug, two channels
Send via HTTP
curl · fetch · any client
Start by product
Choose the integration path that fits your stack. All options use the same API and templates.
PushMirror
< 5 min1-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.
Copy znd_live_… from your project. Put it in ZINDUA_API_KEY on the server only (create challenges).
Push → Config → rotate secret (znd_sec_…). Put in ZINDUA_WEBHOOK_SECRET to verify x-zindua-signature.
Firebase Console → Cloud Messaging → Server key. Paste under Push → Config. Not in the download zip.
Create a push challenge from Node.js, Python, PHP, or .NET using your znd_live_ key.
const challenge = await zindua.pushMirror.create({
to: "+243832499559",
type: "emoji",
purpose: "login"
});Listen to the SSE event stream for instant approval without polling HTTP.
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
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
Provider
SendGrid, Gmail, Outlook, Mailgun, custom SMTP
From name
Acme
From email
noreply@acme.com
Credentials
API key or OAuth (depends on provider)
DNS checklist only. Does not replace provider verification or SMTP credentials.
Where to open these screens
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.acme.com
Proves you control the zone. Required for Verified status.
zindua._domainkey.acme.com
Public key for future signing; align with your ESP today.
acme.com
Hint based on your Service provider (e.g. include:sendgrid.net).
_dmarc.acme.com
Policy record for receivers.
bounce.acme.com
Optional; shown when ZINDUA_BOUNCE_MX_HOST is set.
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.
Recommended workflow
Compare pricing and ESP features on /compare.
Dashboard → Service
Connect SendGrid with your API key.
SendGrid dashboard
Authenticate domain acme.com (Sender Authentication).
Dashboard → Domains & DNS
Add domain acme.com, open Configure DNS.
Your backend
Send with the project API key.
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
All messages use this endpoint. Change channel per request without changing URL or API key.
Authorization: Bearer znd_live_xxx
Copy your project key from Dashboard → Projects → your project (starts with znd_live_ or znd_test_).
| Field | Required | Description |
|---|---|---|
| to | Yes | Recipient. Email if channel is email (default). E.164 phone with + for 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": "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
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
https://zindua.run/api/v1/sendRequired headers
| Header | Value | Notes |
|---|---|---|
| Authorization | Bearer znd_live_xxxxxxxx | Your project API key from the dashboard. |
| Content-Type | application/json | Request body must be JSON. |
cURL (email)
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 -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)
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
Error response (JSON)
# Example error (401)
{
"error": "Missing or invalid API key. Use: Authorization: Bearer znd_live_xxx"
}Real projects
Default template language is English. Developers can send with lang fr or es after creating those template versions in the dashboard.
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
Try in 60 seconds
ZINDUA_API_KEY=znd_test_xxxxxxxxxxxxxxxxxxxxxxxx
ZINDUA_APP_NAME=LoginDemo
# Slug from Dashboard -> Templates (e.g. otp-verification)
ZINDUA_TEMPLATE_SLUG=otp-verificationInstall
$ 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
Step by step
How Zindua Works
Four steps from dashboard to delivery on email or WhatsApp.
Email: Dashboard → Services (Gmail, Outlook, SMTP). WhatsApp: scan QR under Dashboard → WhatsApp. Messages go out from your accounts.
One slug per template. Email body is HTML; WhatsApp body is short text. Use {{variables}} on both channels.
POST /api/v1/send with channel email or whatsapp. Same API key, same logs. Free plan starts with WhatsApp OTP only.
View logs in the dashboard. Webhooks fire for email.delivered and email.failed when configured.
Quickstart
< 5 minStart 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-appZindua CLI
Send OTP tests and diagnose your API key from the terminal. Works with any stack.
npx @zindua/cli@latest doctorCLI
< 1 minSend OTP tests, push/render templates (React Email HTML), inspect your project, and diagnose your API key from the terminal.
npx @zindua/cli@latest doctorReads ZINDUA_API_KEY from env or .env.local. Never pass the key as a positional argument.
Checks key format, GET /project, and email/WhatsApp channel readiness.
npx @zindua/cli@latest doctorQueue a message via your connected channels. Add --json for CI scripts.
npx @zindua/cli@latest send --to user@example.com --template otp-verification --var code=482910Project name, plan, API key suffix, channel status.
npx @zindua/cli@latest projectSynced template slugs, languages, and variables.
npx @zindua/cli@latest templates listImport React Email HTML into a hosted locale. See zindua.run/react-email.
npx @zindua/cli@latest templates push --slug welcome --lang en --subject "Welcome {{name}}" --html ./welcome.html --defaultEscape hatch: interpolate {{vars}} without delivery. Prefer send for production.
npx @zindua/cli@latest templates render --template welcome --var name=Ada --out preview.html# 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 --defaultMCP for Cursor & Claude
< 5 minLocal 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.
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.
Before you start
Where to put mcp.json
.cursor/mcp.jsonAt the root of your app repo (FastAPI, ASP.NET, Laravel, Next.js, …). Teammates who open the folder in Cursor get the same tools.
~/.cursor/mcp.jsonIn your user home folder. Available in every workspace on your machine.
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 …macOS: Cmd+Shift+J · Windows/Linux: Ctrl+Shift+J. Or create the JSON file manually (see tree below).
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.
Save, reload the Cursor window, confirm the zindua server is connected under Tools & MCP.
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.
{
"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.
{
"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
{
"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.”
Start here. Doctor + templates + nextSteps. Sets stopCodegen if channels are not ready.
API key, project reachability, email ready, WhatsApp ready.
Certified snippets for nextjs, fastapi, aspnet, php, nodejs from a real template slug.
Dry-run to/channel/slug/variables. Does not send.
Send a test only after you confirm to + template. Blocks znd_live_ unless forceLive.
Poll delivery until terminal status + fix hints.
Exact template slugs, languages, and variables.
Score OTP-like templates for an intent (login_otp, …).
Recent deliveries for this project API key.
Channel readiness + dashboard setupUrl (no QR in chat).
Bind a site URL to this API key (WordPress).
Slash command that runs the closed loop. Cursor only — Claude: ask in natural language.
Full integration guide
# Any HTTP clientexport ZINDUA_API_KEY="znd_live_xxxxxxxxxxxxxxxxxxxx"# https://zindua.run/api/v1/send
# Authorization: Bearer znd_live_xxxcurl -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 -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" }
}'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" }
}'Full integration guide
npm install @zindua/sdk# .env.local — one key per Zindua project (server only)
ZINDUA_KEY_MELLIA=znd_live_xxxxxxxxxxxxxxxxxxxxxxxx
ZINDUA_KEY_OVERLOOK=znd_live_yyyyyyyyyyyyyyyyyyyyyyyy// 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! }),
};// Pick tenant → project key (mellia | overlook)
await zinduaByTenant.mellia.send({
to: 'user@example.com',
template: 'welcome',
variables: { name: 'Alex' },
});await zinduaByTenant.overlook.send({
to: '+243812345678',
channel: 'whatsapp',
template: 'otp-verification',
variables: { code: '4592', app: 'MonApp' },
});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) }Full integration guide
npm install @zindua/sdkZINDUA_API_KEY=znd_live_xxxxxxxxxxxxxxxxxxxx// server/zindua.ts (backend only)
import { Zindua } from '@zindua/sdk';
export const zindua = new Zindua({
apiKey: process.env.ZINDUA_API_KEY!,
});// 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);
}// 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' },
}),
});// Backend maps phone → Zindua send:
await zindua.send({
to: phone,
channel: 'whatsapp',
template: 'otp-verification',
lang: 'fr',
variables: { code },
});Full integration guide
pip install zindua-sdkexport ZINDUA_API_KEY=znd_live_xxxxxxxxxxxxxxxxxxxximport os
from zindua import Zindua
zindua = Zindua(api_key=os.environ["ZINDUA_API_KEY"])result = await zindua.send(
to="user@example.com",
channel="email",
template="reset-password",
variables={"name": "Sarah", "link": "https://..."},
)
print(result.log_id, result.status)result = await zindua.send(
to="+243812345678",
channel="whatsapp",
template="otp-verification",
variables={"code": "4592"},
)
print(result.log_id)result = await zindua.send(
to="+243812345678",
channel="whatsapp",
template="otp-verification",
lang="fr",
variables={"code": "4592"},
)
print(result.lang_fallback)Full integration guide
# pubspec.yaml
dependencies:
http: ^1.2.0// Pass key from your backend. Never ship znd_live_ in the app// Call YOUR backend; it holds the Zindua API key.
// Direct Zindua calls from mobile are not recommended.// Your backend POST https://zindua.run/api/v1/send
// Body: { "to": "customer@example.com", "template": "order-shipped", ... }// Your backend:
// { "to": "+243812345678", "channel": "whatsapp", "template": "otp-verification", ... }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',
}),
);Full integration guide
# Use your backend. No Zindua key in the appZINDUA_API_KEY=znd_live_... # server .env only// Node/Express backend with @zindua/sdk or HTTP POST https://zindua.run/api/v1/send// Backend:
await zindua.send({
to: email,
channel: 'email',
template: 'verify-email',
variables: { link },
});await zindua.send({
to: phone,
channel: 'whatsapp',
template: 'otp-verification',
variables: { code },
});await fetch('https://api.yourapp.com/send-otp', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phone: '+243812345678', code, lang: 'fr' }),
});Templates
One slug for email and WhatsApp. Update copy in the dashboard without redeploying your app.
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.WhatsApp channel
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.
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 /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.
Temporarily stop outbound WhatsApp from your number. API returns a clear error until you resume.
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://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
Free starts with WhatsApp OTP. Upgrade for email API and higher WhatsApp quotas. Same POST /api/v1/send on every plan.
| Plan | Email API | WhatsApp quota | |
|---|---|---|---|
| Free | Yes | Yes | 100 / month |
| Pro | Yes | Yes | 15,000 / month |
| Me | Yes | Yes | Unlimited |
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.
Multilingual messages
Each template can have multiple language versions for email and WhatsApp. Zindua picks the right one from your send() call.
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.
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
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.
{
"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.0Official 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.
Gradle (Kotlin DSL)
implementation("run.zindua:zindua-sdk:1.0.0")Maven
<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.
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
Node/TypeScript SDK matches POST /api/v1/send. Other stacks use the REST endpoint from your backend.
Official Node.js SDK on npm
npm install @zindua/sdkAlready installed? Upgrade for Guardian
npm install @zindua/sdk@1.4.0One 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.
PHP SDK
Official PHP SDK for Laravel, Symfony, WordPress custom, and cron scripts.
composer require zindua/sdkcomposer update zindua/sdkPackagistPython SDK
Official Python SDK for FastAPI, Django, Flask, and async scripts. send(), get_log(), attachments.
pip install zindua-sdkpip install zindua-sdk==1.2.0PyPIJava 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.NET SDK
Official .NET SDK for ASP.NET Core. AddZindua DI, SendAsync for email and WhatsApp OTP.
dotnet add package Zindua.SdkNuGetWhatsApp anti-ban Guardian
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.
npm install @zindua/sdk@1.4.0composer update zindua/sdkpip install zindua-sdk==1.2.0dotnet add package Zindua.Sdk --version 1.1.0<dependency>
<groupId>run.zindua</groupId>
<artifactId>zindua-sdk</artifactId>
<version>1.0.0</version>
</dependency>Où placer vos identifiants révocables lors du téléchargement d'un exemple ou SDK
ZINDUA_API_KEY=znd_live_xxxxxxxxxxxxxxxxSert à authentifier vos requêtes backend vers Zindua (envoi d'OTP, création de push challenge).
ZINDUA_WEBHOOK_SECRET=znd_sec_e08ca8c9...Sert à valider la signature HMAC x-zindua-signature sur votre serveur d'événement Push.
Starter officiel Next.js App Router avec support OTP et PushMirror.
Plugin Fastify ultra-rapide pour Node.js avec vérification HMAC.
Routes asynchrones Python avec httpx et vérification de signature webhook.
zindua/sdk pour Laravel, Symfony, WordPress custom avec validation d'événements.
Zindua.Sdk NuGet pour ASP.NET Core et services Webhook arrière-plan.
Un JAR Maven Central pour Spring Boot, Quarkus, Maven et Gradle. Pas de starter Spring séparé. Clé côté serveur.
Extension officielle pour la connexion OTP et validation Push WooCommerce.
Guide @zindua/sdk — Express, NestJS, Remix, Hono. Voir zindua.run/nodejs.
Outil terminal : envoi d'OTP, test de push, diagnostic de clé API.
Connectez Cursor ou Claude avec @zindua/mcp pour scaffold et tests fermés.
zindua-sdk pour FastAPI, Django et scripts asynchrones. send() & webhooks.
L'app mobile communique avec votre serveur backend qui sécurise la clé API.
Applications iOS/Android n'intégrant jamais la clé API dans les binaires.
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 project | API key | Usage |
|---|---|---|
| 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 |
One project + one key is fine when every customer shares the same sender, templates, and quota. For separate clients, prefer two projects.
# .env — server only, never in the front-end
ZINDUA_KEY_MELLIA=znd_live_xxxxxxxxxxxxxxxxxxxxxxxx
ZINDUA_KEY_OVERLOOK=znd_live_yyyyyyyyyyyyyyyyyyyyyyyyimport { 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
Keep your integration safe. Here's what matters.
Use znd_live_ keys only on the server (env vars, secrets manager). Never in mobile apps, browsers, or public repos.
Send znd_live_… in Authorization: Bearer. Never in URL (?api_key=), JSON body, or X-Api-Key — blocked by the API.
Your app calls your API route; your API route calls Zindua. The end user never sees Zindua credentials.
Each Zindua project has its own key. A request only accesses that project's templates, service, and logs.
After QR scan, the linked session is stored encrypted on our side. You only use your API key; you never receive session tokens.
Browser calls must match allowed origins under Dashboard → Settings → Integrations. Server-to-server calls without Origin are unaffected.
When WEBHOOK_SIGNING_SECRET is set on the server, validate X-Zindua-Signature (sha256=…) before trusting events.