Withdrawals
Pay out your organization's earnings to Telebirr, M-Pesa or CBE Birr from code, and track each payout until it lands.
A withdrawal moves your organization's earnings to a phone wallet. This page is the end-to-end guide: what you need, how much arrives, how to send one safely, and how to know it landed. For every field and status code, see API: Withdrawals.
Before you start
Withdrawals from the API need all of:
- a production organization and a
jamidev_live_…token (sandbox earnings are simulated and can't be withdrawn); - Developer Mode on for the organization's owner;
- approved KYC for the owner.
If one is missing, createWithdrawal returns 400 with a message naming it.
How much arrives
The JamiDev fee and any government tax are cut from the amount you withdraw, not added
on top. You withdraw amount; the account receives netAmount.
| You withdraw | Fee (7%) | Tax | Arrives |
|---|---|---|---|
| 100.00 ETB | 7.00 ETB | 0.00 ETB | 93.00 ETB |
| 5,000.00 ETB | 350.00 ETB | 0.00 ETB | 4,650.00 ETB |
| 20,000.00 ETB | 1,400.00 ETB | 0.00 ETB | 18,600.00 ETB |
The table uses the default rates. Jami can set a different fee on your account, and the tax
rate is a Jami platform setting. The server applies the real rates and returns them on every
withdrawal as feeAmount, taxAmount and netAmount. Each part is rounded to the nearest
santim.
Preview offline with the SDK. This makes no request:
import { Jami } from 'jami-sdk';
Jami.computeWithdrawalQuote(500000);
// { amount: 500000, feeAmount: 35000, taxAmount: 0, netAmount: 465000, feeRate: 0.07, taxRate: 0 }Limits
| Limit | Value |
|---|---|
| Minimum per withdrawal | 100.00 ETB (10000 santim) |
| Paid out automatically | Up to 10,000.00 ETB. Larger amounts wait for manual review. |
| Per organization, rolling 24 hours | 100,000.00 ETB. Past it, 429. |
| Maximum per withdrawal | Your organization's withdrawable earnings |
Send a withdrawal
Check what you can withdraw
const jami = new Jami({ token: process.env.JAMI_TOKEN! }); // jamidev_live_…
const { balanceMinor, eligible } = await jami.getBalance();balanceMinor is what this organization has earned and not yet withdrawn. eligible is
false, with a balance of 0, when the organization isn't in production.
Create it with an idempotency key
const withdrawal = await jami.createWithdrawal({
amount: 500000, // 5,000.00 ETB, gross
gateway: 'telebirr', // 'telebirr' | 'mpesa' | 'cbe'
account: '251911223344', // destination phone
idempotencyKey: `payout-${payoutId}`, // one key per payout you intend to make
});
withdrawal.status; // 'processing' or 'completed' (≤ 10,000 ETB), else 'pending'
withdrawal.netAmount; // 465000Generate the key once for each payout you mean to make and store it with your own
record. If the request times out, call again with the same key: you get the original
withdrawal back (200 instead of 201) and nothing is paid twice.
Wait for the result
Listen for the withdrawal.paid and withdrawal.failed webhooks, or
poll:
const latest = await jami.getWithdrawal(withdrawal._id);Statuses
| Status | Meaning |
|---|---|
pending | Over 10,000 ETB; waiting for manual review. |
reviewing | Under review. |
approved | Approved; waiting to be paid out. |
processing | The payout is being sent. |
completed | Paid. netAmount reached the account. withdrawal.paid is sent. |
rejected | Rejected, or the payout failed. The amount returns to your balance. withdrawal.failed is sent. |
A payout that fails at the gateway can go back to approved for a retry, which also sends
withdrawal.failed. If the retry succeeds you'll then get withdrawal.paid.
Errors
| Status | When | What to do |
|---|---|---|
400 | Bad amount or gateway, invalid phone, not a production organization, Developer Mode off, KYC not approved, or more than the organization's earnings | Read the message; fix the input or the account setup. |
402 | Your earnings cover it, but the owner's Jami wallet doesn't right now, for example because money was spent elsewhere or another payout is still on hold | Withdraw less, or wait for the pending payout to settle. |
429 | The 100,000 ETB rolling-24h cap for this organization | Wait, or withdraw less. |
Why 402 can happen after a healthy balance
Earnings from JamiDev, tips and your shop all sit in one Jami wallet. getBalance reports
what this organization earned, which caps what its token may withdraw. The money itself
leaves the shared wallet, so if it was drawn down elsewhere, a withdrawal within the
organization's earnings can still fail with 402.
In the SDK, 400 raises JamiValidationError, 429 raises JamiRateLimitError, and
402 raises JamiError with status: 402.
Next
- Field-by-field reference: API: Withdrawals.
- Let an agent quote and send payouts with confirmation: Build with AI.
