← Back to blog
tutorial19 min read

Getting Started with the DGateway API in 5 Minutes

A quick start guide to integrating DGateway into your application — from account creation to your first successful payment collection.

Getting Started with the DGateway API in 5 Minutes

Getting Started with the DGateway API in 5 Minutes

DGateway is a unified payment and commerce platform for Africa that lets you accept mobile money (MTN, Airtel) and card payments through a single API. It also provides tools to sell digital products, courses, and templates — but in this guide, we focus on the API.

Integrating payments should not take weeks. With DGateway, you can go from zero to collecting your first payment in under five minutes. This guide walks you through every step — from account creation to a production-ready integration with code examples in Node.js, Go, and Python.


Step 1: Create Your Account

Head to dgateway.io and sign up for an account. You will need a valid email address and a phone number. Once you verify your email, you will land on the DGateway dashboard.

The dashboard is your command center. From here you can manage API keys, view transactions, configure webhooks, and monitor provider health. Take a moment to explore, but do not worry about configuring everything right away — the defaults are sensible.


Step 2: Get Your API Keys

Navigate to Settings > API Keys in your dashboard. You will see two sets of credentials:

  • Test keys — Use these during development. Transactions made with test keys are simulated and no real money moves.
  • Live keys — Use these in production. Transactions are real.

Each set includes a public key and a secret key. The public key identifies your account and can be safely included in client-side code. The secret key must be kept confidential and should only be used on your server.

Copy your test secret key. You will need it for the next step.

API Key Format

Key TypePrefixExampleUse
Test Secretdg_test_sk_dg_test_sk_abc123...Server-side (dev)
Test Publicdg_test_pk_dg_test_pk_xyz789...Client-side (dev)
Live Secretdg_live_sk_dg_live_sk_def456...Server-side (prod)
Live Publicdg_live_pk_dg_live_pk_uvw012...Client-side (prod)

Step 3: Environment Setup

Before writing any code, set up your environment variables. Never hardcode API keys in your source code.

Node.js / Next.js

Create a .env.local file in your project root:

# .env.local
DGATEWAY_SECRET_KEY=dg_test_sk_your_test_key_here
DGATEWAY_PUBLIC_KEY=dg_test_pk_your_test_key_here
DGATEWAY_WEBHOOK_SECRET=whsec_your_webhook_secret_here
DGATEWAY_API_URL=https://api.dgateway.io
NEXT_PUBLIC_DGATEWAY_PUBLIC_KEY=dg_test_pk_your_test_key_here

Go

Create a .env file or set environment variables:

# .env
DGATEWAY_SECRET_KEY=dg_test_sk_your_test_key_here
DGATEWAY_WEBHOOK_SECRET=whsec_your_webhook_secret_here
DGATEWAY_API_URL=https://api.dgateway.io

Python

# .env
DGATEWAY_SECRET_KEY=dg_test_sk_your_test_key_here
DGATEWAY_WEBHOOK_SECRET=whsec_your_webhook_secret_here
DGATEWAY_API_URL=https://api.dgateway.io

Step 4: Make Your First Collection

A collection is a request to receive money from a customer. This is the most common operation for most integrations.

Using cURL

curl -X POST https://api.dgateway.io/v1/collections \
  -H "Authorization: Bearer dg_test_sk_your_test_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 5000,
    "currency": "UGX",
    "phone_number": "256771234567",
    "provider": "mtn",
    "description": "Test payment",
    "callback_url": "https://your-app.com/webhooks/dgateway",
    "metadata": {
      "order_id": "order_12345"
    }
  }'

Request Body Fields

FieldTypeRequiredDescription
amountnumberYesAmount to collect in smallest currency unit. For UGX, this is the full amount (no subunits).
currencystringYesThree-letter currency code: UGX, KES, TZS, RWF, USD
phone_numberstringYes*Customer's number in international format (e.g., 256771234567). *Not required for card payments.
providerstringNomtn, airtel, or card. Omit to let DGateway auto-detect from phone number.
descriptionstringNoHuman-readable description shown on the customer's payment prompt.
callback_urlstringNoURL for webhook notifications. Falls back to your app's default webhook URL.
metadataobjectNoYour custom data. Stored and returned in webhooks for matching.

Using Node.js (TypeScript)

// lib/dgateway.ts
const DGATEWAY_API_URL =
  process.env.DGATEWAY_API_URL || "https://api.dgateway.io";
