DGatewayDocsBank Disbursements
UGX · Iotec rail · all 26 banks

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.
For per-recipient amounts under ~UGX 500,000 to individual end-users, mobile money (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=iotec

Response

{
  "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.

BankSWIFT / BIC
ABC Capital Bank LimitedABCFUGKAXXX
Absa Bank Uganda LimitedBARCUGKXXXX
Bank of Africa Uganda LtdAFRIUGKAXXX
Bank of Baroda (Uganda) LimitedBARBUGKAXXX
Bank of India (Uganda) LtdBKIDUGKAXXX
Bank of UgandaUGBAUGKAXXX
Cairo Bank UgandaCAIEUGKAXXX
Centenary Rural Development Bank LimitedCERBUGKAXXX
Citibank Uganda LimitedCITIUGKAXXX
DFCU Bank LimitedDFCUUGKAXXX
Diamond Trust Bank Uganda LimitedDTKEUGKAXXX
Ecobank UgandaECOCUGKAXXX
Equity Bank Uganda LtdEQBLUGKAXXX
Exim Bank (Uganda) LimitedEXTNUGKAXXX
Finance Trust Bank LtdFTBLUGKAXXX
Guaranty Trust Bank (Uganda) LtdGTBIUGKAXXX
Housing Finance Bank LtdHFINUGKAXXX
I and M Bank (Uganda) LimitedORINUGKAXXX
KCB Bank Uganda LimitedKCBLUGKAXXX
NCBA Bank Uganda LimitedCBAFUGKAXXX
Opportunity BankOPUGUGKAXXX
PostBank Uganda LimitedUGPBUGKAXXX
Stanbic Bank Uganda LimitedSBICUGK0XXX
Standard Chartered Bank Uganda LimitedSCBLUGKAXXX
Top Finance Bank LimitedTOPFUGKAXXX
Tropical Bank LtdTROAUGKAXXX
United Bank for Africa (Uganda) LtdUNAFUGKAXXX
This list is correct at the time of writing. The authoritative source is always 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:

FieldSourceExample
bank_idThe provider_bank_id from /v1/payments/banks5a3b8c2d-9f2a-...
bank_codeThe 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

FieldRequiredNotes
amountYesWhole UGX. Decimals are accepted but rounded.
currencyYesUGX for bank disbursements (Iotec rail is UGX only).
methodYes"bank" or "bank_transfer". Defaults to "mobile_money" if omitted.
bank_idOne ofThe provider_bank_id from /v1/payments/banks.
bank_codeOne ofSWIFT/BIC code from the table above.
account_numberYesRecipient bank account number, digits only.
account_nameYesAccount holder name exactly as registered with the bank.
bank_transfer_typeNoOne of EFT (default), RTGS, INTERNAL, SWIFT.
descriptionNoUp to 500 chars. Shown on the recipient bank statement where space allows.
providerNoDefaults 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

TypeWhen to useSettlement
EFT (default)Routine payouts — payroll, supplier payments, marketplace settlements.Same-day for transfers booked before the cut-off, otherwise T+1.
RTGSLarge or time-critical transfers (typically > UGX 20M).Real-time during banking hours; pricier per transfer.
INTERNALRecipient banks with Stanbic — Iotec's settlement bank. Cheapest, fastest.Within minutes.
SWIFTCross-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.

TierFee (UGX)
Up to 250,0005,000
Up to 500,0006,000
Up to 1,000,0009,000
Up to 2,000,00013,500
Up to 50,000,00016,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"
  }
}
Verify the 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

CodeWhat it meansFix
ACCOUNT_NUMBER_REQUIREDYou sent method=bank without an account number.Add account_number to the payload.
ACCOUNT_NAME_REQUIREDMissing account_name.Add the registered holder name.
BANK_REQUIREDNeither bank_id nor bank_code was provided.Pass one (see the bank table).
INSUFFICIENT_BALANCEWallet doesn't cover amount + provider_fee + platform_fee.Top up via collections, or lower the amount.
APP_NOT_VERIFIEDKYC or Bank Transfer Approval is missing.Complete verification first.
RATE_LIMIT_EXCEEDEDYou hit the 2-per-24h withdrawal cap.Wait for the window to reset, or request a higher cap.
PROVIDER_LINE_DOWNIotec 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.