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

@jomma/sdk

v1.5.0

Published

Typed client for the Jomma payment verification API.

Readme

@jomma/sdk

Typed client for the Jomma payment verification API.

Thin by design: types, signing, and retries. No business logic — anything that decides what a payment means stays on the server, where there is exactly one copy of it.

pnpm add @jomma/sdk

Requires Node 20+ (uses fetch and node:crypto).


Creating a payment request

import { Jomma } from '@jomma/sdk'

const jomma = new Jomma({
  apiKey: process.env.JOMMA_KEY!,
  baseUrl: process.env.JOMMA_URL,
})

const intent = await jomma.intents.create({
  amount: 120000,                 // poisha — ৳1,200.00
  clientReference: order.id,
  payerMsisdn: order.payerMsisdn, // optional, boosts match confidence
  ttlSeconds: 300,
  idempotencyKey: order.id,       // see below
})

// Show the buyer these three things, and nothing else.
intent.receiving_account.msisdn   // 8801799887766
intent.amount                     // 120000
intent.ref_code                   // K7M2

Always pass idempotencyKey. Your order id is the right value. Without one the SDK generates a fresh key per call, so a retried request allocates a second reference code and a second amount lock — exactly the collision the lock exists to prevent. Replaying the same key within 24 hours returns the original intent.

Polling the pay page

const current = await jomma.intents.get(intent.id)

switch (current.status) {
  case 'matched':  return fulfil(order)
  case 'partial':  return showShortfall(current.shortfall)
  case 'over':     return confirmAndOfferCredit(current.excess)
  case 'expired':  return releaseStock(order)
  case 'open':     return // keep polling, every 2–3s
}

GET /v1/intents/:id is rate limited at 600/min per key — polling is expected.


Verifying a webhook

export async function POST(req: Request) {
  const event = await jomma.webhooks.construct(
    await req.text(),                          // raw body, not a parsed object
    req.headers.get('x-jomma-signature')!,
    process.env.JOMMA_WEBHOOK_SECRET!,
  )
  // throws on bad signature or stale timestamp

  switch (event.type) {
    case 'payment.succeeded': /* … */ break
    case 'payment.reversed':  /* … */ break
  }

  return new Response(null, { status: 200 })
}

Three things this gets right, and that are easy to get wrong by hand:

  • Raw body. JSON.parse then JSON.stringify can reorder keys and change whitespace. The HMAC covers the exact bytes that were sent.
  • Timestamp tolerance. Five minutes, so a captured request cannot be replayed a day later.
  • Constant-time comparison. A naive === leaks the signature one byte at a time.

Delivery is at-least-once. The same event_id may arrive twice; make your handler idempotent. Retries run at 10s, 1m, 5m, 30m, 2h, 6h, 24h.

payment.reversed deserves special handling: it means Jomma previously said money arrived and is now retracting that. Your app must be able to un-fulfil an order.


The manual path

When automatic matching doesn't fire, the buyer types their TrxID:

const result = await jomma.submissions.create({
  intentId: intent.id,
  trxId: 'BK7X2M9QP1',
  senderMsisdn: '8801712345678',
  claimedAmount: 120000,
})

Nine resolutions. Jomma supplies the numbers; you write the words, because different clients word these differently.

| resolution | What happened | What to render | |---|---|---| | exact | Found, everything matches | Confirmed | | sender_mismatch | Found, paid from another number | Confirmed (flagged server-side) | | underpaid | Short — shortfall and top_up included | Ask for the remainder | | overpaid | Over — excess included | Confirm, offer credit or refund | | not_found_recent | Nothing observed, intent under 10 min old | "Still waiting" — keep polling | | not_found_stale | Nothing observed, older | "Check the TrxID", offer help | | already_used | That TrxID paid a different order | Refuse, offer support | | wrong_type | An agent cash-in, not a send-money | "Being reviewed manually" | | expired_intent | The intent already expired | "Restoring your order" |

Rate limited to 5 per intent per hour. The SDK does not retry submissions — burning an attempt on a network blip is the wrong trade.


Checking account health

const accounts = await jomma.accounts.list()
const usable = accounts.filter((a) => a.status === 'active')

if (usable.length === 0) {
  // Do not render a pay page. Nothing is accepting payments.
}

degraded means the account still works but something is wrong — surface a fallback rather than a dead end.


Errors

Every failure is a JommaError carrying the request_id from the response. Keep it: it is what turns "a payment failed yesterday" into one log line.

import { JommaError } from '@jomma/sdk'

try {
  await jomma.intents.create({ /* … */ })
} catch (error) {
  if (error instanceof JommaError) {
    console.error(error.code, error.requestId)
    if (error.code === 'no_healthy_account') return showFallbackInstructions()
    if (error.retryable) return retryLater()
  }
  throw error
}

| code | HTTP | Meaning | |---|---|---| | unauthorized | 401 | Bad or revoked key | | forbidden | 403 | That intent belongs to another app | | not_found | 404 | Unknown intent | | validation_failed | 422 | Includes details | | no_capacity | 409 | No free amount slot — retry shortly | | lock_taken | 409 | Extend failed; the amount was claimed | | duplicate_submission | 409 | TrxID already applied elsewhere | | rate_limited | 429 | Honours Retry-After | | no_healthy_account | 503 | Every account down or disabled |

The client retries GETs and idempotent POSTs on 429, 5xx, and network failures — twice by default, with jittered backoff, honouring Retry-After.


Options

new Jomma({
  apiKey: process.env.JOMMA_KEY!,
  baseUrl: 'https://jomma.example.com',  // default http://localhost:3000
  timeoutMs: 15_000,
  maxRetries: 2,
  fetch: customFetch,                    // for testing
})