DGS-Pay API v2
Dashboard
API Reference · v2

DGS-Pay Integration Guide

Complete reference for collecting and disbursing payments in Rwanda — Mobile Money, Cards, Payment Links, Wallet, and Webhooks.

MTN & Airtel Visa & Mastercard AES-256-GCM Live Webhooks
Overview

Getting Started

DGS-Pay is a payment infrastructure layer built specifically for Rwanda. It lets you collect funds from customers via Mobile Money (MTN & Airtel) or card (Visa/Mastercard), disburse to bank accounts and mobile wallets, generate shareable payment links, manage your merchant wallet, and track everything through real-time webhooks and the merchant portal.

All API requests are made over HTTPS to a versioned base URL. You develop against a Sandbox environment that mirrors production but uses simulated money, then switch the base URL to go live — no code changes beyond that.

Mobile Money

MTN & Airtel Rwanda

Card Payments

Visa & Mastercard

Disbursements

Bank & Mobile transfers

Base URLs

Every API endpoint is appended to one of these base URLs depending on your environment:

Sandbox / Test
https://test.pay.digitalservicescenter.rw/generation/v2
Production / Live
https://pay.digitalservicescenter.rw/generation/v2

Request Format

All requests must use Content-Type: application/json and include your API credentials in the headers. Here is a minimal example using cURL:

cURL
curl -X POST \
  https://pay.digitalservicescenter.rw/generation/v2/charges \
  -H "X-DGS-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 2000,
    "currency": "RWF"
  }'
Security

Authentication

Every DGS-Pay API request requires two headers: your API key and your account number. Both are available in your merchant portal under Settings → API Settings. Test and live environments each have their own separate set of credentials.

Required Headers

Include these on every request:

HTTP Headers
X-DGS-API-Key: YOUR_DGS_API_KEY
X-DGS-ACCOUNT-NUMBER: YOUR_DGS_ACCOUNT_NUMBER
Content-Type: application/json

cURL Example with Auth

cURL
curl -X GET \
  https://pay.digitalservicescenter.rw/generation/v2/balance \
  -H "X-DGS-API-Key: sk_live_xxxxxxxxxxxxxxxxxxxx" \
  -H "X-DGS-ACCOUNT-NUMBER: ACC-0001234" \
  -H "Content-Type: application/json"

IP Whitelisting (Production only)

In production you can restrict API access to a specific set of server IP addresses. Any request arriving from an IP not on your whitelist will be rejected with a 91 Auth Error. Configure your allowed IPs in Settings → API Settings in the merchant portal. This feature is not enforced in sandbox.

Security Rules — Never Skip These
  • Never embed your API key in client-side JavaScript or mobile app source code — keys must only exist on your server.
  • Store keys as environment variables (e.g. .env files) and keep them out of version control.
  • Enable IP whitelisting before switching to production.
  • Rotate your API keys periodically and immediately if you suspect exposure.
Configuration

Environments & Testing

DGS-Pay operates two isolated environments. The sandbox mimics production exactly but uses simulated money — no real funds move. When you are ready to go live, simply swap the base URL. Credentials are environment-specific and must not be mixed.

Sandbox / Test
https://test.pay.digitalservicescenter.rw/generation/v2
Production / Live
https://pay.digitalservicescenter.rw/generation/v2
Merchant Portal Access your dashboard at https://pay.digitalservicescenter.rw/smart-merchant/ — the same portal serves both environments via environment tabs.

Simulating Scenarios with scenario_key

In sandbox, you can force specific outcomes by including a scenario_key field in your request body. This saves you from needing real cards, phones, or network responses to test your integration logic.

Scenario TypeAllowed ValuesWhat it simulates
Card Authorization scenario:auth_pin
scenario:auth_pin_3ds
scenario:auth_3ds
scenario:auth_avs
Forces the charge response to require PIN, 3DS, AVS, or combined auth.
Issuer Responses issuer:approved
issuer:insufficient_funds
issuer:incorrect_pin
issuer:expired_card
issuer:invalid_cvv
issuer:do_not_honor
issuer:system_error
Simulates the card bank's response after you authorize.
Mobile Money scenario:auth_pin
scenario:auth_redirect
auth_pin = payment auto-approved. auth_redirect = redirect flow triggered.
Transfers / Disbursements scenario:successful
scenario:insufficient_balance
scenario:invalid_currency
Simulates outcomes of wallet disbursements.

