← Back to blog
tutorial8 min read

Managing Transactions & Withdrawals via the DGateway API

Learn how to fetch, filter, and manage transactions and withdrawals programmatically. Add status check buttons, manual status overrides, and build withdrawal dashboards for your users.

Managing Transactions & Withdrawals via the DGateway API

Managing Transactions & Withdrawals via the DGateway API

DGateway is a unified payment and commerce platform for Africa. If you're building a dashboard that shows your users their transaction history, withdrawal status, or lets them request payouts — this guide covers every API endpoint you need.


Authentication

All endpoints require your API key in the X-API-Key header:

curl -H "X-API-Key: dgw_live_your_key_here" \
  https://dgatewayapi.desispay.com/v1/payments/transactions

1. Listing Transactions

Fetch All Transactions

GET /v1/payments/transactions?page=1&page_size=20&mode=live

Query Parameters:

ParamTypeDescription
pageintPage number (default: 1)
page_sizeintItems per page (default: 20, max: 100)
modestringlive or test (default: all)
statusstringFilter by: pending, completed, failed
directionstringcollect or disburse
phonestringSearch by phone number

Response:

{
  "data": [
    {
      "id": 142,
      "reference": "dgw_abc123def456",
      "provider_slug": "iotec",
      "provider_ref": "39463627506",
      "direction": "collect",
      "amount": 50000,
      "platform_fee": 4000,
      "net_amount": 46000,
      "currency": "UGX",
      "status": "completed",
      "phone_number": "256770123456",
      "description": "Payment for order #1234",
      "failure_reason": "",
      "is_test": false,
      "created_at": "2026-04-09T14:03:00Z"
    }
  ],
  "meta": {
    "total": 118,
    "page": 1,
    "page_size": 20,
    "pages": 6
  }
}

Get Single Transaction

GET /v1/payments/transactions/:reference

Returns the full transaction object by its reference (e.g., dgw_abc123def456).


2. Checking Transaction Status

Verify (Webhook Alternative)

POST /v1/webhooks/verify
Content-Type: application/json

{
  "reference": "dgw_abc123def456"
}

Response:

{
  "data": {
    "reference": "dgw_abc123def456",
    "status": "completed",
    "amount": 50000,
    "currency": "UGX"
  }
}

When to use: Call this endpoint when polling for payment status (every 5 seconds) or when you suspect a webhook was missed.

Active Provider Check

The verify endpoint actively queries the payment provider (Iotec/Relworx) if the transaction is still pending. This means even if the webhook hasn't arrived, you'll get the real status from the provider.


3. Requesting a Withdrawal

Create Withdrawal

POST /v1/payments/disburse
Content-Type: application/json

{
  "amount": 50000,
  "currency": "UGX",
  "phone_number": "256770123456",
  "provider": "iotec",
  "description": "Payout to vendor"
}

Response:

{
  "data": {
    "reference": "dgw_wd_xyz789",
    "provider_ref": "39448500011",
    "status": "pending",
    "amount": 50000,
    "currency": "UGX"
  },
  "message": "Disbursement initiated"
}

Important notes:

  • The amount is deducted from your app's available balance (collected net minus already withdrawn)
  • Minimum withdrawal: 500 UGX
  • The phone number must be in format 256XXXXXXXXX or 0XXXXXXXXX
  • Provider must be iotec or relworx

Check Withdrawal Status

Use the same verify endpoint:

POST /v1/webhooks/verify

{
  "reference": "dgw_wd_xyz789"
}

4. Listing Withdrawals

Fetch Withdrawal History

GET /v1/payments/withdrawals?page=1&page_size=20

Response:

{
  "data": [
    {
      "id": 312,
      "reference": "dgw_wd_xyz789",
      "amount": 60000,
      "currency": "UGX",
      "method": "mobile_money",
      "destination": "+256744574953",
      "destination_name": "JOHN DOE",
      "provider_slug": "iotec",
      "status": "completed",
      "created_at": "2026-04-15T02:19:00Z"
    }
  ]
}

5. Building a Transaction Dashboard

Here's a complete Next.js implementation for displaying transactions with status check buttons:

