What is DGateway?
DGateway is a unified payment gateway that lets you collect and disburse payments across multiple providers through a single API. Instead of integrating each payment provider separately, you make one API call and DGateway routes to your chosen provider (defaults to Iotec if not specified).
DGateway handles provider credentials at the platform level. Your app only needs a DGateway API key — no need to manage individual provider API keys in your frontend or backend code.
| Provider | Type | Currencies |
|---|---|---|
| Iotec | Mobile Money | UGX |
| Relworx | Mobile Money | UGX, KES, TZS, RWF |
| Stripe | Card | USD, EUR, GBP, 135+ currencies |
Architecture Overview
Your Next.js app never calls payment providers directly. All communication goes through DGateway, which holds the provider credentials and handles routing, normalization, and status tracking.
Your App ──(API Key)──► DGateway ──(Platform Credentials)──► Iotec / Stripe / ...
1. DGateway validates your API key2. Routes to the provider you specify (defaults to Iotec if omitted)3. Processes the payment using platform credentials4. Returns a unified response with a reference IDDGATEWAY_API_KEY stays server-side only.provider field explicitly in your requests (e.g. "provider": "iotec"). While DGateway defaults to iotec when omitted, specifying the provider gives you full control over routing and makes your integration predictable — especially when you work with multiple providers or currencies.Prerequisites
- A running DGateway server (default: https://dgatewayapi.desispay.com)
- An app created in the DGateway admin dashboard
- An API key generated for your app
- Node.js 18+ with a Next.js 14+ project (App Router)
- For Stripe: @stripe/react-stripe-js and @stripe/stripe-js packages
Testing & Test Numbers
Important: Test vs Live API Keys
DGateway provides two types of API keys: Test keys (prefixed dgw_test_) and Live keys (prefixed dgw_live_). They behave differently:
| Test Key (dgw_test_) | Live Key (dgw_live_) | |
|---|---|---|
| Phone numbers | Must use test numbers only | Must use real phone numbers only |
| Money movement | No real money is charged | Real money is collected/disbursed |
| Transactions | Marked as test (not withdrawable) | Marked as live (withdrawable) |
Do NOT use test numbers with live keys
Using test phone numbers with a live API key will return an error: TEST_NUMBER_ON_LIVE_KEY. This is enforced to prevent accidental test transactions from appearing as real revenue.
Test Phone Numbers
Use these numbers only with your dgw_test_ API key:
| Number Pattern | Result | Description | Example |
|---|---|---|---|
| 011177777(0-9) | Success | Transaction completes successfully | 0111777771 |
| 011177799(0-9) | Failed | Transaction fails | 0111777991 |
| 011177778(0-9) | Pending | Transaction stays in pending state | 0111777781 |
| 011177779(0-9) | SentToVendor | Transaction is being processed by vendor (e.g. MTN) | 0111777791 |
You can also use the 256 prefix format: 256111777771 is equivalent to 0111777771.
Step 1 — Environment Setup
Create a .env.local file at the root of your Next.js project. These variables are server-side only — they will never be exposed to the browser.
# .env.local
# DGateway APIDGATEWAY_API_URL='https://dgatewayapi.desispay.com'DGATEWAY_API_KEY='dgw_live_your_api_key_here'| Variable | Description |
|---|---|
DGATEWAY_API_URL | URL of the DGateway server |
DGATEWAY_API_KEY | API key from the DGateway admin panel (Apps → API Keys) |
NEXT_PUBLIC_ to these variables. They must remain server-side.Step 2 — Create the DGateway API Client
Create a server-side client that wraps the DGateway REST API. Only import this file in API routes or server components.
// lib/dgateway.ts — server-side only
const API_URL = process.env.DGATEWAY_API_URL || "https://dgatewayapi.desispay.com";const API_KEY = process.env.DGATEWAY_API_KEY || "";
interface CollectParams { amount: number; currency: string; phone_number: string; provider?: string; // "iotec" | "stripe" | "relworx" — defaults to "iotec" description?: string; metadata?: Record<string, unknown>;}
export async function collectPayment(params: CollectParams) { const res = await fetch(`${API_URL}/v1/payments/collect`, { method: "POST", headers: { "Content-Type": "application/json", "X-Api-Key": API_KEY, }, body: JSON.stringify(params), }); return res.json();}
export async function verifyTransaction(reference: string) { const res = await fetch(`${API_URL}/v1/webhooks/verify`, { method: "POST", headers: { "Content-Type": "application/json", "X-Api-Key": API_KEY, }, body: JSON.stringify({ reference }), }); return res.json();}Step 3 — Build the Checkout API Route
Create a Next.js API route that proxies payment requests to DGateway. This keeps your API key on the server and lets you add any business logic (auth, order validation, etc.) before charging the customer.
// app/api/checkout/route.tsimport { NextRequest, NextResponse } from "next/server";import { collectPayment } from "@/lib/dgateway";
export async function POST(request: NextRequest) { const body = await request.json(); const { amount, currency, phone_number, provider, description } = body;
if (!amount || !currency) { return NextResponse.json( { error: { code: "VALIDATION_ERROR", message: "amount and currency are required" } }, { status: 400 } ); }
const result = await collectPayment({ amount, currency, phone_number: phone_number || "0000000000", provider, description, });
if (result.error) { return NextResponse.json(result, { status: 400 }); }
return NextResponse.json(result);}Collect response
// Standard response for all providers{ "data": { "reference": "txn_abc123def456", "provider": "iotec", "status": "pending", "amount": 37500, "currency": "UGX" }}
// Stripe additionally returns:{ "data": { "reference": "txn_abc123def456", "provider": "stripe", "status": "pending", "client_secret": "pi_xxx_secret_yyy", "stripe_publishable_key": "pk_live_..." }}Step 4 — Build the Status Polling Route
Create a second API route for checking payment status. Your frontend will call this every few seconds until the payment completes or fails.
// app/api/checkout/status/route.tsimport { NextRequest, NextResponse } from "next/server";import { verifyTransaction } from "@/lib/dgateway";
export async function POST(request: NextRequest) { const { reference } = await request.json();
if (!reference) { return NextResponse.json( { error: { code: "VALIDATION_ERROR", message: "reference is required" } }, { status: 400 } ); }
const result = await verifyTransaction(reference); return NextResponse.json(result);}The verify endpoint returns the same status fields as the collect response. Poll every 5 seconds and stop when status is completed or failed.
Step 5 — Build the Checkout UI
The checkout page needs to handle two distinct flows depending on the payment method the user picks. Define your payment states upfront:
type PaymentMethod = "iotec" | "stripe" | null;type PaymentStatus = | "idle" // Selecting method / entering info | "creating" // API call in progress | "awaiting_card" // Stripe Elements mounted, waiting for card entry | "processing" // Payment submitted, polling for result | "completed" // Payment succeeded | "failed"; // Payment failedAccepting Mobile Money (Iotec)
Mobile money payments work in three steps: your app sends a collect request with the payer's phone number, DGateway forwards it to Iotec which sends a USSD prompt to the user's phone, then your app polls for status until they confirm.
const createIotecPayment = async (phoneNumber: string) => { setStatus("creating");
const res = await fetch("/api/checkout", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ amount: 37500, // Amount in UGX currency: "UGX", phone_number: phoneNumber, // e.g. "256771234567" provider: "iotec", description: "Order #1234", }), });
const data = await res.json();
if (data.error) { setError(data.error.message); setStatus("idle"); return; }
setReference(data.data.reference); setStatus("processing"); pollStatus(data.data.reference);};Phone number field
Iotec requires the phone number with country code (no + prefix):
<input type="tel" placeholder="e.g. 256771234567" value={phone} onChange={(e) => setPhone(e.target.value)}/><p className="text-xs text-gray-500"> Enter with country code (e.g. 256 for Uganda)</p>Accepting Card Payments (Stripe)
Card payments use Stripe Elements for PCI-compliant card input. DGateway creates the PaymentIntent on its server and returns the client_secret and stripe_publishable_key — you never hardcode the Stripe keys in your app.
// Install Stripe packages first:// pnpm add @stripe/react-stripe-js @stripe/stripe-js
import { loadStripe } from "@stripe/stripe-js";import { Elements, PaymentElement, useStripe, useElements } from "@stripe/react-stripe-js";
const createStripePayment = async () => { setStatus("creating");
const res = await fetch("/api/checkout", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ amount: 10.00, currency: "USD", provider: "stripe", description: "Order #1234", }), });
const data = await res.json(); const { client_secret, stripe_publishable_key, reference } = data.data;
setReference(reference); setClientSecret(client_secret); setStripePromise(loadStripe(stripe_publishable_key)); setStatus("awaiting_card");};Mounting Stripe Elements
{status === "awaiting_card" && clientSecret && stripePromise && ( <Elements stripe={stripePromise} options={{ clientSecret, appearance: { theme: "night", variables: { colorPrimary: "#6c5ce7" } }, }} > <StripeCardForm onSuccess={() => { setStatus("processing"); pollStatus(reference); }} onError={(msg) => setError(msg)} /> </Elements>)}
function StripeCardForm({ onSuccess, onError }) { const stripe = useStripe(); const elements = useElements();
const handleSubmit = async (e) => { e.preventDefault(); const { error, paymentIntent } = await stripe.confirmPayment({ elements, redirect: "if_required", }); if (error) { onError(error.message); return; } if (paymentIntent?.status === "succeeded") onSuccess(); };
return ( <form onSubmit={handleSubmit}> <PaymentElement /> <button type="submit">Pay Now</button> </form> );}Status Polling
Both mobile money and card payments use the same polling mechanism. After a payment is initiated, poll every 5 seconds for up to 5 minutes.
const pollStatus = useCallback((ref: string) => { let attempts = 0; const maxAttempts = 60; // 60 × 5s = 5 minutes
const poll = async () => { if (attempts >= maxAttempts) { setError("Payment verification timed out."); setStatus("failed"); return; } attempts++;
try { const res = await fetch("/api/checkout/status", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ reference: ref }), }); const data = await res.json();
if (data.data?.status === "completed") { setStatus("completed"); return; } if (data.data?.status === "failed") { setError("Payment failed. Please try again."); setStatus("failed"); return; } } catch { // Network error — keep polling }
setTimeout(poll, 5000); };
setTimeout(poll, 3000); // First poll after 3s}, []);| Provider | Start polling when... |
|---|---|
| Iotec | Immediately after collectPayment() returns |
| Stripe | After stripe.confirmPayment() succeeds |
Payment States
Handle each state with a distinct UI to give users clear feedback at every step of the payment flow.
// Successif (status === "completed") return ( <div className="text-center"> <CheckCircle className="text-green-500 mx-auto" /> <h1>Payment Successful!</h1> <p>Reference: {reference}</p> </div>);
// Failedif (status === "failed") return ( <div className="text-center"> <XCircle className="text-red-500 mx-auto" /> <p>{error}</p> <button onClick={() => { setStatus("idle"); setMethod(null); }}> Try Again </button> </div>);
// Processing (waiting for Iotec confirmation or Stripe verification)if (status === "processing") return ( <div className="text-center"> <Loader2 className="animate-spin mx-auto" /> <p> {method === "iotec" ? "Check your phone and confirm the payment prompt." : "Verifying your payment..."} </p> </div>);Currency Conversion
If your products are priced in USD but you accept mobile money in UGX, convert the amount before sending to DGateway. For production, fetch the rate from a live exchange rate API.
const USD_TO_UGX = 3750; // Demo rate — use a real API in production
const totalUSD = 10.00;const totalUGX = Math.round(totalUSD * USD_TO_UGX); // 37,500 UGX
// Send the correct amount per providerbody: JSON.stringify({ amount: provider === "iotec" ? totalUGX : totalUSD, currency: provider === "iotec" ? "UGX" : "USD", provider,})Error Handling
DGateway returns errors in a consistent format across all providers:
{ "error": { "code": "PROVIDER_ERROR", "message": "The Payer field is required" }}| Code | HTTP | Description | Action |
|---|---|---|---|
| VALIDATION_ERROR | 400 | Missing or invalid request fields | Check your request body |
| INVALID_PHONE | 400 | Phone number format invalid. Must be 256XXXXXXXXX or 0XXXXXXXXX | Fix phone format |
| INVALID_CURRENCY | 400 | Currency not supported. Accepted: UGX, KES, TZS, RWF, USD, EUR, GBP | Use valid currency |
| INVALID_PROVIDER | 400 | Provider not supported. Accepted: iotec, relworx, stripe | Use valid provider |
| AUTHENTICATION_ERROR | 401 | Invalid or missing API key | Verify DGATEWAY_API_KEY |
| PROVIDER_ERROR | 400 | The payment provider returned an error | Show message to user |
| PROVIDER_LINE_DOWN | 503 | A specific vendor line (e.g. MTN or Airtel) is down | Retry later or use a different network |
| INSUFFICIENT_BALANCE | 400 | Withdrawal amount exceeds available balance | Reduce withdrawal amount |
| ACCOUNT_HAS_ACTIVITY | 403 | Cannot delete account with transaction history | Contact support |
| NOT_FOUND | 404 | Transaction or resource not found | Check the reference/ID |
| RATE_LIMIT | 429 | Too many requests | Back off and retry |
// Always wrap API calls in try/catchtry { const res = await fetch("/api/checkout", { ... }); const data = await res.json();
if (data.error) { setError(data.error.message || "Payment failed"); setStatus("idle"); return; }} catch { setError("Network error. Please try again."); setStatus("idle");}Transaction Failure Reasons
When a transaction fails, the API returns a failure_reason field in the transaction object. This gives you a human-readable explanation of why the transaction failed, which you can display to your users.
// GET /v1/payments/transactions/:ref{ "data": { "reference": "txn_abc123", "status": "failed", "failure_reason": "The Payer account has insufficient funds", "amount": 5000, "currency": "UGX", "provider_slug": "iotec" }}failure_reason field is only present when the transaction status is failed. Always check this field to provide meaningful error messages to your end users.Provider Health Status
DGateway monitors payment provider lines (MTN, Airtel) with automated health checks. You can check the current status before making requests to avoid timeouts on down lines.
// GET /api/provider-health (no auth required){ "data": [ { "provider_slug": "iotec", "vendor": "mtn", "currency": "UGX", "is_healthy": true, "last_check_at": "2026-03-25T06:00:00Z", "last_error": "" }, { "provider_slug": "iotec", "vendor": "airtel", "currency": "UGX", "is_healthy": false, "last_error": "Service temporarily unavailable", "failing_since": "2026-03-24T16:21:00Z" } ]}When a line is down and you attempt a collect or disburse with a phone number on that network, the API returns a 503 with error code PROVIDER_LINE_DOWN:
{ "error": { "code": "PROVIDER_LINE_DOWN", "message": "iotec AIRTEL line is currently experiencing service issues..." }}Phone Number Validation
All API endpoints that accept phone numbers enforce strict validation. Only these formats are accepted:
| Format | Example | Description |
|---|---|---|
| 256XXXXXXXXX | 256771234567 | International format (12 digits) |
| 0XXXXXXXXX | 0771234567 | Local format (10 digits) |
Phone numbers must contain only digits (no spaces, dashes, or letters). The + prefix is optional and will be stripped. Invalid numbers return INVALID_PHONE error.
Requesting Withdrawals
Once your app has collected payments and built up a balance, you can request withdrawals to cash out your funds via mobile money or bank transfer. Withdrawals are processed instantly — DGateway disburses the funds immediately upon request.
Withdrawal flow
1. Your server sends POST /v1/withdrawals with amount, method & destination (provider defaults to "iotec")2. DGateway immediately disburses funds to the destination via the specified provider3. If disbursement succeeds → status is "completed"4. If disbursement fails → status is "failed" with a note explaining the reasonAdd withdrawal helper to your API client
// lib/dgateway.ts — add to your existing server-side client
interface WithdrawalParams { amount: number; currency: string; provider?: string; // "iotec" or "relworx" — defaults to "iotec" method: "mobile_money" | "bank_transfer"; destination: string; // Phone number or bank account destination_name?: string; // Recipient name bank_name?: string; // Required for bank_transfer note?: string; // Optional note}
export async function requestWithdrawal(params: WithdrawalParams) { const res = await fetch(`${API_URL}/v1/withdrawals`, { method: "POST", headers: { "Content-Type": "application/json", "X-Api-Key": API_KEY, }, body: JSON.stringify(params), }); return res.json();}Create an API route for withdrawals
// app/api/withdraw/route.tsimport { NextRequest, NextResponse } from "next/server";import { requestWithdrawal } from "@/lib/dgateway";
export async function POST(request: NextRequest) { const body = await request.json(); const { amount, currency, provider, method, destination, destination_name, bank_name, note } = body;
if (!amount || !currency || !method || !destination) { return NextResponse.json( { error: { code: "VALIDATION_ERROR", message: "amount, currency, method and destination are required" } }, { status: 400 } ); }
const result = await requestWithdrawal({ amount, currency, provider, // optional — defaults to "iotec" on the server method, destination, destination_name, bank_name, note, });
if (result.error) { return NextResponse.json(result, { status: 400 }); }
return NextResponse.json(result);}Request body fields
| Field | Type | Required | Description |
|---|---|---|---|
| amount | number | Yes | Withdrawal amount (must be > 0) |
| currency | string | Yes | ISO 4217 code (UGX, USD, KES...) |
| provider | string | No | Provider slug — defaults to "iotec" if omitted |
| method | string | Yes | "mobile_money" or "bank_transfer" |
| destination | string | Yes | Phone number (mobile money) or account number (bank) |
| destination_name | string | No | Name of the recipient |
| bank_name | string | No | Bank name (required for bank_transfer) |
| note | string | No | Optional note or reason for the withdrawal |
Example: Mobile Money withdrawal
// POST /v1/withdrawals{ "amount": 500000, "currency": "UGX", "provider": "iotec", "method": "mobile_money", "destination": "256771234567", "destination_name": "John Doe", "note": "Monthly payout"}
// Response (201 Created){ "data": { "id": 12, "app_id": 1, "amount": 500000, "currency": "UGX", "provider_slug": "iotec", "method": "mobile_money", "destination": "256771234567", "destination_name": "John Doe", "status": "completed", "note": "Monthly payout", "created_at": "2025-06-15T10:30:00Z" }, "message": "Withdrawal disbursed successfully"}Example: Bank Transfer withdrawal
{ "amount": 2000000, "currency": "UGX", "provider": "relworx", "method": "bank_transfer", "destination": "0012345678901", "destination_name": "Acme Ltd", "bank_name": "Stanbic Bank Uganda", "note": "Q2 revenue withdrawal"}completed. If it fails, the status is failed with a note explaining the reason.Listing Withdrawals
Fetch a paginated list of your withdrawal requests. You can filter by status and control pagination with query parameters.
// lib/dgateway.ts — add to your existing client
export async function listWithdrawals(params?: { page?: number; per_page?: number; status?: string;}) { const query = new URLSearchParams(); if (params?.page) query.set("page", String(params.page)); if (params?.per_page) query.set("per_page", String(params.per_page)); if (params?.status) query.set("status", params.status);
const res = await fetch(`${API_URL}/v1/withdrawals?${query.toString()}`, { headers: { "X-Api-Key": API_KEY }, }); return res.json();}Query parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
| page | number | 1 | Page number |
| per_page | number | 20 | Results per page (max 100) |
| status | string | all | Filter by status: pending, completed, failed, rejected |
Response
// GET /v1/withdrawals?page=1&per_page=10&status=pending{ "data": [ { "id": 12, "app_id": 1, "amount": 500000, "currency": "UGX", "method": "mobile_money", "destination": "256771234567", "destination_name": "John Doe", "status": "pending", "note": "Monthly payout", "created_at": "2025-06-15T10:30:00Z" } ], "meta": { "page": 1, "per_page": 10, "total": 1, "total_pages": 1 }}Withdrawal statuses
| Status | Description |
|---|---|
| pending | Request submitted, disbursement in progress |
| completed | Funds have been sent to the destination |
| failed | Disbursement failed (check note for reason) |
| rejected | Request was rejected by admin |
Kenya Integration Guide (KES)
This guide covers everything a Kenyan developer needs to integrate DGateway and accept M-Pesa and card payments in Kenya Shillings (KES).
Available Providers for Kenya
| Provider | Currency | Collect | Disburse | Best For |
|---|---|---|---|---|
| Relworx | KES | Yes | Yes | M-Pesa collections & disbursements |
| Stripe | KES | Yes | No | International card payments (collect only) |
provider: "relworx" for M-Pesa or "stripe" for card payments. If omitted, defaults to "iotec" (which does not support KES).1. Environment Setup
# .env.localDGATEWAY_API_URL='https://dgatewayapi.desispay.com'DGATEWAY_API_KEY='dgw_live_your_api_key_here'2. Collect M-Pesa Payment (KES)
Send the phone number in international format with the 254 country code (no + prefix).
// Server-side: lib/dgateway.tsconst API_URL = process.env.DGATEWAY_API_URL!;const API_KEY = process.env.DGATEWAY_API_KEY!;
export async function collectKES(phone: string, amount: number, description?: string) { const res = await fetch(`${API_URL}/v1/payments/collect`, { method: "POST", headers: { "Content-Type": "application/json", "X-Api-Key": API_KEY, }, body: JSON.stringify({ amount, // e.g. 1500 (KES) currency: "KES", phone_number: phone, // e.g. "254712345678" provider: "relworx", // required description, }), }); return res.json();}
// API route: app/api/checkout/route.tsexport async function POST(request: Request) { const { phone, amount, description } = await request.json(); const result = await collectKES(phone, amount, description); return Response.json(result);}3. Frontend — M-Pesa Checkout
const handleMpesaPayment = async () => { setStatus("creating");
const res = await fetch("/api/checkout", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ phone: "254712345678", // Customer's Safaricom number amount: 1500, // KES 1,500 description: "Order #100", }), });
const data = await res.json(); if (data.error) { setError(data.error.message); return; }
// Customer receives STK push on their phone setReference(data.data.reference); setStatus("processing"); pollStatus(data.data.reference); // Poll every 5s until completed/failed};4. Disburse to M-Pesa (KES)
// Send money to a Kenyan M-Pesa numberexport async function disburseKES(phone: string, amount: number) { const res = await fetch(`${API_URL}/v1/payments/disburse`, { method: "POST", headers: { "Content-Type": "application/json", "X-Api-Key": API_KEY, }, body: JSON.stringify({ amount, currency: "KES", phone_number: phone, // e.g. "254712345678" provider: "relworx", // required description: "Payout", }), }); return res.json();}Phone Number Format (Kenya)
| Network | Format | Example |
|---|---|---|
| Safaricom (M-Pesa) | 254XXXXXXXXX | 254712345678 |
| Airtel Kenya | 254XXXXXXXXX | 254733456789 |
254 country code without the + prefix. Remove any leading zero — e.g. 0712345678 becomes 254712345678.Rwanda Integration Guide (RWF)
This guide covers everything a Rwandan developer needs to integrate DGateway and accept Mobile Money payments in Rwandan Franc (RWF).
Available Providers for Rwanda
| Provider | Currency | Collect | Disburse | Best For |
|---|---|---|---|---|
| Relworx | RWF | Yes | Yes | MTN MoMo & Airtel Money collections and payouts |
provider: "relworx". If omitted, defaults to "iotec" (which does not support RWF).1. Environment Setup
# .env.localDGATEWAY_API_URL='https://dgatewayapi.desispay.com'DGATEWAY_API_KEY='dgw_live_your_api_key_here'2. Collect Mobile Money Payment (RWF)
Send the phone number in international format with the 250 country code (no + prefix).
// Server-side: lib/dgateway.tsconst API_URL = process.env.DGATEWAY_API_URL!;const API_KEY = process.env.DGATEWAY_API_KEY!;
export async function collectRWF(phone: string, amount: number, description?: string) { const res = await fetch(`${API_URL}/v1/payments/collect`, { method: "POST", headers: { "Content-Type": "application/json", "X-Api-Key": API_KEY, }, body: JSON.stringify({ amount, // e.g. 5000 (RWF) currency: "RWF", phone_number: phone, // e.g. "250781234567" provider: "relworx", // required description, }), }); return res.json();}
// API route: app/api/checkout/route.tsexport async function POST(request: Request) { const { phone, amount, description } = await request.json(); const result = await collectRWF(phone, amount, description); return Response.json(result);}3. Frontend — Mobile Money Checkout
const handleMomoPayment = async () => { setStatus("creating");
const res = await fetch("/api/checkout", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ phone: "250781234567", // Customer's MTN MoMo number amount: 5000, // RWF 5,000 description: "Order #200", }), });
const data = await res.json(); if (data.error) { setError(data.error.message); return; }
// Customer receives USSD prompt on their phone setReference(data.data.reference); setStatus("processing"); pollStatus(data.data.reference); // Poll every 5s until completed/failed};4. Disburse to Mobile Money (RWF)
// Send money to a Rwandan mobile money numberexport async function disburseRWF(phone: string, amount: number) { const res = await fetch(`${API_URL}/v1/payments/disburse`, { method: "POST", headers: { "Content-Type": "application/json", "X-Api-Key": API_KEY, }, body: JSON.stringify({ amount, currency: "RWF", phone_number: phone, // e.g. "250781234567" provider: "relworx", // required description: "Payout", }), }); return res.json();}Phone Number Format (Rwanda)
| Network | Format | Example |
|---|---|---|
| MTN Rwanda (MoMo) | 250XXXXXXXXX | 250781234567 |
| Airtel Rwanda | 250XXXXXXXXX | 250731234567 |
250 country code without the + prefix. Remove any leading zero — e.g. 0781234567 becomes 250781234567.Tanzania Integration Guide (TZS)
This guide covers everything a Tanzanian developer needs to integrate DGateway and accept Mobile Money payments in Tanzanian Shilling (TZS).
Available Providers for Tanzania
| Provider | Currency | Collect | Disburse | Best For |
|---|---|---|---|---|
| Relworx | TZS | Yes | Yes | M-Pesa, Tigo Pesa & Airtel Money |
provider: "relworx". If omitted, defaults to "iotec" (which does not support TZS).1. Environment Setup
# .env.localDGATEWAY_API_URL='https://dgatewayapi.desispay.com'DGATEWAY_API_KEY='dgw_live_your_api_key_here'2. Collect Mobile Money Payment (TZS)
Send the phone number in international format with the 255 country code (no + prefix).
// Server-side: lib/dgateway.tsconst API_URL = process.env.DGATEWAY_API_URL!;const API_KEY = process.env.DGATEWAY_API_KEY!;
export async function collectTZS(phone: string, amount: number, description?: string) { const res = await fetch(`${API_URL}/v1/payments/collect`, { method: "POST", headers: { "Content-Type": "application/json", "X-Api-Key": API_KEY, }, body: JSON.stringify({ amount, // e.g. 10000 (TZS) currency: "TZS", phone_number: phone, // e.g. "255712345678" provider: "relworx", // required description, }), }); return res.json();}
// API route: app/api/checkout/route.tsexport async function POST(request: Request) { const { phone, amount, description } = await request.json(); const result = await collectTZS(phone, amount, description); return Response.json(result);}3. Frontend — Mobile Money Checkout
const handleMobilePayment = async () => { setStatus("creating");
const res = await fetch("/api/checkout", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ phone: "255712345678", // Customer's Vodacom M-Pesa number amount: 10000, // TZS 10,000 description: "Order #300", }), });
const data = await res.json(); if (data.error) { setError(data.error.message); return; }
// Customer receives USSD prompt on their phone setReference(data.data.reference); setStatus("processing"); pollStatus(data.data.reference); // Poll every 5s until completed/failed};4. Disburse to Mobile Money (TZS)
// Send money to a Tanzanian mobile money numberexport async function disburseTZS(phone: string, amount: number) { const res = await fetch(`${API_URL}/v1/payments/disburse`, { method: "POST", headers: { "Content-Type": "application/json", "X-Api-Key": API_KEY, }, body: JSON.stringify({ amount, currency: "TZS", phone_number: phone, // e.g. "255712345678" provider: "relworx", // required description: "Payout", }), }); return res.json();}Phone Number Format (Tanzania)
| Network | Format | Example |
|---|---|---|
| Vodacom (M-Pesa) | 255XXXXXXXXX | 255712345678 |
| Tigo (Tigo Pesa) | 255XXXXXXXXX | 255652345678 |
| Airtel Tanzania | 255XXXXXXXXX | 255682345678 |
255 country code without the + prefix. Remove any leading zero — e.g. 0712345678 becomes 255712345678.Uganda Integration Guide (UGX)
This guide covers everything a Ugandan developer needs to integrate DGateway and accept Mobile Money and card payments in Uganda Shillings (UGX).
Available Providers for Uganda
| Provider | Currency | Collect | Disburse | Best For |
|---|---|---|---|---|
| Iotec | UGX | Yes | Yes | MTN MoMo & Airtel Money |
| Relworx | UGX | Yes | Yes | MTN MoMo & Airtel Money (alternative) |
| Stripe | UGX | Yes | No | International card payments (collect only) |
provider is omitted, it defaults to "iotec". Use "relworx" as an alternative for mobile money, or "stripe" for card payments (collect only).1. Environment Setup
# .env.localDGATEWAY_API_URL='https://dgatewayapi.desispay.com'DGATEWAY_API_KEY='dgw_live_your_api_key_here'2. Collect Mobile Money Payment (UGX)
Send the phone number in international format with the 256 country code (no + prefix).
// Server-side: lib/dgateway.tsconst API_URL = process.env.DGATEWAY_API_URL!;const API_KEY = process.env.DGATEWAY_API_KEY!;
export async function collectUGX(phone: string, amount: number, description?: string) { const res = await fetch(`${API_URL}/v1/payments/collect`, { method: "POST", headers: { "Content-Type": "application/json", "X-Api-Key": API_KEY, }, body: JSON.stringify({ amount, // e.g. 50000 (UGX) currency: "UGX", phone_number: phone, // e.g. "256771234567" provider: "iotec", // required — "iotec" or "relworx" description, }), }); return res.json();}
// API route: app/api/checkout/route.tsexport async function POST(request: Request) { const { phone, amount, description } = await request.json(); const result = await collectUGX(phone, amount, description); return Response.json(result);}3. Frontend — Mobile Money Checkout
const handleMomoPayment = async () => { setStatus("creating");
const res = await fetch("/api/checkout", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ phone: "256771234567", // Customer's MTN MoMo number amount: 50000, // UGX 50,000 description: "Order #400", }), });
const data = await res.json(); if (data.error) { setError(data.error.message); return; }
// Customer receives USSD prompt on their phone setReference(data.data.reference); setStatus("processing"); pollStatus(data.data.reference); // Poll every 5s until completed/failed};4. Disburse to Mobile Money (UGX)
// Send money to a Ugandan mobile money numberexport async function disburseUGX(phone: string, amount: number) { const res = await fetch(`${API_URL}/v1/payments/disburse`, { method: "POST", headers: { "Content-Type": "application/json", "X-Api-Key": API_KEY, }, body: JSON.stringify({ amount, currency: "UGX", phone_number: phone, // e.g. "256771234567" provider: "iotec", // required — "iotec" or "relworx" description: "Payout", }), }); return res.json();}Phone Number Format (Uganda)
| Network | Format | Example |
|---|---|---|
| MTN Uganda (MoMo) | 256XXXXXXXXX | 256771234567 |
| Airtel Uganda | 256XXXXXXXXX | 256751234567 |
256 country code without the + prefix. Remove any leading zero — e.g. 0771234567 becomes 256771234567.Recurring Payments (Subscriptions)
DGateway supports recurring payments through a plan-based subscription model. You create a plan (e.g. "Monthly Premium — UGX 50,000/month"), subscribe customers to it, and charge them each billing cycle via Mobile Money or Card.
iotec if omitted.How It Works
1. Create a Plan → Define amount, currency, interval (daily/weekly/monthly/yearly)2. Subscribe a Customer → Attach a customer to a plan (email, name, phone)3. Charge Each Cycle → POST to /charge to initiate a Mobile Money prompt4. Manage Lifecycle → Pause, resume, or cancel subscriptions via APIManaging Plans
A plan defines the recurring billing terms — how much, how often, and in what currency.
Create a Plan
POST /v1/subscriptions/plansX-Api-Key: dgw_live_your_key_here
{ "name": "Monthly Premium", "description": "Premium access billed monthly", "amount": 50000, "currency": "UGX", "interval": "monthly", // "daily" | "weekly" | "monthly" | "yearly" | "custom" "interval_days": 0, // only used when interval is "custom" "trial_days": 7, // optional — free trial period "grace_days": 3, // optional — days after due date before marking overdue "setup_fee": 10000, // optional — one-time fee on first charge "max_cycles": 0, // 0 = unlimited "metadata": {} // optional}
// Response (201){ "data": { "id": 1, "name": "Monthly Premium", "amount": 50000, "currency": "UGX", "interval": "monthly", "is_active": true, "created_at": "2026-03-14T10:00:00Z" }}List & Manage Plans
| Method | Endpoint | Description |
|---|---|---|
| GET | /v1/subscriptions/plans | List all plans |
| GET | /v1/subscriptions/plans/:id | Get a single plan |
| PUT | /v1/subscriptions/plans/:id | Update plan details (name, trial, setup fee, etc.) |
| DELETE | /v1/subscriptions/plans/:id | Deactivate a plan (no new subscriptions) |
Subscription Lifecycle
Create a Subscription
Subscribe a customer to an existing plan.
POST /v1/subscriptionsX-Api-Key: dgw_live_your_key_here
{ "plan_id": 1, "customer_email": "jane@example.com", "customer_name": "Jane Doe", "customer_phone": "256771234567", "provider": "iotec", // optional — defaults to "iotec" "start_now": true, // true = first charge immediately "metadata": {} // optional}
// Response (201){ "data": { "id": 5, "plan_id": 1, "customer_email": "jane@example.com", "customer_name": "Jane Doe", "status": "active", "current_cycle": 1, "next_due_date": "2026-04-14T10:00:00Z", "created_at": "2026-03-14T10:00:00Z" }}Subscription States
| Status | Description |
|---|---|
| active | Subscription is running — charges can be initiated |
| paused | Temporarily paused — no charges until resumed |
| cancelled | Permanently cancelled — cannot be reactivated |
| past_due | Payment is overdue beyond the grace period |
| trialing | In the free trial period — no charges yet |
Pause, Resume & Cancel
| Action | Endpoint | Effect |
|---|---|---|
| Pause | POST /v1/subscriptions/:id/pause | Stops billing until resumed |
| Resume | POST /v1/subscriptions/:id/resume | Resumes billing from the next cycle |
| Cancel | POST /v1/subscriptions/:id/cancel | Permanently cancels the subscription |
// Example: pause a subscriptionPOST /v1/subscriptions/5/pauseX-Api-Key: dgw_live_your_key_here
// Response{ "message": "Subscription paused" }
// Example: cancel a subscriptionPOST /v1/subscriptions/5/cancelX-Api-Key: dgw_live_your_key_here
// Response{ "message": "Subscription cancelled" }Charging Subscribers
When a billing cycle is due, call the charge endpoint to initiate a Mobile Money prompt on the subscriber's phone. The customer confirms the payment just like a one-time collect.
POST /v1/subscriptions/5/chargeX-Api-Key: dgw_live_your_key_here
{ "phone_number": "256771234567", "provider": "iotec" // optional — defaults to "iotec"}
// Response (200){ "data": { "reference": "txn_sub_abc123", "status": "pending", "amount": 50000, "currency": "UGX", "provider": "iotec", "subscription_id": 5, "cycle": 2 }, "message": "Subscription charge initiated"}POST /v1/webhooks/verify with the returned reference — the same way you would for a one-time payment.Typical Integration Flow
// 1. On your backend — create a plan onceconst plan = await dgateway("/v1/subscriptions/plans", { method: "POST", body: { name: "Pro Plan", amount: 50000, currency: "UGX", interval: "monthly" },});
// 2. When a user subscribesconst sub = await dgateway("/v1/subscriptions", { method: "POST", body: { plan_id: plan.data.id, customer_email: user.email, customer_name: user.name, customer_phone: user.phone, start_now: true, },});
// 3. On each billing cycle (cron job or manual trigger)const charge = await dgateway(`/v1/subscriptions/${sub.data.id}/charge`, { method: "POST", body: { phone_number: user.phone },});
// 4. Poll for payment completionconst status = await dgateway("/v1/webhooks/verify", { method: "POST", body: { reference: charge.data.reference },});Integration Examples
DGateway is a REST API — integrate it from any language. Below are quick-start examples for collecting a payment in popular languages.
Node.js / TypeScript
const DGATEWAY_API = "https://dgatewayapi.desispay.com";const API_KEY = process.env.DGATEWAY_API_KEY!;
async function collectPayment(amount: number, currency: string, phone: string) { const res = await fetch(`${DGATEWAY_API}/v1/payments/collect`, { method: "POST", headers: { "X-Api-Key": API_KEY, "Content-Type": "application/json", }, body: JSON.stringify({ amount, currency, phone_number: phone }), }); return res.json();}
// Usageconst result = await collectPayment(50000, "UGX", "256771234567");console.log(result.data.reference);Go
package main
import ( "bytes" "encoding/json" "fmt" "net/http" "os")
func collectPayment(amount float64, currency, phone string) (map[string]interface{}, error) { body, _ := json.Marshal(map[string]interface{}{ "amount": amount, "currency": currency, "phone_number": phone, })
req, _ := http.NewRequest("POST", "https://dgatewayapi.desispay.com/v1/payments/collect", bytes.NewReader(body), ) req.Header.Set("X-Api-Key", os.Getenv("DGATEWAY_API_KEY")) req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close()
var result map[string]interface{} json.NewDecoder(resp.Body).Decode(&result) return result, nil}
func main() { result, _ := collectPayment(50000, "UGX", "256771234567") fmt.Println(result)}PHP / Laravel
<?php// Using Laravel's Http facadeuse Illuminate\Support\Facades\Http;
function collectPayment(float $amount, string $currency, string $phone): array{ $response = Http::withHeaders([ 'X-Api-Key' => config('services.dgateway.key'), ])->post('https://dgatewayapi.desispay.com/v1/payments/collect', [ 'amount' => $amount, 'currency' => $currency, 'phone_number' => $phone, ]);
return $response->json();}
// Usage$result = collectPayment(50000, 'UGX', '256771234567');$reference = $result['data']['reference'];Python
import osimport requests
DGATEWAY_API = "https://dgatewayapi.desispay.com"API_KEY = os.environ["DGATEWAY_API_KEY"]
def collect_payment(amount: float, currency: str, phone: str) -> dict: response = requests.post( f"{DGATEWAY_API}/v1/payments/collect", headers={ "X-Api-Key": API_KEY, "Content-Type": "application/json", }, json={ "amount": amount, "currency": currency, "phone_number": phone, }, ) return response.json()
# Usageresult = collect_payment(50000, "UGX", "256771234567")print(result["data"]["reference"])Rust
use reqwest::Client;use serde_json::{json, Value};use std::env;
async fn collect_payment( amount: f64, currency: &str, phone: &str,) -> Result<Value, reqwest::Error> { let api_key = env::var("DGATEWAY_API_KEY").expect("DGATEWAY_API_KEY not set");
let client = Client::new(); let res = client .post("https://dgatewayapi.desispay.com/v1/payments/collect") .header("X-Api-Key", &api_key) .json(&json!({ "amount": amount, "currency": currency, "phone_number": phone, })) .send() .await? .json::<Value>() .await?;
Ok(res)}
#[tokio::main]async fn main() { let result = collect_payment(50000.0, "UGX", "256771234567").await.unwrap(); println!("{}", result["data"]["reference"]);}cURL
curl -X POST https://dgatewayapi.desispay.com/v1/payments/collect \ -H "X-Api-Key: dgw_live_your_key_here" \ -H "Content-Type: application/json" \ -d '{ "amount": 50000, "currency": "UGX", "phone_number": "256771234567" }'X-Api-Key header for authentication. Replace the key with your actual API key from the dashboard. The same pattern applies to all endpoints — disburse, withdraw, subscriptions, etc."provider": "iotec" (or "relworx" / "stripe") in your request body. This ensures predictable routing and makes debugging easier.Case Study: WordPress NGO Website
A non-profit organisation in Uganda runs a WordPress website to collect donations from supporters via Mobile Money. They need a simple donate button on their site — no coding required.
The Challenge
- No developer on the team — everything must be no-code or low-code
- Supporters donate via MTN and Airtel Mobile Money (UGX)
- Some international donors want to pay by card (USD)
- Need a way to track all donations in one place
- Must be able to withdraw funds to the organisation's mobile money account
Solution: DGateway WordPress Plugin + Payment Links
Option A — WordPress Plugin (Recommended)
Install the DGateway WordPress plugin. Add a shortcode to any page or post and supporters can donate directly.
[dgateway_payment amount="0" currency="UGX" description="Donation to Save the Children Uganda" button_text="Donate Now" provider="iotec"]amount="0" lets the donor enter any amount. See the WordPress Plugin Guide for full setup instructions.Option B — Payment Link (Zero Code)
Create a payment link in the DGateway dashboard with flexible pricing. Share it on the website, WhatsApp, social media, or email newsletters.
// No code needed — just create in the dashboard:Name: "Donate to Our Cause"Amount: 0 (donor enters amount)Currency: UGXSuccess URL: https://our-ngo.org/thank-you
// Share the generated link:https://dgatewayadmin.desispay.com/store/pay/donate-to-our-causeEmbedding on the WordPress Site
<!-- Option 1: Link to the checkout page --><a href="https://dgatewayadmin.desispay.com/store/pay/donate-to-our-cause" class="donate-button"> Donate Now</a>
<!-- Option 2: Embed as an iframe --><iframe src="https://dgatewayadmin.desispay.com/store/pay/donate-to-our-cause" width="100%" height="600" frameborder="0" style="border-radius: 12px; max-width: 500px;"></iframe>Tracking Donations
All donations appear in the DGateway admin dashboard with donor phone numbers, amounts, and timestamps. Export to Excel for reporting to stakeholders.
Withdrawing Funds
The NGO treasurer can withdraw accumulated donations from the dashboard to the organisation's mobile money account — instant disbursement.
// From the dashboard or via API:POST /v1/withdrawals{ "amount": 2500000, "currency": "UGX", "provider": "iotec", "method": "mobile_money", "destination": "256771234567", "destination_name": "Save the Children Uganda", "note": "March donations payout"}Case Study: E-Commerce Store
An online store selling electronics in Uganda needs to accept mobile money from local customers and card payments from international buyers.
Architecture
┌──────────────────────┐│ Next.js Storefront ││ ││ Product listing ││ Cart (Zustand) ││ Checkout page │└──────────┬───────────┘ │ POST /api/checkout │ ▼┌──────────────────────┐ ┌───────────────────┐│ Next.js API Route │─────►│ DGateway ││ (server-side) │ │ ││ │◄─────│ iotec / stripe │└──────────────────────┘ └───────────────────┘Checkout Flow
// 1. Customer selects payment method on checkout pageconst paymentMethod = "mobile_money"; // or "card"
// 2. For Mobile Money — collect via Iotecconst res = await fetch("/api/checkout", { method: "POST", body: JSON.stringify({ amount: cart.total, currency: "UGX", phone_number: "256771234567", provider: "iotec", metadata: { order_id: order.id, items: cart.items }, }),});const { reference } = await res.json();// Poll for completion...
// 3. For Card payments — collect via Stripeconst res = await fetch("/api/checkout", { method: "POST", body: JSON.stringify({ amount: cart.totalUSD, currency: "USD", provider: "stripe", metadata: { order_id: order.id }, }),});const { client_secret, stripe_publishable_key } = await res.json();// Use Stripe Elements to complete card paymentServer-Side API Route
// app/api/checkout/route.tsimport { NextResponse } from "next/server";
const DGATEWAY = "https://dgatewayapi.desispay.com";const KEY = process.env.DGATEWAY_API_KEY!;
export async function POST(req: Request) { const body = await req.json();
const res = await fetch(`${DGATEWAY}/v1/payments/collect`, { method: "POST", headers: { "X-Api-Key": KEY, "Content-Type": "application/json" }, body: JSON.stringify({ amount: body.amount, currency: body.currency, phone_number: body.phone_number, provider: body.provider, description: `Order #${body.metadata.order_id}`, metadata: body.metadata, }), });
const data = await res.json(); return NextResponse.json(data);}After Payment
When the transaction completes, mark the order as paid, send a confirmation email, and update inventory. For digital products, generate a download link.
Case Study: SaaS Subscription Billing
A project management SaaS serving teams in East Africa needs monthly recurring billing via Mobile Money. Customers subscribe to a plan and are charged automatically each month.
Setup: Create Plans
// Create plans on your backend (run once)
// Basic Plan — UGX 30,000/monthawait fetch("https://dgatewayapi.desispay.com/v1/subscriptions/plans", { method: "POST", headers: { "X-Api-Key": KEY, "Content-Type": "application/json" }, body: JSON.stringify({ name: "Basic Plan", amount: 30000, currency: "UGX", interval: "monthly", trial_days: 14, grace_days: 3, }),});
// Pro Plan — UGX 80,000/month with setup feeawait fetch("https://dgatewayapi.desispay.com/v1/subscriptions/plans", { method: "POST", headers: { "X-Api-Key": KEY, "Content-Type": "application/json" }, body: JSON.stringify({ name: "Pro Plan", amount: 80000, currency: "UGX", interval: "monthly", trial_days: 14, setup_fee: 20000, }),});Subscribe a Customer
// When a user picks a plan on your pricing pageconst sub = await fetch("https://dgatewayapi.desispay.com/v1/subscriptions", { method: "POST", headers: { "X-Api-Key": KEY, "Content-Type": "application/json" }, body: JSON.stringify({ plan_id: 1, // Basic Plan customer_email: "team@company.ug", customer_name: "Acme Ltd", customer_phone: "256771234567", provider: "iotec", start_now: false, // start with 14-day trial }),});
// sub.data.subscription.status → "trialing"// sub.data.subscription.next_due_date → 14 days from nowMonthly Charge (Cron Job)
// Run daily via cron — check for due subscriptions and charge them
// 1. List active subscriptionsconst subs = await fetch( "https://dgatewayapi.desispay.com/v1/subscriptions?status=active", { headers: { "X-Api-Key": KEY } }).then(r => r.json());
// 2. For each subscription with next_due_date <= todayfor (const sub of subs.data) { if (new Date(sub.next_due_date) <= new Date()) { // Charge the customer const charge = await fetch( `https://dgatewayapi.desispay.com/v1/subscriptions/${sub.id}/charge`, { method: "POST", headers: { "X-Api-Key": KEY, "Content-Type": "application/json" }, body: JSON.stringify({ phone_number: sub.customer_phone, provider: "iotec", }), } ).then(r => r.json());
console.log(`Charged ${sub.customer_name}: ${charge.data.reference}`); }}
// 3. Poll each charge reference to confirm payment// 4. If payment fails after grace period, pause or cancel the subscriptionHandling Lifecycle Events
| Event | Action |
|---|---|
| Trial ends | First charge is initiated, subscription moves to "active" |
| Payment succeeds | Next cycle created, user keeps access |
| Payment fails | Retry after 1 day. After grace_days (3), mark "past_due" |
| User cancels | POST /subscriptions/:id/cancel — access until end of current period |
| User upgrades | Cancel old subscription, create new one on the higher plan |
Case Study: School Fees Collection
A school management system needs to collect tuition fees from parents via Mobile Money and track payments per student, term, and class.
Collecting a Fee Payment
// When a parent pays school feesconst payment = await fetch("https://dgatewayapi.desispay.com/v1/payments/collect", { method: "POST", headers: { "X-Api-Key": KEY, "Content-Type": "application/json" }, body: JSON.stringify({ amount: 450000, currency: "UGX", phone_number: parentPhone, provider: "iotec", description: `School fees - ${studentName} - Term 2`, metadata: { student_id: "STU-2024-0042", student_name: studentName, class: "Senior 3", term: "Term 2", academic_year: "2026", fee_type: "tuition", }, }),});
const reference = payment.data.reference;// Save reference to your school database for reconciliationPolling & Reconciliation
// Poll for payment completionconst status = await fetch("https://dgatewayapi.desispay.com/v1/webhooks/verify", { method: "POST", headers: { "X-Api-Key": KEY, "Content-Type": "application/json" }, body: JSON.stringify({ reference }),}).then(r => r.json());
if (status.data.status === "completed") { // Mark fee as paid in your school system await db.feePayments.update({ where: { reference }, data: { status: "paid", paid_at: new Date(), provider: status.data.provider, }, });
// Send SMS/email receipt to parent await sendReceipt(parentPhone, { student: studentName, amount: 450000, term: "Term 2", reference, });}Reporting
Use the DGateway API to list all transactions with metadata filters, or export from the dashboard to Excel for the bursar's records.
// List all fee payments for Term 2const transactions = await fetch( "https://dgatewayapi.desispay.com/v1/payments/transactions?per_page=100", { headers: { "X-Api-Key": KEY } }).then(r => r.json());
// Filter by metadata on your endconst term2Payments = transactions.data.filter( (tx) => JSON.parse(tx.metadata || "{}").term === "Term 2");
// Generate reportconst totalCollected = term2Payments .filter(tx => tx.status === "completed") .reduce((sum, tx) => sum + tx.amount, 0);
console.log(`Term 2 collected: UGX ${totalCollected.toLocaleString()}`);Case Study: ISP Monthly Billing
An Internet Service Provider in Uganda collects monthly payments from subscribers for internet packages. Packages range from UGX 50,000 to UGX 300,000/month.
Architecture
┌──────────────────┐ ┌───────────────┐ ┌──────────────┐│ ISP Billing │────►│ DGateway │────►│ Iotec ││ System │ │ │ │ (MTN/Airtel)││ │◄────│ Webhooks │◄────│ ││ - Customer DB │ └───────────────┘ └──────────────┘│ - Package mgmt ││ - Auto-billing │└──────────────────┘Using Subscription Plans
// Create internet package plansconst packages = [ { name: "Home Basic 10Mbps", amount: 50000, interval: "monthly" }, { name: "Home Plus 25Mbps", amount: 100000, interval: "monthly" }, { name: "Business 50Mbps", amount: 200000, interval: "monthly" }, { name: "Enterprise 100Mbps", amount: 300000, interval: "monthly" },];
for (const pkg of packages) { await fetch("https://dgatewayapi.desispay.com/v1/subscriptions/plans", { method: "POST", headers: { "X-Api-Key": KEY, "Content-Type": "application/json" }, body: JSON.stringify({ ...pkg, currency: "UGX", grace_days: 5, // 5 days before disconnecting }), });}Auto-Billing Subscribers
// Cron job: runs daily at 8 AM// Charges all subscribers whose next_due_date is today
const subs = await fetch( "https://dgatewayapi.desispay.com/v1/subscriptions?status=active&per_page=100", { headers: { "X-Api-Key": KEY } }).then(r => r.json());
const today = new Date().toISOString().split("T")[0];
for (const sub of subs.data) { const dueDate = sub.next_due_date?.split("T")[0]; if (dueDate !== today) continue;
try { const charge = await fetch( `https://dgatewayapi.desispay.com/v1/subscriptions/${sub.id}/charge`, { method: "POST", headers: { "X-Api-Key": KEY, "Content-Type": "application/json" }, body: JSON.stringify({ phone_number: sub.customer_phone, provider: "iotec", }), } ).then(r => r.json());
// Log charge reference for tracking await logCharge(sub.id, charge.data.reference);
// Send SMS to customer: "Your internet bill of UGX X has been initiated" await sendSMS(sub.customer_phone, `Your ${sub.plan.name} payment of UGX ${sub.plan.amount.toLocaleString()} has been initiated. Please approve the Mobile Money prompt.` ); } catch (err) { console.error(`Failed to charge sub ${sub.id}:`, err); }}Grace Period & Disconnection
If a subscriber doesn't pay within the grace period (5 days), the system can automatically pause their subscription and disconnect their internet service.
// Check for overdue subscriptionsconst overdue = await fetch( "https://dgatewayapi.desispay.com/v1/subscriptions?status=past_due", { headers: { "X-Api-Key": KEY } }).then(r => r.json());
for (const sub of overdue.data) { // Disconnect internet service in your RADIUS/PPPoE system await disconnectCustomer(sub.customer_email);
// Notify customer await sendSMS(sub.customer_phone, "Your internet has been disconnected due to non-payment. " + "Please contact us to reactivate." );}Case Study: Microfinance & Lending
A microfinance application disburses small loans to borrowers via Mobile Money and collects repayments on a weekly or monthly schedule.
Disbursing a Loan
// When a loan is approved, send money to the borrower's phoneconst disbursement = await fetch( "https://dgatewayapi.desispay.com/v1/payments/disburse", { method: "POST", headers: { "X-Api-Key": KEY, "Content-Type": "application/json" }, body: JSON.stringify({ amount: 500000, // UGX 500,000 loan currency: "UGX", phone_number: "256771234567", // borrower's phone provider: "iotec", description: `Loan disbursement - ${loanId}`, }), }).then(r => r.json());
// Save transaction referenceawait db.loans.update({ where: { id: loanId }, data: { disbursement_ref: disbursement.data.reference, disbursement_status: disbursement.data.status, disbursed_at: new Date(), },});Collecting Repayments
// Collect a weekly repayment from the borrowerconst repayment = await fetch( "https://dgatewayapi.desispay.com/v1/payments/collect", { method: "POST", headers: { "X-Api-Key": KEY, "Content-Type": "application/json" }, body: JSON.stringify({ amount: 60000, // weekly repayment amount currency: "UGX", phone_number: borrower.phone, provider: "iotec", description: `Loan repayment - Week ${weekNumber}`, metadata: { loan_id: loanId, week: weekNumber, total_weeks: 10, borrower_id: borrower.id, }, }), }).then(r => r.json());
// Poll for completionconst interval = setInterval(async () => { const status = await fetch( "https://dgatewayapi.desispay.com/v1/webhooks/verify", { method: "POST", headers: { "X-Api-Key": KEY, "Content-Type": "application/json" }, body: JSON.stringify({ reference: repayment.data.reference }), } ).then(r => r.json());
if (status.data.status === "completed") { clearInterval(interval); await recordRepayment(loanId, weekNumber, repayment.data.reference); } else if (status.data.status === "failed") { clearInterval(interval); await markRepaymentFailed(loanId, weekNumber); }}, 15000); // poll every 15 secondsUsing Subscriptions for Auto-Repayment
For structured loans with fixed repayment schedules, use DGateway subscriptions to automate the collection cycle.
// Create a repayment plan (e.g., 10 weekly payments of UGX 60,000)const plan = await fetch( "https://dgatewayapi.desispay.com/v1/subscriptions/plans", { method: "POST", headers: { "X-Api-Key": KEY, "Content-Type": "application/json" }, body: JSON.stringify({ name: `Loan Repayment - ${loanId}`, amount: 60000, currency: "UGX", interval: "weekly", max_cycles: 10, // loan is fully repaid after 10 payments grace_days: 2, }), }).then(r => r.json());
// Subscribe the borrowerconst sub = await fetch( "https://dgatewayapi.desispay.com/v1/subscriptions", { method: "POST", headers: { "X-Api-Key": KEY, "Content-Type": "application/json" }, body: JSON.stringify({ plan_id: plan.data.id, customer_email: borrower.email, customer_name: borrower.name, customer_phone: borrower.phone, provider: "iotec", start_now: true, }), }).then(r => r.json());
// Subscription auto-completes after 10 payments// status will transition: active → completedWallet & Withdrawals
The microfinance company withdraws collected repayments from their DGateway wallet to their business mobile money account for operational use.
// Check available balanceconst wallets = await fetch( "https://dgatewayapi.desispay.com/v1/wallets", { headers: { "X-Api-Key": KEY } }).then(r => r.json());
const ugxBalance = wallets.data.find( (w) => w.provider === "iotec" && w.currency === "UGX")?.balance || 0;
// Withdraw if balance exceeds thresholdif (ugxBalance >= 1000000) { await fetch("https://dgatewayapi.desispay.com/v1/withdrawals", { method: "POST", headers: { "X-Api-Key": KEY, "Content-Type": "application/json" }, body: JSON.stringify({ amount: ugxBalance, currency: "UGX", provider: "iotec", method: "mobile_money", destination: "256700123456", destination_name: "MicroLend Ltd", note: "Weekly operational withdrawal", }), });}API Reference
POST /v1/payments/collect
Initiate a payment collection from a customer.
// Request headersX-Api-Key: dgw_live_your_key_hereContent-Type: application/json
// Request body{ "amount": 37500, "currency": "UGX", "phone_number": "256771234567", "provider": "iotec", // optional — defaults to "iotec" if omitted "description": "Order #1234", "metadata": { "order_id": "123" } // optional}
// Response{ "data": { "reference": "txn_abc123", "provider": "iotec", "status": "pending", "amount": 37500, "currency": "UGX" }}| Field | Type | Required | Description |
|---|---|---|---|
| amount | number | Yes | Payment amount |
| currency | string | Yes | ISO 4217 code (UGX, USD, KES…) |
| phone_number | string | Mobile money | Phone with country code |
| provider | string | No | Provider slug — defaults to "iotec" if omitted |
| description | string | No | Human-readable payment description |
| metadata | object | No | Arbitrary key-value data |
POST /v1/webhooks/verify
Check the current status of a transaction by its reference.
// Request{ "reference": "txn_abc123" }
// Response{ "data": { "reference": "txn_abc123", "status": "completed", // "pending" | "completed" | "failed" "provider": "iotec", "amount": 37500, "currency": "UGX" }}POST /v1/withdrawals
Request a withdrawal from your app balance.
// Request headersX-Api-Key: dgw_live_your_key_hereContent-Type: application/json
// Request body{ "amount": 500000, "currency": "UGX", "provider": "iotec", // optional — defaults to "iotec" "method": "mobile_money", // "mobile_money" | "bank_transfer" "destination": "256771234567", "destination_name": "John Doe", // optional "bank_name": "", // required for bank_transfer "note": "Monthly payout" // optional}
// Response (201){ "data": { "id": 12, "app_id": 1, "amount": 500000, "currency": "UGX", "provider_slug": "iotec", "method": "mobile_money", "destination": "256771234567", "destination_name": "John Doe", "status": "completed", "note": "Monthly payout", "created_at": "2025-06-15T10:30:00Z" }, "message": "Withdrawal disbursed successfully"}GET /v1/withdrawals
List withdrawal requests for your app. Supports pagination and status filtering.
// Request headersX-Api-Key: dgw_live_your_key_here
// Query parameters: ?page=1&per_page=20&status=pending
// Response{ "data": [ /* array of withdrawal objects */ ], "meta": { "page": 1, "per_page": 20, "total": 5, "total_pages": 1 }}Full Example Project
A complete working e-commerce example with cart, checkout, mobile money, and Stripe card payments is in the DGateway GitHub repository.
examples/ecommerce-app/├── .env.local # DGATEWAY_API_URL + DGATEWAY_API_KEY├── lib/│ └── dgateway.ts # Server-side DGateway client├── app/│ ├── api/│ │ └── checkout/│ │ ├── route.ts # POST → DGateway collect│ │ └── status/route.ts # POST → DGateway verify│ ├── checkout/page.tsx # Full checkout UI│ └── page.tsx # Product listing└── components/ └── zustand-cart.tsx # Cart with ZustandUsing WordPress?
The DGateway WordPress Plugin lets you accept payments with shortcodes or WooCommerce — no custom code required. Supports one-time payments, recurring subscriptions, Mobile Money, and Cards.
WordPress Plugin Guide →Frequently Asked Questions
What payment methods does DGateway support?
MTN Mobile Money, Airtel Money (via Iotec & Relworx), and international card payments (Visa, Mastercard via Stripe) — all through a single API.
Does DGateway support recurring payments / subscriptions?
Yes. You can create billing plans (daily, weekly, monthly, yearly, or custom intervals), subscribe customers, and charge them each cycle via the /v1/subscriptions endpoints. See the Recurring Payments section above for full details.
Which currencies are supported?
UGX, KES, TZS, and RWF for mobile money. USD, EUR, and GBP for card payments via Stripe.
How do I handle failed subscription charges?
Poll the transaction status after charging. If it fails, you can retry by calling the charge endpoint again. Use grace_days on the plan to allow a buffer before marking a subscription as past due.
What happens if a provider is not specified?
DGateway defaults to Iotec. You can override this by passing the provider field (iotec, relworx, or stripe) on any request.
Can I test before going live?
Yes. Use a test API key (prefixed dgw_test_) with test phone numbers (e.g. 0111777771) to create sandbox transactions. No real money is moved. Important: test phone numbers are blocked on live keys — never mix them.
How do I receive payouts?
Collected funds accumulate in your provider wallet. Request a withdrawal via the API or dashboard — instant disbursement to mobile money or bank.
Which languages can I integrate with?
DGateway is a standard REST API — integrate from any language. We provide examples for Node.js, Go, PHP/Laravel, Python, Rust, and cURL.
Claude Code Skill
If you use Claude Code (or any AI coding assistant that supports agent skills), you can install the DGateway skill to get AI-powered help writing integration code.
Install the skill
# Install via URLclaude skill add --url https://dgateway.desispay.com/skill/SKILL.md
# Or manually — copy the skill file to your projectmkdir -p .claude/skillscurl -o .claude/skills/dgateway.md https://dgateway.desispay.com/skill/SKILL.mdWhat it provides
Once installed, your AI assistant will automatically understand:
- All DGateway API endpoints and their request/response formats
- Error codes and how to handle them gracefully
- Phone number validation rules (256XXXXXXXXX or 0XXXXXXXXX)
- Provider health checking before making requests
- Commission rates and balance calculations
- Webhook payload structure and status polling
- Integration patterns in TypeScript, Go, PHP, Python, and cURL
Usage examples
# Ask Claude to help you integrate> "Help me collect a payment of 50,000 UGX via MTN Mobile Money"> "How do I handle the PROVIDER_LINE_DOWN error?"> "Write a webhook handler for DGateway transaction events"> "Set up recurring billing with DGateway subscriptions"