Example — Approved Card in Sandbox

JSON — Request Body (append to normal card payload)
{
  "scenario_key": "scenario:auth_pin&issuer:approved",
}
Collections

Mobile Money API

Collect payments directly from MTN or Airtel Money wallets in Rwanda. When you initiate a charge, the customer receives an STK push prompt on their handset — they enter their PIN to approve, and your webhook is notified of the outcome. No card details or card infrastructure needed.

Mobile Money payments are currently limited to Rwanda (RWF). Multi-currency and additional country support is planned for API v3.

Rwanda Only The currency field must be RWF. Any other currency will return a validation error (90).

Initiate a Mobile Money Payment

Send a POST /charges request with the customer and payment details. DGS-Pay forwards the STK push to the customer's phone and returns a pending status while waiting for the customer's approval.

POST/charges
ParameterTypeRequiredDescription
amountnumberRequiredPayment amount. Must be a positive number.
currencystringRequiredISO 4217 code. Must be RWF.
payment_typestringRequiredMust be mobile_money.
customer_emailstringRequiredCustomer's email address.
customer_first_namestringRequiredCustomer's first name.
customer_last_namestringRequiredCustomer's last name.
customer_phonestringRequiredPhone number without country code, e.g. 789654321.
customer_phone_country_codestringRequiredCountry code without +, e.g. 250.
redirect_urlstringRequiredURL to redirect the customer to after payment completes.
mobile_money_networkstringRequiredMTN or Airtel.
mobile_money_phonestringRequiredPhone number that will receive the STK push.
narrationstringOptionalShort description shown to the customer, e.g. Invoice #5678.
cURL
curl -X POST \
  https://pay.digitalservicescenter.rw/generation/v2/charges \
  -H "X-DGS-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 2000,
    "currency": "RWF",
    "payment_type": "mobile_money",
    "customer_email": "jane.doe@example.com",
    "customer_first_name": "Jane",
    "customer_last_name": "Doe",
    "customer_phone": "789654321",
    "customer_phone_country_code": "250",
    "redirect_url": "https://yourdomain.com/payment-success",
    "narration": "Payment for Invoice #5678",
    "mobile_money_network": "MTN",
    "mobile_money_phone": "789654321"
  }'
JSON
{
  "amount": 2000,
  "currency": "RWF",
  "payment_type": "mobile_money",
  "customer_email": "jane.doe@example.com",
  "customer_first_name": "Jane",
  "customer_last_name": "Doe",
  "customer_phone": "789654321",
  "customer_phone_country_code": "250",
  "redirect_url": "https://yourdomain.com/payment-success",
  "narration": "Payment for Invoice #5678",
  "mobile_money_network": "MTN",
  "mobile_money_phone": "789654321"
}

Response Format

DGS-Pay returns one of three states. The initial response is almost always pending — the payment completes asynchronously when the customer approves or rejects the STK push.

JSON — STK Push sent, awaiting customer approval
{
  "status": "success",
  "message": "Charge initiated",
  "transaction_id": "123456789",
  "dgs_reference": "dgs_123456789",
  "amount": 2000,
  "currency": "RWF",
  "transaction_status": "pending",
  "payment_method_type": "mobile_money",
  "next_action_type": "redirect_url",
  "next_action_redirect_url": "https://provider.com/authorize",
  "display_text": "Complete payment on your mobile device"
}
JSON — Payment completed successfully (via webhook)
{
  "status": "success",
  "message": "Payment completed",
  "transaction_id": "987654321",
  "dgs_reference": "dgs_987654321",
  "amount": 2000,
  "currency": "RWF",
  "transaction_status": "successful",
  "payment_method_type": "mobile_money",
  "mobile_money_network": "MTN",
  "mobile_money_phone": "789654321",
  "mobile_money_country_code": "250"
}
JSON — Payment declined or cancelled
{
  "status": "failed",
  "message": "Charge failed",
  "transaction_id": "123456789",
  "dgs_reference": "dgs_123456789",
  "amount": 2000,
  "currency": "RWF",
  "transaction_status": "failed",
  "payment_method_type": "mobile_money",
  "issuer_response_type": "declined",
  "issuer_response_code": "05",
  "processor_response": "Insufficient funds"
}
Best Practice Because the customer approves asynchronously, always use a webhook to get notified of the final outcome rather than polling. Do not mark an order as paid based on the pending response alone.
Collections