const DGATEWAY_SECRET_KEY = process.env.DGATEWAY_SECRET_KEY!;
 
interface CollectionRequest {
  amount: number;
  currency: string;
  phone_number?: string;
  provider?: "mtn" | "airtel" | "card";
  description?: string;
  callback_url?: string;
  metadata?: Record<string, string>;
}
 
interface Transaction {
  id: string;
  status: "pending" | "successful" | "failed" | "expired";
  amount: number;
  currency: string;
  provider: string;
  created_at: string;
}
 
export async function createCollection(
  data: CollectionRequest,
): Promise<Transaction> {
  const response = await fetch(`${DGATEWAY_API_URL}/v1/collections`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${DGATEWAY_SECRET_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify(data),
  });
 
  if (!response.ok) {
    const error = await response.json();
    throw new Error(error.error?.message || "Collection failed");
  }
 
  const { data: transaction } = await response.json();
  return transaction;
}
 
// Usage
const transaction = await createCollection({
  amount: 50000,
  currency: "UGX",
  phone_number: "256771234567",
  provider: "mtn",
  description: "Order #1234 - DGateway T-Shirt",
  callback_url: "https://your-app.com/api/webhooks/dgateway",
  metadata: {
    order_id: "order_1234",
    customer_email: "john@example.com",
  },
});
 
console.log(`Transaction ID: ${transaction.id}`);
console.log(`Status: ${transaction.status}`); // "pending"

Using Go

package payments
 
import (
	"bytes"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
)
 
type CollectionRequest struct {
	Amount      int               `json:"amount"`
	Currency    string            `json:"currency"`
	PhoneNumber string            `json:"phone_number,omitempty"`
	Provider    string            `json:"provider,omitempty"`
	Description string            `json:"description,omitempty"`
	CallbackURL string            `json:"callback_url,omitempty"`
	Metadata    map[string]string `json:"metadata,omitempty"`
}
 
type Transaction struct {
	ID        string `json:"id"`
	Status    string `json:"status"`
	Amount    int    `json:"amount"`
	Currency  string `json:"currency"`
	Provider  string `json:"provider"`
	CreatedAt string `json:"created_at"`
}
 
type APIResponse struct {
	Data Transaction `json:"data"`
}
 
func CreateCollection(req CollectionRequest) (*Transaction, error) {
	apiURL := os.Getenv("DGATEWAY_API_URL")
	secretKey := os.Getenv("DGATEWAY_SECRET_KEY")
 
	body, err := json.Marshal(req)
	if err != nil {
		return nil, fmt.Errorf("marshal request: %w", err)
	}
 
	httpReq, err := http.NewRequest("POST", apiURL+"/v1/collections", bytes.NewReader(body))
	if err != nil {
		return nil, fmt.Errorf("create request: %w", err)
	}
 
	httpReq.Header.Set("Authorization", "Bearer "+secretKey)
	httpReq.Header.Set("Content-Type", "application/json")
 
	resp, err := http.DefaultClient.Do(httpReq)
	if err != nil {
		return nil, fmt.Errorf("send request: %w", err)
	}
	defer resp.Body.Close()
 
	if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
		return nil, fmt.Errorf("API returned status %d", resp.StatusCode)
	}
 
	var apiResp APIResponse
	if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil {
		return nil, fmt.Errorf("decode response: %w", err)
	}
 
	return &apiResp.Data, nil
}
 
// Usage
func main() {
	txn, err := CreateCollection(CollectionRequest{
		Amount:      50000,
		Currency:    "UGX",
		PhoneNumber: "256771234567",
		Provider:    "mtn",
		Description: "Order #1234",
		CallbackURL: "https://your-app.com/webhooks/dgateway",
		Metadata: map[string]string{
			"order_id": "order_1234",
		},
	})
	if err != nil {
		log.Fatal(err)
	}
 
	fmt.Printf("Transaction: %s, Status: %s\n", txn.ID, txn.Status)
}

Using Python

import os
import requests
 
DGATEWAY_API_URL = os.getenv("DGATEWAY_API_URL", "https://api.dgateway.io")
DGATEWAY_SECRET_KEY = os.getenv("DGATEWAY_SECRET_KEY")
 
