DGS-Pay Integration Guide
Complete reference for collecting and disbursing payments in Rwanda — Mobile Money, Cards, Payment Links, Wallet, and Webhooks.
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:
https://test.pay.digitalservicescenter.rw/generation/v2
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 -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" }'
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:
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 -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.
- 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.
.envfiles) 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.
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.
https://test.pay.digitalservicescenter.rw/generation/v2
https://pay.digitalservicescenter.rw/generation/v2
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 Type | Allowed Values | What it simulates |
|---|---|---|
| Card Authorization | scenario:auth_pinscenario:auth_pin_3dsscenario:auth_3dsscenario:auth_avs |
Forces the charge response to require PIN, 3DS, AVS, or combined auth. |
| Issuer Responses | issuer:approvedissuer:insufficient_fundsissuer:incorrect_pinissuer:expired_cardissuer:invalid_cvvissuer:do_not_honorissuer:system_error |
Simulates the card bank's response after you authorize. |
| Mobile Money | scenario:auth_pinscenario:auth_redirect |
auth_pin = payment auto-approved. auth_redirect = redirect flow triggered. |
| Transfers / Disbursements | scenario:successfulscenario:insufficient_balancescenario:invalid_currency |
Simulates outcomes of wallet disbursements. |
Example — Approved Card in Sandbox
{
"scenario_key": "scenario:auth_pin&issuer:approved",
}
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.
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.
| Parameter | Type | Required | Description |
|---|---|---|---|
amount | number | Required | Payment amount. Must be a positive number. |
currency | string | Required | ISO 4217 code. Must be RWF. |
payment_type | string | Required | Must be mobile_money. |
customer_email | string | Required | Customer's email address. |
customer_first_name | string | Required | Customer's first name. |
customer_last_name | string | Required | Customer's last name. |
customer_phone | string | Required | Phone number without country code, e.g. 789654321. |
customer_phone_country_code | string | Required | Country code without +, e.g. 250. |
redirect_url | string | Required | URL to redirect the customer to after payment completes. |
mobile_money_network | string | Required | MTN or Airtel. |
mobile_money_phone | string | Required | Phone number that will receive the STK push. |
narration | string | Optional | Short description shown to the customer, e.g. Invoice #5678. |
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" }'
{
"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.
{
"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"
}
{
"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"
}
{
"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"
}
pending response alone.
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.
- 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
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
// 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
- 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.
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>" }'
{
"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.
transaction_status equals "successful" AND the response contains no next_action_type field.
{
"dgs_reference": "dgs_123456789",
"auth_type": "pin",
"auth_pin_encrypted": "<base64_encrypted_pin>",
"auth_pin_nonce": "abc123def456"
}
{
"dgs_reference": "dgs_123456789",
"auth_type": "otp",
"auth_otp": "123456"
}
{
"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_type | Required Fields | When triggered |
|---|---|---|
pin | auth_pin_encrypted, auth_pin_nonce | Card issuer requires PIN verification. |
otp | auth_otp | 3DS OTP sent to customer's registered number. |
avs | avs_line1, avs_country (minimum) | Issuer requires billing address match. |
Payment Links
Payment Links let you generate shareable URLs that accept card, mobile money, bank transfer, or USSD payments — without writing a checkout form yourself. Useful for invoices, donations, event tickets, and any scenario where you need to collect money without a custom flow.
Links can be fixed-amount (you set the price) or flexible (the payer chooses within limits). They support use caps, expiry dates, custom slugs, and per-link analytics.
Endpoints
| Method | Path | Description |
|---|---|---|
| POST | /payment-links | Create a new link |
| GET | /payment-links | List all links (paginated) |
| GET | /payment-links/{id} | Get link details + charge stats |
| PATCH | /payment-links/{id} | Update a link |
| DELETE | /payment-links/{id} | Archive a link (irreversible) |
Create a Payment Link
| Field | Required | Description |
|---|---|---|
title | Required | Display title shown on the payment page, e.g. Invoice #123. |
amount_type | Required | fixed — exact amount. flexible — payer picks amount. |
currency | Required | ISO 4217 code, e.g. RWF. |
amount | If fixed | The exact amount to collect. |
min_amount | Optional | Minimum payer amount (flexible only). |
max_amount | Optional | Maximum payer amount (flexible only). |
payment_types | Optional | Array of accepted methods: card, mobile_money, ussd, bank_transfer. Defaults to all. |
slug | Optional | Custom URL slug. Auto-generated if omitted. |
description | Optional | Extended description shown on the payment page. |
redirect_url | Optional | Where to send the payer after payment completes. |
max_uses | Optional | Maximum number of successful payments. null = unlimited. |
expires_at | Optional | Expiry date/time in format YYYY-MM-DD HH:MM:SS. |
collect_phone | Optional | Ask payer for phone number. Default true. |
collect_name | Optional | Ask payer for name. Default true. |
collect_address | Optional | Ask payer for address. Default false. |
logo_url | Optional | Logo image URL shown on the payment page. |
metadata | Optional | Custom JSON object for your internal reference. |
curl -X POST \ https://pay.digitalservicescenter.rw/generation/v2/payment-links \ -H "X-DGS-API-Key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "title": "Invoice #123", "amount_type": "fixed", "amount": 50000, "currency": "RWF", "payment_types": ["card", "mobile_money"], "max_uses": 1, "expires_at": "2026-12-31 23:59:59" }'
{
"title": "Invoice #123",
"amount_type": "fixed",
"amount": 50000,
"currency": "RWF",
"payment_types": ["card", "mobile_money"],
"max_uses": 1,
"expires_at": "2026-12-31 23:59:59"
}
{
"status": "success",
"message": "Payment link created",
"link": {
"id": "25a5643d-31de-490d-808d-82af72fdd097",
"slug": "invoice-123",
"url": "https://pay.digitalservicescenter.rw/generation/pay/invoice-123",
"title": "Invoice #123",
"amount_type": "fixed",
"amount": 50000,
"currency": "RWF",
"payment_types": ["card", "mobile_money"],
"use_count": 0,
"max_uses": 1,
"status": "active",
"expires_at": "2026-12-31 23:59:59",
"created_at": "2026-04-03 10:00:00"
}
}
Get Link Details & Stats
curl -X GET \ https://pay.digitalservicescenter.rw/generation/v2/payment-links/25a5643d-31de-490d-808d-82af72fdd097 \ -H "X-DGS-API-Key: YOUR_API_KEY"
{
"status": "success",
"link": { /* same fields as create response */ },
"stats": {
"total_charges": 4,
"successful_charges": 1,
"pending_charges": 0,
"failed_charges": 3,
"total_collected": 500,
"currency": "RWF"
},
"recent_charges": [
{
"id": "5de825d0-...",
"amount": "500.00",
"currency": "RWF",
"status": "success",
"created_at": "2026-03-09 15:56:32"
}
]
}
Archive (Delete) a Link
{
"status": "success",
"message": "Payment link archived"
}
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.
| Field | Type | Required | Description |
|---|---|---|---|
amount | number | Required | Gross amount in RWF to send from your wallet. |
destination_type | string | Required | bank or mobile_money. |
recipient | object | Required | Recipient details. Fields vary by destination type. |
narration | string | Optional | Short note shown on the recipient's statement. |
callback_url | string | Optional | Override the default webhook URL for this transfer only. |
Bank Recipient Fields
| Field | Required |
|---|---|
bank_key | Required |
account_number | Required |
name | Optional |
branch | Optional |
Mobile Money Recipient Fields
| Field | Required |
|---|---|
msisdn | Required |
network | Required |
country | Required |
name | Optional |
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 -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" } }'
0788123456) or international format (+250788123456), both are accepted.
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.
| Event | Description |
|---|---|
disbursement.success | Transfer confirmed by provider. Recipient has been credited. |
disbursement.failed | Transfer could not be completed. Check fail_reason. |
{
"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"
}
{
"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.
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; }
- Index
dgs_referencein 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_reasonbefore re-attempting a failed transfer.
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.
curl -X GET \ https://pay.digitalservicescenter.rw/generation/v2/banks/RW \ -H "X-DGS-API-Key: YOUR_API_KEY"
{
"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
}
]
}
- Use the
bank_keyvalue as thebank_keyfield in your disbursement request. - When
branch_requiredis1, include abranchfield in the recipient object. - Check
typeto distinguish banks ("bank") from mobile money providers ("mobile_money").
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.
curl -X GET \ https://pay.digitalservicescenter.rw/generation/v2/balance \ -H "X-DGS-API-Key: YOUR_API_KEY"
{
"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
}
]
}
| Field | Description |
|---|---|
pending_balance | Funds received but not yet settled (e.g. in processing). |
available_balance | Funds immediately available for disbursement or withdrawal. |
total_balance | Sum of pending and available — your total wallet value. |
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.
| Field | Type | Description |
|---|---|---|
action | string | "preview" — read-only rate check. "convert" — execute and move funds. |
from_currency | string | Source currency (e.g. USD). |
to_currency | string | Target currency (e.g. EUR). |
from_amount | number | Amount in from_currency to convert. Must be > 0. |
{
"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
}
{
"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
}
{
"status": "failed",
"message": "Insufficient USD balance",
"error_code": "VALIDATION_ERROR",
"available_balance": 50,
"required": 100,
"currency": "USD"
}
- 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.
Conversion Webhook
Sent automatically when a currency conversion changes status. Use this to update your records and reconcile wallet balances.
{
"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"
}
{
"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"
}
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
| Method | Path | Description |
|---|---|---|
| GET | /transactions | Paginated list with optional filters |
| GET | /transactions/{id} | Single transaction with full details |
Query Parameters (List)
| Parameter | Type | Description |
|---|---|---|
page | integer | Page number. Default: 1. |
limit | integer | Records per page. Default: 20, max: 100. |
status | string | Filter: pending, success, or failed. |
payment_link_id | string | Filter transactions from a specific payment link. |
dgs_reference | string | Find by your internal DGS reference. |
flw_reference | string | Find by Flutterwave reference. |
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 -X GET \ https://pay.digitalservicescenter.rw/generation/v2/transactions/dgs-reference \ -H "X-DGS-API-Key: YOUR_API_KEY"
List Response
{
"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
}
}
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.
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 $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']); ?>
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' }); });
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' })
- Always verify
X-DGS-Signaturebefore 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_referenceas an idempotency key.
Payment Event Payloads
{
"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"
}
{
"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"
}
| Field | Description |
|---|---|
event | Event type: payment.success or payment.failed. |
dgs_reference | Your transaction ID — use this for idempotency checks. |
merchant_fee | Fee charged to your account for this transaction. |
net_amount | Amount credited to your merchant wallet (amount − merchant_fee). |
environment | "test" or "production". |
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.
| Code | Label | Meaning & Next Steps |
|---|---|---|
00 | Success | Request processed successfully. |
90 | Validation Error | Invalid JSON structure or missing required fields. Check your request body against the parameter table. |
91 | Auth Error | API key is missing, invalid, or blocked by IP whitelist. Verify your credentials in the dashboard. |
92 | Not Found | The resource (transaction, payment link, etc.) does not exist or belongs to a different account. |
93 | Idempotency Error | An idempotency key conflict was detected. Use a unique key per request. |
99 | Server Error | Unexpected 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:
| Field | Layer | Meaning |
|---|---|---|
status: "success" | API level | The request was accepted and a response was produced. Does not mean the payment succeeded. |
status: "pending" | Payment level | Transaction initiated; awaiting customer authorization (e.g. STK push). |
status: "failed" | Payment level | Payment declined or rejected by the provider or customer. |
error_code | API level | Specific error category (see table above). Only present when the API itself could not process your request. |
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.