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

jami-sdk

v0.4.0

Published

Official TypeScript SDK for JamiDev — Jami's payments & product API for Ethiopian developers. Server: create checkouts, poll status, list orders, withdraw, verify webhooks, and drive a user's Jami wallet over OAuth. Browser: a zero-secret storefront that

Readme

jami-sdk

Official TypeScript SDK for JamiDev — Jami's payments & product API for developers in Ethiopia. Sell digital products and get paid in ETB through Telebirr, M-Pesa, and CBE Birr, with a hosted checkout, webhooks, and a free sandbox.

  • Zero dependencies — a thin fetch wrapper that works in Node 18+, edge runtimes (Cloudflare Workers, Vercel Edge), and modern browsers.
  • Fully typed: discriminated results, typed webhook events, typed errors.
  • Money is always integer ETB minor units (santim): 10000 = 100.00 ETB. Buyers pay exactly the listed price — taxes (government tax + 7% JamiDev usage fee) are deducted at withdrawal, never added at checkout.

Install

npm install jami-sdk

Quickstart

Create an API token in the JamiDev dashboard (Developer → API Tokens). Sandbox organizations issue jamidev_test_… tokens, production organizations jamidev_live_….

import { Jami } from 'jami-sdk';

const jami = new Jami({ token: process.env.JAMI_TOKEN! });

// 1. Create a checkout for one of your products
const checkout = await jami.createCheckout({
  productId: '665f1c2e8b1a2f0012ab34cd', // the product's id (not the public p_… token)
  customer: { email: '[email protected]', phone: '251911223344' },
  gateway: 'telebirr', // 'telebirr' | 'mpesa' | 'cbe'
});

if ('orderId' in checkout) {
  // Free product — completed instantly.
  console.log('order', checkout.orderId);
} else if (checkout.mode === 'redirect') {
  // Send the buyer to the hosted payment page.
  console.log('redirect to', checkout.checkoutUrl);
} else {
  // mode === 'direct': the buyer confirms on their phone — poll until done.
  const status = await jami.waitForCheckout(checkout.sessionId);
  console.log(status.status, status.orderId); // 'completed', 'ord_…'
}

// 2. List orders
const { items, total } = await jami.listOrders({ status: 'paid', limit: 20 });
console.log(`${total} paid orders`, items[0]?.amount); // amount in santim

Sandbox testing

Use a jamidev_test_ token from a sandbox organization. Magic phone numbers: 251900000001 → instant success, 251900000002 → instant failure. No real money moves.

Withdrawals

Pay out your organization's earnings to a Telebirr, M-Pesa, or CBE account. The 7% JamiDev usage fee is cut from the amount you withdraw (not added on top) — you withdraw amount, the fee (and any tax) is deducted, and the destination receives the net. Preview the cut offline with Jami.computeWithdrawalQuote; the server resolves your org's real rate and is authoritative.

getBalance reports your org's own earnings — that's the ceiling on what you can withdraw. The funds themselves leave the owner's unified Jami wallet, so a withdrawal can still fail with 402 if that wallet was drawn down elsewhere even when the balance looked sufficient.

Requires a production org with Developer Mode on and approved KYC; minimum withdrawal is 100.00 ETB (10000 santim). Withdrawals of 10,000 ETB or less are paid out automatically; larger amounts are held for manual review. A rolling-24h cap of 100,000 ETB per org returns 429 past the limit.

import { Jami } from 'jami-sdk';

const jami = new Jami({ token: process.env.JAMI_TOKEN! });

// 1. Preview what reaches the destination (pure — no request).
const quote = Jami.computeWithdrawalQuote(10_000); // withdraw 100.00 ETB (the minimum)
// → { amount: 10000, feeAmount: 700, taxAmount: 0, netAmount: 9300, feeRate: 0.07, taxRate: 0 }

// 2. Make sure your org earnings cover the gross amount.
const { balanceMinor } = await jami.getBalance(); // santim — your org's earnings
if (balanceMinor < 10_000) throw new Error('insufficient balance');

// 3. Withdraw. Pass an idempotencyKey so a retry never double-withdraws.
const withdrawal = await jami.createWithdrawal({
  amount: 10_000,                   // gross santim; 7% is cut from this
  gateway: 'telebirr',              // 'telebirr' | 'mpesa' | 'cbe'
  account: '251911223344',          // the payout phone
  idempotencyKey: 'payout-2026-08-12-001',
});
console.log(withdrawal.status, withdrawal.netAmount); // 'completed', 9300