def create_collection(
    amount: int,
    currency: str,
    phone_number: str = None,
    provider: str = None,
    description: str = None,
    callback_url: str = None,
    metadata: dict = None,
) -> dict:
    """Create a payment collection via DGateway API."""
    payload = {
        "amount": amount,
        "currency": currency,
    }
    if phone_number:
        payload["phone_number"] = phone_number
    if provider:
        payload["provider"] = provider
    if description:
        payload["description"] = description
    if callback_url:
        payload["callback_url"] = callback_url
    if metadata:
        payload["metadata"] = metadata
 
    response = requests.post(
        f"{DGATEWAY_API_URL}/v1/collections",
        json=payload,
        headers={
            "Authorization": f"Bearer {DGATEWAY_SECRET_KEY}",
            "Content-Type": "application/json",
        },
    )
    response.raise_for_status()
    return response.json()["data"]
 
 
# Usage
transaction = create_collection(
    amount=50000,
    currency="UGX",
    phone_number="256771234567",
    provider="mtn",
    description="Order #1234",
    callback_url="https://your-app.com/webhooks/dgateway",
    metadata={"order_id": "order_1234"},
)
 
print(f"Transaction ID: {transaction['id']}")
print(f"Status: {transaction['status']}")

API Response

The API will respond with a transaction object:

{
  "data": {
    "id": "txn_abc123",
    "status": "pending",
    "amount": 5000,
    "currency": "UGX",
    "provider": "mtn",
    "phone_number": "256771234567",
    "description": "Test payment",
    "metadata": {
      "order_id": "order_12345"
    },
    "created_at": "2026-03-30T10:00:00Z"
  }
}

In test mode, the transaction will automatically transition to successful after a few seconds.


Step 5: Handle Webhooks

When a transaction changes status — from pending to successful, failed, or expired — DGateway sends a POST request to your callback URL with the transaction details.

Webhook Payload

{
  "event": "collection.completed",
  "timestamp": "2026-03-30T10:00:05Z",
  "data": {
    "id": "txn_abc123",
    "status": "successful",
    "amount": 5000,
    "currency": "UGX",
    "provider": "mtn",
    "phone_number": "256771234567",
    "metadata": {
      "order_id": "order_12345"
    },
    "completed_at": "2026-03-30T10:00:05Z"
  }
}

Webhook Event Types

EventWhen It Fires
collection.completedCustomer successfully paid
collection.failedPayment attempt failed
collection.expiredCustomer did not confirm in time
disbursement.completedMoney sent to recipient
disbursement.failedDisbursement failed
subscription.renewedRecurring payment charged
subscription.cancelledSubscription cancelled
refund.processedRefund issued

Webhook Handler in Next.js (App Router)

// app/api/webhooks/dgateway/route.ts
import { NextRequest, NextResponse } from "next/server";
import crypto from "crypto";
 
function verifySignature(
  payload: string,
  signature: string,
  secret: string,
): boolean {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(payload)
    .digest("hex");
  return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
 
export async function POST(req: NextRequest) {
  const signature = req.headers.get("x-dgateway-signature");
  const rawBody = await req.text();
 
  // 1. Verify signature
  if (
    !signature ||
    !verifySignature(rawBody, signature, process.env.DGATEWAY_WEBHOOK_SECRET!)
  ) {
    return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
  }
 
  const { event, data } = JSON.parse(rawBody);
 
  // 2. Check for duplicates (idempotency)
  const existing = await db.webhookLog.findUnique({
    where: { transactionId: data.id },
  });
  if (existing) {
    return NextResponse.json({ received: true }, { status: 200 });
  }
  await db.webhookLog.create({ data: { transactionId: data.id, event } });
 
  // 3. Process the event
  switch (event) {
    case "collection.completed":
      if (data.status === "successful") {
        await fulfillOrder(data.metadata.order_id);
      }
      break;
    case "collection.failed":
      await markOrderFailed(data.metadata.order_id);
      break;
    case "collection.expired":
      await expireOrder(data.metadata.order_id);
      break;
  }
 
  // 4. Return 200 quickly
  return NextResponse.json({ received: true }, { status: 200 });
}

Webhook Handler in Go

package handlers
 
import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"io"
	"net/http"
	"os"
)
 
type WebhookPayload struct {
	Event string          `json:"event"`
	Data  json.RawMessage `json:"data"`
}
 
type TransactionData struct {
	ID       string            `json:"id"`
	Status   string            `json:"status"`
	Amount   int               `json:"amount"`
	Currency string            `json:"currency"`
	Provider string            `json:"provider"`
	Metadata map[string]string `json:"metadata"`
}
 
