npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@codemina/lumenta-sdk-nodejs

v1.0.1

Published

Official Node.js SDK for the Lumenta WhatsApp Business API

Readme

Lumenta Node.js SDK

npm version License: MIT

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-nodejs

Requires 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);

Notetarget_url must point at hooks.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). The Retry-After header 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 AbortSignal for 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.