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 the transaction to the right provider based on the currency and transaction type.
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 |
| PesaPal | Mobile Money / Card | KES, UGX, TZS, USD |
| 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 key
2. Auto-selects the best provider based on currency & direction
3. Processes the payment using platform credentials
4. Returns a unified response with a reference IDDGATEWAY_API_KEY stays server-side only.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
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'| 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" | "pesapal"
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 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.
// 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"
}
}| Code | Description | Action |
|---|---|---|
| VALIDATION_ERROR | Missing or invalid request fields | Check your request body |
| AUTHENTICATION_ERROR | Invalid or missing API key | Verify DGATEWAY_API_KEY |
| PROVIDER_ERROR | The payment provider returned an error | Show message to user |
| NOT_FOUND | Transaction or resource not found | Check the reference/ID |
| RATE_LIMIT | Too many requests | Back 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");
}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 — auto-selected 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 | Specific provider slug |
| 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"
}
}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