@zindua/sdk 1.2.3 → 1.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +417 -158
- package/dist/client.js +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/validate.js +9 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,261 +1,534 @@
|
|
|
1
1
|
# @zindua/sdk
|
|
2
2
|
|
|
3
|
-
Official **server-side** SDK for [Zindua](https://zindua.run):
|
|
3
|
+
Official **server-side** SDK for [Zindua](https://zindua.run): send transactional **email** and **WhatsApp** messages with one API.
|
|
4
4
|
|
|
5
|
-
|
|
|
6
|
-
|
|
7
|
-
|
|
|
8
|
-
|
|
|
9
|
-
| Pricing
|
|
10
|
-
| Dashboard | [zindua.run/login](https://zindua.run/login) |
|
|
5
|
+
| | |
|
|
6
|
+
|---|---|
|
|
7
|
+
| Full API reference | [zindua.run/developers](https://zindua.run/developers) |
|
|
8
|
+
| Dashboard (keys, templates, services) | [zindua.run/login](https://zindua.run/login) |
|
|
9
|
+
| Pricing | [zindua.run/pricing](https://zindua.run/pricing) |
|
|
11
10
|
|
|
12
|
-
|
|
11
|
+
```bash
|
|
12
|
+
npm install @zindua/sdk
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
**Requirements:** Node.js 18+, run only on your **backend** (not in the browser).
|
|
16
|
+
|
|
17
|
+
**Dependencies:** `@zindua/sdk` has **no runtime npm dependencies**. After `npm install`, you are done — no second package to add. The client uses Node’s built-in `fetch` (Node 18+).
|
|
13
18
|
|
|
14
19
|
---
|
|
15
20
|
|
|
16
|
-
##
|
|
21
|
+
## Quick start
|
|
22
|
+
|
|
23
|
+
### 1. Create a Zindua project
|
|
17
24
|
|
|
18
|
-
|
|
25
|
+
1. Sign in at [zindua.run/login](https://zindua.run/login).
|
|
26
|
+
2. Open **Projects** → create a project (or use an existing one).
|
|
27
|
+
3. Copy the API key (`znd_live_…` for production, `znd_test_…` for sandbox).
|
|
28
|
+
4. In the project: connect **Service** (Gmail, SMTP, …) for email, and/or link **WhatsApp** for OTP.
|
|
29
|
+
5. Create at least one **template** (e.g. slug `otp`, `welcome`). For several languages, add one version per language in Dashboard → Templates (and set the project **default language** in project settings).
|
|
19
30
|
|
|
20
|
-
|
|
31
|
+
### 2. Install and configure
|
|
21
32
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
| **Pro** | 50 | 10 |
|
|
26
|
-
| **Team** | 1000 | 100 |
|
|
33
|
+
```bash
|
|
34
|
+
npm install @zindua/sdk
|
|
35
|
+
```
|
|
27
36
|
|
|
28
|
-
|
|
37
|
+
```bash
|
|
38
|
+
# .env — never commit real keys; never expose in frontend code
|
|
39
|
+
ZINDUA_API_KEY=znd_live_xxxxxxxxxxxxxxxxxxxxxxxx
|
|
40
|
+
```
|
|
29
41
|
|
|
30
42
|
```typescript
|
|
31
|
-
|
|
43
|
+
import { Zindua } from "@zindua/sdk";
|
|
44
|
+
|
|
45
|
+
export const zindua = new Zindua({
|
|
46
|
+
apiKey: process.env.ZINDUA_API_KEY!,
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### 3. Send a message
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
// Email (default channel)
|
|
32
54
|
await zindua.send({
|
|
33
55
|
to: "user@example.com",
|
|
34
56
|
template: "welcome",
|
|
35
|
-
lang: "fr",
|
|
36
57
|
variables: { name: "Alex" },
|
|
37
58
|
});
|
|
38
59
|
|
|
39
|
-
// WhatsApp
|
|
60
|
+
// WhatsApp — E.164 phone with + (channel is required for WhatsApp)
|
|
40
61
|
await zindua.send({
|
|
41
62
|
to: "+243812345678",
|
|
42
63
|
channel: "whatsapp",
|
|
43
|
-
template: "otp
|
|
44
|
-
|
|
45
|
-
variables: { code: "4592", app: "MonApp" },
|
|
64
|
+
template: "otp",
|
|
65
|
+
variables: { code: "482910" },
|
|
46
66
|
});
|
|
47
67
|
```
|
|
48
68
|
|
|
49
|
-
|
|
69
|
+
### Channel and `to` must match
|
|
50
70
|
|
|
51
|
-
|
|
71
|
+
The SDK checks **before** calling the API (same rules as [zindua.run](https://zindua.run)). A phone number cannot be sent as email, and an email cannot be sent on WhatsApp.
|
|
52
72
|
|
|
53
|
-
|
|
73
|
+
| `channel` | Valid `to` | Rejected (not sent) |
|
|
74
|
+
|-----------|------------|---------------------|
|
|
75
|
+
| `email` (default) | `user@example.com` | `+243812345678` |
|
|
76
|
+
| `whatsapp` | `+243812345678` | `user@example.com` |
|
|
54
77
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
| Project + API key (`znd_live_…` or `znd_test_…`) | All | Dashboard → **Projects** → your project |
|
|
59
|
-
| Email **Service** connected (Gmail, Outlook, SendGrid, SMTP, …) | `channel: "email"` | Dashboard → **Service** |
|
|
60
|
-
| WhatsApp connected (QR) | `channel: "whatsapp"` | Dashboard → **WhatsApp** |
|
|
61
|
-
| Template slug (e.g. `welcome`, `otp-verification`) | All | Dashboard → **Templates** |
|
|
62
|
-
| **Service** on Free or **Pro/Team** plan | Email via API when Service is connected (Free) or full quotas (paid) | Dashboard → **Service** / **Billing** |
|
|
78
|
+
```typescript
|
|
79
|
+
// ❌ Phone with default channel email — rejected locally (INVALID_EMAIL)
|
|
80
|
+
await zindua.send({ to: "+243812345678", template: "otp", variables: { code: "1" } });
|
|
63
81
|
|
|
64
|
-
|
|
82
|
+
// ❌ Email with WhatsApp channel — rejected locally (INVALID_PHONE)
|
|
83
|
+
await zindua.send({
|
|
84
|
+
to: "user@example.com",
|
|
85
|
+
channel: "whatsapp",
|
|
86
|
+
template: "otp",
|
|
87
|
+
variables: { code: "1" },
|
|
88
|
+
});
|
|
65
89
|
|
|
66
|
-
|
|
67
|
-
|
|
90
|
+
// ✅ User chose email for OTP
|
|
91
|
+
await zindua.send({
|
|
92
|
+
to: "user@example.com",
|
|
93
|
+
channel: "email",
|
|
94
|
+
template: "otp",
|
|
95
|
+
variables: { code: "482910" },
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// ✅ User chose WhatsApp for OTP
|
|
99
|
+
await zindua.send({
|
|
100
|
+
to: "+243812345678",
|
|
101
|
+
channel: "whatsapp",
|
|
102
|
+
template: "otp",
|
|
103
|
+
variables: { code: "482910" },
|
|
104
|
+
});
|
|
105
|
+
```
|
|
68
106
|
|
|
69
107
|
---
|
|
70
108
|
|
|
71
|
-
##
|
|
109
|
+
## Languages (multilingual projects)
|
|
72
110
|
|
|
73
|
-
|
|
74
|
-
npm install @zindua/sdk
|
|
75
|
-
```
|
|
111
|
+
When you create a project, you choose a **default language** (Dashboard → project settings). Each template can have **several language versions** (French, English, Swahili, …) with its own subject and body.
|
|
76
112
|
|
|
77
|
-
|
|
78
|
-
|
|
113
|
+
When you call `send()`, pass **`lang` only if you want a specific version**. If you omit it, Zindua uses the **project default**. If the language you ask for does not exist on that template, Zindua uses the template’s **default language** and sets `langFallback: true` in the response.
|
|
114
|
+
|
|
115
|
+
| What you send | What Zindua uses |
|
|
116
|
+
|---------------|------------------|
|
|
117
|
+
| No `lang` | Project default (e.g. `fr`) |
|
|
118
|
+
| `lang: "en"` and English exists on the template | English version |
|
|
119
|
+
| `lang: "de"` but only `fr` / `en` exist | Default language for that template + `langFallback: true` |
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
// Default language of the project
|
|
123
|
+
await zindua.send({
|
|
124
|
+
to: "user@example.com",
|
|
125
|
+
template: "otp",
|
|
126
|
+
variables: { code: "482910" },
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Explicit language (email or WhatsApp — same `lang` for both channels)
|
|
130
|
+
const result = await zindua.send({
|
|
131
|
+
to: "+243812345678",
|
|
132
|
+
channel: "whatsapp",
|
|
133
|
+
template: "otp",
|
|
134
|
+
lang: "fr",
|
|
135
|
+
variables: { code: "482910" },
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
console.log(result.langUsed); // e.g. "fr" — language actually rendered
|
|
139
|
+
console.log(result.langFallback); // true if `lang` was missing and fallback was used
|
|
79
140
|
```
|
|
80
141
|
|
|
81
|
-
|
|
82
|
-
|
|
142
|
+
OTP with the user’s locale (optional):
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
await zindua.send({
|
|
146
|
+
to: contact,
|
|
147
|
+
channel,
|
|
148
|
+
template: "otp",
|
|
149
|
+
lang: userLocale, // e.g. "fr", "en" — omit if you want project default
|
|
150
|
+
variables: { code },
|
|
151
|
+
});
|
|
83
152
|
```
|
|
84
153
|
|
|
154
|
+
Allowed codes: short ISO 639-1 style (`fr`, `en`, `sw`, `en-us`). Invalid format is rejected by the SDK before the HTTP call.
|
|
155
|
+
|
|
85
156
|
---
|
|
86
157
|
|
|
87
|
-
##
|
|
158
|
+
## Plans and channels
|
|
88
159
|
|
|
89
|
-
|
|
160
|
+
| Plan | Email API | WhatsApp | Email / month | WhatsApp OTP / month |
|
|
161
|
+
|------|-----------|----------|---------------|----------------------|
|
|
162
|
+
| **Free** | Yes, if a **Service** is connected on the project | Yes | 25,000 | 200 |
|
|
163
|
+
| **Pro** | Yes | Yes | 160,000 | 20,000 |
|
|
164
|
+
| **Team** | Yes | Yes | High volume | Unlimited |
|
|
90
165
|
|
|
91
|
-
|
|
166
|
+
On **Free**, email works through **your** connected provider (Gmail, SMTP, etc.). **Pro** and **Team** raise template counts and monthly limits. Details: [pricing](https://zindua.run/pricing).
|
|
92
167
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
168
|
+
| | Free | Pro / Team |
|
|
169
|
+
|---|------|------------|
|
|
170
|
+
| Templates per project | 5 | 50+ |
|
|
171
|
+
| Languages per template | 3 | 10+ |
|
|
97
172
|
|
|
98
|
-
|
|
173
|
+
Use `znd_test_…` keys to try the API without consuming live quota (sandbox behaviour).
|
|
99
174
|
|
|
100
|
-
|
|
175
|
+
---
|
|
101
176
|
|
|
102
|
-
|
|
103
|
-
|----------|---------|--------|
|
|
104
|
-
| `ZINDUA_API_BASE_URL` | `https://zindua.run/api/v1` | Used when `baseUrl` is omitted in constructor |
|
|
105
|
-
| Constructor `baseUrl` | env or default | Overrides env; must be **HTTPS** in production (`http://localhost` allowed for local dev) |
|
|
177
|
+
## One project or several?
|
|
106
178
|
|
|
107
|
-
|
|
179
|
+
| Setup | When it fits |
|
|
180
|
+
|-------|----------------|
|
|
181
|
+
| **One Zindua project, one API key** | One product, one sender, shared templates and quota. |
|
|
182
|
+
| **Several Zindua projects, one key each** | Several clients or brands: separate templates, Gmail accounts, logs, and quotas. |
|
|
108
183
|
|
|
109
|
-
|
|
110
|
-
|--------|---------|-----|
|
|
111
|
-
| `timeoutMs` | `30000` | `120000` |
|
|
184
|
+
### Several keys in one backend
|
|
112
185
|
|
|
113
|
-
|
|
186
|
+
```bash
|
|
187
|
+
ZINDUA_KEY_CLIENT_A=znd_live_aaaaaaaaaaaaaaaaaaaaaaaa
|
|
188
|
+
ZINDUA_KEY_CLIENT_B=znd_live_bbbbbbbbbbbbbbbbbbbbbbbb
|
|
189
|
+
```
|
|
114
190
|
|
|
115
191
|
```typescript
|
|
116
192
|
import { Zindua } from "@zindua/sdk";
|
|
117
193
|
|
|
118
|
-
|
|
119
|
-
apiKey: process.env.
|
|
120
|
-
|
|
121
|
-
|
|
194
|
+
const clients = {
|
|
195
|
+
clientA: new Zindua({ apiKey: process.env.ZINDUA_KEY_CLIENT_A! }),
|
|
196
|
+
clientB: new Zindua({ apiKey: process.env.ZINDUA_KEY_CLIENT_B! }),
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
await clients.clientA.send({
|
|
200
|
+
to: "user@example.com",
|
|
201
|
+
template: "otp",
|
|
202
|
+
variables: { code: "123456" },
|
|
122
203
|
});
|
|
123
204
|
```
|
|
124
205
|
|
|
206
|
+
Each key only accesses **its** Zindua project (templates, service, usage).
|
|
207
|
+
|
|
125
208
|
---
|
|
126
209
|
|
|
127
|
-
##
|
|
210
|
+
## Several templates in one application
|
|
128
211
|
|
|
129
|
-
|
|
212
|
+
You do **not** need a separate SDK client per template. One `Zindua` instance, change the **`template` slug** and **`variables`** per send:
|
|
130
213
|
|
|
131
214
|
```typescript
|
|
215
|
+
// OTP
|
|
132
216
|
await zindua.send({
|
|
133
217
|
to: "user@example.com",
|
|
134
|
-
template: "
|
|
218
|
+
template: "otp",
|
|
219
|
+
variables: { code: "482910" },
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// Welcome email — different slug, different variables
|
|
223
|
+
await zindua.send({
|
|
224
|
+
to: "user@example.com",
|
|
225
|
+
template: "user-welcome",
|
|
135
226
|
variables: { name: "Alex" },
|
|
136
227
|
});
|
|
137
228
|
```
|
|
138
229
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
Phone numbers must be **E.164** with a leading `+` (e.g. `+243812345678`).
|
|
230
|
+
Keep slugs in one place so your code stays clear:
|
|
142
231
|
|
|
143
232
|
```typescript
|
|
233
|
+
const Template = {
|
|
234
|
+
otp: "otp",
|
|
235
|
+
welcome: "user-welcome",
|
|
236
|
+
resetPassword: "password-reset",
|
|
237
|
+
} as const;
|
|
238
|
+
|
|
144
239
|
await zindua.send({
|
|
145
|
-
to:
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
variables: { code: "4592", app: "MyApp" },
|
|
240
|
+
to: email,
|
|
241
|
+
template: Template.welcome,
|
|
242
|
+
variables: { name },
|
|
149
243
|
});
|
|
150
244
|
```
|
|
151
245
|
|
|
152
|
-
|
|
246
|
+
Each template defines its own `{{variables}}` in the dashboard. The SDK does not merge templates: one `send()` call = one template slug.
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## Framework examples
|
|
251
|
+
|
|
252
|
+
Use the same pattern everywhere: **your server** holds the API key; the client app calls **your** API.
|
|
253
|
+
|
|
254
|
+
### Next.js (App Router)
|
|
153
255
|
|
|
154
256
|
```typescript
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
257
|
+
// lib/zindua.ts
|
|
258
|
+
import { Zindua } from "@zindua/sdk";
|
|
259
|
+
|
|
260
|
+
export const zindua = new Zindua({ apiKey: process.env.ZINDUA_API_KEY! });
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
// app/api/notify/route.ts
|
|
265
|
+
import { zindua } from "@/lib/zindua";
|
|
266
|
+
import { NextResponse } from "next/server";
|
|
267
|
+
|
|
268
|
+
export async function POST(req: Request) {
|
|
269
|
+
const { channel, contact, code, lang } = await req.json();
|
|
270
|
+
if (channel !== "email" && channel !== "whatsapp") {
|
|
271
|
+
return NextResponse.json({ error: "channel must be email or whatsapp" }, { status: 400 });
|
|
272
|
+
}
|
|
273
|
+
// contact = email string OR +243… phone, depending on channel
|
|
274
|
+
const result = await zindua.send({
|
|
275
|
+
to: contact,
|
|
276
|
+
channel,
|
|
277
|
+
template: "otp",
|
|
278
|
+
...(lang ? { lang } : {}),
|
|
279
|
+
variables: { code },
|
|
280
|
+
});
|
|
281
|
+
return NextResponse.json(result, { status: 202 });
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
### Express
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
import express from "express";
|
|
289
|
+
import { Zindua } from "@zindua/sdk";
|
|
290
|
+
|
|
291
|
+
const app = express();
|
|
292
|
+
app.use(express.json());
|
|
293
|
+
|
|
294
|
+
const zindua = new Zindua({ apiKey: process.env.ZINDUA_API_KEY! });
|
|
295
|
+
|
|
296
|
+
app.post("/api/notify", async (req, res) => {
|
|
297
|
+
const { channel, contact, code } = req.body;
|
|
298
|
+
if (channel !== "email" && channel !== "whatsapp") {
|
|
299
|
+
return res.status(400).json({ error: "channel must be email or whatsapp" });
|
|
300
|
+
}
|
|
301
|
+
try {
|
|
302
|
+
const result = await zindua.send({
|
|
303
|
+
to: contact,
|
|
304
|
+
channel,
|
|
305
|
+
template: "otp",
|
|
306
|
+
variables: { code },
|
|
307
|
+
});
|
|
308
|
+
res.status(202).json(result);
|
|
309
|
+
} catch (err) {
|
|
310
|
+
res.status(500).json({ error: "Send failed" });
|
|
311
|
+
}
|
|
162
312
|
});
|
|
163
313
|
```
|
|
164
314
|
|
|
165
|
-
###
|
|
315
|
+
### Fastify
|
|
166
316
|
|
|
167
317
|
```typescript
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
318
|
+
import Fastify from "fastify";
|
|
319
|
+
import { Zindua } from "@zindua/sdk";
|
|
320
|
+
|
|
321
|
+
const zindua = new Zindua({ apiKey: process.env.ZINDUA_API_KEY! });
|
|
322
|
+
const app = Fastify();
|
|
323
|
+
|
|
324
|
+
app.post<{ Body: { channel: "email" | "whatsapp"; contact: string; code: string } }>(
|
|
325
|
+
"/notify",
|
|
326
|
+
async (req, reply) => {
|
|
327
|
+
const { channel, contact, code } = req.body;
|
|
328
|
+
if (channel !== "email" && channel !== "whatsapp") {
|
|
329
|
+
return reply.status(400).send({ error: "channel must be email or whatsapp" });
|
|
330
|
+
}
|
|
331
|
+
const result = await zindua.send({
|
|
332
|
+
to: contact,
|
|
333
|
+
channel,
|
|
334
|
+
template: "otp",
|
|
335
|
+
variables: { code },
|
|
336
|
+
});
|
|
337
|
+
return reply.status(202).send(result);
|
|
338
|
+
}
|
|
339
|
+
);
|
|
176
340
|
```
|
|
177
341
|
|
|
178
|
-
|
|
342
|
+
### Hono
|
|
343
|
+
|
|
344
|
+
```typescript
|
|
345
|
+
import { Hono } from "hono";
|
|
346
|
+
import { Zindua } from "@zindua/sdk";
|
|
347
|
+
|
|
348
|
+
const zindua = new Zindua({ apiKey: process.env.ZINDUA_API_KEY! });
|
|
349
|
+
const app = new Hono();
|
|
350
|
+
|
|
351
|
+
app.post("/notify", async (c) => {
|
|
352
|
+
const { channel, contact, code } = await c.req.json();
|
|
353
|
+
if (channel !== "email" && channel !== "whatsapp") {
|
|
354
|
+
return c.json({ error: "channel must be email or whatsapp" }, 400);
|
|
355
|
+
}
|
|
356
|
+
const result = await zindua.send({
|
|
357
|
+
to: contact,
|
|
358
|
+
channel,
|
|
359
|
+
template: "otp",
|
|
360
|
+
variables: { code },
|
|
361
|
+
});
|
|
362
|
+
return c.json(result, 202);
|
|
363
|
+
});
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### Mobile apps (React Native, Flutter)
|
|
367
|
+
|
|
368
|
+
Do **not** embed `@zindua/sdk` or `znd_live_…` in the app. Call your backend route above from the device.
|
|
179
369
|
|
|
180
370
|
---
|
|
181
371
|
|
|
182
|
-
##
|
|
372
|
+
## Configuration options
|
|
183
373
|
|
|
184
|
-
|
|
374
|
+
| Option | Default | Description |
|
|
375
|
+
|--------|---------|-------------|
|
|
376
|
+
| `apiKey` | — | **Required.** `znd_live_…` or `znd_test_…` from the dashboard. |
|
|
377
|
+
| `baseUrl` | `https://zindua.run/api/v1` | Override only for local testing (`http://localhost:3000/api/v1`). |
|
|
378
|
+
| `timeoutMs` | `30000` | Request timeout (max `120000`). |
|
|
185
379
|
|
|
186
|
-
```
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
380
|
+
```typescript
|
|
381
|
+
const zindua = new Zindua({
|
|
382
|
+
apiKey: process.env.ZINDUA_API_KEY!,
|
|
383
|
+
baseUrl: process.env.ZINDUA_API_BASE_URL, // optional
|
|
384
|
+
timeoutMs: 45_000,
|
|
385
|
+
});
|
|
190
386
|
```
|
|
191
387
|
|
|
192
|
-
|
|
193
|
-
import { Zindua } from "@zindua/sdk";
|
|
388
|
+
---
|
|
194
389
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
390
|
+
## Send options
|
|
391
|
+
|
|
392
|
+
| Field | Required | Description |
|
|
393
|
+
|-------|----------|-------------|
|
|
394
|
+
| `to` | Yes | **Email** if `channel` is `email` (default). **E.164 phone** (`+243…`) if `channel` is `whatsapp`. Mismatches are rejected by the SDK and by the API. |
|
|
395
|
+
| `template` | Yes | Template slug from your dashboard. |
|
|
396
|
+
| `channel` | No | `"email"` (default) or `"whatsapp"`. Must match the format of `to`. |
|
|
397
|
+
| `lang` | No | Optional. `fr`, `en`, `sw`, … — if omitted, project default; if missing on template, fallback + `langFallback: true`. |
|
|
398
|
+
| `variables` | No | `{{placeholders}}` in the template. |
|
|
399
|
+
| `cc`, `bcc`, `replyTo` | No | Email only. |
|
|
400
|
+
|
|
401
|
+
---
|
|
402
|
+
|
|
403
|
+
## API key, recipient, and delivery — what is checked when
|
|
404
|
+
|
|
405
|
+
| Situation | When you know | What to do |
|
|
406
|
+
|-----------|---------------|------------|
|
|
407
|
+
| Wrong or missing API key | Immediately (`401`, `INVALID_API_KEY` / `API_KEY_NOT_FOUND`) | Copy key from Dashboard → Projects |
|
|
408
|
+
| Phone sent as email (or the opposite) | Immediately in the SDK (`INVALID_EMAIL` / `INVALID_PHONE`, HTTP status `0`) | Match `channel` and `to` (see above) |
|
|
409
|
+
| Malformed email or phone | Immediately (SDK + API `400`) | Fix format: `user@domain.com`, `+243812345678` |
|
|
410
|
+
| Typo in email (`user@gmial.com`) | **Not** blocked at send time | Request may succeed (`202`); provider may bounce later |
|
|
411
|
+
| Email inbox does not exist | **Not** verified upfront | Check **Dashboard → Logs** with `logId`; configure **webhooks** (`email.delivered` / `email.failed`) |
|
|
412
|
+
| Phone valid in E.164 but **no WhatsApp** on that number | **Not** verified upfront | WhatsApp send can fail after accept; status `failed` in logs |
|
|
413
|
+
| Template slug wrong | API `404` `TEMPLATE_NOT_FOUND` | Create template or fix slug |
|
|
414
|
+
| No Gmail/SMTP / WhatsApp not linked | API `422` | Connect **Service** or **WhatsApp** on the project |
|
|
199
415
|
|
|
200
|
-
|
|
416
|
+
**Important:** a successful `send()` returns `logId` and often `status: "queued"`. That means Zindua accepted the message and will deliver through **your** connected email service or WhatsApp session. It does not guarantee the address exists or that WhatsApp is installed on that number.
|
|
417
|
+
|
|
418
|
+
```typescript
|
|
419
|
+
const result = await zindua.send({
|
|
201
420
|
to: "user@example.com",
|
|
202
421
|
template: "otp",
|
|
203
|
-
variables: { code: "
|
|
422
|
+
variables: { code: "123456" },
|
|
204
423
|
});
|
|
424
|
+
|
|
425
|
+
// Save logId — check delivery in the dashboard or via webhooks
|
|
426
|
+
console.log(result.logId, result.status, result.langUsed);
|
|
205
427
|
```
|
|
206
428
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
| **1 Zindua project** | Same sender and templates for everyone (one key) |
|
|
429
|
+
Optional in **your** app (before calling Zindua): stricter email regex, confirmation field, or a phone library — the SDK already enforces platform format and channel rules.
|
|
430
|
+
|
|
431
|
+
---
|
|
211
432
|
|
|
212
|
-
|
|
433
|
+
## Successful response
|
|
434
|
+
|
|
435
|
+
```typescript
|
|
436
|
+
const result = await zindua.send({ /* … */ });
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
| Field | Meaning |
|
|
440
|
+
|-------|---------|
|
|
441
|
+
| `success` | `true` |
|
|
442
|
+
| `status` | `queued`, `processing`, or `sent` (test keys often return `sent` immediately). |
|
|
443
|
+
| `logId` | Id for support / logs in the dashboard. |
|
|
444
|
+
| `channel` | `email` or `whatsapp` |
|
|
445
|
+
| `langUsed` | Language version used for this send |
|
|
446
|
+
| `langFallback` | `true` if requested `lang` was missing and a fallback version was used |
|
|
447
|
+
| `context` | Workspace snapshot (see below). |
|
|
448
|
+
|
|
449
|
+
### `context` object
|
|
450
|
+
|
|
451
|
+
The API returns metadata so you can log or debug without exposing secrets:
|
|
452
|
+
|
|
453
|
+
```json
|
|
454
|
+
{
|
|
455
|
+
"context": {
|
|
456
|
+
"project": { "name": "My App", "slug": "my-app" },
|
|
457
|
+
"apiKey": { "mode": "live", "suffix": "ejho" },
|
|
458
|
+
"plan": { "slug": "free", "name": "Free", "emailQuota": 25000, "emailsUsed": 12 },
|
|
459
|
+
"channels": { "email": true, "whatsapp": false }
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
- **`context.apiKey.suffix`** — last 4 characters of the key (match in the dashboard).
|
|
465
|
+
- **`context.plan`** — current plan and usage for the workspace linked to this key.
|
|
466
|
+
- **`context.channels`** — whether email / WhatsApp is ready for this project.
|
|
467
|
+
|
|
468
|
+
The full API key is **never** returned in responses.
|
|
213
469
|
|
|
214
470
|
---
|
|
215
471
|
|
|
216
472
|
## Errors
|
|
217
473
|
|
|
474
|
+
Wrap sends in `try/catch` and handle `ZinduaError`:
|
|
475
|
+
|
|
218
476
|
```typescript
|
|
219
477
|
import { Zindua, ZinduaError } from "@zindua/sdk";
|
|
220
478
|
|
|
221
479
|
try {
|
|
222
|
-
await zindua.send({ to: "
|
|
480
|
+
await zindua.send({ to: "user@example.com", template: "otp", variables: { code: "1" } });
|
|
223
481
|
} catch (e) {
|
|
224
482
|
if (e instanceof ZinduaError) {
|
|
225
|
-
console.
|
|
226
|
-
//
|
|
483
|
+
console.log(e.status); // HTTP status
|
|
484
|
+
console.log(e.code); // machine-readable code
|
|
485
|
+
console.log(e.message); // human-readable
|
|
486
|
+
console.log(e.details); // optional: hint, context, retryAfterSec, …
|
|
227
487
|
}
|
|
228
|
-
throw e;
|
|
229
488
|
}
|
|
230
489
|
```
|
|
231
490
|
|
|
232
|
-
|
|
491
|
+
### Error codes
|
|
492
|
+
|
|
493
|
+
| Code | HTTP | What it means | What to do |
|
|
494
|
+
|------|------|---------------|------------|
|
|
495
|
+
| `INVALID_API_KEY` | 401 | Missing or malformed key. | Use `Authorization: Bearer znd_live_…` (24 chars after prefix). |
|
|
496
|
+
| `API_KEY_NOT_FOUND` | 401 | Key not linked to a project. | Copy the key from Dashboard → your project. |
|
|
497
|
+
| `ORIGIN_NOT_ALLOWED` | 403 | Browser origin blocked. | Prefer server-side SDK; or add origin in Dashboard → Settings → Integrations. |
|
|
498
|
+
| `MISSING_FIELDS` | 400 / 0 | `to` or `template` missing. | Send both. Status `0` = caught by the SDK before HTTP. |
|
|
499
|
+
| `INVALID_EMAIL` | 400 / 0 | Bad email, or **phone used with channel email**. | Use `user@domain.com`, or set `channel: "whatsapp"` for `+243…`. |
|
|
500
|
+
| `INVALID_PHONE` | 400 / 0 | Bad WhatsApp number, or **email used with channel whatsapp**. | Use `+243812345678`, or set `channel: "email"` for an address. |
|
|
501
|
+
| `NO_SUBSCRIPTION` | 403 | Workspace has no active plan. | Open Dashboard → Billing or contact support. |
|
|
502
|
+
| `EMAIL_NOT_AVAILABLE` | 403 | Email not allowed on current plan setup. | Connect a Service on the project (Free), or upgrade plan. |
|
|
503
|
+
| `EMAIL_SERVICE_NOT_CONFIGURED` | 422 | No Gmail/SMTP connected. | Dashboard → project → **Service**. |
|
|
504
|
+
| `QUOTA_EXCEEDED` | 429 | Monthly email limit reached. | Upgrade plan or wait for reset. |
|
|
505
|
+
| `WHATSAPP_NOT_AVAILABLE` | 403 | WhatsApp not on plan. | Check [pricing](https://zindua.run/pricing). |
|
|
506
|
+
| `WHATSAPP_NOT_CONNECTED` | 422 | WhatsApp not linked. | Dashboard → project → **WhatsApp** → scan QR. |
|
|
507
|
+
| `WHATSAPP_PAUSED` | 422 | WhatsApp session paused. | Resume in the dashboard. |
|
|
508
|
+
| `WHATSAPP_QUOTA_EXCEEDED` | 429 | Monthly WhatsApp limit reached. | Upgrade or wait. |
|
|
509
|
+
| `RATE_LIMIT_EXCEEDED` | 429 | Sending too fast (WhatsApp). | Wait `retryAfterSec` from `e.details`, then retry. |
|
|
510
|
+
| `TEMPLATE_NOT_FOUND` | 404 | Unknown template slug. | Create template or fix slug; check `availableTemplateSlugs` in details. |
|
|
511
|
+
| `TEMPLATE_NO_CONTENT` | 404 | Template has no language version. | Add content in Dashboard → Templates. |
|
|
512
|
+
| `TEMPLATE_NO_WHATSAPP_BODY` | 422 | No plain text for WhatsApp. | Add a text body for that language. |
|
|
513
|
+
| `QUEUE_UNAVAILABLE` | 503 | Temporary delivery queue issue. | Retry shortly; contact support if it persists. |
|
|
514
|
+
| `BROWSER_FORBIDDEN` | — | SDK used in browser. | Move the call to your backend. |
|
|
515
|
+
| `REQUEST_TIMEOUT` | — | Request exceeded `timeoutMs`. | Increase timeout or retry. |
|
|
516
|
+
|
|
517
|
+
Many errors include a **`context`** field (same shape as success) with `plan` and `project` to help you debug.
|
|
233
518
|
|
|
234
519
|
---
|
|
235
520
|
|
|
236
|
-
##
|
|
237
|
-
|
|
238
|
-
- **Server only** — throws in browser environments (`BROWSER_FORBIDDEN`).
|
|
239
|
-
- **Bearer header only** — `Authorization: Bearer znd_live_…`. Keys in URL, JSON body, or `X-Api-Key` are rejected by the platform API.
|
|
240
|
-
- **HTTPS** for `baseUrl` except `http://localhost` in development.
|
|
241
|
-
- **No credentials in URL** — rejects `baseUrl` with embedded username/password.
|
|
242
|
-
- **Input validation** — E.164 phones, email format, template slug, variable size limits (aligned with the platform API).
|
|
243
|
-
- **Redirects disabled** — `fetch` uses `redirect: "error"`.
|
|
244
|
-
- **CORS** — optional origin allowlist in Dashboard → Settings → Integrations (browser calls only).
|
|
245
|
-
|
|
246
|
-
---
|
|
247
|
-
|
|
248
|
-
## Mobile & SPA
|
|
249
|
-
|
|
250
|
-
Never put `znd_live_` keys in React Native, Flutter, or browser code. Call your own backend; your backend uses this SDK.
|
|
521
|
+
## Best practices
|
|
251
522
|
|
|
252
|
-
|
|
523
|
+
1. Store the API key in environment variables or a secrets manager.
|
|
524
|
+
2. Call Zindua from your **API routes** only, not from React/Vue/mobile bundles.
|
|
525
|
+
3. Use **`znd_test_…`** in staging; **`znd_live_…`** in production.
|
|
526
|
+
4. Pin the SDK version in `package.json`, e.g. `"@zindua/sdk": "1.2.6"`.
|
|
527
|
+
5. One Zindua project per client when quotas, senders, or templates must stay isolated.
|
|
253
528
|
|
|
254
529
|
---
|
|
255
530
|
|
|
256
|
-
##
|
|
257
|
-
|
|
258
|
-
Any language can call the same endpoint:
|
|
531
|
+
## Without the SDK (HTTP)
|
|
259
532
|
|
|
260
533
|
```bash
|
|
261
534
|
curl -X POST https://zindua.run/api/v1/send \
|
|
@@ -264,34 +537,20 @@ curl -X POST https://zindua.run/api/v1/send \
|
|
|
264
537
|
-d '{"to":"user@example.com","template":"welcome","variables":{"name":"Alex"}}'
|
|
265
538
|
```
|
|
266
539
|
|
|
267
|
-
|
|
540
|
+
See [HTTP / cURL](https://zindua.run/developers#http).
|
|
268
541
|
|
|
269
542
|
---
|
|
270
543
|
|
|
271
|
-
## API
|
|
544
|
+
## API reference
|
|
272
545
|
|
|
273
546
|
| Export | Description |
|
|
274
547
|
|--------|-------------|
|
|
275
548
|
| `Zindua` | Client class |
|
|
276
|
-
| `ZinduaError` |
|
|
549
|
+
| `ZinduaError` | Error type (`status`, `code`, `message`, `details`) |
|
|
550
|
+
| `ZinduaSendResult` | Success payload type |
|
|
551
|
+
| `ZinduaSendContext` | Type of `result.context` |
|
|
277
552
|
| `DEFAULT_API_BASE` | `https://zindua.run/api/v1` |
|
|
278
|
-
| `LIMITS` |
|
|
279
|
-
|
|
280
|
-
---
|
|
281
|
-
|
|
282
|
-
## Local development (monorepo)
|
|
283
|
-
|
|
284
|
-
```bash
|
|
285
|
-
cd packages/zindua-js
|
|
286
|
-
npm run build
|
|
287
|
-
npm test
|
|
288
|
-
```
|
|
289
|
-
|
|
290
|
-
From the app repo root:
|
|
291
|
-
|
|
292
|
-
```bash
|
|
293
|
-
npm install file:./packages/zindua-js
|
|
294
|
-
```
|
|
553
|
+
| `LIMITS` | Client-side validation limits |
|
|
295
554
|
|
|
296
555
|
---
|
|
297
556
|
|
package/dist/client.js
CHANGED
|
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.Zindua = void 0;
|
|
4
4
|
const errors_1 = require("./errors");
|
|
5
5
|
const validate_1 = require("./validate");
|
|
6
|
-
const SDK_VERSION = "1.2.
|
|
6
|
+
const SDK_VERSION = "1.2.6";
|
|
7
7
|
const USER_AGENT = `Zindua-JS/${SDK_VERSION}`;
|
|
8
8
|
function buildPayload(options, channel) {
|
|
9
9
|
const to = (0, validate_1.validateRecipient)(options.to, channel);
|
package/dist/errors.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type ZinduaErrorCode = "INVALID_API_KEY" | "INVALID_BASE_URL" | "BROWSER_FORBIDDEN" | "INVALID_OPTIONS" | "
|
|
1
|
+
export type ZinduaErrorCode = "INVALID_API_KEY" | "INVALID_BASE_URL" | "BROWSER_FORBIDDEN" | "INVALID_OPTIONS" | "INVALID_EMAIL" | "INVALID_PHONE" | "MISSING_FIELDS" | "INVALID_TEMPLATE" | "INVALID_CHANNEL" | "INVALID_LANG" | "INVALID_VARIABLES" | "REQUEST_TIMEOUT" | "INVALID_RESPONSE" | "API_ERROR";
|
|
2
2
|
/** Structured error — never includes the API key in the message. */
|
|
3
3
|
export declare class ZinduaError extends Error {
|
|
4
4
|
readonly status: number;
|
package/dist/validate.js
CHANGED
|
@@ -107,20 +107,26 @@ function validateRecipient(to, channel) {
|
|
|
107
107
|
if (!recipient || recipient.length > exports.LIMITS.maxToLength) {
|
|
108
108
|
throw new errors_1.ZinduaError("to is required and must be under 320 characters.", {
|
|
109
109
|
status: 0,
|
|
110
|
-
code: "
|
|
110
|
+
code: "MISSING_FIELDS",
|
|
111
111
|
});
|
|
112
112
|
}
|
|
113
113
|
if (channel === "email") {
|
|
114
|
+
if (E164_RE.test(recipient)) {
|
|
115
|
+
throw new errors_1.ZinduaError('A phone number cannot be sent on channel "email". Use an email address (e.g. user@example.com) or set channel to "whatsapp".', { status: 0, code: "INVALID_EMAIL" });
|
|
116
|
+
}
|
|
114
117
|
if (!EMAIL_RE.test(recipient)) {
|
|
115
118
|
throw new errors_1.ZinduaError("Invalid email address for channel email.", {
|
|
116
119
|
status: 0,
|
|
117
|
-
code: "
|
|
120
|
+
code: "INVALID_EMAIL",
|
|
118
121
|
});
|
|
119
122
|
}
|
|
120
123
|
return recipient;
|
|
121
124
|
}
|
|
125
|
+
if (EMAIL_RE.test(recipient)) {
|
|
126
|
+
throw new errors_1.ZinduaError('An email address cannot be sent on channel "whatsapp". Use E.164 with + (e.g. +243812345678) or set channel to "email".', { status: 0, code: "INVALID_PHONE" });
|
|
127
|
+
}
|
|
122
128
|
if (!E164_RE.test(recipient)) {
|
|
123
|
-
throw new errors_1.ZinduaError("WhatsApp to must be E.164 with leading + (e.g. +243812345678).", { status: 0, code: "
|
|
129
|
+
throw new errors_1.ZinduaError("WhatsApp to must be E.164 with leading + (e.g. +243812345678).", { status: 0, code: "INVALID_PHONE" });
|
|
124
130
|
}
|
|
125
131
|
return recipient;
|
|
126
132
|
}
|