func verifySignature(payload []byte, signature, secret string) bool {
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write(payload)
	expected := hex.EncodeToString(mac.Sum(nil))
	return hmac.Equal([]byte(signature), []byte(expected))
}
 
func WebhookHandler(w http.ResponseWriter, r *http.Request) {
	body, err := io.ReadAll(r.Body)
	if err != nil {
		http.Error(w, "Bad request", http.StatusBadRequest)
		return
	}
 
	// Verify signature
	signature := r.Header.Get("X-DGateway-Signature")
	secret := os.Getenv("DGATEWAY_WEBHOOK_SECRET")
	if !verifySignature(body, signature, secret) {
		http.Error(w, "Invalid signature", http.StatusUnauthorized)
		return
	}
 
	var payload WebhookPayload
	if err := json.Unmarshal(body, &payload); err != nil {
		http.Error(w, "Invalid JSON", http.StatusBadRequest)
		return
	}
 
	var txnData TransactionData
	json.Unmarshal(payload.Data, &txnData)
 
	switch payload.Event {
	case "collection.completed":
		if txnData.Status == "successful" {
			fulfillOrder(txnData.Metadata["order_id"])
		}
	case "collection.failed":
		markOrderFailed(txnData.Metadata["order_id"])
	}
 
	w.WriteHeader(http.StatusOK)
	w.Write([]byte("OK"))
}

Webhook Handler in Python (Flask)

import hmac
import hashlib
import os
import json
from flask import Flask, request, jsonify
 
app = Flask(__name__)
 
def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(
        secret.encode(),
        payload,
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(signature, expected)
 
@app.route("/webhooks/dgateway", methods=["POST"])
def handle_webhook():
    signature = request.headers.get("X-DGateway-Signature", "")
    secret = os.getenv("DGATEWAY_WEBHOOK_SECRET")
 
    if not verify_signature(request.data, signature, secret):
        return jsonify({"error": "Invalid signature"}), 401
 
    payload = request.get_json()
    event = payload["event"]
    data = payload["data"]
 
    if event == "collection.completed" and data["status"] == "successful":
        fulfill_order(data["metadata"]["order_id"])
    elif event == "collection.failed":
        mark_order_failed(data["metadata"]["order_id"])
 
    return jsonify({"received": True}), 200

Step 6: Test Mode and Test Phone Numbers

DGateway's test mode simulates the full payment flow without moving real money. When using test API keys:

BehaviorDescription
Transactions auto-completeTest transactions move to successful after a few seconds
No real chargesNo money is collected from or sent to anyone
Webhooks still fireYour callback URL receives real webhook events
Same API formatRequest and response format is identical to production

Test Phone Numbers

Use these phone numbers in test mode to simulate different outcomes:

Phone NumberProviderSimulated Result
256771000001MTNSuccess (instant)
256771000002MTNSuccess (delayed ~10s)
256771000003MTNFailed (insufficient funds)
256771000004MTNFailed (wrong PIN)
256771000005MTNExpired (timeout)
256701000001AirtelSuccess (instant)
256701000002AirtelFailed
256701000003AirtelExpired

Testing with cURL

# Test a successful MTN payment
curl -X POST https://api.dgateway.io/v1/collections \
  -H "Authorization: Bearer dg_test_sk_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 10000,
    "currency": "UGX",
    "phone_number": "256771000001",
    "provider": "mtn",
    "description": "Test successful payment",
    "callback_url": "https://your-app.com/webhooks/dgateway"
  }'
 
# Test a failed payment (insufficient funds)
curl -X POST https://api.dgateway.io/v1/collections \
  -H "Authorization: Bearer dg_test_sk_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 10000,
    "currency": "UGX",
    "phone_number": "256771000003",
    "provider": "mtn",
    "description": "Test failed payment",
    "callback_url": "https://your-app.com/webhooks/dgateway"
  }'
 
# Test an expired payment (timeout)
curl -X POST https://api.dgateway.io/v1/collections \
  -H "Authorization: Bearer dg_test_sk_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 10000,
    "currency": "UGX",
    "phone_number": "256771000005",
    "provider": "mtn",
    "description": "Test expired payment",
    "callback_url": "https://your-app.com/webhooks/dgateway"
  }'

