# DGateway — Unified Payment API for East Africa # Full LLM Reference — Last updated 2026-03-25 > DGateway is a unified payment aggregation API. One API key, one endpoint format, > one response shape — regardless of which payment provider processes the transaction. > Supports Mobile Money (MTN, Airtel) and Card Payments (Visa, Mastercard) across > Uganda, Kenya, Tanzania, and Rwanda. ## Base URL - Production: https://dgatewayapi.desispay.com - Admin Dashboard: https://dgatewayadmin.desispay.com ## Authentication All V1 API endpoints require an API key in the header: X-Api-Key: dgw_live_xxxxxxxxxxxxx API keys are prefixed: - dgw_live_ → Live mode (real money) - dgw_test_ → Test mode (sandbox, no real charges) Test mode limitations: - Only Iotec and Stripe test environments available (Relworx unavailable in test) - Cannot create withdrawals --- ## Payment Providers | Provider | Methods | Currencies | Collect | Disburse | |----------|----------------------|-------------------|---------|----------| | iotec | MTN MoMo, Airtel | UGX | Yes | Yes | | relworx | MTN, Airtel, Visa | UGX, KES, TZS, RWF | Yes | Yes | | stripe | Visa, Mastercard | USD, EUR, GBP, KES, UGX | Yes | No | Default provider: If `provider` is omitted, defaults to `iotec`. Best practice: Always pass the `provider` field explicitly. --- ## Endpoints — Quick Reference ### Payments POST /v1/payments/collect Collect money from a customer POST /v1/payments/disburse Send money to a phone number GET /v1/payments/transactions List transactions (paginated) GET /v1/payments/transactions/:ref Get transaction by reference POST /v1/webhooks/verify Poll/verify transaction status ### Wallets & Withdrawals GET /v1/wallets Get wallet balances per provider/currency POST /v1/withdrawals Create withdrawal request GET /v1/withdrawals List withdrawals (paginated) ### Provider Health (No Auth Required) GET /api/provider-health Check health status of all provider lines (MTN, Airtel) ### Subscription Plans POST /v1/subscriptions/plans Create a billing plan GET /v1/subscriptions/plans List plans GET /v1/subscriptions/plans/:id Get plan details PUT /v1/subscriptions/plans/:id Update plan DELETE /v1/subscriptions/plans/:id Deactivate plan ### Subscriptions POST /v1/subscriptions Subscribe a customer to a plan GET /v1/subscriptions List subscriptions GET /v1/subscriptions/:id Get subscription with cycles POST /v1/subscriptions/:id/charge Charge the next pending cycle POST /v1/subscriptions/:id/pause Pause subscription POST /v1/subscriptions/:id/resume Resume subscription POST /v1/subscriptions/:id/cancel Cancel subscription ### Store & Checkout (Public — No Auth) GET /api/store/shop/:appSlug Get store storefront GET /api/store/:slug Get product or payment link POST /api/store/:slug/pay Initiate checkout payment GET /api/store/status/:reference Check payment status GET /api/store/download/:txRef Get download URL (digital product) ### Webhooks (Public — No Auth) POST /v1/webhooks/:provider Receive inbound webhook from provider --- ## Endpoint Details ### POST /v1/payments/collect Collect money from a customer via Mobile Money or Card. Request: ```json { "amount": 50000, // required, number, >0 "currency": "UGX", // required, string (UGX|KES|TZS|RWF|USD|EUR|GBP) "phone_number": "256771234567", // required for mobile money (256XXXXXXXXX or 0XXXXXXXXX) "provider": "iotec", // optional, defaults to "iotec" (iotec|relworx|stripe) "description": "Order #1234", // optional, max 500 chars "callback_url": "https://myapp.com/hook", // optional, URL for webhook callback "metadata": { "order_id": "123" } // optional, arbitrary JSON } ``` Response (200): ```json { "data": { "reference": "dgw_abc123xyz", "provider": "iotec", "provider_ref": "iotec_txn_456", "status": "pending", "amount": 50000, "currency": "UGX", "phone_number": "256771234567", "raw_response": {}, "client_secret": "", "stripe_publishable_key": "" }, "message": "Collection initiated" } ``` Notes: - For Stripe: response includes `client_secret` and `stripe_publishable_key` for frontend Elements - Customer receives a USSD prompt on their phone (mobile money) - Poll status via POST /v1/webhooks/verify with the `reference` - If the vendor line (MTN/Airtel) is down, returns 503 PROVIDER_LINE_DOWN ### POST /v1/payments/disburse Send money to a phone number (mobile money only). Request: ```json { "amount": 25000, // required, number, >0 "currency": "UGX", // required, string, 3 chars "phone_number": "256771234567", // required (256XXXXXXXXX or 0XXXXXXXXX) "provider": "iotec", // optional (iotec|relworx — Stripe cannot disburse) "description": "Loan disbursement" // optional, max 500 chars } ``` Response (200): ```json { "data": { "reference": "dgw_xyz789abc", "provider": "iotec", "status": "pending", "amount": 25000, "currency": "UGX" }, "message": "Disbursement initiated" } ``` ### GET /v1/payments/transactions List all transactions for your app. Query parameters: - page (int, default 1) - per_page (int, default 20, max 100) - status (string: pending|completed|failed) - direction (string: collect|disburse) - provider (string: iotec|relworx|stripe) - currency (string: UGX|KES|TZS|RWF|USD) - from (ISO date) - to (ISO date) Response (200): ```json { "data": [ { "reference": "dgw_abc123xyz", "provider": "iotec", "direction": "collect", "amount": 50000, "platform_fee": 4000, "net_amount": 46000, "currency": "UGX", "status": "completed", "failure_reason": "", "phone_number": "256771234567", "description": "Order #1234", "is_test": false, "created_at": "2026-03-14T10:00:00Z" } ], "meta": { "page": 1, "per_page": 20, "total": 150, "total_pages": 8 } } ``` Notes: - `failure_reason` is populated when status is "failed" — use it to show meaningful errors to users - `platform_fee` and `net_amount` show the commission breakdown ### GET /v1/payments/transactions/:ref Get a single transaction by reference. Response includes `failure_reason` when failed. ### POST /v1/webhooks/verify Poll a transaction's current status from the provider. Request: ```json { "reference": "dgw_abc123xyz" } ``` Response (200): ```json { "data": { "reference": "dgw_abc123xyz", "status": "completed", "provider": "iotec", "amount": 50000, "currency": "UGX" }, "message": "Transaction status verified" } ``` ### GET /v1/wallets Get wallet balances per provider and currency. Response (200): ```json { "data": [ { "provider": "iotec", "balance": 150000.50, "currency": "UGX" }, { "provider": "relworx", "balance": 50000.00, "currency": "KES" } ], "message": "Wallet balances retrieved" } ``` Balance formula: balance = SUM(net_amount from completed collects) - SUM(amount from pending/approved/completed withdrawals) ### POST /v1/withdrawals Withdraw funds from your app wallet to a phone number or bank account. Request: ```json { "amount": 500000, // required, number, >0, must not exceed available balance "currency": "UGX", // required, string "provider": "iotec", // optional, defaults to "iotec" "method": "mobile_money", // required: "mobile_money" | "bank_transfer" "destination": "256771234567", // required, phone (256XXXXXXXXX or 0XXXXXXXXX) or account number "destination_name": "John Doe", // optional "bank_name": "", // required if method is "bank_transfer" "note": "Monthly payout" // optional } ``` Response (201): ```json { "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": "2026-03-14T10:30:00Z" }, "message": "Withdrawal disbursed successfully" } ``` Notes: - Mobile money withdrawals are auto-disbursed immediately - Balance is validated per provider: you cannot withdraw more than your provider-specific balance - Error if insufficient balance: { "error": { "code": "INSUFFICIENT_BALANCE", "message": "..." } } - Test API keys cannot create withdrawals - Status: pending → completed (success) or failed (with error in note field) ### GET /v1/withdrawals List withdrawals. Supports query parameters: page, per_page, status. ### GET /api/provider-health Check the health status of provider vendor lines (MTN, Airtel). No authentication required. Response (200): ```json { "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" } ] } ``` Notes: - Health checks run automatically daily at 6 AM UTC - When a line is unhealthy, collect/disburse requests to that vendor return 503 PROVIDER_LINE_DOWN - Use this endpoint to proactively show users which lines are available ### POST /v1/subscriptions/plans Create a recurring billing plan. Request: ```json { "name": "Monthly Premium", // required, max 255 chars "description": "Premium access", // optional, max 2000 chars "amount": 50000, // required, >0 "currency": "UGX", // required, 3 chars "interval": "monthly", // required: daily|weekly|monthly|yearly|custom "interval_days": 0, // required if interval=custom, >0 "trial_days": 7, // optional, 0 = no trial "grace_days": 3, // optional, default 3 "setup_fee": 10000, // optional, one-time fee on first charge "max_cycles": 0, // optional, 0 = unlimited "providers": ["iotec", "relworx"], // optional, allowed providers "metadata": {} // optional } ``` ### POST /v1/subscriptions Subscribe a customer to a plan. Request: ```json { "plan_id": 1, // required "customer_email": "jane@example.com", // required, valid email "customer_name": "Jane Doe", // required, max 255 chars "customer_phone": "256771234567", // required (256XXXXXXXXX or 0XXXXXXXXX) "provider": "iotec", // optional "start_now": true, // optional, true = skip trial "metadata": {} // optional } ``` Subscription statuses: trialing → active → past_due → cancelled | completed Actions: pause (active→paused), resume (paused→active), cancel (any→cancelled) ### POST /v1/subscriptions/:id/charge Charge the next pending billing cycle. Request: ```json { "phone_number": "256771234567", // required (256XXXXXXXXX or 0XXXXXXXXX) "provider": "iotec" // optional } ``` --- ## Phone Number Validation All API endpoints enforce strict phone number validation. Only these formats are accepted (digits only, no spaces/dashes/letters): | Format | Example | Description | |----------------|-------------|--------------------------------| | 256XXXXXXXXX | 256771234567| International format (12 digits)| | 0XXXXXXXXX | 0771234567 | Local format (10 digits) | The + prefix is optional and will be stripped automatically. Invalid numbers return error code: INVALID_PHONE Uganda vendor detection from phone prefix: - MTN: 077x, 078x, 076x, 031x - Airtel: 070x, 075x, 074x, 020x --- ## Transaction Lifecycle 1. Your app sends a collect/disburse/charge request 2. DGateway creates a transaction with status="pending" and reference="dgw_..." 3. DGateway calls the provider API 4. Customer confirms (mobile money USSD prompt or card payment) 5. Provider sends a webhook to DGateway → status updates to "completed" or "failed" 6. DGateway forwards webhook to your app (if webhook_url configured) 7. OR your app polls via POST /v1/webhooks/verify Statuses: pending → completed | failed When failed, the `failure_reason` field contains a human-readable explanation. --- ## Commission & Fees - Digital products & payment links: 10% commission - Courses: 12% commission - API transactions: 8% commission (default, configurable per app) - Formula: platform_fee = round(amount × rate/100, 2), net_amount = amount - platform_fee - Fee applies to collect transactions only - The `net_amount` is what accumulates in your wallet --- ## Error Response Format All errors follow this shape: ```json { "error": { "code": "ERROR_CODE", "message": "Human-readable description" } } ``` Error codes: | Code | HTTP | Description | |-------------------------|------|----------------------------------------------------------| | VALIDATION_ERROR | 400 | Invalid request body or missing required fields | | INVALID_PHONE | 400 | Phone number format invalid | | INVALID_CURRENCY | 400 | Currency not supported | | INVALID_PROVIDER | 400 | Provider not supported | | PROVIDER_ERROR | 400 | Provider not configured or returned an error | | PROVIDER_LINE_DOWN | 503 | Vendor line (MTN/Airtel) is currently down | | INSUFFICIENT_BALANCE | 400 | Withdrawal amount exceeds available balance | | ACCOUNT_HAS_ACTIVITY | 403 | Cannot delete account with transaction history | | TEST_MODE | 400 | Operation not allowed with test API key | | INVALID_STATUS | 400 | Subscription in wrong state for this action | | NO_PENDING_CYCLE | 400 | No billing cycle to charge | | NOT_FOUND | 404 | Resource not found | | RATE_LIMIT | 429 | Too many requests | | DB_ERROR | 500 | Internal database error | --- ## Rate Limits - POST /v1/payments/collect: 60 requests / minute (per IP) - POST /v1/payments/disburse: 30 requests / minute (per IP) - Auth endpoints: 3-5 requests / 15 minutes (per IP) - Default: 100 requests / minute (per IP) --- ## Webhook Events DGateway normalizes all provider webhooks into a consistent format. Outbound webhook to your app: ```json { "event": "transaction.completed", "data": { "reference": "dgw_abc123xyz", "status": "completed", "amount": 50000, "currency": "UGX", "provider": "iotec", "direction": "collect", "failure_reason": "" }, "timestamp": "2026-03-14T10:35:00Z" } ``` Subscription webhook events: - subscription.created - subscription.payment_completed - subscription.completed (all cycles done) - subscription.cancelled - subscription.paused - subscription.resumed --- ## Integration Examples ### Node.js / TypeScript ```typescript const API = "https://dgatewayapi.desispay.com"; const KEY = process.env.DGATEWAY_API_KEY!; // Check provider health before collecting const health = await fetch(`${API}/api/provider-health`); const { data: lines } = await health.json(); const mtnDown = lines.find((l: any) => l.vendor === "mtn" && !l.is_healthy); if (mtnDown) console.warn("MTN line is down, use Airtel numbers"); // Collect payment const res = await fetch(`${API}/v1/payments/collect`, { method: "POST", headers: { "X-Api-Key": KEY, "Content-Type": "application/json" }, body: JSON.stringify({ amount: 50000, currency: "UGX", phone_number: "256771234567", provider: "iotec" }), }); const { data, error } = await res.json(); if (error) { // Handle specific error codes if (error.code === "PROVIDER_LINE_DOWN") { console.error("Network is down, try a different phone number"); } else if (error.code === "INVALID_PHONE") { console.error("Bad phone format, use 256XXXXXXXXX or 0XXXXXXXXX"); } else { console.error(error.message); } } else { console.log("Reference:", data.reference); // "dgw_..." // Poll status const verify = await fetch(`${API}/v1/webhooks/verify`, { method: "POST", headers: { "X-Api-Key": KEY, "Content-Type": "application/json" }, body: JSON.stringify({ reference: data.reference }), }); const result = await verify.json(); if (result.data.status === "failed") { console.error("Failed:", result.data.failure_reason); } } ``` ### Go ```go body, _ := json.Marshal(map[string]interface{}{ "amount": 50000, "currency": "UGX", "phone_number": "256771234567", "provider": "iotec", }) 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, _ := http.DefaultClient.Do(req) ``` ### PHP / Laravel ```php $response = Http::withHeaders([ 'X-Api-Key' => config('services.dgateway.key'), ])->post('https://dgatewayapi.desispay.com/v1/payments/collect', [ 'amount' => 50000, 'currency' => 'UGX', 'phone_number' => '256771234567', 'provider' => 'iotec', ]); if ($response->failed()) { $error = $response->json('error'); // Handle PROVIDER_LINE_DOWN, INVALID_PHONE, etc. } $reference = $response->json('data.reference'); ``` ### Python ```python import requests resp = requests.post("https://dgatewayapi.desispay.com/v1/payments/collect", headers={"X-Api-Key": API_KEY, "Content-Type": "application/json"}, json={"amount": 50000, "currency": "UGX", "phone_number": "256771234567", "provider": "iotec"}) data = resp.json() if "error" in data: if data["error"]["code"] == "PROVIDER_LINE_DOWN": print("Network down, try later") else: print(f"Error: {data['error']['message']}") else: ref = data["data"]["reference"] ``` ### cURL ```bash # Collect payment curl -X POST https://dgatewayapi.desispay.com/v1/payments/collect \ -H "X-Api-Key: dgw_live_your_key" \ -H "Content-Type: application/json" \ -d '{"amount":50000,"currency":"UGX","phone_number":"256771234567","provider":"iotec"}' # Check provider health (no auth required) curl https://dgatewayapi.desispay.com/api/provider-health ``` --- ## Common Use Cases ### 1. Collect a Mobile Money Payment POST /v1/payments/collect with amount, currency, phone_number, provider ### 2. Accept Card Payments (Stripe) POST /v1/payments/collect with provider="stripe", currency="USD" Use client_secret from response in Stripe Elements on frontend ### 3. Send Money to a User POST /v1/payments/disburse with amount, currency, phone_number, provider ### 4. Set Up Recurring Billing 1. POST /v1/subscriptions/plans — create a plan 2. POST /v1/subscriptions — subscribe a customer 3. POST /v1/subscriptions/:id/charge — charge each cycle 4. POST /v1/webhooks/verify — poll for payment confirmation ### 5. Withdraw Earnings 1. GET /v1/wallets — check available balance 2. POST /v1/withdrawals — request withdrawal to mobile money or bank ### 6. Check Provider Health Before Requests 1. GET /api/provider-health — check which lines are up 2. If a vendor is down, warn users or route to a different number ### 7. Sell Digital Products (No Code) Use the admin dashboard to upload files, set prices, and share checkout links. Buyers pay via mobile money or card and download automatically. ### 8. Handle Failed Transactions 1. GET /v1/payments/transactions/:ref — check status and failure_reason 2. Show failure_reason to your users for transparency --- ## Claude Code Skill Developers using Claude Code can install the DGateway skill for AI-assisted integration: ```bash claude skill add --url https://dgateway.desispay.com/skill/SKILL.md ``` This gives Claude full context about the DGateway API to help write integration code. --- ## Links - Website: https://dgateway.desispay.com - Docs: https://dgateway.desispay.com/docs - LLM Reference: https://dgateway.desispay.com/llms.txt - Admin: https://dgatewayadmin.desispay.com - GitHub: https://github.com/MUKE-coder/dgateway - WhatsApp: https://wa.me/message/5USU26346OWRF1 - Contact: https://dgateway.desispay.com/contact