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

@norialabs/sendkit

v0.1.2

Published

Modular TypeScript/JavaScript SDK for WhatsApp and bulk SMS gateways.

Readme

@norialabs/sendkit

Modular TypeScript/JavaScript SDK for WhatsApp and bulk SMS providers.

Node >=20 is required. The package is ESM-only and designed for Node.js services, workers, and serverless messaging flows.

Use sendkit when you want direct provider wrappers:

  • Meta WhatsApp Cloud API
  • Onfon bulk SMS
  • Africa's Talking SMS

For SendStack email SaaS, use @norialabs/sendstack instead.

Install

npm install @norialabs/sendkit

Import Paths

Import the whole package:

import {
  AfricasTalkingSmsClient,
  MetaWhatsAppClient,
  OnfonSmsClient,
} from "@norialabs/sendkit";

Or import only what you need:

import { MetaWhatsAppClient } from "@norialabs/sendkit/whatsapp";
import { OnfonSmsClient } from "@norialabs/sendkit/sms/onfon";
import { AfricasTalkingSmsClient } from "@norialabs/sendkit/sms/africastalking";
import {
  requireValidMetaSignature,
  resolveMetaSubscriptionChallenge,
} from "@norialabs/sendkit/webhooks";

Available subpaths:

  • @norialabs/sendkit
  • @norialabs/sendkit/whatsapp
  • @norialabs/sendkit/sms
  • @norialabs/sendkit/sms/onfon
  • @norialabs/sendkit/sms/africastalking
  • @norialabs/sendkit/webhooks

Quick Start

WhatsApp

import { MetaWhatsAppClient } from "@norialabs/sendkit/whatsapp";

const whatsapp = new MetaWhatsAppClient({
  accessToken: process.env.META_WHATSAPP_ACCESS_TOKEN!,
  phoneNumberId: process.env.META_WHATSAPP_PHONE_NUMBER_ID!,
});

await whatsapp.sendText({
  recipient: "254700123456",
  text: "Hello from Noria",
});

Onfon SMS

import { OnfonSmsClient } from "@norialabs/sendkit/sms/onfon";

const sms = new OnfonSmsClient({
  accessKey: process.env.ONFON_ACCESS_KEY!,
  apiKey: process.env.ONFON_API_KEY!,
  clientId: process.env.ONFON_CLIENT_ID!,
  defaultSenderId: "NORIALABS",
});

await sms.send({
  messages: [
    { recipient: "254700123456", text: "Your OTP is 123456", reference: "otp-1" },
  ],
});

Africa's Talking SMS

import { AfricasTalkingSmsClient } from "@norialabs/sendkit/sms/africastalking";

const sms = new AfricasTalkingSmsClient({
  apiKey: process.env.AFRICASTALKING_API_KEY!,
  username: process.env.AFRICASTALKING_USERNAME!,
  defaultSenderId: "NORIALABS",
});

await sms.send({
  messages: [
    { recipient: "+254700123456", text: "Your OTP is 123456", reference: "otp-1" },
  ],
});

Provider Coverage

| Provider | Capabilities | | --- | --- | | Meta WhatsApp | Text, templates, media by ID or URL, media upload/get/delete, location, contacts, reactions, interactive buttons/lists, catalog, single product, product list, flows, mark-read, typing indicator, template management, delivery parsing, inbound parsing | | Onfon SMS | Bulk SMS send, scheduled SMS, Unicode/flash flags, balance, groups, templates, delivery report parsing | | Africa's Talking SMS | Bulk SMS send, premium SMS reply, incoming message fetch, subscription create/delete, balance, delivery report parsing |

Non-SMS Africa's Talking products such as Airtime, Voice, USSD, Payments, and Data Bundles are intentionally outside the current sendkit SMS scope.

Shared Transport

Every provider client supports:

  • fetch: custom Fetch implementation
  • timeoutMs: request timeout
  • defaultHeaders: extra default headers
  • retry: retry policy or false
  • hooks: beforeRequest, afterResponse, onError

Per-request options:

  • headers
  • signal
  • timeoutMs
  • retry

Example:

const sms = new OnfonSmsClient({
  accessKey: "access-key",
  apiKey: "api-key",
  clientId: "client-id",
  timeoutMs: 15_000,
  retry: {
    maxAttempts: 3,
    retryMethods: ["GET", "POST"],
    retryOnStatuses: [429, 500, 502, 503, 504],
    retryOnNetworkError: true,
    baseDelayMs: 250,
  },
  hooks: {
    beforeRequest(context) {
      context.headers.set("x-trace-id", "trace-123");
    },
  },
});