Step 7: Other API Endpoints

Check Transaction Status

curl -X GET https://api.dgateway.io/v1/transactions/txn_abc123 \
  -H "Authorization: Bearer dg_test_sk_your_key"

Create a Disbursement (Send Money)

curl -X POST https://api.dgateway.io/v1/disbursements \
  -H "Authorization: Bearer dg_test_sk_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 25000,
    "currency": "UGX",
    "phone_number": "256771234567",
    "provider": "mtn",
    "description": "Payout to vendor",
    "callback_url": "https://your-app.com/webhooks/dgateway",
    "metadata": {
      "payout_id": "payout_001"
    }
  }'

List Transactions

curl -X GET "https://api.dgateway.io/v1/transactions?page=1&page_size=20&status=successful" \
  -H "Authorization: Bearer dg_test_sk_your_key"

Check Provider Health

curl -X GET https://api.dgateway.io/v1/health/providers \
  -H "Authorization: Bearer dg_test_sk_your_key"

Response:

{
  "data": [
    {
      "provider": "iotec",
      "line": "mtn",
      "status": "healthy",
      "last_checked": "2026-03-30T09:00:00Z"
    },
    {
      "provider": "iotec",
      "line": "airtel",
      "status": "healthy",
      "last_checked": "2026-03-30T09:00:00Z"
    },
    {
      "provider": "stripe",
      "line": "card",
      "status": "healthy",
      "last_checked": "2026-03-30T09:00:00Z"
    }
  ]
}

Validate a Coupon

curl -X POST https://api.dgateway.io/v1/coupons/validate \
  -H "Authorization: Bearer dg_test_sk_your_key" \
  -H "Content-Type: application/json" \
  -d '{
    "code": "LAUNCH20",
    "amount": 50000,
    "currency": "UGX"
  }'

Step 8: Complete Next.js Integration Example

Here is a full project walkthrough — from setup to first payment.

Project Setup

npx create-next-app@latest my-dgateway-app --typescript --tailwind --app
cd my-dgateway-app

Environment Variables

# .env.local
DGATEWAY_SECRET_KEY=dg_test_sk_your_key
DGATEWAY_WEBHOOK_SECRET=whsec_your_secret
DGATEWAY_API_URL=https://api.dgateway.io
NEXT_PUBLIC_APP_URL=http://localhost:3000

API Route: Create Payment

// app/api/pay/route.ts
import { NextRequest, NextResponse } from "next/server";
 
export async function POST(req: NextRequest) {
  const { amount, phone_number, provider, order_id } = await req.json();
 
  const response = await fetch(
    `${process.env.DGATEWAY_API_URL}/v1/collections`,
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.DGATEWAY_SECRET_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        amount,
        currency: "UGX",
        phone_number,
        provider,
        description: `Payment for order ${order_id}`,
        callback_url: `${process.env.NEXT_PUBLIC_APP_URL}/api/webhooks/dgateway`,
        metadata: { order_id },
      }),
    },
  );
 
  const data = await response.json();
 
  if (!response.ok) {
    return NextResponse.json(
      { error: data.error?.message || "Payment failed" },
      { status: response.status },
    );
  }
 
  return NextResponse.json(data);
}

API Route: Webhook Handler

// app/api/webhooks/dgateway/route.ts
import { NextRequest, NextResponse } from "next/server";
import crypto from "crypto";
 
