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

@snippe/sdk

v1.0.0

Published

Official JavaScript/TypeScript SDK for the Snippe payment platform (Tanzania mobile money, cards, and payouts).

Downloads

712

Readme

@snippe/sdk

Official JavaScript / TypeScript SDK for Snippe — the Tanzania payment platform (mobile money, cards, and payouts).

  • API version: 2026-01-25
  • Works in Node.js 18+ (uses built-in fetch)
  • Fully typed, ESM + CJS, zero runtime dependencies

Migrating from v0.2.x? The v1.0 release is breaking — channel-namespaced methods (payments.mobile.create instead of payments.create({ payment_type: "mobile" })) and camelCase fields throughout. See MIGRATION.md.

Install

npm install @snippe/sdk

Charge a customer in 5 minutes

The full happy path for mobile money — the most common flow.

1. Initialise the client

import { Snippe } from "@snippe/sdk";

const snippe = new Snippe({
  apiKey: process.env.SNIPPE_API_KEY!, // snp_...
  webhookUrl: "https://example.com/webhooks/snippe",
});

2. Create the payment

const payment = await snippe.payments.mobile.create({
  amount: 500, // TZS, integer (minimum 500)
  phoneNumber: "0781000000", // any TZ format works — auto-normalised
  customer: {
    firstName: "Jane",
    lastName: "Doe",
    email: "[email protected]",
  },
});

console.log(payment.reference, payment.status); // "9015c155-…", "pending"

The customer's phone now has a USSD prompt. They enter their PIN.

3. Learn the result — webhooks

Listen for payment.completed / payment.failed events at webhookUrl. The SDK ships verifyWebhook for HMAC verification — see the Webhooks section.

If you genuinely can't receive webhooks (no public URL, scheduled reconciliation job), see the polling example for the payments.get-in-a-loop pattern.

The SDK auto-generates a short Idempotency-Key (≤30 chars) for every POST. Pass your own via the request options:

await snippe.payments.mobile.create(input, { idempotencyKey: "ord-12345-t1" });

Resources

Payments

Each payment channel has its own typed method — no string discriminators:

await snippe.payments.mobile.create({ amount, phoneNumber, customer });
await snippe.payments.card.create({ amount, phoneNumber, redirectUrl, cancelUrl, customer });

// Cross-channel reads
await snippe.payments.get(reference);
await snippe.payments.list({ limit: 20, status: "completed" });
await snippe.payments.search({ phoneNumber: "255781000000" });
await snippe.payments.retriggerPush(reference);
await snippe.payments.balance();

Card payments require the full billing address (address, city, state, postcode, country); the type system enforces this at compile time.

Hosted checkout

When you want Snippe to render the payment UI for you (multi-method picker, mobile-optimised page, share via SMS/WhatsApp):

const session = await snippe.checkout.create({
  amount: 50_000,
  allowedMethods: ["mobile_money", "card"],
  customer: { name: "Jane Doe", phone: "+255712345678" },
  description: "Order #12345",
  expiresIn: 3600,
});

// Share `paymentLinkUrl` via SMS/WhatsApp, or redirect to `checkoutUrl`
console.log(session.paymentLinkUrl);

Custom-amount (donation / tip jar) sessions:

await snippe.checkout.create({
  allowCustomAmount: true,
  minAmount: 1_000,
  maxAmount: 500_000,
  description: "Donation",
});

Other operations: get, list, cancel.

Disbursements (payouts)

Same channel-namespaced shape. Always preflight with fee() + balance() before sending — the amount + fee debits immediately:

const amount = 50_000;
const { totalAmount } = await snippe.payouts.mobile.fee({ amount });
const { available } = await snippe.payments.balance();

if (available.value < totalAmount) {
  throw new Error("insufficient balance");
}

await snippe.payouts.mobile.send({
  amount,
  phoneNumber: "0781000000",
  recipientName: "Employee Name",
  narration: "Salary January 2026",
});

Bank transfers:

await snippe.payouts.bank.send({
  amount: 100_000,
  bankCode: "CRDB",
  accountNumber: "0150000000000",
  accountName: "Vendor Ltd",
});

