JamiDev

SDK

The official jami package for Node, edge runtimes and browsers.

The official TypeScript SDK wraps the whole REST API in a typed, zero-dependency client. It runs anywhere fetch exists: Node 18+, Cloudflare Workers, Vercel Edge, Deno, Bun, and modern browsers.

Install

npm install jami-sdk

Setup

Create a token in Developer → API Tokens and construct the client:

import { Jami } from 'jami-sdk';

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

jami.environment; // 'test' (jamidev_test_…) or 'live' (jamidev_live_…)

The token shape is validated in the constructor. Amounts everywhere are integer ETB minor units: 10000 = 100.00 ETB.

Create a checkout

const checkout = await jami.createCheckout({
	productId: 'p_GltCYuOxxov', // the product id from your dashboard (the raw ObjectId also works)
	customer: { email: 'buyer@example.com', phone: '251912345678' },
	gateway: 'telebirr', // 'telebirr' | 'mpesa' | 'cbe'
	// amount: 15000,    // pay-what-you-want products only
});

if ('orderId' in checkout) {
	// Free product — completed instantly.
} else if (checkout.mode === 'redirect') {
	// Send the buyer to the hosted payment page.
	location.href = checkout.checkoutUrl!;
} else {
	// mode === 'direct' — the buyer confirms on their phone; poll below.
}

Wait for the payment

waitForCheckout polls the status endpoint (which actively reconciles with the payment provider server-side) until the session completes, expires, or the timeout elapses — it never spins forever:

const status = await jami.waitForCheckout(checkout.sessionId, {
	intervalMs: 2000,   // default
	timeoutMs: 300_000, // default 5 minutes; rejects with code 'poll_timeout'
});

if (status.status === 'completed') {
	console.log('paid!', status.orderId);
}

One-shot polling is jami.getCheckoutStatus(sessionId).

List orders

const { items, total } = await jami.listOrders({ status: 'paid', page: 1, limit: 20 });
// items[].productId / customerId come back populated ({ title } / { email, name })

Withdrawals

Pay out your balance — the 5% usage fee is cut from the amount you withdraw, so you receive the net. Requires a production org with Developer Mode + approved KYC; minimum 1,000.00 ETB (100000 santim). Amounts ≤ 10,000 ETB auto-pay; larger go to manual review. Preview the cut offline with Jami.computeWithdrawalQuote:

const quote = Jami.computeWithdrawalQuote(100000);
// → { amount: 100000, feeAmount: 5000, taxAmount: 0, netAmount: 95000, feeRate: 0.05, taxRate: 0 }

const { balanceMinor } = await jami.getBalance();

const withdrawal = await jami.createWithdrawal({
	amount: 100000,            // gross santim (1,000 ETB); the fee is cut from this
	gateway: 'telebirr',       // 'telebirr' | 'mpesa' | 'cbe'
	account: '251911223344',
	idempotencyKey: 'payout-2026-08-12-001', // optional; makes retries safe
});
withdrawal.status;    // auto-paid → 'processing' | 'completed'; held → 'pending'
withdrawal.netAmount; // 95000 — what reaches the account

await jami.getWithdrawal(withdrawal._id);
await jami.listWithdrawals({ status: 'completed', limit: 20 });

See the Withdrawals API for the full contract.

Verify webhooks

Timing-safe HMAC verification with a replay guard — pass the raw request body:

import { JamiSignatureError } from 'jami-sdk';

const event = await jami.webhooks.verify({
	payload: rawBody,                       // string, exactly as received
	signature: req.headers['x-jamidev-signature'],
	secret: process.env.JAMI_WEBHOOK_SECRET!, // per-subscription secret
});

switch (event.type) {
	case 'order.completed': /* fulfill */ break;
	case 'benefit.granted': /* deliver the perk */ break;
}

Throws JamiSignatureError on any mismatch, malformed header, stale timestamp (default tolerance 300 s), or unknown event type — treat those deliveries as untrusted.

Errors

Every failure is a typed error extending JamiError (status, code, requestId?):

ErrorWhen
JamiValidationError400 — the message names the offending field
JamiAuthError401 — invalid/revoked token; .hint is set when the token was minted for the org's other environment (re-issue it)
JamiRateLimitError429 — checkout is limited to 20 req / 5 min / IP
JamiErroreverything else (404, 502, timeouts)
JamiSignatureErrorwebhook verification failed
import { JamiAuthError } from 'jami-sdk';

try {
	await jami.listOrders();
} catch (err) {
	if (err instanceof JamiAuthError && err.hint) {
		// The org switched environment — create a fresh token in the dashboard.
	}
}

Notes

  • The SDK adds no automatic retries — the status endpoint self-reconciles, and waitForCheckout is the only loop (always bounded by timeoutMs / AbortSignal).
  • Endpoint-by-endpoint details live in the API Reference.

On this page