function verifySignature(
  payload: string,
  signature: string,
  secret: string,
): boolean {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(payload)
    .digest("hex");
  return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
 
export async function POST(req: NextRequest) {
  const rawBody = await req.text();
  const signature = req.headers.get("x-dgateway-signature") || "";
 
  if (
    !verifySignature(rawBody, signature, process.env.DGATEWAY_WEBHOOK_SECRET!)
  ) {
    return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
  }
 
  const { event, data } = JSON.parse(rawBody);
 
  if (event === "collection.completed" && data.status === "successful") {
    // TODO: Fulfill the order
    console.log(
      `Order ${data.metadata.order_id} paid! Amount: ${data.amount} ${data.currency}`,
    );
  }
 
  return NextResponse.json({ received: true });
}

API Route: Check Transaction Status

// app/api/transactions/[id]/route.ts
import { NextRequest, NextResponse } from "next/server";
 
export async function GET(
  req: NextRequest,
  { params }: { params: { id: string } },
) {
  const response = await fetch(
    `${process.env.DGATEWAY_API_URL}/v1/transactions/${params.id}`,
    {
      headers: {
        Authorization: `Bearer ${process.env.DGATEWAY_SECRET_KEY}`,
      },
    },
  );
 
  const data = await response.json();
  return NextResponse.json(data);
}

Checkout Page Component

// app/checkout/page.tsx
"use client";
 
import { useState, useEffect } from "react";
 
export default function CheckoutPage() {
  const [phone, setPhone] = useState("");
  const [provider, setProvider] = useState<"mtn" | "airtel">("mtn");
  const [status, setStatus] = useState<
    "idle" | "processing" | "waiting" | "success" | "failed"
  >("idle");
  const [transactionId, setTransactionId] = useState<string | null>(null);
  const amount = 50000; // UGX 50,000
 
  async function handlePay() {
    setStatus("processing");
 
    try {
      const res = await fetch("/api/pay", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          amount,
          phone_number: `256${phone.replace(/^0/, "")}`,
          provider,
          order_id: `order_${Date.now()}`,
        }),
      });
 
      const { data } = await res.json();
      setTransactionId(data.id);
      setStatus("waiting");
    } catch {
      setStatus("failed");
    }
  }
 
  // Poll for transaction completion
  useEffect(() => {
    if (status !== "waiting" || !transactionId) return;
 
    const interval = setInterval(async () => {
      const res = await fetch(`/api/transactions/${transactionId}`);
      const { data } = await res.json();
 
      if (data.status === "successful") {
        setStatus("success");
        clearInterval(interval);
      } else if (data.status === "failed" || data.status === "expired") {
        setStatus("failed");
        clearInterval(interval);
      }
    }, 3000);
 
    // Timeout after 2 minutes
    const timeout = setTimeout(() => {
      clearInterval(interval);
      if (status === "waiting") setStatus("failed");
    }, 120000);
 
    return () => {
      clearInterval(interval);
      clearTimeout(timeout);
    };
  }, [status, transactionId]);
 
  return (
    <div className="max-w-md mx-auto p-8">
      <h1 className="text-2xl font-bold mb-6">Checkout</h1>
      <p className="text-lg mb-4">Amount: UGX {amount.toLocaleString()}</p>
 
      {status === "idle" && (
        <div className="space-y-4">
          <div className="flex gap-2">
            <button
              className={`flex-1 p-3 rounded border-2 ${
                provider === "mtn"
                  ? "border-yellow-400 bg-yellow-50"
                  : "border-gray-200"
              }`}
              onClick={() => setProvider("mtn")}
            >
              MTN MoMo
            </button>
            <button
              className={`flex-1 p-3 rounded border-2 ${
                provider === "airtel"
                  ? "border-red-500 bg-red-50"
                  : "border-gray-200"
              }`}
              onClick={() => setProvider("airtel")}
            >
              Airtel Money
            </button>
          </div>
 
          <input
            type="tel"
            placeholder="Phone number (e.g., 0771234567)"
            className="w-full p-3 border rounded"
            value={phone}
            onChange={(e) => setPhone(e.target.value)}
          />
 
          <button
            className="w-full bg-purple-600 text-white p-3 rounded font-semibold hover:bg-purple-700"
            onClick={handlePay}
            disabled={!phone}
          >
            Pay UGX {amount.toLocaleString()}
          </button>
        </div>
      )}
 
      {status === "processing" && (
        <div className="text-center py-8">
          <p className="text-gray-600">Initiating payment...</p>
        </div>
      )}
 
      {status === "waiting" && (
        <div className="text-center py-8 space-y-4">
          <div className="animate-spin w-8 h-8 border-4 border-purple-600 border-t-transparent rounded-full mx-auto" />
          <p className="text-lg font-medium">Check your phone</p>
          <p className="text-gray-600">
            A payment prompt has been sent to your{" "}
            {provider === "mtn" ? "MTN" : "Airtel"} phone. Enter your PIN to
            confirm the payment of UGX {amount.toLocaleString()}.
          </p>
        </div>
      )}
 
      {status === "success" && (
        <div className="text-center py-8 space-y-4">
          <div className="text-green-600 text-5xl">&#10003;</div>
          <p className="text-lg font-medium text-green-600">
            Payment successful!
          </p>
          <p className="text-gray-600">
            UGX {amount.toLocaleString()} received. Thank you!
          </p>
        </div>
      )}
 
      {status === "failed" && (
        <div className="text-center py-8 space-y-4">
          <p className="text-lg font-medium text-red-600">Payment failed</p>
          <p className="text-gray-600">
            Please check your balance and try again.
          </p>
          <button
            className="bg-purple-600 text-white px-6 py-2 rounded"
            onClick={() => setStatus("idle")}
          >
            Try Again
          </button>
        </div>
      )}
    </div>
  );
}