API Routes

// app/api/transactions/route.ts
import { NextResponse } from "next/server";
 
const API_URL = process.env.DGATEWAY_API_URL!;
const API_KEY = process.env.DGATEWAY_API_KEY!;
 
export async function GET(req: Request) {
  const { searchParams } = new URL(req.url);
  const page = searchParams.get("page") || "1";
  const mode = searchParams.get("mode") || "live";
 
  const res = await fetch(
    `${API_URL}/v1/payments/transactions?page=${page}&page_size=20&mode=${mode}`,
    { headers: { "X-API-Key": API_KEY } },
  );
 
  return NextResponse.json(await res.json());
}
// app/api/transactions/[ref]/check/route.ts
import { NextResponse } from "next/server";
 
export async function POST(
  req: Request,
  { params }: { params: { ref: string } },
) {
  const res = await fetch(
    `${process.env.DGATEWAY_API_URL}/v1/webhooks/verify`,
    {
      method: "POST",
      headers: {
        "X-API-Key": process.env.DGATEWAY_API_KEY!,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ reference: params.ref }),
    },
  );
 
  return NextResponse.json(await res.json());
}

Transaction Table Component

"use client";
 
import { useState } from "react";
 
interface Transaction {
  reference: string;
  amount: number;
  currency: string;
  status: string;
  phone_number: string;
  direction: string;
  created_at: string;
  failure_reason?: string;
}
 
export function TransactionTable({
  transactions,
}: {
  transactions: Transaction[];
}) {
  const [checking, setChecking] = useState<string | null>(null);
  const [statuses, setStatuses] = useState<Record<string, string>>({});
 
  const checkStatus = async (ref: string) => {
    setChecking(ref);
    try {
      const res = await fetch(`/api/transactions/${ref}/check`, {
        method: "POST",
      });
      const data = await res.json();
      if (data.data?.status) {
        setStatuses((prev) => ({ ...prev, [ref]: data.data.status }));
      }
    } catch {
      alert("Check failed");
    }
    setChecking(null);
  };
 
  return (
    <table className="w-full text-sm">
      <thead>
        <tr className="border-b text-left text-gray-500">
          <th className="py-3 px-4">Reference</th>
          <th className="py-3 px-4">Amount</th>
          <th className="py-3 px-4">Status</th>
          <th className="py-3 px-4">Phone</th>
          <th className="py-3 px-4">Date</th>
          <th className="py-3 px-4">Actions</th>
        </tr>
      </thead>
      <tbody>
        {transactions.map((tx) => {
          const currentStatus = statuses[tx.reference] || tx.status;
          return (
            <tr key={tx.reference} className="border-b hover:bg-gray-50">
              <td className="py-3 px-4 font-mono text-xs">{tx.reference}</td>
              <td className="py-3 px-4 font-semibold">
                {tx.currency} {tx.amount.toLocaleString()}
              </td>
              <td className="py-3 px-4">
                <span
                  className={`px-2 py-1 rounded-full text-xs font-medium ${
                    currentStatus === "completed"
                      ? "bg-green-100 text-green-700"
                      : currentStatus === "failed"
                        ? "bg-red-100 text-red-700"
                        : "bg-yellow-100 text-yellow-700"
                  }`}
                >
                  {currentStatus}
                </span>
              </td>
              <td className="py-3 px-4 text-gray-500">{tx.phone_number}</td>
              <td className="py-3 px-4 text-gray-500">
                {new Date(tx.created_at).toLocaleString()}
              </td>
              <td className="py-3 px-4">
                {currentStatus === "pending" && (
                  <button
                    onClick={() => checkStatus(tx.reference)}
                    disabled={checking === tx.reference}
                    className="px-3 py-1 bg-blue-100 text-blue-700 rounded text-xs font-medium hover:bg-blue-200 disabled:opacity-50"
                  >
                    {checking === tx.reference
                      ? "Checking..."
                      : "⟳ Check Status"}
                  </button>
                )}
                {tx.failure_reason && (
                  <span className="text-xs text-red-500 block mt-1">
                    {tx.failure_reason}
                  </span>
                )}
              </td>
            </tr>
          );
        })}
      </tbody>
    </table>
  );
}

