Bank disbursements API
Send money from your DGateway wallet directly to any Ugandan bank account with a single API call. You don't talk to banks individually — DGateway sits in front of Iotec, which is connected to the Uganda central settlement system, so the same endpoint reaches all 26 licensed commercial banks.
The endpoint is the same one you already use for mobile-money payouts — POST /v1/payments/disburse — you just switch the method field to "bank" and add the destination bank + account.
When to use bank disbursement
- Payroll — salaries to staff bank accounts on a fixed day each month.
- B2B settlements — paying suppliers, vendors, contractors with formal banking relationships.
- Large payouts — single transfers above the typical mobile-money sweet spot (~UGX 5M).
- Recipients who don't have mobile money — or who prefer bank receipts for accounting.
- Marketplaces — paying out seller earnings to their nominated bank account.
method="mobile_money") is typically cheaper and settles in seconds. Bank rails are better for larger amounts or formal payee accounts.Prerequisites
- A live DGateway API key (
dgw_live_*) — test keys can simulate the flow with sandbox responses. - Connection fee paid for your app (the one-time UGX 150,000).
- KYC approved and Bank Transfer Approval granted from the Verification page.
- Sufficient UGX balance in your app wallet (collected funds, minus reserved amounts).
List supported banks
Before you build a UI that lets users pick a bank, fetch the live list. Cache it on your side — the list changes very rarely, and the cached DGateway copy already absorbs the upstream Iotec call.
GET /v1/payments/banks
Host: dgatewayapi.desispay.com
X-API-Key: dgw_live_...
# Optional: filter to a single provider
GET /v1/payments/banks?provider=iotecResponse
{
"data": [
{
"id": 12,
"provider_slug": "iotec",
"provider_bank_id": "5a3b8c2d-...",
"name": "Stanbic Bank Uganda Limited",
"code": "SBICUGK0XXX",
"is_active": true,
"synced_at": "2026-05-12T08:14:21Z"
},
...
]
}On the disburse call you pass back either provider_bank_id (as bank_id) or code (as bank_code) — pick whichever is more convenient for your data model. Both identify the same bank to Iotec.
Full bank table (26 banks)
Canonical SWIFT/BIC codes for every Ugandan bank reachable via the Iotec rail. The code column is what you pass as bank_code.
| Bank | SWIFT / BIC |
|---|---|
| ABC Capital Bank Limited | ABCFUGKAXXX |
| Absa Bank Uganda Limited | BARCUGKXXXX |
| Bank of Africa Uganda Ltd | AFRIUGKAXXX |
| Bank of Baroda (Uganda) Limited | BARBUGKAXXX |
| Bank of India (Uganda) Ltd | BKIDUGKAXXX |
| Bank of Uganda | UGBAUGKAXXX |
| Cairo Bank Uganda | CAIEUGKAXXX |
| Centenary Rural Development Bank Limited | CERBUGKAXXX |
| Citibank Uganda Limited | CITIUGKAXXX |
| DFCU Bank Limited | DFCUUGKAXXX |
| Diamond Trust Bank Uganda Limited | DTKEUGKAXXX |
| Ecobank Uganda | ECOCUGKAXXX |
| Equity Bank Uganda Ltd | EQBLUGKAXXX |
| Exim Bank (Uganda) Limited | EXTNUGKAXXX |
| Finance Trust Bank Ltd | FTBLUGKAXXX |
| Guaranty Trust Bank (Uganda) Ltd | GTBIUGKAXXX |
| Housing Finance Bank Ltd | HFINUGKAXXX |
| I and M Bank (Uganda) Limited | ORINUGKAXXX |
| KCB Bank Uganda Limited | KCBLUGKAXXX |
| NCBA Bank Uganda Limited | CBAFUGKAXXX |
| Opportunity Bank | OPUGUGKAXXX |
| PostBank Uganda Limited | UGPBUGKAXXX |
| Stanbic Bank Uganda Limited | SBICUGK0XXX |
| Standard Chartered Bank Uganda Limited | SCBLUGKAXXX |
| Top Finance Bank Limited | TOPFUGKAXXX |
| Tropical Bank Ltd | TROAUGKAXXX |
| United Bank for Africa (Uganda) Ltd | UNAFUGKAXXX |
GET /v1/payments/banks — if Iotec adds a new bank we'll pick it up on the next sync.How to identify a bank on disburse
You can pass either of these on the disburse request — DGateway accepts either:
| Field | Source | Example |
|---|---|---|
bank_id | The provider_bank_id from /v1/payments/banks | 5a3b8c2d-9f2a-... |
bank_code | The SWIFT / BIC code (same value as the code column above) | SBICUGK0XXX |
POST /v1/payments/disburse
The single endpoint that sends the money. For a bank transfer:
POST /v1/payments/disburse
Host: dgatewayapi.desispay.com
Content-Type: application/json
X-API-Key: dgw_live_...
{
"amount": 250000,
"currency": "UGX",
"method": "bank",
"bank_code": "SBICUGK0XXX",
"account_number": "9030012345678",
"account_name": "ACME LIMITED",
"description": "Invoice #INV-2026-001",
"bank_transfer_type": "EFT"
}Request fields
| Field | Required | Notes |
|---|---|---|
amount | Yes | Whole UGX. Decimals are accepted but rounded. |
currency | Yes | UGX for bank disbursements (Iotec rail is UGX only). |
method | Yes | "bank" or "bank_transfer". Defaults to "mobile_money" if omitted. |
bank_id | One of | The provider_bank_id from /v1/payments/banks. |
bank_code | One of | SWIFT/BIC code from the table above. |
account_number | Yes | Recipient bank account number, digits only. |
account_name | Yes | Account holder name exactly as registered with the bank. |
bank_transfer_type | No | One of EFT (default), RTGS, INTERNAL, SWIFT. |
description | No | Up to 500 chars. Shown on the recipient bank statement where space allows. |
provider | No | Defaults to iotec for UGX bank transfers. |
Response shape
{
"data": {
"reference": "DGW-DSB-...",
"provider_reference": "iotec-...",
"status": "pending",
"amount": 250000,
"currency": "UGX",
"method": "bank",
"platform_fee": 5000,
"provider_fee": 5000,
"created_at": "2026-05-16T10:12:01Z"
},
"message": "Disbursement initiated"
}Store the reference — that's the value you'll see again on the webhook and on GET /v1/payments/transactions/:reference when polling.
EFT vs RTGS vs Internal
| Type | When to use | Settlement |
|---|---|---|
| EFT (default) | Routine payouts — payroll, supplier payments, marketplace settlements. | Same-day for transfers booked before the cut-off, otherwise T+1. |
| RTGS | Large or time-critical transfers (typically > UGX 20M). | Real-time during banking hours; pricier per transfer. |
| INTERNAL | Recipient banks with Stanbic — Iotec's settlement bank. Cheapest, fastest. | Within minutes. |
| SWIFT | Cross-border / non-Uganda destinations (rare). | 1–3 business days; international fees apply. |
Iotec transfer fees
Per transfer, passed through from Iotec. Charged on top of the amount.
| Tier | Fee (UGX) |
|---|---|
| Up to 250,000 | 5,000 |
| Up to 500,000 | 6,000 |
| Up to 1,000,000 | 9,000 |
| Up to 2,000,000 | 13,500 |
| Up to 50,000,000 | 16,500 |
DGateway platform fee
DGateway adds a tiered platform fee on top of the provider fee. The fee is shown in the platform_fee field of the disburse response and the transaction record — your wallet is debited by amount + provider_fee + platform_fee in total.
Withdrawal limits
Bank disbursements share the same wallet-level rate limits as mobile-money payouts. By default each app is capped at:
- Max 2 withdrawals per 24h (admin can raise on request).
- 12h minimum between consecutive withdrawals.
- UGX 100,000 per-payout cap, lifted with Big Withdraw approval.
Settlement times
EFT transfers booked before ~14:00 EAT typically settle the same day; later transfers settle next business day. RTGS is real-time during banking hours (08:30–16:30 EAT, Mon–Fri). The status field on the transaction flips to completed once Iotec confirms credit at the receiving bank.
cURL
curl -X POST https://dgatewayapi.desispay.com/v1/payments/disburse \
-H "Content-Type: application/json" \
-H "X-API-Key: $DGATEWAY_API_KEY" \
-d '{
"amount": 250000,
"currency": "UGX",
"method": "bank",
"bank_code": "SBICUGK0XXX",
"account_number": "9030012345678",
"account_name": "ACME LIMITED",
"description": "Invoice INV-2026-001",
"bank_transfer_type": "EFT"
}'Node.js
const res = await fetch("https://dgatewayapi.desispay.com/v1/payments/disburse", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-API-Key": process.env.DGATEWAY_API_KEY,
},
body: JSON.stringify({
amount: 250_000,
currency: "UGX",
method: "bank",
bank_code: "SBICUGK0XXX",
account_number: "9030012345678",
account_name: "ACME LIMITED",
description: "Invoice INV-2026-001",
bank_transfer_type: "EFT",
}),
});
const { data, error } = await res.json();
if (!res.ok) throw new Error(error?.message ?? "Disburse failed");
console.log("Reference:", data.reference);Python
import os, requests
resp = requests.post(
"https://dgatewayapi.desispay.com/v1/payments/disburse",
headers={"X-API-Key": os.environ["DGATEWAY_API_KEY"]},
json={
"amount": 250000,
"currency": "UGX",
"method": "bank",
"bank_code": "SBICUGK0XXX",
"account_number": "9030012345678",
"account_name": "ACME LIMITED",
"description": "Invoice INV-2026-001",
"bank_transfer_type": "EFT",
},
timeout=30,
)
resp.raise_for_status()
print("Reference:", resp.json()["data"]["reference"])PHP
<?php
$ch = curl_init("https://dgatewayapi.desispay.com/v1/payments/disburse");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"X-API-Key: " . getenv("DGATEWAY_API_KEY"),
],
CURLOPT_POSTFIELDS => json_encode([
"amount" => 250000,
"currency" => "UGX",
"method" => "bank",
"bank_code" => "SBICUGK0XXX",
"account_number" => "9030012345678",
"account_name" => "ACME LIMITED",
"description" => "Invoice INV-2026-001",
"bank_transfer_type" => "EFT",
]),
]);
$resp = json_decode(curl_exec($ch), true);
echo "Reference: " . $resp["data"]["reference"];Go
payload, _ := json.Marshal(map[string]interface{}{
"amount": 250000,
"currency": "UGX",
"method": "bank",
"bank_code": "SBICUGK0XXX",
"account_number": "9030012345678",
"account_name": "ACME LIMITED",
"description": "Invoice INV-2026-001",
"bank_transfer_type": "EFT",
})
req, _ := http.NewRequest("POST",
"https://dgatewayapi.desispay.com/v1/payments/disburse",
bytes.NewReader(payload))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-API-Key", os.Getenv("DGATEWAY_API_KEY"))
resp, err := http.DefaultClient.Do(req)
if err != nil { log.Fatal(err) }
defer resp.Body.Close()Webhook payload
When the transfer status changes (typically pending → completed or failed) DGateway POSTs a transaction.updated event to your configured webhook URL.
{
"event": "transaction.updated",
"data": {
"reference": "DGW-DSB-...",
"provider": "iotec",
"direction": "disburse",
"method": "bank",
"status": "completed",
"amount": 250000,
"currency": "UGX",
"bank_code": "SBICUGK0XXX",
"account_number": "9030012345678",
"account_name": "ACME LIMITED",
"completed_at": "2026-05-16T11:04:22Z"
}
}X-DGateway-Signature header against your webhook secret with HMAC-SHA256 — see the webhook verification snippet in the FAQ.Status polling
If you miss a webhook (or for one-off checks) you can always query a transaction by its reference:
GET /v1/payments/transactions/DGW-DSB-...
X-API-Key: dgw_live_...For bank transfers, status typically lands within a few minutes for INTERNAL and RTGS, and same-day or T+1 for EFT depending on cut-off.
Common errors
| Code | What it means | Fix |
|---|---|---|
ACCOUNT_NUMBER_REQUIRED | You sent method=bank without an account number. | Add account_number to the payload. |
ACCOUNT_NAME_REQUIRED | Missing account_name. | Add the registered holder name. |
BANK_REQUIRED | Neither bank_id nor bank_code was provided. | Pass one (see the bank table). |
INSUFFICIENT_BALANCE | Wallet doesn't cover amount + provider_fee + platform_fee. | Top up via collections, or lower the amount. |
APP_NOT_VERIFIED | KYC or Bank Transfer Approval is missing. | Complete verification first. |
RATE_LIMIT_EXCEEDED | You hit the 2-per-24h withdrawal cap. | Wait for the window to reset, or request a higher cap. |
PROVIDER_LINE_DOWN | Iotec is reporting the bank rail as degraded. | Retry shortly; the dashboard's Provider Health page shows live status. |
Ready to disburse?
Generate an API key from the dashboard, request Bank Transfer Approval, and you're live.