Card Payments API

Accept Visa and Mastercard payments with AES-256-GCM client-side encryption. Sensitive card data (number, expiry, CVV) is encrypted in the browser or mobile app before it ever reaches your server. Your server then forwards the encrypted payload to DGS-Pay, which decrypts and processes it securely.

Card Data Encryption

Before sending a charge request you must encrypt all four card fields using your merchant encryption key (from Settings → API Settings). Test and live environments each have a different key.

PCI Compliance — Mandatory
  • Encrypt card data only in the browser or mobile app — never on your backend server.
  • Never log, store, inspect, or transmit raw card numbers, CVV values, or PINs anywhere.
  • Serve your checkout page exclusively over HTTPS.
  • Do not pass raw card fields to your server even temporarily.

Encryption Flow

Flow Diagram
1. Customer fills card form in browser/app
encryptCard()  runs in browser using DGS Encryption Key
          
2. Browser sends encrypted payload to YOUR server
          
3. Your server forwards encrypted payload to DGS-Pay /charges
          
4. DGS-Pay decrypts with private key and processes payment

JavaScript Encryption Implementation

JavaScript — Browser / Client-side only
// Load your encryption key from the merchant dashboard
const ENC_KEY = "YOUR_ENCRYPTION_KEY_FROM_DASHBOARD";

async function importKey(b64) {
  const raw = Uint8Array.from(atob(b64), c => c.charCodeAt(0));
  return crypto.subtle.importKey(
    'raw', raw, { name: 'AES-GCM' }, false, ['encrypt']
  );
}

function hexNonce() {
  // Produces a 12-byte (24 hex char) nonce — required by DGS-Pay
  const bytes = crypto.getRandomValues(new Uint8Array(6));
  return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
}

async function encryptField(plaintext, key, nonce) {
  const iv  = new TextEncoder().encode(nonce);
  const enc = await crypto.subtle.encrypt(
    { name: 'AES-GCM', iv, tagLength: 128 },
    key,
    new TextEncoder().encode(plaintext)
  );
  return btoa(String.fromCharCode(...new Uint8Array(enc)));
}

async function encryptCard(cardNumber, month, year, cvv) {
  const key   = await importKey(ENC_KEY);
  const nonce = hexNonce(); // all four fields share one nonce

  const [encCard, encMon, encYr, encCvv] = await Promise.all([
    encryptField(cardNumber, key, nonce),
    encryptField(month,      key, nonce),
    encryptField(year,       key, nonce),
    encryptField(cvv,        key, nonce),
  ]);

  return {
    card_encrypted_number:       encCard,
    card_encrypted_expiry_month: encMon,
    card_encrypted_expiry_year:  encYr,
    card_encrypted_cvv:          encCvv,
    card_nonce:                  nonce,
  };
}

// Usage example
const payload = await encryptCard("4532123456789010", "12", "2028", "123");
// payload now contains card_encrypted_* fields + card_nonce
Nonce Rules
  • All four card fields must be encrypted using the same nonce.
  • The nonce must be exactly 12 bytes — 24 hex characters.
  • Generate a new fresh nonce for every transaction; never reuse.

Initiate a Card Payment

After encrypting the card fields on the client, send them along with the transaction details to /charges.

POST/charges
cURL
curl -X POST \
  https://pay.digitalservicescenter.rw/generation/v2/charges \
  -H "X-DGS-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 5000,
    "currency": "RWF",
    "payment_type": "card",
    "customer_email": "john.doe@example.com",
    "customer_first_name": "John",
    "customer_last_name": "Doe",
    "customer_phone": "788123456",
    "customer_phone_country_code": "250",
    "redirect_url": "https://yourdomain.com/payment-success",
    "narration": "Payment for Order #1234",
    "card_encrypted_number": "<encrypted_number>",
    "card_encrypted_expiry_month": "<encrypted_month>",
    "card_encrypted_expiry_year": "<encrypted_year>",
    "card_encrypted_cvv": "<encrypted_cvv>",
    "card_nonce": "<24_hex_nonce>"
  }'
