Zindua
Start Free

One API.
Email + WhatsApp.

A single route: POST /api/v1/send at https://zindua.run/api/v1/send. Use HTTP / cURL from any stack, or the Node SDK if you prefer. Set channel to email or whatsapp.

You bring delivery: Gmail, SendGrid, SMTP, and your WhatsApp line. Zindua is not a hosted ESP like Resend or Unosend. Domain & custom sender setup →

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

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

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/Team 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

  1. 1.Connect Service (Gmail, SendGrid, SMTP, …).
  2. 2.Verify the domain with that provider if required.
  3. 3.Add the same domain here and align SPF/DKIM/DMARC.
  4. 4.Set fromEmail in Service to an address on that domain.
  5. 5.Call POST /api/v1/send.

Compare pricing and ESP features on /compare.

Example: send from noreply@acme.com with SendGrid

  1. 1

    Dashboard → Service

    Connect SendGrid with your API key.

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

    SendGrid dashboard

    Authenticate domain acme.com (Sender Authentication).

    • Complete their DNS steps before going live.
  3. 3

    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.
  4. 4

    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
https://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.

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

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.

< 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
< 1 min

CLI

Send OTP tests, inspect your project, list templates, and diagnose your API key from the terminal. No code required.

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
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
{}

HTTP / cURL

Full integration guide

1Install
terminal
# Any HTTP client
2Set API Key
.env
export ZINDUA_API_KEY="znd_live_xxxxxxxxxxxxxxxxxxxx"
3Initialize
terminal
# https://zindua.run/api/v1/send
# Authorization: Bearer znd_live_xxx
4Send 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" }
  }'
5Send 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" }
  }'
6Language (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

1Install
terminal
npm install @zindua/sdk
2Set API Key
.env
# .env.local — one key per Zindua project (server only)
ZINDUA_KEY_MELLIA=znd_live_xxxxxxxxxxxxxxxxxxxxxxxx
ZINDUA_KEY_OVERLOOK=znd_live_yyyyyyyyyyyyyyyyyyyyyyyy
3Initialize
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! }),
};
4Send email
send-email-route.ts
// Pick tenant → project key (mellia | overlook)
await zinduaByTenant.mellia.send({
  to: 'user@example.com',
  template: 'welcome',
  variables: { name: 'Alex' },
});
5Send WhatsApp
send-whatsapp-route.ts
await zinduaByTenant.overlook.send({
  to: '+243812345678',
  channel: 'whatsapp',
  template: 'otp-verification',
  variables: { code: '4592', app: 'MonApp' },
});
6Language (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

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

export const zindua = new Zindua({
  apiKey: process.env.ZINDUA_API_KEY!,
});
4Send 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);
}
5Send 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' },
  }),
});
6Language (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

1Install
terminal
pip install requests
2Set API Key
.env
export ZINDUA_API_KEY=znd_live_xxxxxxxxxxxxxxxxxxxx
3Initialize
app.py
import os
import requests

API_URL = "https://zindua.run/api/v1/send"
API_KEY = os.environ["ZINDUA_API_KEY"]
4Send email
send-email-app.py
response = requests.post(
    API_URL,
    headers={
        "Authorization": f"Bearer {API_KEY}",
        "Content-Type": "application/json",
    },
    json={
        "to": "user@example.com",
        "channel": "email",
        "template": "reset-password",
        "variables": {"name": "Sarah", "link": "https://..."},
    },
    timeout=30,
)
response.raise_for_status()
print(response.json())
5Send WhatsApp
send-whatsapp-app.py
response = requests.post(
    API_URL,
    headers={"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"},
    json={
        "to": "+243812345678",
        "channel": "whatsapp",
        "template": "otp-verification",
        "variables": {"code": "4592"},
    },
    timeout=30,
)
response.raise_for_status()
6Language (optional)
i18n-app.py
requests.post(
    API_URL,
    headers={"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"},
    json={
        "to": "+243812345678",
        "channel": "whatsapp",
        "template": "otp-verification",
        "lang": "fr",
        "variables": {"code": "4592"},
    },
    timeout=30,
).raise_for_status()

Flutter

Full integration guide

1Install
pubspec.yaml
# pubspec.yaml
dependencies:
  http: ^1.2.0
2Set API Key
.env
// Pass key from your backend. Never ship znd_live_ in the app
3Initialize
main.dart
// Call YOUR backend; it holds the Zindua API key.
// Direct Zindua calls from mobile are not recommended.
4Send email
send-email-main.dart
// Your backend POST https://zindua.run/api/v1/send
// Body: { "to": "customer@example.com", "template": "order-shipped", ... }
5Send WhatsApp
send-whatsapp-main.dart
// Your backend:
// { "to": "+243812345678", "channel": "whatsapp", "template": "otp-verification", ... }
6Language (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

1Install
terminal
# Use your backend. No Zindua key in the app
2Set API Key
.env
ZINDUA_API_KEY=znd_live_... # server .env only
3Initialize
VerifyScreen.tsx
// Node/Express backend with @zindua/sdk or HTTP POST https://zindua.run/api/v1/send
4Send email
send-email-VerifyScreen.tsx
// Backend:
await zindua.send({
  to: email,
  channel: 'email',
  template: 'verify-email',
  variables: { link },
});
5Send WhatsApp
send-whatsapp-VerifyScreen.tsx
await zindua.send({
  to: phone,
  channel: 'whatsapp',
  template: 'otp-verification',
  variables: { code },
});
6Language (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

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

How templates work

1

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

2

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

3

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

4

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)

1

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

2

Open Dashboard → your project → WhatsApp → Connect.

3

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

4

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

5

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

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 Yes200 / month
Pro Yes Yes20,000 / month
Team Yes YesUnlimited

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

Pro: Email API + WhatsApp. Connect Gmail/SMTP and your WhatsApp line.

Team: Full channels for production scale.

Compare plans

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

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"
  }
}

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

install.sh
npm install @zindua/sdk

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/sdkPackagist
View PHP SDK
Official
Node.js / TypeScript

Zindua class with typed send(). Works in Next.js, Express, Remix, Hono, and Node 18+.

npm install @zindua/sdk
Official
CLI

Terminal tools: send OTP tests, inspect project, list templates, diagnose API key.

npx @zindua/cli@latest doctor
Official
PHP

zindua/sdk for Laravel, Symfony, WordPress custom. Server-side send() with validation.

composer require zindua/sdk
HTTP
Python

Use requests/httpx against the send endpoint (see Quickstart → Python).

requests + REST
HTTP
Flutter / Dart

Mobile apps call your API; your server holds the znd_live_ key.

Your backend proxy
HTTP
React Native

Same as Flutter: never embed the Zindua API key in the app binary.

Your backend proxy
{}REST
Any Language

Language-agnostic HTTP API. Use from Go, Ruby, PHP, Java, or anything with HTTP.

https://zindua.run/api/v1
Official
React (via Node)

Use the Node SDK in your backend. Never expose API keys in client-side React.

npm install @zindua/sdk

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

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.