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

@kiriminow/sdk

v0.1.0

Published

TypeScript SDK for the Kirimi WhatsApp API

Readme

@kiriminow/sdk

JSR npm

TypeScript SDK for the Kirimi WhatsApp API.

  • TypeScript-first, fully typed
  • ESM only — no CommonJS
  • Zero external dependencies
  • Native fetch — works in Node 18+, Deno, Bun, browser, Cloudflare Workers, and any edge runtime
  • Native FormData for file uploads

Installation

Deno (JSR)

deno add jsr:@kiriminow/sdk

Node.js (JSR)

npx jsr add @kiriminow/sdk

Bun (JSR)

bunx jsr add @kiriminow/sdk

npm

npm install @kiriminow/sdk

Quick Start

import { KirimiClient } from '@kiriminow/sdk';

const client = new KirimiClient({
  userCode: 'YOUR_USER_CODE',
  secret: 'YOUR_SECRET',
});

const resp = await client.sendMessage({
  deviceId: 'YOUR_DEVICE_ID',
  receiver: '628111222333',
  message: 'Halo dari Kirimi SDK!',
});

console.log(resp.success); // true

Constructor Options

const client = new KirimiClient({
  userCode: 'USER',        // required
  secret: 'SECRET',        // required
  baseUrl: 'https://api.kirimi.id', // optional, default shown
  timeout: 30000,          // optional, ms, default 30s
  fetch: customFetch,      // optional, inject custom fetch
});

Auth travels in the request body (user_code + secret), never in a header. Every method returns Promise<KirimiResponse<T>>:

interface KirimiResponse<T = unknown> {
  success: boolean;
  data: T | null;
  message?: string;
}

Two send paths, don't mix them

  • QR Scan device — unofficial, free-form text any time, supports groups. Can drop.
  • WABA (Meta Cloud API) — official, stable. Business-initiated messages must use an approved template. Free-form replies only inside the 24h customer service window (wabaReply). No group support. Uses wabaId, never deviceId.

For WABA, a success response means Meta accepted the message (delivery_status: "accepted"), not that it was delivered. The final state arrives via webhook as message.sent, message.ack, message.failed.

Methods

WhatsApp (QR device)

await client.sendMessage({ deviceId, receiver, message, mediaUrl?, fileName?,
  enableTypingEffect?, typingSpeedMs?, quotedMessageId? });

await client.sendMessageFast({ deviceId, receiver, message, mediaUrl?, fileName?, quotedMessageId? });

await client.sendMessageFile({ deviceId, receiver, file, fileName?, message?, caption?, quotedMessageId? });

await client.broadcastMessage({ deviceId, label, numbers: [...], message,
  delay?, delayMin?, delayMax?, mediaUrl?, fileName?, startedAt? });

sendMessageFile accepts Blob, File, or Uint8Array (max 50 MB). broadcastMessage takes numbers as an array (max 1000) and requires label. The server clamps delay to 30–3600 seconds.

WABA (Cloud API)

await client.sendWabaMessage({ wabaId, to, templateName, variables?, header?, buttons? });

await client.wabaReply({ wabaId, to, message });        // free text within 24h window
await client.wabaConversations({ limit?, page? });      // numbers still in the window
await client.wabaTemplatesSync({ wabaId });             // refresh template status from Meta
await client.wabaSendOtp({ wabaId, to, templateName });
await client.wabaVerifyOtp({ wabaId, to, otpCode });

Reply message shapes:

{ type: 'text', text: 'Halo' }
{ type: 'image', media_url: 'https://…', caption: 'Brosur' }   // also audio/video
{ type: 'document', media_url: 'https://…', filename: 'a.pdf' }
{ type: 'interactive', interactive: { /* Meta interactive object */ } }

Template header (required for media or dynamic text headers):

await client.sendWabaMessage({
  wabaId: '1000000000',
  to: '628111222333',
  templateName: 'order_update',
  variables: ['Budi', 'INV-001'],
  header: { type: 'document', link: 'https://cdn.example.com/invoice.pdf', filename: 'invoice.pdf' },
});

Devices