Auto-paid withdrawals (≤ 10,000 ETB) come back processing/completed immediately. Larger ones land as pending, are reviewed (reviewing/approved/rejected), then paid out (processing → completed). Track completion via the withdrawal.paid / withdrawal.failed webhooks (below), or poll jami.getWithdrawal(id).

The 7% fee is money JamiDev keeps; santim rounding is round-half-up (round(amount * 0.07)). Checkout prices are unaffected — buyers still pay exactly the listed price; fees only apply at withdrawal.

Sign in with Jami + Wallet

JamiWallet is a second, separate surface from the org client above. Here an end user signs in through Jami's OAuth consent screen and grants your app scoped access to their own Jami wallet — read the balance (wallet:read) and send money to other Jami users (wallet:send). It's a confidential client: the secret and the user's tokens stay on your server.

Accessing a user's wallet needs two things, and the SDK enforces both:

  1. The user's OAuth consent — the resource-bound access token from the flow below.
  2. Your JamiDev API key (apiKey) — the same jamidev_live_… / jamidev_test_… key as the org client. It travels on every wallet call so Jami knows which product is acting; without it the call is rejected. This is how wallet access is attributed to a registered JamiDev organization.
import { JamiWallet } from 'jami-sdk';

const wallet = new JamiWallet({
  apiKey: process.env.JAMI_API_KEY!,        // identifies your JamiDev product
  clientId: process.env.JAMI_CLIENT_ID!,    // your confidential OAuth client
  clientSecret: process.env.JAMI_CLIENT_SECRET!,
  redirectUri: 'https://yourapp.com/callback',
});

// 1. Redirect the user to sign in. Persist state + codeVerifier in their session.
const auth = await wallet.createAuthorization();
redirect(auth.url);

// 2. On the callback: reject unless the returned `state` matches what you stored,
//    then exchange the code for a resource-bound token set.
const tokens = await wallet.exchangeCode({ code, codeVerifier });

// 3. Use the wallet.
const balance = await wallet.getBalance(tokens.accessToken); // { availableMinor, totalMinor, currency }
const { transfer, deduped } = await wallet.send(tokens.accessToken, {
  recipientHandle: 'abebe',   // or recipientUserId
  amountMinor: 5000,          // 50.00 ETB, in santim
  idempotencyKey: crypto.randomUUID(),
});

