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

@mutwave/express

v0.4.0

Published

Mutwave server-side SDK for Node.js/Express backends — call Mutwave's API with your app's secret key (create and manage customer accounts, move money, sell airtime/data/bills, reply to support tickets) and verify incoming Mutwave webhooks.

Readme

@mutwave/express

Mutwave's server-side SDK for Node.js/Express backends. Where @mutwave/js / @mutwave/nextjs / @mutwave/react-native let your customers talk to Mutwave from a browser or mobile app, @mutwave/express is for your own backend talking to Mutwave directly — with your app's secret key, no cookies, no logged-in session, no origin allowlist.

Use it to:

  • Charge a customer through Flutterwave, Paystack, OPay, Monnify, Bachs, or Squad — one method call, any provider, no separate integration per provider.
  • Create and look up customer accounts from your own signup flow.
  • Move money, sell airtime/data/cable/electricity, and read transaction history on a customer's behalf.
  • Reply to (or resolve, or hand over) a support ticket as your app, when "developer-first support" is turned on for your app.
  • Verify that a webhook claiming to be from Mutwave actually is.

If you're building a browser or mobile frontend for your own end users instead, you want @mutwave/nextjs, @mutwave/js, or @mutwave/react-native — those authenticate a specific logged-in customer with a publishable key; this package authenticates your backend with your secret key and can act on any of your app's customers.

Install

npm install @mutwave/express

express is an optional peer dependency — only needed if you use mutwaveWebhookMiddleware. Everything else in this package works with any Node HTTP framework, or none at all. Requires Node 18+ (uses the built-in fetch).

Quick start

import { mutwave } from "@mutwave/express";

const client = mutwave.client({
  secretKey: process.env.MUTWAVE_SECRET_KEY!, // sk_live_... or sk_test_...
});

const { account, user } = await client.account.create({
  firstName: "Ada",
  lastName: "Obi",
  email: "[email protected]",
  phoneNumber: "08012345678",
  bvn: "22123456789",
  password: "a-strong-password",
  // passcode is optional — leave it out and the account starts on a known
  // placeholder that blocks money movement until the customer sets a real
  // one themselves (client.account.requestPasscodeChange()/
  // confirmPasscodeChange(), called from a customer-facing SDK — only the
  // account owner can change their own passcode).
});
// Doesn't hand back a usable session — Mutwave emails the customer a
// one-time code. Relay their reply from your own UI to get a real JWT:
const { token } = await client.account.verifyOtp("[email protected]", otp);

⚠️ Secret key — server only

secretKey is your app's full-access key (Settings → API Keys → Live/Test Secret key). It can move money and read every customer's data. Never ship it to a browser, a mobile app, or any client you don't control — mutwave.client({ secretKey }) should only ever run on a server you own. If you need customer-facing code to talk to Mutwave, use the publishable key with @mutwave/js/@mutwave/nextjs/@mutwave/react-native instead.

