@enkaliprime/kinetic-sdk
v0.3.0
Published
Official JavaScript/TypeScript SDK for the Kinetic Developer API — by EnkaliPrime
Readme
@enkaliprime/kinetic-sdk
Official JavaScript/TypeScript SDK for the Kinetic WhatsApp Developer API
Send WhatsApp messages from Node.js, browsers, Next.js, React Native, Expo, and Electron — with full TypeScript support and zero runtime dependencies.
Features
- WhatsApp messaging API — send text messages through your linked Kinetic business line
- Group messaging — list participating groups and post alerts to WhatsApp groups
- Webhooks — verify signed inbound events and test delivery to your endpoints
- Bearer authentication —
kin_live_/kin_test_API keys from the Kinetic Developer Console - Fetch-only HTTP client — works everywhere
fetchis available (Node 18+, browsers, React Native, Expo) - Zero dependencies — no React, no axios, no Node-only modules in the core package
- Typed errors —
KineticAuthError,KineticValidationError,KineticApiError,KineticNetworkError - Injectable
fetch— custom implementations for tests, polyfills, or React Native - Configurable timeout — abort requests after
timeoutMs(default 30s)
Installation
npm install @enkaliprime/kinetic-sdkyarn add @enkaliprime/kinetic-sdkpnpm add @enkaliprime/kinetic-sdkRequirements: Node.js 18+ (for native fetch), or any environment with a fetch polyfill.
Quick start
1. Get an API key
- Sign in at kinetic.africa
- Open Developers → API Keys
- Create a key with Send Messages (and Group Messaging if you post to groups)
- Copy the
kin_live_…secret when shown (it is only displayed once)
2. Send your first message
import { Kinetic } from "@enkaliprime/kinetic-sdk";
const kinetic = new Kinetic({
apiKey: process.env.KINETIC_API_KEY!,
});
const result = await kinetic.messages.send({
to: "+264811234567",
message: "Weekly report generated.",
});
console.log(result.id); // msg_a1b2c3d4e5f6
console.log(result.status); // queued3. Environment variable (recommended)
KINETIC_API_KEY=kin_live_your_secret_hereNever commit API keys to source control. Use environment variables or a secrets manager.
Client configuration
import { Kinetic } from "@enkaliprime/kinetic-sdk";
const kinetic = new Kinetic({
apiKey: "kin_live_xxxxxxxxxxxxxxxxxxxxxxxx",
baseUrl: "https://kinetic-4cfj.onrender.com", // optional — this is the default
timeoutMs: 30_000, // optional — request timeout
fetch: globalThis.fetch, // optional — custom fetch impl
});| Option | Required | Default | Description |
|--------|----------|---------|-------------|
| apiKey | Yes | — | Your kin_live_ or kin_test_ API key |
| baseUrl | No | https://kinetic-4cfj.onrender.com | Kinetic API base URL (no trailing slash) |
| fetch | No | globalThis.fetch | Custom fetch for tests or React Native |
| timeoutMs | No | 30000 | Abort request after this many milliseconds |
Local development
Point at a local WhatsApp gateway while developing:
const kinetic = new Kinetic({
apiKey: process.env.KINETIC_API_KEY!,
baseUrl: "http://localhost:3001",
});Run the gateway with npm run gateway:dev in the aura-chat project.
API reference
Kinetic
Main client. Exposes messages, groups, and webhooks resources.
const kinetic = new Kinetic({ apiKey: "kin_live_..." });kinetic.messages.send(input)
Send a text message to a WhatsApp number via your linked business line.
Parameters
| Field | Type | Description |
|-------|------|-------------|
| to | string | E.164 phone number (e.g. +264811234567) |
| message | string | Message body (plain text) |
Returns Promise<SendMessageResponse>
type SendMessageResponse = {
id: string; // e.g. msg_a1b2c3d4e5f6
status: "queued" | "sent";
};Example
const { id, status } = await kinetic.messages.send({
to: "+264813523159",
message: "Your appointment is confirmed for tomorrow at 10:00.",
});HTTP mapping
POST /v1/messages/send
Authorization: Bearer kin_live_...
Content-Type: application/json
{ "to": "+264...", "message": "..." }The business is inferred from your API key — do not send businessId in the request body.
kinetic.groups.list()
List WhatsApp groups your linked business account participates in.
Requires group_messaging on your API key.
Returns Promise<ListGroupsResponse>
type WhatsAppGroup = {
id: string; // e.g. [email protected]
name: string;
participants: number;
};
type ListGroupsResponse = {
groups: WhatsAppGroup[];
};Example
const { groups } = await kinetic.groups.list();
for (const group of groups) {
console.log(group.id, group.name, group.participants);
}HTTP mapping
GET /v1/groups
Authorization: Bearer kin_live_...kinetic.groups.send(input)
Send a text message to a WhatsApp group.
Requires group_messaging on your API key.
Parameters
| Field | Type | Description |
|-------|------|-------------|
| group_id | string | Group JID (120363…@g.us) or numeric id |
| message | string | Message body (plain text) |
Returns Promise<SendGroupMessageResponse>
type SendGroupMessageResponse = {
id: string; // e.g. grp_a1b2c3d4e5f6
status: "queued" | "sent";
};Example
const { id, status } = await kinetic.groups.send({
group_id: "[email protected]",
message: "Safety alert: weekly report is ready.",
});HTTP mapping
POST /v1/groups/send
Authorization: Bearer kin_live_...
Content-Type: application/json
{ "group_id": "120363…@g.us", "message": "..." }Group delivery with direct-message fallback
For safety or ops alerts, prefer the group when you have a group_id. If group delivery fails (missing permission, bot not in group, WhatsApp disconnected), fall back to direct messages — for example numbers in KINETIC_SAFETY_FALLBACK_PHONES:
async function sendSafetyAlert(
kinetic: Kinetic,
groupId: string,
message: string,
fallbackPhones: string[],
) {
try {
return await kinetic.groups.send({ group_id: groupId, message });
} catch (err) {
console.warn("Group send failed, falling back to direct messages", err);
for (const to of fallbackPhones) {
await kinetic.messages.send({ to, message });
}
}
}Use kinetic.groups.list() to discover valid group_id values for your linked line.
kinetic.webhooks.test(input?)
Send a signed test message.sent event to your registered webhook endpoint(s).
Parameters
| Field | Type | Description |
|-------|------|-------------|
| webhook_id | string | Optional — target one webhook; omit to ping all active endpoints subscribed to message.sent |
Returns Promise<TestWebhookResponse>
type TestWebhookResponse = {
queued: number; // delivery rows queued
};Example
// Ping all active webhooks for this business
const { queued } = await kinetic.webhooks.test();
// Ping a specific endpoint
await kinetic.webhooks.test({ webhook_id: "your-webhook-uuid" });HTTP mapping
POST /v1/webhooks/test
Authorization: Bearer kin_live_...
Content-Type: application/json
{} // or { "webhook_id": "..." }Webhooks — receive events
When you register a webhook URL in the Developer Console, Kinetic POSTs signed JSON events to your server.
Event envelope
{
"id": "evt_a1b2c3d4e5f67890",
"type": "message.sent",
"created_at": "2026-06-20T12:00:00.000Z",
"data": {
"message_id": "msg_a1b2c3d4e5f6",
"to": "+264811234567",
"status": "queued"
}
}Event types
| Type | When fired |
|------|------------|
| message.sent | Outbound message accepted by the API |
| message.failed | Outbound send failed |
| message.delivered | Delivery receipt (coming soon) |
| message.read | Read receipt (coming soon) |
| message.incoming | Inbound direct message (coming soon) |
| group.reply | Inbound group message (coming soon) |
| media.received | Inbound media (coming soon) |
| ai.handover | AI handover (coming soon) |
Subscribe to events when creating a webhook in the console. Each endpoint has a signing secret (shown once at creation).
Verify signatures
Kinetic signs each request with HMAC-SHA256:
X-Kinetic-Signature: t=<unix_seconds>,v1=<hex_hmac>
X-Kinetic-Event-Id: evt_...
X-Kinetic-Event-Type: message.sentSigned payload: ${timestamp}.${rawBody} using your webhook signing secret.
import {
verifyAndParseWebhook,
verifyWebhookSignature,
parseWebhookEvent,
KineticWebhookSignatureError,
WEBHOOK_SIGNATURE_HEADER,
type MessageSentEventData,
type MessageFailedEventData,
} from "@enkaliprime/kinetic-sdk";
// Recommended — verify + parse in one call
const event = await verifyAndParseWebhook<MessageSentEventData>({
secret: process.env.KINETIC_WEBHOOK_SECRET!,
rawBody: requestBodyString,
signatureHeader: req.headers["x-kinetic-signature"],
toleranceSec: 300, // optional — default 5 minutes
});
switch (event.type) {
case "message.sent":
console.log("Sent:", event.data.message_id, event.data.to ?? event.data.group_id);
break;
case "message.failed":
console.error("Failed:", event.data.error);
break;
}Lower-level helpers:
const ok = await verifyWebhookSignature(secret, rawBody, signatureHeader);
const event = parseWebhookEvent(rawBody); // parse only — verify first in productionExpress webhook handler
Use the raw body (not express.json() alone) so the signature matches:
import express from "express";
import {
verifyAndParseWebhook,
KineticWebhookSignatureError,
} from "@enkaliprime/kinetic-sdk";
const app = express();
app.post(
"/webhooks/kinetic",
express.raw({ type: "application/json" }),
async (req, res) => {
try {
const rawBody = req.body.toString("utf8");
const event = await verifyAndParseWebhook({
secret: process.env.KINETIC_WEBHOOK_SECRET!,
rawBody,
signatureHeader: req.headers["x-kinetic-signature"] as string,
});
// Handle event asynchronously if needed
console.log(event.type, event.data);
res.sendStatus(200);
} catch (err) {
if (err instanceof KineticWebhookSignatureError) {
return res.status(401).json({ error: err.message });
}
res.status(500).json({ error: "Webhook handler failed" });
}
},
);Next.js App Router
// app/api/webhooks/kinetic/route.ts
import { verifyAndParseWebhook, KineticWebhookSignatureError } from "@enkaliprime/kinetic-sdk";
import { NextResponse } from "next/server";
export async function POST(req: Request) {
const rawBody = await req.text();
try {
const event = await verifyAndParseWebhook({
secret: process.env.KINETIC_WEBHOOK_SECRET!,
rawBody,
signatureHeader: req.headers.get("x-kinetic-signature"),
});
console.log(event.type, event.data);
return NextResponse.json({ received: true });
} catch (err) {
if (err instanceof KineticWebhookSignatureError) {
return NextResponse.json({ error: err.message }, { status: 401 });
}
throw err;
}
}Framework examples
Node.js (ESM)
import { Kinetic } from "@enkaliprime/kinetic-sdk";
const kinetic = new Kinetic({ apiKey: process.env.KINETIC_API_KEY! });
await kinetic.messages.send({
to: "+264811234567",
message: "Hello from Node",
});Next.js API route
// app/api/notify/route.ts
import { Kinetic, KineticError } from "@enkaliprime/kinetic-sdk";
import { NextResponse } from "next/server";
const kinetic = new Kinetic({ apiKey: process.env.KINETIC_API_KEY! });
export async function POST(req: Request) {
const { to, message } = await req.json();
try {
const result = await kinetic.messages.send({ to, message });
return NextResponse.json(result);
} catch (err) {
if (err instanceof KineticError) {
return NextResponse.json({ error: err.message }, { status: err.status || 500 });
}
throw err;
}
}Express
import express from "express";
import { Kinetic, KineticAuthError } from "@enkaliprime/kinetic-sdk";
const app = express();
const kinetic = new Kinetic({ apiKey: process.env.KINETIC_API_KEY! });
app.post("/notify", express.json(), async (req, res) => {
try {
const result = await kinetic.messages.send(req.body);
res.json(result);
} catch (err) {
if (err instanceof KineticAuthError) {
return res.status(401).json({ error: err.message });
}
res.status(500).json({ error: "Send failed" });
}
});React Native / Expo
import { Kinetic } from "@enkaliprime/kinetic-sdk";
const kinetic = new Kinetic({
apiKey: process.env.EXPO_PUBLIC_KINETIC_API_KEY!,
// fetch is available globally in modern RN / Expo
});
await kinetic.messages.send({
to: "+264811234567",
message: "Hello from the app",
});Raw cURL (no SDK)
curl -X POST "https://kinetic-4cfj.onrender.com/v1/messages/send" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer kin_live_YOUR_KEY" \
-d '{"to":"+264811234567","message":"Hello from cURL"}'Error handling
All API errors extend KineticError:
import {
Kinetic,
KineticAuthError,
KineticValidationError,
KineticApiError,
KineticNetworkError,
KineticWebhookSignatureError,
KineticError,
} from "@enkaliprime/kinetic-sdk";
try {
await kinetic.messages.send({ to: "+264...", message: "Hi" });
} catch (err) {
if (err instanceof KineticAuthError) {
// 401 — invalid, revoked, or missing API key
console.error("Check your kin_live_ key");
} else if (err instanceof KineticValidationError) {
// 400 — bad phone number, empty message, etc.
console.error("Invalid request:", err.message);
} else if (err instanceof KineticApiError) {
// 5xx — server or WhatsApp session error
console.error("API error:", err.status, err.message);
} else if (err instanceof KineticNetworkError) {
// timeout, DNS, offline
console.error("Network error:", err.message);
} else if (err instanceof KineticError) {
console.error(err.status, err.message, err.body);
}
}| Error class | HTTP status | Typical cause |
|-------------|-------------|---------------|
| KineticAuthError | 401 | Missing/invalid Bearer token, revoked key |
| KineticValidationError | 400 | Missing to or message, malformed body |
| KineticApiError | 4xx/5xx | Server errors, WhatsApp not connected |
| KineticNetworkError | — | Timeout, connection refused, DNS failure |
| KineticWebhookSignatureError | — | Invalid or missing webhook signature / payload |
TypeScript
Full types are exported from the package:
import type {
KineticClientOptions,
ListGroupsResponse,
SendGroupMessageInput,
SendGroupMessageResponse,
SendMessageInput,
SendMessageResponse,
WhatsAppGroup,
KineticErrorBody,
WebhookEvent,
WebhookEventType,
MessageSentEventData,
MessageFailedEventData,
TestWebhookInput,
TestWebhookResponse,
} from "@enkaliprime/kinetic-sdk";API keys & permissions
Keys are created in the Kinetic Developer Console and prefixed by environment:
| Prefix | Environment |
|--------|-------------|
| kin_live_ | Production |
| kin_test_ | Test (when enabled for your workspace) |
Each key has granular permissions:
| Permission | Description |
|------------|-------------|
| send_messages | Required for messages.send() |
| group_messaging | Required for groups.list() and groups.send() |
| templates | Template messages (coming soon) |
| media | Media messages (coming soon) |
Keys are stored as SHA-256 hashes server-side. The full secret is shown once at creation or rotation.
Roadmap
Shipped in v0.2.x:
kinetic.messages.send({ to, message });
kinetic.groups.list();
kinetic.groups.send({ group_id, message });
kinetic.webhooks.test({ webhook_id? });
verifyAndParseWebhook({ secret, rawBody, signatureHeader });Shipped in v0.1.x:
kinetic.messages.send({ to, message });
kinetic.groups.list();
kinetic.groups.send({ group_id, message });Planned:
// kinetic.media.send({ to, url, caption });
// kinetic.templates.send({ ... });
// Inbound events: message.incoming, message.delivered, message.readDevelopment
Clone the sdk monorepo:
cd kinetic-sdks/packages/sdk
npm install
npm run build
npm testSupport
- Developer docs: kinetic.africa/developers
- Issues: github.com/Auwanga/sdk/issues
- Email: [email protected]
License
MIT © EnkaliPrime
