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-sdkSetup
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?):
| Error | When |
|---|---|
JamiValidationError | 400 — the message names the offending field |
JamiAuthError | 401 — invalid/revoked token; .hint is set when the token was minted for the org's other environment (re-issue it) |
JamiRateLimitError | 429 — checkout is limited to 20 req / 5 min / IP |
JamiError | everything else (404, 502, timeouts) |
JamiSignatureError | webhook 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
waitForCheckoutis the only loop (always bounded bytimeoutMs/AbortSignal). - Endpoint-by-endpoint details live in the API Reference.