The SDK validates client-side that payment amounts ≥ 500 TZS and payouts ≥ 5000 TZS before hitting the wire — saves a round trip and an idempotency-key burn.

Webhooks

Snippe signs every webhook with HMAC-SHA256 over {timestamp}.{raw_body}. The SDK ships a verifyWebhook helper that handles signature verification, constant-time comparison, and replay protection (rejects events older than 5 minutes).

Critical: use middleware that gives you the raw request body, not a parsed one. Re-serialising JSON will break the signature.

import express from "express";
import { verifyWebhook, SnippeWebhookVerificationError } from "@snippe/sdk";

const app = express();

app.post(
  "/webhooks/snippe",
  express.raw({ type: "application/json" }),
  (req, res) => {
    try {
      const event = verifyWebhook({
        rawBody: req.body, // Buffer, untouched
        signature: req.header("X-Webhook-Signature"),
        timestamp: req.header("X-Webhook-Timestamp"),
        signingKey: process.env.SNIPPE_WEBHOOK_SECRET!,
      });

      // Deduplicate on event.id (Snippe may deliver the same event twice)
      switch (event.type) {
        case "payment.completed":
          // event.data.amount is { value, currency }, NOT a plain integer
          console.log("paid", event.data.reference, event.data.amount.value);
          break;
        case "payment.failed":
          break;
        case "payout.completed":
          break;
      }

      // Respond 2xx within 30s — process heavy work async
      res.status(200).send("OK");
    } catch (err) {
      if (err instanceof SnippeWebhookVerificationError) {
        res.status(400).send("Invalid signature");
      } else {
        res.status(500).send("Internal error");
      }
    }
  },
);

Webhook payloads keep snake_case field names (event.data.failure_reason, event.data.completed_at) to match the API exactly — useful when correlating with logged raw bodies. Everything else in the SDK is camelCase.

Errors

All API failures throw SnippeError (or a subclass). Branch on errorCode, not message:

import {
  SnippeError,
  SnippeRateLimitError,
  SnippeValidationError,
} from "@snippe/sdk";

try {
  await snippe.payments.mobile.create(input);
} catch (err) {
  if (err instanceof SnippeRateLimitError) {
    const wait = err.rateLimit?.resetSeconds ?? 60;
    // back off for `wait` seconds...
  } else if (err instanceof SnippeValidationError) {
    // fix the request, don't retry
  } else if (err instanceof SnippeError && err.retryable) {
    // 5xx / PAY_001 / network — retry with the same idempotency key
  } else {
    throw err;
  }
}

PAY_001 gotcha: the single most common cause is an Idempotency-Key longer than 30 characters. The SDK auto-generates 24-char keys, and it proactively rejects oversized user-supplied keys before they hit the wire.

Configuration

| Option | Type | Default | Description | | ------------- | ---------------------------- | ------------------------ | ------------------------------------------------------- | | apiKey | string | — | Required. snp_... from the Snippe Dashboard. | | environment | "sandbox" \| "production" | "production" | Target environment. | | baseUrl | string | https://api.snippe.sh | Override the API base URL. | | timeoutMs | number | 30000 | Per-request timeout. | | webhookUrl | string | — | Default webhookUrl applied to create / send calls. | | fetch | typeof fetch | globalThis.fetch | Custom fetch (e.g. undici, node-fetch). |

Utilities

import { normalisePhone, generateIdempotencyKey } from "@snippe/sdk";

normalisePhone("0781000000"); // "255781000000"
generateIdempotencyKey();      // "snp-…" (≤30 chars)

Examples

Full runnable scenarios — mobile money, card, hosted checkout, donation sessions, mobile and bank payouts, an Express webhook handler, a retry loop, and polling-based reconciliation — live in the examples/ directory on GitHub.

Migration

Upgrading from v0.2.x? See the full migration guide. v1.0 is breaking but the upgrade is mostly mechanical (rename fields, route through channel sub-resources).

Contributing

See CONTRIBUTING.md for local development, testing, and release workflow.

License

MIT — see LICENSE.