JSON
{
  "amount": 5000,
  "currency": "RWF",
  "payment_type": "card",
  "customer_email": "john.doe@example.com",
  "customer_first_name": "John",
  "customer_last_name": "Doe",
  "customer_phone": "788123456",
  "customer_phone_country_code": "250",
  "redirect_url": "https://yourdomain.com/payment-success",
  "narration": "Payment for Order #1234",
  "card_encrypted_number": "<encrypted_number>",
  "card_encrypted_expiry_month": "<encrypted_month>",
  "card_encrypted_expiry_year": "<encrypted_year>",
  "card_encrypted_cvv": "<encrypted_cvv>",
  "card_nonce": "<24_hex_nonce>"
}

Payment Authorization

Some cards require additional verification after the initial charge. The charge response will include a next_action_type field telling you what the customer must do. You then submit their input to /authorize.

1
POST /charges
2
Check next_action_type
3
Collect customer input
4
POST /authorize
Completion Criteria A card payment is fully complete only when transaction_status equals "successful" AND the response contains no next_action_type field.
POST/authorize
JSON — PIN Authorization
{
  "dgs_reference": "dgs_123456789",
  "auth_type": "pin",
  "auth_pin_encrypted": "<base64_encrypted_pin>",
  "auth_pin_nonce": "abc123def456"
}
JSON — OTP Authorization (sent to customer's phone/email)
{
  "dgs_reference": "dgs_123456789",
  "auth_type": "otp",
  "auth_otp": "123456"
}
JSON — AVS Address Verification
{
  "dgs_reference": "dgs_123456789",
  "auth_type": "avs",
  "avs_line1": "123 Main Street",
  "avs_line2": "Apt 4B",
  "avs_city": "Kigali",
  "avs_state": "Kigali City",
  "avs_country": "RW",
  "avs_postal_code": "00000"
}
auth_typeRequired FieldsWhen triggered
pinauth_pin_encrypted, auth_pin_nonceCard issuer requires PIN verification.
otpauth_otp3DS OTP sent to customer's registered number.
avsavs_line1, avs_country (minimum)Issuer requires billing address match.
Disbursements

Disburse Funds

The Disbursement API lets you send money from your RWF merchant wallet to a recipient's bank account or mobile money wallet. This is useful for paying out vendors, refunding customers, or distributing salaries.

The merchant's wallet is debited the full amount. A fee is then deducted from the recipient's end — the recipient receives amount − fee. Always verify the amount exceeds the applicable fee before initiating.

Fee Deducted From Recipient You pay the gross amount. The recipient receives the net. If you want the recipient to receive exactly 10,000 RWF and the fee is 100 RWF, you must send 10,100 RWF.
POST/disburse
FieldTypeRequiredDescription
amountnumberRequiredGross amount in RWF to send from your wallet.
destination_typestringRequiredbank or mobile_money.
recipientobjectRequiredRecipient details. Fields vary by destination type.
narrationstringOptionalShort note shown on the recipient's statement.
callback_urlstringOptionalOverride the default webhook URL for this transfer only.

Bank Recipient Fields

FieldRequired
bank_keyRequired
account_numberRequired
nameOptional
branchOptional

Mobile Money Recipient Fields

FieldRequired
msisdnRequired
networkRequired
countryRequired
nameOptional
cURL — Bank Disbursement
curl -X POST \
  https://pay.digitalservicescenter.rw/generation/v2/disburse \
  -H "X-DGS-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 10000,
    "destination_type": "bank",
    "recipient": {
      "bank_key": "kcb_rw",
      "account_number": "1234567890",
      "name": "John Doe",
      "branch": "Kigali Main"
    },
    "narration": "Vendor payment April 2026"
  }'
cURL — Mobile Money Disbursement
curl -X POST \
  https://pay.digitalservicescenter.rw/generation/v2/disburse \
  -H "X-DGS-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 5000,
    "destination_type": "mobile_money",
    "recipient": {
      "msisdn": "+250788123456",
      "network": "MTN",
      "country": "RW",
      "name": "Jane Doe"
    }
  }'
Phone Normalization Phone numbers are automatically normalized — you can pass local format (0788123456) or international format (+250788123456), both are accepted.
Disbursements

Disbursement Webhook

The POST /disburse response only confirms the transfer is queued for processing. The actual outcome — success or failure — is delivered via a webhook after the provider processes the transfer. You must handle this webhook to reliably update your records.

