npm.io
0.1.0 • Published 3d ago

jami-sdk

Licence
MIT
Version
0.1.0
Deps
0
Size
95 kB
Vulns
0
Weekly
0

jami

Official TypeScript SDK for JamiDevJami'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 (30% government + 5% Jami usage fee) are deducted at withdrawal, never added at checkout.

Install

npm install jami

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';

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: 'buyer@example.com', 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.

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';

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
        break;
      case 'order.created':
      case 'benefit.granted':
      case 'checkout.session.expired':
        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. 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.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: support@jami.bio.

MIT Jami

Keywords