@zindua/sdk 1.2.4 → 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 +208 -24
- 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
|
@@ -14,6 +14,8 @@ npm install @zindua/sdk
|
|
|
14
14
|
|
|
15
15
|
**Requirements:** Node.js 18+, run only on your **backend** (not in the browser).
|
|
16
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+).
|
|
18
|
+
|
|
17
19
|
---
|
|
18
20
|
|
|
19
21
|
## Quick start
|
|
@@ -24,7 +26,7 @@ npm install @zindua/sdk
|
|
|
24
26
|
2. Open **Projects** → create a project (or use an existing one).
|
|
25
27
|
3. Copy the API key (`znd_live_…` for production, `znd_test_…` for sandbox).
|
|
26
28
|
4. In the project: connect **Service** (Gmail, SMTP, …) for email, and/or link **WhatsApp** for OTP.
|
|
27
|
-
5. Create at least one **template** (e.g. slug `otp`, `welcome`).
|
|
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).
|
|
28
30
|
|
|
29
31
|
### 2. Install and configure
|
|
30
32
|
|
|
@@ -55,7 +57,45 @@ await zindua.send({
|
|
|
55
57
|
variables: { name: "Alex" },
|
|
56
58
|
});
|
|
57
59
|
|
|
58
|
-
// WhatsApp —
|
|
60
|
+
// WhatsApp — E.164 phone with + (channel is required for WhatsApp)
|
|
61
|
+
await zindua.send({
|
|
62
|
+
to: "+243812345678",
|
|
63
|
+
channel: "whatsapp",
|
|
64
|
+
template: "otp",
|
|
65
|
+
variables: { code: "482910" },
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Channel and `to` must match
|
|
70
|
+
|
|
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.
|
|
72
|
+
|
|
73
|
+
| `channel` | Valid `to` | Rejected (not sent) |
|
|
74
|
+
|-----------|------------|---------------------|
|
|
75
|
+
| `email` (default) | `user@example.com` | `+243812345678` |
|
|
76
|
+
| `whatsapp` | `+243812345678` | `user@example.com` |
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
// ❌ Phone with default channel email — rejected locally (INVALID_EMAIL)
|
|
80
|
+
await zindua.send({ to: "+243812345678", template: "otp", variables: { code: "1" } });
|
|
81
|
+
|
|
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
|
+
});
|
|
89
|
+
|
|
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
|
|
59
99
|
await zindua.send({
|
|
60
100
|
to: "+243812345678",
|
|
61
101
|
channel: "whatsapp",
|
|
@@ -66,6 +106,55 @@ await zindua.send({
|
|
|
66
106
|
|
|
67
107
|
---
|
|
68
108
|
|
|
109
|
+
## Languages (multilingual projects)
|
|
110
|
+
|
|
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.
|
|
112
|
+
|
|
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
|
|
140
|
+
```
|
|
141
|
+
|
|
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
|
+
});
|
|
152
|
+
```
|
|
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
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
69
158
|
## Plans and channels
|
|
70
159
|
|
|
71
160
|
| Plan | Email API | WhatsApp | Email / month | WhatsApp OTP / month |
|
|
@@ -118,6 +207,46 @@ Each key only accesses **its** Zindua project (templates, service, usage).
|
|
|
118
207
|
|
|
119
208
|
---
|
|
120
209
|
|
|
210
|
+
## Several templates in one application
|
|
211
|
+
|
|
212
|
+
You do **not** need a separate SDK client per template. One `Zindua` instance, change the **`template` slug** and **`variables`** per send:
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
// OTP
|
|
216
|
+
await zindua.send({
|
|
217
|
+
to: "user@example.com",
|
|
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",
|
|
226
|
+
variables: { name: "Alex" },
|
|
227
|
+
});
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Keep slugs in one place so your code stays clear:
|
|
231
|
+
|
|
232
|
+
```typescript
|
|
233
|
+
const Template = {
|
|
234
|
+
otp: "otp",
|
|
235
|
+
welcome: "user-welcome",
|
|
236
|
+
resetPassword: "password-reset",
|
|
237
|
+
} as const;
|
|
238
|
+
|
|
239
|
+
await zindua.send({
|
|
240
|
+
to: email,
|
|
241
|
+
template: Template.welcome,
|
|
242
|
+
variables: { name },
|
|
243
|
+
});
|
|
244
|
+
```
|
|
245
|
+
|
|
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
|
+
|
|
121
250
|
## Framework examples
|
|
122
251
|
|
|
123
252
|
Use the same pattern everywhere: **your server** holds the API key; the client app calls **your** API.
|
|
@@ -137,10 +266,16 @@ import { zindua } from "@/lib/zindua";
|
|
|
137
266
|
import { NextResponse } from "next/server";
|
|
138
267
|
|
|
139
268
|
export async function POST(req: Request) {
|
|
140
|
-
const {
|
|
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
|
|
141
274
|
const result = await zindua.send({
|
|
142
|
-
to:
|
|
275
|
+
to: contact,
|
|
276
|
+
channel,
|
|
143
277
|
template: "otp",
|
|
278
|
+
...(lang ? { lang } : {}),
|
|
144
279
|
variables: { code },
|
|
145
280
|
});
|
|
146
281
|
return NextResponse.json(result, { status: 202 });
|
|
@@ -159,11 +294,16 @@ app.use(express.json());
|
|
|
159
294
|
const zindua = new Zindua({ apiKey: process.env.ZINDUA_API_KEY! });
|
|
160
295
|
|
|
161
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
|
+
}
|
|
162
301
|
try {
|
|
163
302
|
const result = await zindua.send({
|
|
164
|
-
to:
|
|
165
|
-
|
|
166
|
-
|
|
303
|
+
to: contact,
|
|
304
|
+
channel,
|
|
305
|
+
template: "otp",
|
|
306
|
+
variables: { code },
|
|
167
307
|
});
|
|
168
308
|
res.status(202).json(result);
|
|
169
309
|
} catch (err) {
|
|
@@ -181,14 +321,22 @@ import { Zindua } from "@zindua/sdk";
|
|
|
181
321
|
const zindua = new Zindua({ apiKey: process.env.ZINDUA_API_KEY! });
|
|
182
322
|
const app = Fastify();
|
|
183
323
|
|
|
184
|
-
app.post<{ Body: { email: string; code: string } }>(
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
+
);
|
|
192
340
|
```
|
|
193
341
|
|
|
194
342
|
### Hono
|
|
@@ -201,9 +349,13 @@ const zindua = new Zindua({ apiKey: process.env.ZINDUA_API_KEY! });
|
|
|
201
349
|
const app = new Hono();
|
|
202
350
|
|
|
203
351
|
app.post("/notify", async (c) => {
|
|
204
|
-
const {
|
|
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
|
+
}
|
|
205
356
|
const result = await zindua.send({
|
|
206
|
-
to:
|
|
357
|
+
to: contact,
|
|
358
|
+
channel,
|
|
207
359
|
template: "otp",
|
|
208
360
|
variables: { code },
|
|
209
361
|
});
|
|
@@ -239,15 +391,45 @@ const zindua = new Zindua({
|
|
|
239
391
|
|
|
240
392
|
| Field | Required | Description |
|
|
241
393
|
|-------|----------|-------------|
|
|
242
|
-
| `to` | Yes | Email
|
|
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. |
|
|
243
395
|
| `template` | Yes | Template slug from your dashboard. |
|
|
244
|
-
| `channel` | No | `"email"` (default) or `"whatsapp"`. |
|
|
245
|
-
| `lang` | No | `fr`, `en`, … —
|
|
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`. |
|
|
246
398
|
| `variables` | No | `{{placeholders}}` in the template. |
|
|
247
399
|
| `cc`, `bcc`, `replyTo` | No | Email only. |
|
|
248
400
|
|
|
249
401
|
---
|
|
250
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 |
|
|
415
|
+
|
|
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({
|
|
420
|
+
to: "user@example.com",
|
|
421
|
+
template: "otp",
|
|
422
|
+
variables: { code: "123456" },
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
// Save logId — check delivery in the dashboard or via webhooks
|
|
426
|
+
console.log(result.logId, result.status, result.langUsed);
|
|
427
|
+
```
|
|
428
|
+
|
|
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
|
+
---
|
|
432
|
+
|
|
251
433
|
## Successful response
|
|
252
434
|
|
|
253
435
|
```typescript
|
|
@@ -260,6 +442,8 @@ const result = await zindua.send({ /* … */ });
|
|
|
260
442
|
| `status` | `queued`, `processing`, or `sent` (test keys often return `sent` immediately). |
|
|
261
443
|
| `logId` | Id for support / logs in the dashboard. |
|
|
262
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 |
|
|
263
447
|
| `context` | Workspace snapshot (see below). |
|
|
264
448
|
|
|
265
449
|
### `context` object
|
|
@@ -311,9 +495,9 @@ try {
|
|
|
311
495
|
| `INVALID_API_KEY` | 401 | Missing or malformed key. | Use `Authorization: Bearer znd_live_…` (24 chars after prefix). |
|
|
312
496
|
| `API_KEY_NOT_FOUND` | 401 | Key not linked to a project. | Copy the key from Dashboard → your project. |
|
|
313
497
|
| `ORIGIN_NOT_ALLOWED` | 403 | Browser origin blocked. | Prefer server-side SDK; or add origin in Dashboard → Settings → Integrations. |
|
|
314
|
-
| `MISSING_FIELDS` | 400 | `to` or `template` missing. | Send both
|
|
315
|
-
| `INVALID_EMAIL` | 400 | Bad email
|
|
316
|
-
| `INVALID_PHONE` | 400 | Bad WhatsApp number
|
|
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. |
|
|
317
501
|
| `NO_SUBSCRIPTION` | 403 | Workspace has no active plan. | Open Dashboard → Billing or contact support. |
|
|
318
502
|
| `EMAIL_NOT_AVAILABLE` | 403 | Email not allowed on current plan setup. | Connect a Service on the project (Free), or upgrade plan. |
|
|
319
503
|
| `EMAIL_SERVICE_NOT_CONFIGURED` | 422 | No Gmail/SMTP connected. | Dashboard → project → **Service**. |
|
|
@@ -339,7 +523,7 @@ Many errors include a **`context`** field (same shape as success) with `plan` an
|
|
|
339
523
|
1. Store the API key in environment variables or a secrets manager.
|
|
340
524
|
2. Call Zindua from your **API routes** only, not from React/Vue/mobile bundles.
|
|
341
525
|
3. Use **`znd_test_…`** in staging; **`znd_live_…`** in production.
|
|
342
|
-
4. Pin the SDK version in `package.json`, e.g. `"@zindua/sdk": "1.2.
|
|
526
|
+
4. Pin the SDK version in `package.json`, e.g. `"@zindua/sdk": "1.2.6"`.
|
|
343
527
|
5. One Zindua project per client when quotas, senders, or templates must stay isolated.
|
|
344
528
|
|
|
345
529
|
---
|
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
|
}
|