MutwaveClient keeps secretKey in a real private class field (#secretKey), not just a TypeScript private — it will never show up in Object.keys(client), console.log(client), or JSON.stringify(client), so an accidental debug log of the client object can't leak it. Handling the raw string itself (env vars, logs, error messages you write yourself) is still on you — never log process.env.MUTWAVE_SECRET_KEY.

Whichever key you pass — live or test — decides every call's mode automatically. There's no separate testMode flag to set.

Modules

client.payment

One integration for every provider — swap provider and the request/response shape, and the webhook confirmation, stay identical.

const { checkoutUrl, reference } = await client.payment.charge("flutterwave", {
  amount: 5000,
  firstName: "Ada",
  lastName: "Obi",
  email: "[email protected]",
  phoneNumber: "08012345678", // optional
  reference: "order-1029",    // optional — your own order/idempotency reference
  description: "Order #1029", // optional
  redirectUrl: "https://yourapp.com/thank-you", // optional — defaults to your app's webhook callback URL
});

// Redirect the customer to complete payment:
res.redirect(checkoutUrl);

provider is one of "flutterwave" | "paystack" | "opay" | "monnify" | "bachs" | "squad". No pre-existing Mutwave Account/userId is needed — this charges anyone, and the money lands directly in your app's own withdrawable balance, never a Mutwave wallet.

This call only returns a checkout link — it is not proof of payment. Confirm the charge server-side by listening for the transaction.success event on your webhook (see Webhooks below); event.data.reference matches the reference this call returned.

client.account

// Create a customer (User + Mutwave Account) under your app. passcode is
// optional — see "Quick start" above.
const { user, account } = await client.account.create({
  firstName, lastName, email, phoneNumber, bvn, password,
});
// No usable session yet — Mutwave emailed a one-time code. Relay it from
// your own UI:
const { token, requiresPasswordSetup } = await client.account.verifyOtp(email, otp);
// `token` is a 7-day JWT for this customer — hand it to your own
// frontend/mobile app if it needs to call Mutwave directly afterward,
// without asking them to log in again. Every subsequent login re-sends a
// code too (not just this first one) — verifyOtp() is what redeems those
// as well.

const { user, account } = await client.account.getByEmail("[email protected]");
const { balance } = await client.account.getBalance(account._id);

await client.account.changePasscode({ email, currentPasscode, newPasscode });

// A direct internal-ledger debit — requires the customer's own JWT + passcode,
// not just your secret key, since it's moving one specific customer's money.
await client.account.debit(token, { passcode: "1234", amount: 500 });

// Relay a message into a customer's (unticketed) support chat log.
await client.account.sendChatMessage(account._id, { message: "...", senderName: "Support Bot" });

// Set a customer's own push-notification-sound override for one event —
// the same override you can otherwise only set from Settings → Customers
// on the dashboard, done here server-side (e.g. for your own "pick a
// sound" UI). `sound` is a data: URI or hosted URL; build it however you
// like, e.g. from a file your own backend already has:
const sound = "data:audio/mpeg;base64," + fs.readFileSync(path).toString("base64");
await client.account.setSound(account._id, "transactionSuccess", sound);

await client.account.clearSound(account._id, "transactionSuccess"); // fall back to the app-wide default again

client.transaction

const all = await client.transaction.list(user._id);
const { transactions, hasMore } = await client.transaction.list(user._id, { skip: 0, count: 20 });

await client.transaction.transferWithinApp({ accountId, accountNumber, amount, passcode });
await client.transaction.transferToBank({ accountId, accountNumber, bankCode, amount, passcode, narration });

const banks = await client.transaction.listBanks();
const { account_name } = await client.transaction.resolveExternalAccount({ accountNumber, bankCode });
const { data } = await client.transaction.resolveInternalAccount(accountNumber); // one of your own app's accounts

client.vtu

const billers = await client.vtu.getBillers("CABLE");
const items = await client.vtu.getItems({ billerCode, category: "CABLE" });

await client.vtu.buyAirtime({ accountId, billerCode, itemCode, customer: "08012345678", amount: 500 });
await client.vtu.buyData({ accountId, billerCode, itemCode, customer: "08012345678" });
await client.vtu.buyCable({ accountId, billerCode, itemCode, customer: "1234567890" });
await client.vtu.buyElectricity({ accountId, billerCode, itemCode, customer: "01234567890" });

client.ticket

Only relevant if your app has developer-first support turned on (Settings → Customer Support). While a ticket is in "developer" status, Mutwave's AI and staff can't see it at all — your backend owns the conversation until you resolve it or hand it off.

await client.ticket.reply(ticketId, "Thanks for reaching out — looking into it now.");
await client.ticket.handOver(ticketId); // → a Mutwave support agent takes over
await client.ticket.resolve(ticketId);  // → you close it out yourself

client.app

const { app, features } = await client.app.getPublicConfig();

client.auth

const { keyType } = await client.auth.whoAmI(); // "production" | "testing"

Cheap way to confirm your secret key is valid — useful in a startup or health-check route; throws MutwaveApiError if the key is wrong or revoked.

Webhooks

Mutwave signs every webhook with HMAC-SHA256 over the raw JSON body, using your app's webhook secret (Settings → Webhooks), sent as the x-mutwave-signature header. Always verify this before trusting a webhook payload — without it, anyone who knows your webhook URL could POST fake events (e.g. a fake transaction.success).

Easiest: mutwaveWebhookMiddleware

import express from "express";
import { mutwave, mutwaveWebhookMiddleware } from "@mutwave/express";

const client = mutwave.client({ secretKey: process.env.MUTWAVE_SECRET_KEY! });
const app = express();

// Mount this BEFORE any express.json() on the same path — the middleware
// needs the untouched raw request bytes to verify the signature against.
app.post(
  "/webhooks/mutwave",
  mutwaveWebhookMiddleware(process.env.MUTWAVE_WEBHOOK_SECRET!),
  (req, res) => {
    const event = req.mutwaveEvent!; // typed, verified — safe to trust

    switch (event.event) {
      case "transaction.success":
        // credit the user, update your own records
        break;
      case "ticket.customerMessage":
        // a customer replied on a ticket your backend owns
        client.ticket.reply(event.data.ticketId as string, "On it!");
        break;
    }

    res.sendStatus(200);
  }
);

If your app already has a global express.json() mounted, this route needs to be exempted from it (register this route before the global parser, or scope the global parser to only the paths that need it) — express.json() consumes the raw body stream, and by the time it's done there's nothing left for the signature check to verify against.

Manual: constructEvent / isValidSignature

For any other framework (Fastify, raw http, a serverless function), get the raw request body as a Buffer or string — not JSON.stringify() of an already-parsed object, which can differ in key order/whitespace from what was actually signed — and verify it yourself:

import { constructEvent, MutwaveWebhookSignatureError } from "@mutwave/express";

// rawBody must be the exact bytes Mutwave sent, captured before JSON parsing.
try {
  const event = constructEvent(rawBody, req.headers["x-mutwave-signature"], webhookSecret);
  // event.event, event.data, event.timestamp
} catch (err) {
  if (err instanceof MutwaveWebhookSignatureError) {
    return res.status(401).send("Invalid signature");
  }
  throw err;
}

// Or just a boolean, if you don't need the parsed event:
import { isValidSignature } from "@mutwave/express";
isValidSignature(rawBody, req.headers["x-mutwave-signature"], webhookSecret); // boolean

Event types

event.event is typed as MutwaveWebhookEventType, covering every event Mutwave can send: transaction.success, transaction.failed, account.created, account.suspended, account.reactivated, ticket.created, ticket.assigned, ticket.escalated, ticket.resolved, ticket.customerMessage, ticket.handedBack, withdrawal.simulated, loan.applied, loan.approved, loan.rejected, loan.disbursed, loan.repaymentSucceeded, loan.repaymentFailed, loan.overdue, loan.defaulted, loan.repaid, card.created, card.transactionApproved, card.transactionDeclined, card.frozen, card.unfrozen, card.terminated, card.physicalOrdered, card.physicalShipped. Turn on the specific events you want under Settings → Webhooks — Mutwave only sends what you've subscribed to.

Errors

Every failed API call throws MutwaveApiError (.message, .status). Signature verification failures throw MutwaveWebhookSignatureError separately, so you can tell "Mutwave rejected the request" apart from "this webhook wasn't actually from Mutwave":

import { MutwaveApiError } from "@mutwave/express";

try {
  await client.account.create({ ... });
} catch (err) {
  if (err instanceof MutwaveApiError) {
    console.error(`Mutwave API error (${err.status}): ${err.message}`);
  }
  throw err;
}

Example

See example/ for a complete, runnable Express server: create a customer, list banks, and verify an incoming webhook end to end.

npm run build            # from the repo root — builds dist/ so the example can require it
cd example && npm install
MUTWAVE_SECRET_KEY=sk_test_... MUTWAVE_WEBHOOK_SECRET=whsec_... npm start