Send money
The Send Money API lets you pay out from your IntaSend wallet to M-Pesa, bank accounts, M-Pesa tills and paybills, cross-border mobile money, and other IntaSend accounts.
A payout request is a batch (a "file") containing one or more transactions. Every
transaction in a batch uses the same provider, currency and country.
The flow
initiate ──► (preview) ──► approve ──► balance check ──► sending ──► complete
│ ▲
└── requires_approval: "NO" skips the manual approve step
- Initiate a batch with
POST /api/v1/send-money/initiate/. - Approve it with
POST /api/v1/send-money/approve/. You can skip this step withrequires_approval: "NO". - Some bank routes also need an OTP confirmation.
- Track the batch with status, or receive the result on your
callback_urlor a webhook.
Initiating requires an API key (or user) with the Level-2 or Administrator role. Approving requires Level-3 or Administrator. With this split, one key can prepare payouts and a different key approves them.
1. Initiate
- cURL
- Python
- Node.js
curl -X POST https://api.intasend.com/api/v1/send-money/initiate/ \
-H "Authorization: Bearer $INTASEND_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"currency": "KES",
"provider": "MPESA-B2C",
"requires_approval": "YES",
"callback_url": "https://example.com/intasend/payouts",
"batch_reference": "payroll-2026-09",
"transactions": [
{"name": "Jane Doe", "account": "254712345678", "amount": "150", "narrative": "Salary"}
]
}'
import os, requests
resp = requests.post(
"https://api.intasend.com/api/v1/send-money/initiate/",
headers={"Authorization": f"Bearer {os.environ['INTASEND_SECRET_KEY']}"},
json={
"currency": "KES",
"provider": "MPESA-B2C",
"requires_approval": "YES",
"transactions": [
{"name": "Jane Doe", "account": "254712345678", "amount": "150", "narrative": "Salary"}
],
},
)
batch = resp.json()
print(batch["tracking_id"], batch["status"])
const res = await fetch('https://api.intasend.com/api/v1/send-money/initiate/', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.INTASEND_SECRET_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
currency: 'KES',
provider: 'MPESA-B2C',
requires_approval: 'YES',
transactions: [{name: 'Jane Doe', account: '254712345678', amount: '150', narrative: 'Salary'}],
}),
});
const batch = await res.json();
Request fields
| Field | Required | Description |
|---|---|---|
currency | Yes | Wallet currency to pay from. One of KES, USD, EUR, GBP, GHS, NGN, UGX, TZS, XAF, XOF, RWF, CDF. |
provider | Yes | Payout rail. See Providers. |
country | No | ISO-2 destination country. Defaults to KE. It is required for cross-border payouts; see Countries. |
wallet_id | No | Wallet to debit. It must match currency. Defaults to your settlement wallet for that currency. A working wallet must have can_disburse enabled. |
requires_approval | No | "YES" (default) or "NO". With "NO", the batch is approved automatically. |
callback_url | No | URL that receives the final batch result. See Callbacks. |
batch_reference | No | Your own reference for the batch (max 70 characters). |
transactions | Yes | List of transactions (below). |
Transaction fields
| Field | Required | Description |
|---|---|---|
account | Yes | The destination: phone number, bank account, till, paybill or meter number. Digits only: no +, dashes or letters. Max 24 characters. |
amount | Yes | Amount to send, up to 2 decimal places. |
name | Depends | Beneficiary name. Required for bank transfers (RTGS/EFT, NG and UG banks). Letters, numbers, spaces, -, _ and : only. |
narrative | No | Payment description or remarks. Same character rules as name. |
bank_code | Depends | Required for bank payouts. See bank codes. |
operator_code | Depends | Mobile network operator. Required in Tanzania (VODACOM, AIRTEL, TIGO, HALOTEL). |
account_type | Depends | TillNumber or PayBill. Required for MPESA-B2B. |
account_reference | Depends | Paybill account number. Required when account_type is PayBill. Max 20 characters. |
notify_phone | Depends | Phone number that receives the KPLC token. Required for KPLC. |
category_name | No | Name of a payout category you've set up, for reporting. |
idempotency_key | No | Unique key per transaction. A repeat request with the same key is rejected, so you can retry safely. |
Set a unique idempotency_key on every transaction (for example your internal payout ID).
If a network error makes you unsure whether a request went through, resend it: duplicates
are rejected with duplicate transaction detected.
Response
{
"file_id": "Y8Q2KLM",
"tracking_id": "8b0a3d0c-6f4e-4a8e-9a53-2f3c1b1f6e0d",
"batch_reference": "payroll-2026-09",
"status": "Preview and approve",
"status_code": "BP103",
"nonce": "a1b2c3",
"wallet": {"wallet_id": "XZ3LKQ", "currency": "KES", "current_balance": "12500.00", "available_balance": "12500.00"},
"transactions": [
{
"transaction_id": "KQ9PLXR",
"status": "Pending",
"status_code": "TP101",
"request_reference_id": "…",
"name": "Jane Doe",
"account": "254712345678",
"amount": "150.00",
"narrative": "Salary"
}
],
"charge_estimate": "10.00",
"total_amount_estimate": "160.00",
"total_amount": "150.00",
"transactions_count": 1
}
Keep the tracking_id: you need it to approve the batch and check its status.
2. Approve
Send back the tracking_id, the nonce and the transactions array from the initiate response:
curl -X POST https://api.intasend.com/api/v1/send-money/approve/ \
-H "Authorization: Bearer $INTASEND_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"tracking_id": "8b0a3d0c-6f4e-4a8e-9a53-2f3c1b1f6e0d",
"nonce": "a1b2c3",
"transactions": [ /* the transactions array from initiate */ ]
}'
The batch moves to CHECKING-ACCOUNT-BALANCE (BP104). IntaSend then debits your wallet and sends each transaction.
You can only approve a batch while it is in PREVIEW-AND-APPROVE (BP103).
3. Check status
curl -X POST https://api.intasend.com/api/v1/send-money/status/ \
-H "Authorization: Bearer $INTASEND_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{"tracking_id": "8b0a3d0c-6f4e-4a8e-9a53-2f3c1b1f6e0d"}'
The response adds actual_charges, paid_amount and failed_amount. Each transaction also gets
provider, provider_reference (for example the M-Pesa receipt number), provider_account_name and charge.
See Status codes for every batch and transaction state.
Cancel
You can cancel a batch before it starts sending, while it is in BP103, BP104 or BF105 and
every transaction is still pending:
curl -X POST https://api.intasend.com/api/v1/send-money/cancel/ \
-H "Authorization: Bearer $INTASEND_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{"file_id": "Y8Q2KLM"}'
Callbacks
If you set callback_url, IntaSend POSTs the full batch result (the same body as the status
response) once the batch completes, fails on low balance or is cancelled.
The callback is sent once and is not retried, so don't rely on it alone. Also subscribe to the
send_money_event webhook, which fires on every batch state change, includes
your challenge for verification and can be replayed from the dashboard. Keep polling
status as a fallback.
Refunds of failed transactions
When a batch completes, the amount of any failed transaction, including its charge, is credited back to the wallet you paid from.