WhatsApp: Meta Cloud API

Construction

const whatsapp = new MetaWhatsAppClient({
  accessToken: process.env.META_WHATSAPP_ACCESS_TOKEN!,
  phoneNumberId: process.env.META_WHATSAPP_PHONE_NUMBER_ID!,
  whatsappBusinessAccountId: process.env.META_WHATSAPP_WHATSAPP_BUSINESS_ACCOUNT_ID,
  appSecret: process.env.META_WHATSAPP_APP_SECRET,
  webhookVerifyToken: process.env.META_WHATSAPP_WEBHOOK_VERIFY_TOKEN,
});

whatsappBusinessAccountId is required only for template management methods.

From Env

const whatsapp = MetaWhatsAppClient.fromEnv();

Supported env vars:

  • META_WHATSAPP_ACCESS_TOKEN
  • META_WHATSAPP_PHONE_NUMBER_ID
  • META_WHATSAPP_WHATSAPP_BUSINESS_ACCOUNT_ID
  • META_WHATSAPP_APP_SECRET
  • META_WHATSAPP_WEBHOOK_VERIFY_TOKEN
  • META_WHATSAPP_API_VERSION
  • META_WHATSAPP_BASE_URL
  • META_WHATSAPP_TIMEOUT_SECONDS

WhatsApp Method Reference

| Method | Purpose | | --- | --- | | sendText(request, options?) | Send text messages with optional URL preview | | sendTemplate(request, options?) | Send approved template messages, including text/media/button parameters | | sendMedia(request, options?) | Send image, audio, document, sticker, or video by uploaded media ID or public URL | | sendLocation(request, options?) | Send a location pin | | sendContacts(request, options?) | Send one or more contacts | | sendReaction(request, options?) | React to an existing message | | sendInteractive(request, options?) | Send reply-button or list interactive messages | | sendCatalog(request, options?) | Send catalog messages | | sendProduct(request, options?) | Send a single-product message | | sendProductList(request, options?) | Send a multi-product list | | sendFlow(request, options?) | Send a WhatsApp Flow interactive message | | markMessageRead(request, options?) | Mark an inbound message as read | | sendTypingIndicator(request, options?) | Mark an inbound message as read and show a typing indicator | | uploadMedia(request, options?) | Upload media bytes to Meta | | getMedia(mediaId, options?) | Get media metadata and download URL | | deleteMedia(mediaId, options?) | Delete uploaded media | | listTemplates(request?, options?) | List templates for a WABA | | getTemplate(templateId, fields?, options?) | Fetch one template | | createTemplate(request, options?) | Create a template | | updateTemplate(templateId, request, options?) | Update a template | | deleteTemplate(request, options?) | Delete a template by name, ID, or IDs | | parseEvents(payload) | Parse delivery/read/failed webhook statuses | | parseInboundMessages(payload) | Parse inbound messages | | parseEvent(payload) | Return the first parsed delivery event or null | | parseInboundMessage(payload) | Return the first parsed inbound message or null |

Text

await whatsapp.sendText({
  recipient: "254700123456",
  text: "Plain text message",
  previewUrl: true,
  replyToMessageId: "wamid.previous",
});

Templates

Template messages support text, media, and button parameters. For media template headers, pass a parameter of type image, video, or document and either provide the provider-specific object through providerOptions, or pass value to use an uploaded media ID.

await whatsapp.sendTemplate({
  recipient: "254700123456",
  templateName: "order_update",
  languageCode: "en",
  components: [
    {
      type: "header",
      parameters: [
        {
          type: "document",
          providerOptions: {
            document: {
              id: "media-id",
              filename: "invoice.pdf",
            },
          },
        },
      ],
    },
    {
      type: "body",
      parameters: [
        { type: "text", value: "NORIA-123" },
        { type: "text", value: "Ready for pickup" },
      ],
    },
    {
      type: "button",
      subType: "quick_reply",
      index: 0,
      parameters: [{ type: "payload", value: "track-order" }],
    },
  ],
});

Media And Attachments

Meta WhatsApp calls these files “media”. You can send media by public URL, send media by uploaded ID, or upload bytes first and then use the returned media ID.

await whatsapp.sendMedia({
  recipient: "254700123456",
  mediaType: "image",
  link: "https://example.com/product.jpg",
  caption: "Preview",
});

