jami
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
fetchwrapper 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,.hinttells 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.
productIdis the product's database id, visible in the dashboard — not the publicp_…checkout token.- The SDK never retries automatically;
waitForCheckoutis the only loop and it always terminates.
Docs & support
Full docs: the JamiDev developer documentation. Questions: support@jami.bio.
MIT Jami