Step 9: Go Live

Once you are confident that your integration works correctly in test mode, switching to production is simple:

  1. Replace your test API keys with your live API keys in your environment variables.
  2. Ensure your webhook endpoint is publicly accessible and uses HTTPS.
  3. Update your callback URL if it differs between environments.
# .env.production
DGATEWAY_SECRET_KEY=dg_live_sk_your_live_key
DGATEWAY_WEBHOOK_SECRET=whsec_your_live_webhook_secret
DGATEWAY_API_URL=https://api.dgateway.io

That is it. No separate approval process, no lengthy review. When you are ready, flip the keys and start collecting real payments.

Pre-Launch Checklist

CheckStatus
Webhook signature verification implementedRequired
Idempotent webhook handler (handles duplicates)Required
HTTPS endpoint for webhooksRequired
Error handling for failed/expired transactionsRequired
Test mode fully tested with all scenariosRequired
Live API keys stored securely (env vars, not code)Required
Callback URL updated for production domainRequired
Provider credentials configured in DGateway dashboardRequired
Transaction status polling implemented (for UI)Recommended
Webhook retry handling understoodRecommended

Troubleshooting FAQ

"My webhook is not being called"

  1. Check the callback URL — Is it publicly accessible? Can you reach it from the internet?
  2. Check HTTPS — DGateway requires HTTPS for production webhooks.
  3. Check your webhook logs — Go to your dashboard and look at the webhook delivery logs. They show the response code your server returned.
  4. Test locally — Use a tool like ngrok to expose your local server for testing.
# Expose your local server
ngrok http 3000
 
# Use the ngrok URL as your callback_url
# https://abc123.ngrok.io/api/webhooks/dgateway

"Signature verification is failing"

  1. Use the raw request body — Do not parse JSON before verifying. Use the raw string/bytes.
  2. Check the webhook secret — Make sure you are using the webhook secret, not the API secret key.
  3. Use timing-safe comparison — Use crypto.timingSafeEqual (Node.js) or hmac.compare_digest (Python).

"Transaction stays in pending status"

  1. In test mode — Test transactions auto-complete after a few seconds. Wait and check again.
  2. In production — The customer has not confirmed on their phone yet. The push prompt may have timed out. Check if the customer received the prompt.
  3. Provider issues — Check provider health in your dashboard. If a provider line is down, DGateway will notify you.

"I am getting a 401 Unauthorized error"

  1. Check the API key prefix — Test keys start with dg_test_sk_, live keys with dg_live_sk_.
  2. Check the Authorization header — Format must be Bearer YOUR_KEY with a space after "Bearer".
  3. Check key status — Keys can be revoked from the dashboard. Generate a new one if needed.

"Amount is wrong in the transaction"

  1. UGX has no subunits — UGX 5,000 is 5000, not 500000. Unlike USD where $5 might be 500 cents, UGX amounts are face value.
  2. USD uses cents — $5.00 should be 500 if your currency is USD.

"Duplicate charges on the customer"

  1. Implement idempotency — Use the transaction id to prevent processing the same webhook twice.
  2. Use metadata — Attach your order_id in metadata and check if the order has already been fulfilled before processing.

What Comes Next

This guide covered the basics — creating a collection and handling the result via webhooks. But DGateway can do much more:

  • Disbursements — Send money to mobile money accounts and bank accounts.
  • Subscription billing — Create recurring payment plans with automatic charging.
  • Payment links — Generate shareable links that accept payments without any code.
  • Transaction queries — Check the status of any transaction at any time via the API.
  • Digital products — Sell files with automatic delivery after payment.
  • Courses — Build and sell online courses with video streaming.
  • Events — Sell event tickets with QR code check-in.
  • Coupons — Create discount codes with usage caps and expiry.
  • Webhooks dashboard — Monitor, debug, and re-deliver webhooks from the UI.

Explore the full API documentation at docs.dgateway.io and build something great.