DGS-Pay API v2
Dashboard
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.

Need Help?

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

Contact Support