await client.createDevice({ packageId, voucherCode? });
await client.connectDevice({ deviceId });
await client.renewDevice({ deviceId, packageId, voucherCode? });
await client.listDevices({ page?, limit? });
await client.deviceStatus({ deviceId });
await client.deviceStatusEnhanced({ deviceId });

User

await client.userInfo();

Contacts

await client.saveContact({ nama, nomor, deviceId? });
await client.saveContactsBulk({ contacts: [{ nama, nomor }], deviceId? }); // max 1000

Existing numbers are skipped, not overwritten.

OTP v2 (recommended)

method is one of whatsapp (alias waba), device, or waba_user.

// Via the official Kirimi provider — Rp 595 per delivered OTP, no own number needed
await client.sendOtpV2({ phone, method: 'whatsapp', appName: 'MyApp' });

// Via your own connected device — free
await client.sendOtpV2({ phone, method: 'device', deviceId,
  customMessage: 'Kode OTP kamu: {{otp}}' });

// Via your own WABA + AUTHENTICATION template — free, Meta bills your WABA
await client.sendOtpV2({ phone, method: 'waba_user', wabaId, templateName: 'otp_login' });

await client.verifyOtpV2({ phone, otpCode: '123456' });

customMessage must contain {{otp}} and be 10–500 characters.

OTP v1 (legacy)

await client.generateOtp({ deviceId, phone, otpLength?, otpType?, customOtpText?,
  customOtpMessage?, enableTypingEffect?, typingSpeedMs? });
await client.validateOtp({ deviceId, phone, otp });

otpType is 'numeric' | 'alphabetic' | 'alphanumeric'. customOtpMessage must contain the {otp} placeholder (single braces).

OTP Reverse (customer-initiated)

const { data } = await client.otpReverseCreate({
  phone, deviceId, appName?, callbackUrl?,
  customMessage: 'VERIFY {{token}} {{phone}}',
});
// Send data.message_text to the customer; they reply with it to your device.

await client.otpReverseStatus({ token });

Status is pending | verified | phone_mismatch | expired. The token is valid 10 minutes and single use. When verification completes, Kirimi POSTs to callbackUrl with the header x-kirimi-event: otp-reverse.verified.

Packages & Deposits

await client.listPackages();
await client.createDeposit({ nominal });        // min 100 IDR
await client.depositStatus({ ref });
await client.cancelDeposit({ ref });            // must still be unpaid
await client.listDeposits({ page?, limit?, status? });

Payment links are valid 24 hours; a maximum of 2 unpaid deposits may exist at once.

Error Handling

import { KirimiClient, KirimiApiError, KirimiTimeoutError, KirimiError } from '@kiriminow/sdk';

try {
  await client.sendMessage({ deviceId, receiver, message });
} catch (err) {
  if (err instanceof KirimiApiError) {
    console.error(`API error ${err.statusCode}:`, err.message);
    console.error('Response:', err.responseData);
  } else if (err instanceof KirimiTimeoutError) {
    console.error('Request timed out');
  } else if (err instanceof KirimiError) {
    console.error('SDK error:', err.message);
  }
}

| Status | Meaning | |---|---| | 400 | invalid or missing params | | 401 | wrong user_code / secret | | 402 | insufficient balance (sendOtpV2 whatsapp) | | 403 | feature not in package / subscription inactive | | 404 | not found | | 429 | rate limited | | 500 | server error | | 502 | number undeliverable | | 503 | provider outage |

Usage in Cloudflare Workers / Edge Runtimes

import { KirimiClient } from '@kiriminow/sdk';

export default {
  async fetch(request: Request): Promise<Response> {
    const client = new KirimiClient({
      userCode: env.KIRIMI_USER_CODE,
      secret: env.KIRIMI_SECRET,
    });

    const resp = await client.sendMessage({
      deviceId: env.KIRIMI_DEVICE_ID,
      receiver: '628111222333',
      message: 'Hello from the edge!',
    });

    return Response.json(resp);
  },
};

Testing / Mocking

Inject fetch in the constructor to mock:

const client = new KirimiClient({
  userCode: 'test',
  secret: 'test',
  fetch: async (url, init) => {
    return new Response(JSON.stringify({ success: true, data: null }), {
      status: 200,
      headers: { 'Content-Type': 'application/json' },
    });
  },
});

License

MIT — see LICENSE.