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 org's earnings — the 7% usage fee is cut from the amount you withdraw, so you receive the net. getBalance is your org's own earnings (the withdrawal ceiling); funds leave the owner's unified wallet. Requires a production org with Developer Mode + approved KYC; minimum 100.00 ETB (10000 santim), and a rolling-24h cap of 100,000 ETB per org. Amounts ≤ 10,000 ETB auto-pay; larger go to manual review. Preview the cut offline with Jami.computeWithdrawalQuote:

const quote = Jami.computeWithdrawalQuote(10000);
// → { amount: 10000, feeAmount: 700, taxAmount: 0, netAmount: 9300, feeRate: 0.07, taxRate: 0 }

const { balanceMinor } = await jami.getBalance(); // your org's earnings

const withdrawal = await jami.createWithdrawal({
	amount: 10000,             // gross santim (100 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; // 9300 — what reaches the account

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

See the Withdrawals API for the full contract.

Sign in with Jami + Wallet

JamiWallet is a separate surface from the Jami org client above. Here an end user signs in through Jami's OAuth consent screen and grants your app scoped access to their own Jami wallet — read the balance (wallet:read) and send money to other Jami users (wallet:send). It's a confidential OAuth client, so the secret and the user's tokens stay on your server. See the Wallet page for the API contract and the OAuth/OIDC connection, and Sign in with Jami for the provider, registration, and consent details.

Accessing a user's wallet needs two things, and the SDK sends both on every wallet call:

  1. The user's OAuth consent — the resource-bound access token from the flow below.
  2. Your JamiDev API key (apiKey) — the same jamidev_live_… / jamidev_test_… key as the org client. It travels in the X-JamiDev-Key header so Jami knows which product is acting; without it the call is rejected.
import { JamiWallet } from 'jami-sdk';

const wallet = new JamiWallet({
	apiKey: process.env.JAMI_API_KEY!,        // identifies your JamiDev product
	clientId: process.env.JAMI_CLIENT_ID!,    // your confidential OAuth client
	clientSecret: process.env.JAMI_CLIENT_SECRET!,
	redirectUri: 'https://yourapp.com/callback',
});

1. Send the user to sign in

createAuthorization builds the authorize URL and generates a fresh PKCE verifier, state, and nonce. Persist state and codeVerifier in the user's session — you need them on the callback.

const auth = await wallet.createAuthorization();
// store auth.state + auth.codeVerifier in the session, then:
redirect(auth.url);

2. Handle the callback

Reject the request unless the returned state matches what you stored, then exchange the code for a resource-bound token set:

const tokens = await wallet.exchangeCode({ code, codeVerifier }); // { accessToken, refreshToken, expiresAt, idToken, … }
const me = wallet.parseIdentity(tokens.idToken);                  // { sub, name, handle, … } for display

Refresh when the access token nears expiry (tokens.expiresAt is epoch ms, computed ~30s early); the resource binding is preserved:

if (tokens.expiresAt && Date.now() >= tokens.expiresAt) {
	tokens = await wallet.refresh(tokens.refreshToken!); // rotates — persist the new set
}

3. Read and send

Amounts are integer santim. send is idempotent on idempotencyKey — a reused key returns the original transfer with deduped: true and moves no money.

const balance = await wallet.getBalance(tokens.accessToken);
// → { currency: 'ETB', availableMinor: 125000, totalMinor: 125000 }

const { transfer, deduped } = await wallet.send(tokens.accessToken, {
	recipientHandle: 'abebe',        // or recipientUserId — provide exactly one
	amountMinor: 5000,               // 50.00 ETB
	idempotencyKey: crypto.randomUUID(),
});
// transfer.status === 'completed'; deduped === false on a fresh send

The SDK always requests resource=<issuer> so Jami mints a verifiable, resource-bound token the wallet endpoints accept (an unbound token is rejected) — you never construct that string. The sender must have approved KYC to send. Business failures raise JamiWalletError with the server's code (kyc_required, insufficient_funds, daily_cap_exceeded, recipient_not_found, self_transfer, …); a bad/expired token or a missing JamiDev key raises JamiAuthError (401).

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