const uploaded = await whatsapp.uploadMedia({
  filename: "menu.pdf",
  mimeType: "application/pdf",
  content: Buffer.from("file-bytes"),
});

await whatsapp.sendMedia({
  recipient: "254700123456",
  mediaType: "document",
  mediaId: uploaded.mediaId,
  filename: "menu.pdf",
});

await whatsapp.getMedia(uploaded.mediaId);
await whatsapp.deleteMedia(uploaded.mediaId);

Supported media types:

  • image
  • audio
  • document
  • sticker
  • video

Location, Contacts, And Reactions

await whatsapp.sendLocation({
  recipient: "254700123456",
  latitude: -1.286389,
  longitude: 36.817223,
  name: "Nairobi Office",
  address: "Nairobi, Kenya",
});

await whatsapp.sendContacts({
  recipient: "254700123456",
  contacts: [
    {
      name: { formattedName: "Noria Support", firstName: "Noria" },
      phones: [{ phone: "+254700000000", type: "WORK" }],
      emails: [{ email: "[email protected]", type: "WORK" }],
    },
  ],
});

await whatsapp.sendReaction({
  recipient: "254700123456",
  messageId: "wamid.inbound",
  emoji: "👍",
});

Interactive, Catalog, Product, And Flow Messages

await whatsapp.sendInteractive({
  recipient: "254700123456",
  interactiveType: "button",
  bodyText: "Choose one",
  buttons: [
    { identifier: "yes", title: "Yes" },
    { identifier: "no", title: "No" },
  ],
});

await whatsapp.sendInteractive({
  recipient: "254700123456",
  interactiveType: "list",
  bodyText: "Choose a product",
  buttonText: "View options",
  sections: [
    {
      title: "Products",
      rows: [
        { identifier: "sku-1", title: "Starter" },
        { identifier: "sku-2", title: "Pro" },
      ],
    },
  ],
});

await whatsapp.sendCatalog({
  recipient: "254700123456",
  bodyText: "Browse our catalog",
});

await whatsapp.sendProduct({
  recipient: "254700123456",
  catalogId: "catalog-1",
  productRetailerId: "sku-1",
});

await whatsapp.sendProductList({
  recipient: "254700123456",
  catalogId: "catalog-1",
  header: { type: "text", text: "Featured" },
  sections: [
    {
      title: "Top Picks",
      productItems: [{ productRetailerId: "sku-1" }],
    },
  ],
});

await whatsapp.sendFlow({
  recipient: "254700123456",
  flowCta: "Start",
  flowId: "flow-1",
  flowAction: "navigate",
});

Read Receipts And Typing Indicator

await whatsapp.markMessageRead({
  messageId: "wamid.inbound",
});

await whatsapp.sendTypingIndicator({
  messageId: "wamid.inbound",
});

Template Management

const list = await whatsapp.listTemplates({
  limit: 20,
  status: ["approved"],
  fields: ["name", "status", "category", "language", "components"],
});

await whatsapp.getTemplate("template-id");

await whatsapp.createTemplate({
  name: "order_update",
  language: "en_US",
  category: "utility",
  components: [
    {
      type: "body",
      text: "Order {{1}} is ready",
      example: {
        body_text: [["NORIA-123"]],
      },
    },
  ],
});

await whatsapp.updateTemplate("template-id", {
  category: "utility",
});

await whatsapp.deleteTemplate({ templateId: "template-id" });

WhatsApp Webhooks

const deliveryEvents = whatsapp.parseEvents(metaWebhookPayload);
const inboundMessages = whatsapp.parseInboundMessages(metaWebhookPayload);

parseInboundMessages supports inbound text, media, location, contacts, button replies, interactive replies, reactions, and unsupported message fallback metadata.

SMS: Shared Types

All SMS providers use the shared send shape:

await sms.send({
  senderId: "NORIALABS",
  messages: [
    {
      recipient: "254700123456",
      text: "Hello",
      reference: "internal-id",
      metadata: { accountId: "acct_1" },
    },
  ],
  scheduleAt: new Date("2026-06-26T09:00:00.000Z"),
  isUnicode: false,
  isFlash: false,
  providerOptions: {},
});

providerOptions is passed through to the underlying provider payload when you need provider-specific fields.

SMS: Onfon

Construction

