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.

ProviderTypeCurrencies
IotecMobile MoneyUGX
RelworxMobile MoneyUGX, KES, TZS, RWF
StripeCardUSD, 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 key
2. Routes to the provider you specify (defaults to Iotec if omitted)
3. Processes the payment using platform credentials
4. Returns a unified response with a reference ID
Key principle: API routes on your server call DGateway. The browser never touches the DGateway API directly. Your DGATEWAY_API_KEY stays server-side only.
Best practice: Always pass the 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 numbersMust use test numbers onlyMust use real phone numbers only
Money movementNo real money is chargedReal money is collected/disbursed
TransactionsMarked 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 PatternResultDescriptionExample
011177777(0-9)SuccessTransaction completes successfully0111777771
011177799(0-9)FailedTransaction fails0111777991
011177778(0-9)PendingTransaction stays in pending state0111777781
011177779(0-9)SentToVendorTransaction 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 API
DGATEWAY_API_URL='https://dgatewayapi.desispay.com'
DGATEWAY_API_KEY='dgw_live_your_api_key_here'
VariableDescription
DGATEWAY_API_URLURL of the DGateway server
DGATEWAY_API_KEYAPI key from the DGateway admin panel (Apps → API Keys)
Never add 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.ts
import { 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.ts
import { 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 failed

Accepting 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
}, []);
ProviderStart polling when...
IotecImmediately after collectPayment() returns
StripeAfter stripe.confirmPayment() succeeds

Payment States

Handle each state with a distinct UI to give users clear feedback at every step of the payment flow.

// Success
if (status === "completed") return (
<div className="text-center">
<CheckCircle className="text-green-500 mx-auto" />
<h1>Payment Successful!</h1>
<p>Reference: {reference}</p>
</div>
);
// Failed
if (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 provider
body: 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"
}
}
CodeHTTPDescriptionAction
VALIDATION_ERROR400Missing or invalid request fieldsCheck your request body
INVALID_PHONE400Phone number format invalid. Must be 256XXXXXXXXX or 0XXXXXXXXXFix phone format
INVALID_CURRENCY400Currency not supported. Accepted: UGX, KES, TZS, RWF, USD, EUR, GBPUse valid currency
INVALID_PROVIDER400Provider not supported. Accepted: iotec, relworx, stripeUse valid provider
AUTHENTICATION_ERROR401Invalid or missing API keyVerify DGATEWAY_API_KEY
PROVIDER_ERROR400The payment provider returned an errorShow message to user
PROVIDER_LINE_DOWN503A specific vendor line (e.g. MTN or Airtel) is downRetry later or use a different network
INSUFFICIENT_BALANCE400Withdrawal amount exceeds available balanceReduce withdrawal amount
ACCOUNT_HAS_ACTIVITY403Cannot delete account with transaction historyContact support
NOT_FOUND404Transaction or resource not foundCheck the reference/ID
RATE_LIMIT429Too many requestsBack off and retry
// Always wrap API calls in try/catch
try {
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"
}
}
The 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..."
}
}
Health checks run automatically every day at 6:00 AM UTC. The status is updated in real-time when issues are detected or resolved.

Phone Number Validation

All API endpoints that accept phone numbers enforce strict validation. Only these formats are accepted:

FormatExampleDescription
256XXXXXXXXX256771234567International format (12 digits)
0XXXXXXXXX0771234567Local 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 provider
3. If disbursement succeeds → status is "completed"
4. If disbursement fails → status is "failed" with a note explaining the reason

Add 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.ts
import { 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

FieldTypeRequiredDescription
amountnumberYesWithdrawal amount (must be > 0)
currencystringYesISO 4217 code (UGX, USD, KES...)
providerstringNoProvider slug — defaults to "iotec" if omitted
methodstringYes"mobile_money" or "bank_transfer"
destinationstringYesPhone number (mobile money) or account number (bank)
destination_namestringNoName of the recipient
bank_namestringNoBank name (required for bank_transfer)
notestringNoOptional 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"
}
Withdrawals are processed instantly. If disbursement succeeds, the status is 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

ParameterTypeDefaultDescription
pagenumber1Page number
per_pagenumber20Results per page (max 100)
statusstringallFilter 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

StatusDescription
pendingRequest submitted, disbursement in progress
completedFunds have been sent to the destination
failedDisbursement failed (check note for reason)
rejectedRequest 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

ProviderCurrencyCollectDisburseBest For
RelworxKESYesYesM-Pesa collections & disbursements
StripeKESYesNoInternational card payments (collect only)
For Kenya, use provider: "relworx" for M-Pesa or "stripe" for card payments. If omitted, defaults to "iotec" (which does not support KES).

1. Environment Setup

# .env.local
DGATEWAY_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.ts
const 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.ts
export 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 number
export 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)

NetworkFormatExample
Safaricom (M-Pesa)254XXXXXXXXX254712345678
Airtel Kenya254XXXXXXXXX254733456789
Always use the 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

ProviderCurrencyCollectDisburseBest For
RelworxRWFYesYesMTN MoMo & Airtel Money collections and payouts
For Rwanda, use provider: "relworx". If omitted, defaults to "iotec" (which does not support RWF).

1. Environment Setup

# .env.local
DGATEWAY_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.ts
const 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.ts
export 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 number
export 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)

NetworkFormatExample
MTN Rwanda (MoMo)250XXXXXXXXX250781234567
Airtel Rwanda250XXXXXXXXX250731234567
Always use the 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