Webhook is the Source of Truth Do not mark a transfer as completed based on the API response alone. Only the webhook event confirms final status.
EventDescription
disbursement.successTransfer confirmed by provider. Recipient has been credited.
disbursement.failedTransfer could not be completed. Check fail_reason.
JSON — Webhook Payload
{
  "event": "disbursement.success",
  "dgs_reference": "DGS123456789",
  "flw_transfer_id": 987654,
  "status": "success",
  "amount": 10000,
  "fee": 100,
  "recipient_gets": 9900,
  "currency": "RWF",
  "destination_type": "bank",
  "recipient": {
    "account_number": "1234567890",
    "bank_key": "kcb_rw",
    "name": "John Doe"
  },
  "proof": "FLW-PROOF-123456"
}
JSON — Webhook Payload
{
  "event": "disbursement.failed",
  "dgs_reference": "DGS123456789",
  "flw_transfer_id": 987654,
  "status": "failed",
  "amount": 10000,
  "fee": 100,
  "recipient_gets": 9900,
  "currency": "RWF",
  "destination_type": "mobile_money",
  "fail_reason": "Invalid account details"
}

Idempotent Handler — Pseudocode

Because webhooks can be retried on network failure, your handler must be idempotent. Use dgs_reference as the unique key to detect duplicates.

Pseudocode
function handleWebhook(payload) {
  const ref = payload.dgs_reference;

  if (alreadyProcessed(ref)) {
    return HTTP_200; // duplicate — acknowledge but skip
  }

  if (payload.event === "disbursement.success") {
    markTransferSuccess(ref, payload.proof);
  } else if (payload.event === "disbursement.failed") {
    markTransferFailed(ref, payload.fail_reason);
  }

  recordAsProcessed(ref);
  return HTTP_200;
}
Best Practices
  • Index dgs_reference in your database for fast idempotency lookups.
  • Return HTTP 200 even for duplicates — anything else triggers retries.
  • Log every incoming payload for auditing and debugging.
  • Always investigate fail_reason before re-attempting a failed transfer.
Reference

List Banks & Providers

Retrieve the current list of supported banks and mobile money providers for a given country. Always use this endpoint dynamically rather than hard-coding bank keys — providers change over time.

GET/banks/{country}
cURL
curl -X GET \
  https://pay.digitalservicescenter.rw/generation/v2/banks/RW \
  -H "X-DGS-API-Key: YOUR_API_KEY"
JSON
{
  "status": "success",
  "country": "RW",
  "data": [
    {
      "bank_key": "bk_rw",
      "name": "Banque de Kigali",
      "type": "bank",
      "code": "40",
      "currency": "RWF",
      "branch_required": 0
    },
    {
      "bank_key": "mtn_rw",
      "name": "MTN Mobile Money",
      "type": "mobile_money",
      "network": "MTN",
      "currency": "RWF",
      "branch_required": 0
    }
  ]
}
Using This Response
  • Use the bank_key value as the bank_key field in your disbursement request.
  • When branch_required is 1, include a branch field in the recipient object.
  • Check type to distinguish banks ("bank") from mobile money providers ("mobile_money").
Wallet

Merchant Balances

Retrieve available, pending, and total balances for all currencies in your merchant wallet. Use this before disbursements to verify you have sufficient funds.

GET/balance
cURL
curl -X GET \
  https://pay.digitalservicescenter.rw/generation/v2/balance \
  -H "X-DGS-API-Key: YOUR_API_KEY"
JSON
{
  "status": "success",
  "error_code": "00",
  "merchant_id": "merchant_uuid",
  "balances": [
    {
      "currency": "USD",
      "pending_balance": 50.00,
      "available_balance": 200.00,
      "total_balance": 250.00
    },
    {
      "currency": "RWF",
      "pending_balance": 1000.00,
      "available_balance": 5000.00,
      "total_balance": 6000.00
    }
  ]
}
FieldDescription
pending_balanceFunds received but not yet settled (e.g. in processing).
available_balanceFunds immediately available for disbursement or withdrawal.
total_balanceSum of pending and available — your total wallet value.
Wallet

Currency Conversion

Convert funds between currencies within your merchant wallet. Use action: "preview" to see the expected conversion amount and rate without moving any money, then call again with action: "convert" to execute.