const sms = new OnfonSmsClient({
  accessKey: process.env.ONFON_ACCESS_KEY!,
  apiKey: process.env.ONFON_API_KEY!,
  clientId: process.env.ONFON_CLIENT_ID!,
  defaultSenderId: "NORIALABS",
});

From Env

const sms = OnfonSmsClient.fromEnv();

Supported env vars:

  • ONFON_ACCESS_KEY
  • ONFON_API_KEY
  • ONFON_CLIENT_ID
  • ONFON_SENDER_ID
  • ONFON_BASE_URL
  • ONFON_TIMEOUT_SECONDS

Onfon Method Reference

| Method | Purpose | | --- | --- | | send(request, options?) | Send one or more SMS messages | | getBalance(options?) | Read SMS balance | | listGroups(options?) | List contact groups | | createGroup(request, options?) | Create a contact group | | updateGroup(groupId, request, options?) | Update a contact group | | deleteGroup(groupId, options?) | Delete a contact group | | listTemplates(options?) | List SMS templates | | createTemplate(request, options?) | Create an SMS template | | updateTemplate(templateId, request, options?) | Update an SMS template | | deleteTemplate(templateId, options?) | Delete an SMS template | | parseDeliveryReport(payload) | Parse delivery-report callbacks |

Onfon Examples

const result = await sms.send({
  senderId: "NORIALABS",
  messages: [
    { recipient: "254700123456", text: "Hello there", reference: "msg-1" },
    { recipient: "254711111111", text: "Hello again", reference: "msg-2" },
  ],
  isUnicode: false,
});

await sms.getBalance();

const group = await sms.createGroup({ name: "VIP Customers" });
await sms.updateGroup(group.resourceId!, { name: "Priority Customers" });
await sms.deleteGroup(group.resourceId!);

const template = await sms.createTemplate({
  name: "otp",
  body: "Your OTP is {{1}}",
});
await sms.updateTemplate(template.resourceId!, {
  name: "otp",
  body: "Use code {{1}}",
});
await sms.deleteTemplate(template.resourceId!);

const report = sms.parseDeliveryReport({
  messageId: "abc123",
  mobile: "254700123456",
  status: "Delivered",
});

SMS: Africa's Talking

Construction

const sms = new AfricasTalkingSmsClient({
  apiKey: process.env.AFRICASTALKING_API_KEY!,
  username: process.env.AFRICASTALKING_USERNAME!,
  defaultSenderId: "NORIALABS",
});

Use AFRICASTALKING_SANDBOX_SMS_BASE_URL for sandbox clients:

const sandboxSms = new AfricasTalkingSmsClient({
  apiKey: process.env.AFRICASTALKING_API_KEY!,
  username: "sandbox",
  baseUrl: AFRICASTALKING_SANDBOX_SMS_BASE_URL,
});

From Env

const sms = AfricasTalkingSmsClient.fromEnv();

Supported env vars:

  • AFRICASTALKING_API_KEY
  • AFRICASTALKING_USERNAME
  • AFRICASTALKING_SENDER_ID
  • AFRICASTALKING_BASE_URL
  • AFRICASTALKING_TIMEOUT_SECONDS

Fallback env vars are also accepted:

  • AFRICAS_TALKING_API_KEY
  • AFRICAS_TALKING_USERNAME
  • AFRICAS_TALKING_SENDER_ID
  • AFRICAS_TALKING_BASE_URL

Africa's Talking Method Reference

| Method | Purpose | | --- | --- | | send(request, options?) | Send normal bulk SMS | | sendPremium(request, options?) | Send premium SMS replies using keyword and link ID | | fetchMessages(request?, options?) | Fetch incoming SMS messages | | createSubscription(request, options?) | Opt a phone number into a premium SMS subscription | | deleteSubscription(request, options?) | Remove a premium SMS subscription | | getBalance(options?) | Read account balance | | parseDeliveryReport(payload) | Parse delivery-report callbacks |

Normal SMS

await sms.send({
  senderId: "NORIALABS",
  messages: [
    { recipient: "+254700123456", text: "Hello", reference: "msg-1" },
    { recipient: "+254711111111", text: "Hello", reference: "msg-2" },
  ],
  providerOptions: {
    enqueue: "1",
  },
});

The client groups messages by text because Africa's Talking accepts one message body per request and many recipients.

Premium SMS

await sms.sendPremium({
  recipient: "+254700123456",
  shortCode: "22384",
  keyword: "NORIA",
  linkId: "link-id-from-inbound-message",
  text: "Thanks for subscribing",
  retryDurationInHours: 2,
});