ProviderCurrencyCollectDisburseBest For
RelworxTZSYesYesM-Pesa, Tigo Pesa & Airtel Money
For Tanzania, use provider: "relworx". If omitted, defaults to "iotec" (which does not support TZS).

1. Environment Setup

# .env.local
DGATEWAY_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.ts
const 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.ts
export 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 number
export 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)

NetworkFormatExample
Vodacom (M-Pesa)255XXXXXXXXX255712345678
Tigo (Tigo Pesa)255XXXXXXXXX255652345678
Airtel Tanzania255XXXXXXXXX255682345678
Always use the 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

ProviderCurrencyCollectDisburseBest For
IotecUGXYesYesMTN MoMo & Airtel Money
RelworxUGXYesYesMTN MoMo & Airtel Money (alternative)
StripeUGXYesNoInternational card payments (collect only)
If 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.local
DGATEWAY_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.ts
const 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.ts
export 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 number
export 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)

NetworkFormatExample
MTN Uganda (MoMo)256XXXXXXXXX256771234567
Airtel Uganda256XXXXXXXXX256751234567
Always use the 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.

Subscriptions use the same providers as one-time payments. The provider defaults to 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 prompt
4. Manage Lifecycle → Pause, resume, or cancel subscriptions via API

Managing Plans

A plan defines the recurring billing terms — how much, how often, and in what currency.

Create a Plan

POST /v1/subscriptions/plans
X-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

MethodEndpointDescription
GET/v1/subscriptions/plansList all plans
GET/v1/subscriptions/plans/:idGet a single plan
PUT/v1/subscriptions/plans/:idUpdate plan details (name, trial, setup fee, etc.)
DELETE/v1/subscriptions/plans/:idDeactivate a plan (no new subscriptions)

Subscription Lifecycle

Create a Subscription

Subscribe a customer to an existing plan.

POST /v1/subscriptions
X-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

StatusDescription
activeSubscription is running — charges can be initiated
pausedTemporarily paused — no charges until resumed
cancelledPermanently cancelled — cannot be reactivated
past_duePayment is overdue beyond the grace period
trialingIn the free trial period — no charges yet

Pause, Resume & Cancel

ActionEndpointEffect
PausePOST /v1/subscriptions/:id/pauseStops billing until resumed
ResumePOST /v1/subscriptions/:id/resumeResumes billing from the next cycle
CancelPOST /v1/subscriptions/:id/cancelPermanently cancels the subscription
// Example: pause a subscription
POST /v1/subscriptions/5/pause
X-Api-Key: dgw_live_your_key_here
// Response
{ "message": "Subscription paused" }
// Example: cancel a subscription
POST /v1/subscriptions/5/cancel
X-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/charge
X-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"
}
After charging, poll the transaction status using 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 once
const plan = await dgateway("/v1/subscriptions/plans", {
method: "POST",
body: { name: "Pro Plan", amount: 50000, currency: "UGX", interval: "monthly" },
});
// 2. When a user subscribes
const 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 completion
const 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();
}
// Usage
const 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 facade
use 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 os
import 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()
# Usage
result = 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"
}'
All examples above use the 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.
Tip: We recommend always including "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"]
Setting 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: UGX
Success URL: https://our-ngo.org/thank-you
// Share the generated link:
https://dgatewayadmin.desispay.com/store/pay/donate-to-our-cause

Embedding 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 page
const paymentMethod = "mobile_money"; // or "card"
// 2. For Mobile Money — collect via Iotec
const 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 Stripe
const 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 payment

Server-Side API Route

// app/api/checkout/route.ts
import { 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/month
await 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 fee
await 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 page
const 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 now

Monthly Charge (Cron Job)

// Run daily via cron — check for due subscriptions and charge them
// 1. List active subscriptions
const 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 <= today
for (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 subscription

Handling Lifecycle Events

EventAction
Trial endsFirst charge is initiated, subscription moves to "active"
Payment succeedsNext cycle created, user keeps access
Payment failsRetry after 1 day. After grace_days (3), mark "past_due"
User cancelsPOST /subscriptions/:id/cancel — access until end of current period
User upgradesCancel 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 fees
const 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 reconciliation

Polling & Reconciliation

// Poll for payment completion
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 }),
}).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 2
const 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 end
const term2Payments = transactions.data.filter(
(tx) => JSON.parse(tx.metadata || "{}").term === "Term 2"
);
// Generate report
const 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 plans
const 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 subscriptions
const 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 phone
const 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 reference
await 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 borrower
const 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 completion
const 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 seconds

Using 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 borrower
const 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 → completed

Wallet & Withdrawals

The microfinance company withdraws collected repayments from their DGateway wallet to their business mobile money account for operational use.

// Check available balance
const 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 threshold
if (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 headers
X-Api-Key: dgw_live_your_key_here
Content-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"
}
}
FieldTypeRequiredDescription
amountnumberYesPayment amount
currencystringYesISO 4217 code (UGX, USD, KES…)
phone_numberstringMobile moneyPhone with country code
providerstringNoProvider slug — defaults to "iotec" if omitted
descriptionstringNoHuman-readable payment description
metadataobjectNoArbitrary 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 headers
X-Api-Key: dgw_live_your_key_here
Content-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 headers
X-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 Zustand

Using 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 URL
claude skill add --url https://dgateway.desispay.com/skill/SKILL.md
# Or manually — copy the skill file to your project
mkdir -p .claude/skills
curl -o .claude/skills/dgateway.md https://dgateway.desispay.com/skill/SKILL.md

What 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"
The skill is also available as an LLM reference file (llms.txt) that you can paste into any AI chat for context.