POST/convert
FieldTypeDescription
actionstring"preview" — read-only rate check. "convert" — execute and move funds.
from_currencystringSource currency (e.g. USD).
to_currencystringTarget currency (e.g. EUR).
from_amountnumberAmount in from_currency to convert. Must be > 0.
JSON — Preview response (no funds moved)
{
  "status": "success",
  "message": "Conversion preview — no funds moved",
  "from_currency": "USD",
  "from_amount": 100,
  "to_currency": "EUR",
  "to_amount": 94.50,
  "rate": 0.945000,
  "rate_display": "1 USD = 0.9450 EUR",
  "fee": 0
}
JSON — Execution response (funds debited, credit pending)
{
  "status": "success",
  "message": "Conversion initiated. EUR balance will be credited shortly.",
  "dgs_conversion_ref": "DGSCONV1A2B3C4D",
  "conversion_status": "processing",
  "from_currency": "USD",
  "from_amount": 100,
  "to_currency": "EUR",
  "to_amount": 94.50,
  "rate": 0.945000,
  "fee": 0
}
JSON — Insufficient balance error
{
  "status": "failed",
  "message": "Insufficient USD balance",
  "error_code": "VALIDATION_ERROR",
  "available_balance": 50,
  "required": 100,
  "currency": "USD"
}
Notes
  • Conversion is currently fee-free.
  • Live exchange rates are applied at execution time — rates shown in preview may shift slightly.
  • If the provider rejects the transfer, the source currency balance is automatically refunded.
  • Track conversions using dgs_conversion_ref.
Wallet

Conversion Webhook

Sent automatically when a currency conversion changes status. Use this to update your records and reconcile wallet balances.

JSON — Webhook Payload
{
  "event": "conversion.success",
  "dgs_reference": "DGSCONV1A2B3C4D",
  "status": "success",
  "from_currency": "USD",
  "from_amount": 100,
  "to_currency": "EUR",
  "to_amount": 94.50,
  "rate": 0.945,
  "rate_display": "1 USD = 0.9450 EUR"
}
JSON — Webhook Payload
{
  "event": "conversion.failed",
  "dgs_reference": "DGSCONV1A2B3C4D",
  "status": "failed",
  "from_currency": "USD",
  "from_amount": 100,
  "to_currency": "EUR",
  "fail_reason": "Insufficient balance",
  "refund_amount": 100,
  "refund_currency": "USD"
}
Reporting

Transactions

Retrieve transaction history for your merchant account. You can fetch a paginated list with optional filters, or look up a single transaction by its ID. Transaction records include settlement status, payment method details, and links to the payment link if applicable.

Endpoints

MethodPathDescription
GET/transactionsPaginated list with optional filters
GET/transactions/{id}Single transaction with full details

Query Parameters (List)

ParameterTypeDescription
pageintegerPage number. Default: 1.
limitintegerRecords per page. Default: 20, max: 100.
statusstringFilter: pending, success, or failed.
payment_link_idstringFilter transactions from a specific payment link.
dgs_referencestringFind by your internal DGS reference.
flw_referencestringFind by Flutterwave reference.
cURL — Paginated list, filtered by status
curl -X GET \
  "https://pay.digitalservicescenter.rw/generation/v2/transactions?page=1&limit=20&status=success" \
  -H "X-DGS-API-Key: YOUR_API_KEY"
cURL — Single transaction by ID
curl -X GET \
  https://pay.digitalservicescenter.rw/generation/v2/transactions/dgs-reference \
  -H "X-DGS-API-Key: YOUR_API_KEY"

List Response

JSON
{
  "status": "success",
  "data": [
    {
      "id": "uuid1",
      "dgs_reference": "INV-123",
      "amount": 100,
      "currency": "USD",
      "status": "success",
      "payment_method_type": "card",
      "created_at": "2026-04-03 11:00:00"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 50,
    "pages": 3
  }
}
Notifications

Webhooks

Webhooks are HTTP POST requests that DGS-Pay sends to your server when a payment event occurs. They are the reliable mechanism for confirming payment completion — redirect URLs can be blocked or closed by the user, but webhooks are server-to-server.

Configure your webhook URL in the merchant portal under Settings → API Settings. You can also override the URL per-transaction using the callback_url field.

Always Use Webhooks to Confirm Payment Do not mark an order as paid based only on a redirect URL parameter or an API response with status: "pending". Wait for the payment.success webhook event.

Signature Verification

Every webhook request includes an X-DGS-Signature header. This is an HMAC-SHA256 hash of the raw request body, signed with your Webhook Secret (find it in Settings → API Settings). Always verify this signature before processing any webhook payload — it proves the request came from DGS-Pay and was not tampered with.

PHP
<?php
$payload   = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_DGS_SIGNATURE'];
$expected  = hash_hmac('sha256', $payload, DGS_WEBHOOK_SECRET);

if (!hash_equals($expected, $signature)) {
  http_response_code(401);
  exit('Unauthorized');
}

$event = json_decode($payload, true);
// Handle $event['event'] here

http_response_code(200);
echo json_encode(['status' => 'received']);
?>
Node.js / Express
const crypto = require('crypto');

app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const sig      = req.headers['x-dgs-signature'];
  const expected = crypto
    .createHmac('sha256', process.env.DGS_WEBHOOK_SECRET)
    .update(req.body)
    .digest('hex');

  if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
    return res.status(401).send('Unauthorized');
  }

  const event = JSON.parse(req.body);
  // Handle event.event here

  res.json({ status: 'received' });
});
Python / Flask
import hmac, hashlib, os
from flask import Flask, request, jsonify