6. Building a Withdrawal Dashboard

"use client";
 
import { useState } from "react";
 
export function WithdrawalForm() {
  const [amount, setAmount] = useState("");
  const [phone, setPhone] = useState("");
  const [status, setStatus] = useState<
    "idle" | "loading" | "success" | "error"
  >("idle");
  const [result, setResult] = useState<any>(null);
 
  const handleWithdraw = async () => {
    setStatus("loading");
    try {
      const res = await fetch("/api/withdraw", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          amount: Number(amount),
          currency: "UGX",
          phone_number: phone,
          provider: "iotec",
        }),
      });
      const data = await res.json();
      if (!res.ok) throw new Error(data.error?.message || "Withdrawal failed");
      setResult(data.data);
      setStatus("success");
    } catch (err: any) {
      setResult({ error: err.message });
      setStatus("error");
    }
  };
 
  return (
    <div className="max-w-md p-6 border rounded-xl">
      <h2 className="text-lg font-bold mb-4">Request Withdrawal</h2>
 
      <div className="space-y-3 mb-4">
        <input
          type="number"
          min="500"
          value={amount}
          onChange={(e) => setAmount(e.target.value)}
          placeholder="Amount (min 500 UGX)"
          className="w-full border rounded-lg px-3 py-2"
        />
        <input
          type="tel"
          value={phone}
          onChange={(e) => setPhone(e.target.value)}
          placeholder="256700000000"
          className="w-full border rounded-lg px-3 py-2"
        />
      </div>
 
      <button
        onClick={handleWithdraw}
        disabled={!amount || !phone || status === "loading"}
        className="w-full bg-green-600 text-white py-3 rounded-lg font-semibold disabled:opacity-50"
      >
        {status === "loading"
          ? "Processing..."
          : `Withdraw UGX ${Number(amount || 0).toLocaleString()}`}
      </button>
 
      {status === "success" && (
        <div className="mt-4 p-3 bg-green-50 rounded-lg">
          <p className="text-green-700 font-medium">Withdrawal initiated!</p>
          <p className="text-sm text-green-600">
            Reference: {result?.reference}
          </p>
        </div>
      )}
 
      {status === "error" && (
        <div className="mt-4 p-3 bg-red-50 rounded-lg">
          <p className="text-red-700">{result?.error}</p>
        </div>
      )}
    </div>
  );
}

7. Common Issues & Solutions

"Withdrawal shows pending for a long time"

Cause: The provider hasn't confirmed the disbursement yet.

Fix:

  1. Call the verify endpoint to actively check with the provider
  2. If still pending after 5 minutes, the provider may be experiencing delays
  3. Check System Health in the dashboard for line status

"Transaction completed on provider but shows pending in DGateway"

Cause: The webhook from the provider didn't arrive.

Fix:

  1. Call POST /v1/webhooks/verify with the reference — this actively checks with the provider
  2. If the provider confirms completed, DGateway updates the status automatically
  3. For admin: use the recheck button (⟳) on the admin transactions page

"Insufficient balance for withdrawal"

Cause: Your available balance (collected net minus already withdrawn) is less than the requested amount.

Fix:

  1. Check your balance via GET /v1/wallets
  2. Available = total collected (net) - total withdrawn
  3. Pending withdrawals also count against your available balance

"Invalid phone number"

Fix: Phone numbers must be:

  • 256XXXXXXXXX (12 digits, starting with country code)
  • 0XXXXXXXXX (10 digits, starting with 0)
  • No spaces, dashes, or + prefix
  • Must be a valid MTN or Airtel Uganda number

API Reference Summary

EndpointMethodDescription
/v1/payments/collectPOSTCollect payment from customer
/v1/payments/disbursePOSTSend money to phone number
/v1/payments/transactionsGETList all transactions (paginated)
/v1/payments/transactions/:refGETGet single transaction
/v1/payments/withdrawalsGETList all withdrawals
/v1/webhooks/verifyPOSTVerify/check transaction status
/v1/walletsGETGet balance per provider

Need help? Join our WhatsApp Support Group or check the full docs.