Incoming Messages

const inbox = await sms.fetchMessages({
  lastReceivedId: 42,
});

for (const message of inbox.messages) {
  console.log(message.providerMessageId, message.sender, message.text);
}

Premium Subscriptions

await sms.createSubscription({
  phoneNumber: "+254700123456",
  shortCode: "22384",
  keyword: "NORIA",
});

await sms.deleteSubscription({
  phoneNumber: "+254700123456",
  shortCode: "22384",
  keyword: "NORIA",
});

Balance And Delivery Reports

const balance = await sms.getBalance();

const event = sms.parseDeliveryReport({
  id: "at-message-id",
  phoneNumber: "+254700123456",
  status: "Success",
  networkCode: "63902",
  retryCount: "0",
});

Webhooks

Meta Verification Challenge

const challenge = resolveMetaSubscriptionChallenge(
  {
    "hub.mode": "subscribe",
    "hub.verify_token": "verify-me",
    "hub.challenge": "12345",
  },
  "verify-me",
);

Meta Signature Verification

import { requireValidMetaSignature } from "@norialabs/sendkit/webhooks";

requireValidMetaSignature(rawBody, req.headers["x-hub-signature-256"], appSecret);

SMS Delivery Reports

import {
  parseAfricasTalkingSmsDeliveryReport,
  parseOnfonDeliveryReport,
} from "@norialabs/sendkit/webhooks";

const onfonEvent = parseOnfonDeliveryReport(req.query, onfonClient);
const atEvent = parseAfricasTalkingSmsDeliveryReport(req.body, africasTalkingClient);

Errors

Important exported errors:

  • SendKitError
  • ConfigurationError
  • ApiError
  • ProviderError
  • NetworkError
  • TimeoutError
  • WebhookVerificationError

Provider errors include provider response details where available.

import { ProviderError } from "@norialabs/sendkit";

try {
  await sms.send({ messages: [] });
} catch (error) {
  if (error instanceof ProviderError) {
    console.error(error.provider, error.errorCode, error.responseBody);
  }
}

Runtime Exports

Root exports:

  • OnfonSmsClient
  • AfricasTalkingSmsClient
  • MetaWhatsAppClient
  • ONFON_BASE_URL
  • ONFON_SMS_BASE_URL
  • AFRICASTALKING_SMS_BASE_URL
  • AFRICASTALKING_SANDBOX_SMS_BASE_URL
  • META_GRAPH_BASE_URL
  • META_GRAPH_API_VERSION
  • parseAfricasTalkingDeliveryReport
  • parseOnfonDeliveryReport
  • parseAfricasTalkingSmsDeliveryReport
  • resolveMetaSubscriptionChallenge
  • verifyMetaSignature
  • requireValidMetaSignature
  • all exported error classes

Provider-specific subpaths export the same provider classes and related types for that provider.

Important Type Exports

SMS:

  • SmsClient
  • SmsManagementClient
  • SmsSendRequest
  • SmsSendResult
  • SmsSendReceipt
  • SmsMessage
  • SmsBalance
  • SmsGroup
  • SmsTemplate
  • AfricasTalkingPremiumSmsRequest
  • AfricasTalkingFetchMessagesResult
  • AfricasTalkingIncomingMessage
  • AfricasTalkingSubscriptionRequest
  • AfricasTalkingSubscriptionResult

WhatsApp:

  • WhatsAppClient
  • WhatsAppTemplateManagementClient
  • WhatsAppTextRequest
  • WhatsAppTemplateRequest
  • WhatsAppMediaRequest
  • WhatsAppLocationRequest
  • WhatsAppContactsRequest
  • WhatsAppReactionRequest
  • WhatsAppInteractiveRequest
  • WhatsAppCatalogMessageRequest
  • WhatsAppProductMessageRequest
  • WhatsAppProductListRequest
  • WhatsAppFlowMessageRequest
  • WhatsAppReadRequest
  • WhatsAppStatusResult
  • WhatsAppMediaUploadRequest
  • WhatsAppMediaUploadResult
  • WhatsAppManagedTemplate
  • WhatsAppInboundMessage
  • WhatsAppSendResult

Core:

  • RequestOptions
  • RetryPolicy
  • Hooks
  • DeliveryEvent
  • DeliveryState
  • MessageChannel

Versioning And Publish Notes

The package publishes only built JavaScript, declaration files, and this package guide. The publish build uses build:dist, which does not emit sourcemaps.