@codemina/lumenta-sdk-nodejs
v1.0.1
Published
Official Node.js SDK for the Lumenta WhatsApp Business API
Maintainers
Readme
Lumenta Node.js SDK
Official Node.js / TypeScript SDK for the Lumenta WhatsApp Business API. Send messages, manage templates and flows, run campaigns, segment contacts, build automations, and receive webhooks — all with end-to-end types.
Installation
npm install @codemina/lumenta-sdk-nodejs
# or
yarn add @codemina/lumenta-sdk-nodejs
# or
pnpm add @codemina/lumenta-sdk-nodejsRequires Node.js 18.17+ (native fetch). Works in CommonJS and ES Modules.
Quickstart
import { LumentaClient } from '@codemina/lumenta-sdk-nodejs';
const lumenta = new LumentaClient({
apiKey: process.env.LUMENTA_API_KEY!, // pk_live_xxxx.yyyyyyyy
tenantId: process.env.LUMENTA_TENANT_ID, // optional if your key already binds to one tenant
});
const message = await lumenta.messages.send({
to: '441234567890',
body: 'Hello from the Lumenta SDK 👋',
});
console.log(message.id, message.status);Authentication
Generate an API key in the Lumenta portal under Settings → API keys. Keys look like:
pk_live_abcdef0123456789.<long random secret>The full key is shown once at creation time. Store it as a secret — anyone with the key can act on behalf of your tenant.
The SDK sends the key as a Bearer token in the Authorization header. The API also accepts X-Lumenta-Api-Key and X-Api-Key headers as alternatives (used by Zapier and generic tooling); the SDK uses the standard Bearer form.
Scopes
Every API key is minted with one or more scopes. The SDK does not pre-check scopes — calls fail at the API with 403 INSUFFICIENT_SCOPE if the key cannot perform the action.
| Resource | Scopes |
| ------------ | --------------------------------- |
| Messages | messages:read, messages:write |
| Clients | clients:read, clients:write |
| Templates | templates:read, templates:write |
| Campaigns | campaigns:read, campaigns:write |
| Segments | segments:read, segments:write |
| Flows | flows:read, flows:write |
| Auto-replies | campaigns:read, campaigns:write |
| Follow-ups | campaigns:read, campaigns:write |
| Chat widgets | campaigns:read, campaigns:write |
| Suppression | clients:read, clients:write |
| Senders (read-only) | senders:read |
| Dashboard | stats:read |
| Zapier hooks | webhooks:manage |
The wildcard * and per-resource wildcards (messages:*, campaigns:*, …) are also supported by the server.
Configuration
new LumentaClient({
apiKey: 'pk_live_xxxx.yyyy',
baseUrl: 'https://api.lumenta.io', // default
tenantId: 'optional-tenant-uuid',
timeoutMs: 30_000, // default 30 seconds
maxRetries: 2, // GET/PUT only; default 2
fetch: customFetchImpl, // optional override (e.g. for tests)
defaultHeaders: { 'X-Trace-Id': '…' },
userAgent: 'my-app/1.0',
});Every resource method accepts an optional extras object for per-call overrides:
await lumenta.messages.send(
{ to: '441234567890', body: 'Hi' },
{ tenantId: 'other-tenant', timeoutMs: 5_000, maxRetries: 0, signal: ac.signal },
);Messages
// Plain text
await lumenta.messages.send({
to: '441234567890',
body: 'Hello!',
});
// Approved template (by internal UUID)
await lumenta.messages.sendTemplate({
to: '441234567890',
templateId: '550e8400-e29b-41d4-a716-446655440000',
templateVariables: { '1': 'Alex', '2': '22 July' },
});
// External Meta template (by name)
await lumenta.messages.sendTemplate({
to: '441234567890',
externalTemplateId: 'welcome_message',
language: 'en_US',
templateVariables: { '1': 'Alex' },
});
// Media (image / video / document / voice note)
import { readFile } from 'node:fs/promises';
const file = await readFile('./photo.jpg');
await lumenta.messages.sendMedia({
to: '441234567890',
mediaType: 'image',
file,
filename: 'photo.jpg',
caption: 'Here you go',
});
// Voice note (audio re-encoded as a voice message)
await lumenta.messages.sendMedia({
to: '441234567890',
mediaType: 'audio',
file: await readFile('./greeting.ogg'),
voiceNote: true,
});
// Listing (paginated)
const recent = await lumenta.messages.list({ page: 1, limit: 25, direction: 'outbound' });Clients
// Find-or-create on phone
const existing = await lumenta.clients.findByPhone('441234567890');
if (!existing) {
await lumenta.clients.create({ phoneNumber: '441234567890', profileName: 'Alex' });
}
// Bulk import (JSON)
await lumenta.clients.import({
clients: [
{ phoneNumber: '441111111111', profileName: 'A' },
{ phoneNumber: '442222222222' },
],
});
// Bulk import (XLSX upload — columns: phoneNumber, profileName)
import { readFile } from 'node:fs/promises';
const xlsx = await readFile('./contacts.xlsx');
const result = await lumenta.clients.importXlsx(xlsx, { filename: 'contacts.xlsx' });
console.log(result.success, result.failed, result.duplicatesSkipped);Campaigns
// Text campaign
const campaign = await lumenta.campaigns.createText({
senderId: 'sender-uuid',
recipients: { segmentIds: ['vip-segment-uuid'] },
message: 'Flash sale ends tonight!',
});
// Template campaign with variables
await lumenta.campaigns.createTemplate({
senderId: 'sender-uuid',
templateId: 'approved-template-uuid',
recipients: { clientIds: ['c1', 'c2'] },
templateVariables: { '1': 'Friend' },
buttonVariables: { coupon_code: 'SAVE20' },
});
// Schedule for later (ISO-8601, future timestamp at least 5 min ahead)
await lumenta.campaigns.createText({
senderId: 'sender-uuid',
recipients: { segmentIds: ['vip-segment-uuid'] },
message: 'Doors open at 9!',
scheduledFor: '2026-07-22T09:00:00.000Z',
timezone: 'Europe/London',
});
// Resend failed
await lumenta.campaigns.resendFailed('campaign-id'); // all failed
await lumenta.campaigns.resendFailed('campaign-id', { recipientIds: ['msg-id-1'] });
// Cancel a scheduled campaign
await lumenta.campaigns.cancel('campaign-id');
// Per-recipient records (paginated, filterable)
await lumenta.campaigns.listRecords('campaign-id', {
status: ['failed'],
search: '4412',
});Templates
const t = await lumenta.templates.create({
name: 'welcome_message',
category: 'marketing',
language: 'en_US',
body: 'Hi {{1}}, welcome to {{2}}!',
bodyExample: [['Alex', 'Acme']],
headerFormat: 'TEXT',
headerText: 'Welcome!',
footerText: 'Powered by Acme',
buttons: [
{ type: 'QUICK_REPLY', text: 'Get started' },
{ type: 'URL', text: 'Visit site', url: 'https://acme.com' },
],
});
// Upload an image header asset, then pass the handle to create()
import { readFile } from 'node:fs/promises';
const { handle, s3Key } = await lumenta.templates.uploadHeaderMedia({
file: await readFile('./banner.jpg'),
filename: 'banner.jpg',
contentType: 'image/jpeg',
mediaType: 'image',
});
// → create({ ..., headerFormat: 'IMAGE', headerHandle: handle, headerS3Key: s3Key })Segments
const seg = await lumenta.segments.create({ name: 'VIPs', description: '...' });
await lumenta.segments.addClients(seg.id, ['client-uuid-1', 'client-uuid-2']);
const members = await lumenta.segments.listClients(seg.id, { page: 1, limit: 50 });Flows
const flow = await lumenta.flows.create({
name: 'Appointment Booking',
categories: ['APPOINTMENT_BOOKING'],
flowJson: JSON.stringify({ version: '5.0', screens: [/* … */] }),
publish: true,
});
await lumenta.flows.send(flow.id, {
to: '441234567890',
flowCta: 'Book Appointment',
bodyText: 'Please complete the form below.',
});Auto-replies
await lumenta.autoReplies.create({
name: 'Office hours',
matchMode: 'contains',
keywords: ['hours', 'open'],
replyType: 'text',
replyBody: 'We are open Mon–Fri 9–17.',
cooldownSeconds: 300,
});
// Reorder priority (top first)
await lumenta.autoReplies.reorder(['rule-1-id', 'rule-2-id', 'rule-3-id']);Follow-ups
const fu = await lumenta.followUps.create({
name: '24h check-in',
senderId: 'sender-uuid',
messageType: 'text',
messageBody: 'Hey, anything else we can help with?',
windowHours: 24,
minSilenceMinutes: 60,
quietHoursStart: '22:00',
quietHoursEnd: '08:00',
timezone: 'Europe/London',
});
await lumenta.followUps.setStatus(fu.id, 'active');
await lumenta.followUps.runNow(fu.id);Chat widgets
const widget = await lumenta.chatWidgets.create({
name: 'Main site widget',
senderId: 'sender-uuid',
themeColor: '#00C896',
greetingText: 'Hi! How can we help?',
allowedDomains: ['acme.com', 'www.acme.com'],
});
const stats = await lumenta.chatWidgets.stats(widget.id, { windowDays: 30 });Suppression
await lumenta.suppression.suppress('client-uuid', {
scope: 'marketing',
reason: 'user_optout',
notes: 'Replied STOP',
});
const suppressed = await lumenta.suppression.listForClient('client-uuid');
// Release a single scope, or all active suppressions if scope is omitted
await lumenta.suppression.release('client-uuid', { scope: 'marketing' });Dashboard
const stats = await lumenta.dashboard.stats({
from: '2026-04-01T00:00:00Z',
to: '2026-05-01T00:00:00Z',
});Webhook subscriptions (Zapier-style)
const me = await lumenta.zapier.me(); // auth-test
const sub = await lumenta.zapier.subscribe({
target_url: 'https://hooks.zapier.com/hooks/catch/123/abc/',
event: 'message.received',
});
await lumenta.zapier.unsubscribe(sub.id);Note —
target_urlmust point athooks.zapier.com(server-side guard). Use a forwarding service if you need to receive events on a different host.
Receiving webhooks
The SDK exports verification helpers for Meta-style signatures. Use them in your inbound webhook handler to confirm the request really came from Lumenta.
import express from 'express';
import {
verifyMetaSignature,
isFreshMetaPayload,
} from '@codemina/lumenta-sdk-nodejs/webhooks';
const app = express();
// IMPORTANT: capture the *raw* body for signature verification.
app.use(express.json({
verify: (req, _res, buf) => {
(req as any).rawBody = buf;
},
}));
app.post('/webhooks/meta', (req, res) => {
const ok = verifyMetaSignature(
(req as any).rawBody,
req.header('x-hub-signature-256'),
process.env.META_APP_SECRET!,
);
if (!ok || !isFreshMetaPayload(req.body, 300)) {
return res.status(401).end();
}
// … process req.body
res.status(200).end();
});
Error handling
Every error thrown by the SDK extends LumentaError. Specific subclasses let you handle common cases:
import {
LumentaError,
ValidationError,
AuthenticationError,
ForbiddenError,
NotFoundError,
ConflictError,
RateLimitError,
ServerError,
TimeoutError,
NetworkError,
} from '@codemina/lumenta-sdk-nodejs';
try {
await lumenta.messages.send({ to: 'bad', body: 'hi' });
} catch (err) {
if (err instanceof RateLimitError) {
console.warn('rate-limited; retry after', err.retryAfterSeconds, 's');
} else if (err instanceof ValidationError) {
console.warn(err.code, err.details);
} else if (err instanceof LumentaError) {
console.error(err.status, err.code, err.message);
} else {
throw err;
}
}Every error exposes { status, code, message, details, requestId, path } — code mirrors the API's errorCode field (e.g. INVALID_PHONE_NUMBER, INSUFFICIENT_SCOPE).
Retries & timeouts
- The transport automatically retries GET and PUT requests on
429,5xx, and network errors with exponential backoff + jitter (cap 8 s). TheRetry-Afterheader is honoured when present. - Non-idempotent methods (POST, PATCH, DELETE) are not retried by default. Override per call with
{ maxRetries: 1 }if your operation is idempotent. - The default request timeout is 30 s, configurable globally or per call.
- All calls accept an
AbortSignalfor cooperative cancellation.
Escape hatch
Need to call an endpoint the SDK doesn't wrap yet?
const raw = await lumenta.raw<{ data: unknown }>({
method: 'GET',
path: '/v1/some/new-endpoint',
query: { foo: 'bar' },
});Coverage
The SDK wraps every public, API-key-callable endpoint of the Lumenta REST API as of v1, across 13 resources:
messages · clients · templates · campaigns · segments · flows · autoReplies · followUps · chatWidgets · suppression · senders (read-only) · dashboard · zapier
Examples
See examples/ for runnable scripts.
Contributing
Issues and PRs welcome at github.com/codemina/lumenta-sdk-nodejs. See CONTRIBUTING.md.
License
MIT © Codemina Ltd.