The SDK always requests resource=<issuer> so Jami mints a verifiable, resource-bound access token the wallet endpoints accept (an unbound token is rejected) — you never construct that string. The sender must have approved KYC to send. Errors raise JamiWalletError (with the server's code: kyc_required, insufficient_funds, daily_cap_exceeded, recipient_not_found, …) or JamiAuthError (401).

Storefront (browser)

JamiStorefront lets a brand sell their published JamiDev products from their own website, settling money into their Jami wallet — with no secret in the browser and no server in the middle for card checkout. It ships two ways: import it in your frontend bundle, or drop in the standalone <script> build that exposes a JamiStore global.

  • Hosted checkout (card): redirects the buyer to Jami's hosted checkout page for a public product token (p_…) or checkout-link token (cl_…). Fully browser-direct.
  • Pay with Jami wallet: redirects to a confidential server you host (walletPay) that runs the buyer's Sign in with Jami + wallet:send. Needed because wallet:send is a confidential-client scope — a browser can't hold that secret.
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/jami-storefront.global.js"></script>
<div id="shop"></div>
<script>
  const { JamiStorefront, mountStorefront } = window.JamiStore;

  const store = new JamiStorefront({
    // Omit walletPay to offer hosted card checkout only.
    walletPay: { endpoint: 'https://brands.jami.bio/pay/wallet', recipient: 'mybrand' }
  });

  // Built-in dependency-free grid…
  mountStorefront('#shop', {
    storefront: store,
    products: [{ token: 'p_AbC123', title: 'Leather Bag', priceEtb: 2500, image: '…' }]
  });

  // …or wire your own UI:
  store.checkout('p_AbC123');                              // → hosted checkout
  store.payWithWallet({ amountMinor: 250000, reference: 'p_AbC123' }); // → wallet server
</script>

Or in a bundler: import { JamiStorefront, mountStorefront } from 'jami-sdk/storefront'.

Products are yours to supply (token + display fields) — the storefront never needs your API token or a product-list call. Get a product's public p_… token from the JamiDev dashboard; publish the product first (only published products resolve at checkout).

Wallet-pay endpoint contract. When walletPay is set, the wallet button redirects to GET {endpoint}?recipient=<handle>&amountMinor=<santim>&reference=<token>&return_url=<url>. That server (a confidential OAuth client, e.g. your Brand Hub) signs the buyer in with Jami, performs wallet:send to recipient, records the order, and redirects to return_url. Card checkout needs no such server.

Webhooks

JamiDev signs every delivery with X-JamiDev-Signature: t=<unix>,v1=<hmac> — HMAC-SHA256 of "{t}.{rawBody}" with your subscription secret. Verify with the raw, unparsed request body:

import { Jami, JamiSignatureError } from 'jami-sdk';

const jami = new Jami({ token: process.env.JAMI_TOKEN! });

app.post('/webhooks/jami', express.raw({ type: 'application/json' }), async (req, res) => {
  try {
    const event = await jami.webhooks.verify({
      payload: req.body.toString('utf8'),        // RAW body — never re-serialize
      signature: req.header('X-JamiDev-Signature')!,
      secret: process.env.JAMI_WEBHOOK_SECRET!,  // per-subscription secret
    });

    switch (event.type) {
      case 'order.completed':
        // fulfill: event.data holds the order payload; event.livemode is false for sandbox.
        // event.data.sessionId is the same id createCheckout returned — use it to
        // correlate the event back to the checkout you started.
        break;
      case 'withdrawal.paid':
        // a payout settled: event.data holds the withdrawal payload
        break;
      case 'order.created':
      case 'benefit.granted':
      case 'checkout.session.expired':
      case 'withdrawal.failed':
        break;
    }
    res.sendStatus(200);
  } catch (err) {
    if (err instanceof JamiSignatureError) return res.sendStatus(400);
    throw err;
  }
});

Events: order.created · order.completed · benefit.granted · checkout.session.expired · withdrawal.paid · withdrawal.failed. Every order/benefit/session event carries data.sessionId — the same id createCheckout returned — so you can correlate a delivery back to its checkout without storing an orderId mapping yourself. Failed deliveries retry automatically (1m → 5m → 30m → 2h → 12h, 5 attempts).

API

| Method | Description | | --- | --- | | new Jami({ token, baseUrl?, fetch? }) | Token is shape-validated; environment is derived from its prefix. | | jami.createCheckout(params) | POST /checkout. Paid → { sessionId, checkoutUrl, mode }; free → { sessionId, orderId }. | | jami.getCheckoutStatus(sessionId) | GET /checkout/{id}. Server-side reconciliation means polling always resolves. | | jami.waitForCheckout(sessionId, { intervalMs?, timeoutMs?, signal? }) | Polls until completed/expired/order; rejects with code: 'poll_timeout' after timeoutMs (default 5 min). | | jami.listOrders({ page?, limit?, status? }) | GET /orders — newest first, limit ≤ 100. | | Jami.computeWithdrawalQuote(amount, rates?) | Pure/offline. Returns { amount, feeAmount, taxAmount, netAmount, feeRate, taxRate } — the 7% fee cut from amount. | | jami.getBalance() | GET /balance. { balanceMinor, eligible, currency } in santim — your org's own earnings. | | jami.createWithdrawal({ amount, gateway, account, idempotencyKey?, metadata? }) | POST /withdrawals. Debits amount (capped at org earnings), cuts the 7% fee; you receive netAmount. 402 if wallet funds fall short, 429 past the 100k ETB/24h org cap. | | jami.getWithdrawal(id) | GET /withdrawals/{id}. | | jami.listWithdrawals({ page?, limit?, status? }) | GET /withdrawals — newest first, limit ≤ 100. | | jami.webhooks.verify({ payload, signature, secret, toleranceSec? }) | Timing-safe HMAC verify + replay guard (default tolerance 300 s). Returns the typed event. |

Errors

All requests throw typed errors extending JamiError (status, code, requestId?):

  • JamiValidationError (400) — bad request body.
  • JamiAuthError (401) — invalid/revoked token. When the token was minted for the org's other environment, .hint tells you to re-issue it.
  • JamiRateLimitError (429) — checkout is limited to 20 requests / 5 min per IP.
  • JamiSignatureError — webhook verification failed; treat the delivery as untrusted.

Notes

  • Checkout sessions expire after 30 minutes.
  • productId is the product's database id, visible in the dashboard — not the public p_… checkout token.
  • The SDK never retries automatically; waitForCheckout is the only loop and it always terminates.

Docs & support

Full docs: the JamiDev developer documentation. Questions: [email protected].

MIT © Jami