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.

ProviderTypeCurrencies
IotecMobile MoneyUGX
RelworxMobile MoneyUGX, KES, TZS, RWF
PesaPalMobile Money / CardKES, UGX, TZS, USD
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. Auto-selects the best provider based on currency & direction
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.

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'
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" | "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 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"
  }
}
CodeDescriptionAction
VALIDATION_ERRORMissing or invalid request fieldsCheck your request body
AUTHENTICATION_ERRORInvalid or missing API keyVerify DGATEWAY_API_KEY
PROVIDER_ERRORThe payment provider returned an errorShow message to user
NOT_FOUNDTransaction or resource not foundCheck the reference/ID
RATE_LIMITToo 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");
}

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"
  }
}
FieldTypeRequiredDescription
amountnumberYesPayment amount
currencystringYesISO 4217 code (UGX, USD, KES…)
phone_numberstringMobile moneyPhone with country code
providerstringNoSpecific provider slug
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"
  }
}

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