app = Flask(__name__)

@app.route('/webhook', methods=['POST'])
def webhook():
  sig      = request.headers.get('X-DGS-Signature', '')
  secret   = os.environ['DGS_WEBHOOK_SECRET'].encode()
  expected = hmac.new(secret, request.data, hashlib.sha256).hexdigest()

  if not hmac.compare_digest(sig, expected):
    return 'Unauthorized', 401

  event = request.get_json()
  # Handle event['event'] here

  return jsonify({ 'status': 'received' })
Webhook Rules
  • Always verify X-DGS-Signature before processing the payload.
  • Respond with HTTP 200 immediately — delayed or failed responses trigger automatic retries.
  • Webhook delivery is at-least-once — handle duplicates using dgs_reference as an idempotency key.

Payment Event Payloads

JSON
{
  "event": "payment.success",
  "dgs_reference": "dgs_123456789",
  "flw_charge_id": "flw_987654321",
  "amount": 5000,
  "currency": "RWF",
  "merchant_fee": 150,
  "net_amount": 4850,
  "status": "success",
  "timestamp": "2026-04-02T10:30:00Z",
  "environment": "production"
}
JSON
{
  "event": "payment.failed",
  "dgs_reference": "dgs_123456789",
  "flw_charge_id": "flw_987654321",
  "amount": 5000,
  "currency": "RWF",
  "merchant_fee": 0,
  "net_amount": 0,
  "status": "failed",
  "timestamp": "2026-04-02T10:35:00Z",
  "environment": "production"
}
FieldDescription
eventEvent type: payment.success or payment.failed.
dgs_referenceYour transaction ID — use this for idempotency checks.
merchant_feeFee charged to your account for this transaction.
net_amountAmount credited to your merchant wallet (amount − merchant_fee).
environment"test" or "production".
Reference

Error Codes

Every API response includes a status field ("success", "pending", or "failed"). When a request fails at the API level, an error_code field indicates the category of error. Payment-level outcomes (declined card, insufficient mobile money balance) are different — those use transaction_status and processor_response.

CodeLabelMeaning & Next Steps
00SuccessRequest processed successfully.
90Validation ErrorInvalid JSON structure or missing required fields. Check your request body against the parameter table.
91Auth ErrorAPI key is missing, invalid, or blocked by IP whitelist. Verify your credentials in the dashboard.
92Not FoundThe resource (transaction, payment link, etc.) does not exist or belongs to a different account.
93Idempotency ErrorAn idempotency key conflict was detected. Use a unique key per request.
99Server ErrorUnexpected internal error. Safe to retry after a short delay. Contact support if persistent.

Status vs Error Code Explained

These two concepts operate at different layers:

FieldLayerMeaning
status: "success"API levelThe request was accepted and a response was produced. Does not mean the payment succeeded.
status: "pending"Payment levelTransaction initiated; awaiting customer authorization (e.g. STK push).
status: "failed"Payment levelPayment declined or rejected by the provider or customer.
error_codeAPI levelSpecific error category (see table above). Only present when the API itself could not process your request.
Example A charge request that triggers an STK push returns status: "success" (the API accepted it) and transaction_status: "pending" (the payment is waiting for the customer). Later, if the customer rejects the push, your webhook receives status: "failed" — not an error_code.

Need Help?

Our technical team is ready to assist with integration questions, testing, and go-live checks.

Contact Support