Accept Payment
Server to Server
Cards V2

Server to Server (Cards V2)

Accept international card payments with 3D Secure authentication. This integration supports USD, NGN, GHS, and KES currencies.


Payment Flow Overview

┌─────────────────────────────────────────────────────────────────────────┐
│                         CARDS V2 PAYMENT FLOW                           │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  1. ENCRYPT CARD          2. INITIALIZE           3. DEVICE DATA       │
│  ┌──────────────┐         ┌──────────────┐        ┌──────────────┐     │
│  │ Card Number  │         │ Send encrypted│       │ Cardinal     │     │
│  │ CVV, Expiry  │ ──────▶ │ card + amount │ ────▶ │ iframe runs  │     │
│  │ → Encrypted  │         │ → Get tokens  │       │ (~4.5 secs)  │     │
│  └──────────────┘         └──────────────┘        └──────────────┘     │
│                                                          │              │
│                                                          ▼              │
│  6. VERIFY PAYMENT        5. 3DS AUTH             4. ENROLLMENT        │
│  ┌──────────────┐         ┌──────────────┐        ┌──────────────┐     │
│  │ Poll status  │         │ Open popup   │        │ Check if 3DS │     │
│  │ until done   │ ◀────── │ for customer │ ◀───── │ is required  │     │
│  │ → Success!   │         │ to verify    │        │ → Get URL    │     │
│  └──────────────┘         └──────────────┘        └──────────────┘     │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘
💡

Quick Summary: Encrypt card → Initialize → Collect device data → Check 3DS → Authenticate → Verify


Before You Start

You'll need:


Step 1: Encrypt Card Data

Why? Card data must be encrypted before sending to BudPay for PCI compliance.

Endpoint: POST https://backendapi.budpay.com/api/s2s/test/encryption

curl -X POST 'https://backendapi.budpay.com/api/s2s/test/encryption' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_SECRET_KEY' \
  -d '{
    "data": {
      "number": "4000000000001091",
      "expiryMonth": "12",
      "expiryYear": "29",
      "cvv": "484"
    },
    "reference": "ref_21655737521418683"
  }'

Response:

{
  "card": "740a75e2d40b04c29bca8ecd8fda00bc..."
}

Save this: You'll use the card value in Step 2.

FieldDescriptionExample
data.numberCard number4000000000001091
data.expiryMonthExpiry month (MM)12
data.expiryYearExpiry year (YY)29
data.cvvCVV/CVC484
referenceYour unique transaction ID (16+ chars)ref_21655737521418683
Try it out

Step 2: Initialize Transaction

Why? This creates the payment session and returns tokens needed for 3D Secure.

Endpoint: POST https://backendapi.budpay.com/api/s2s/transaction/initialize

curl -X POST 'https://backendapi.budpay.com/api/s2s/transaction/initialize' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer YOUR_SECRET_KEY' \
  -d '{
    "email": "customer@email.com",
    "amount": "10",
    "currency": "USD",
    "reference": "ref_21655737521418683",
    "card": "740a75e2d40b04c29bca8ecd8fda00bc..."
  }'

Response:

{
  "status": true,
  "message": "Customer Authentication Successful",
  "data": {
    "accessToken": "eyJhbGciOiJIUzI1NiIs...",
    "deviceDataCollectionUrl": "https://centinelapistag.cardinalcommerce.com/...",
    "referenceId": "8feb4e54-0e9e-419a-9098-d09b410f81c5"
  }
}

Save this: You'll use referenceId in Step 3.

FieldDescriptionExample
emailCustomer emailcustomer@email.com
amountAmount to charge10
currencyUSD, NGN, GHS, or KESUSD
referenceSame reference from Step 1ref_21655737521418683
cardEncrypted card from Step 1740a75e2d40b...
Try it out

Step 3: Collect Device Data (Frontend)

Why? Cardinal Commerce collects browser/device info for fraud detection. This runs in an iframe on your frontend.

How it works:

  1. Create an iframe pointing to BudPay's device collection URL
  2. Wait for the profile.completed message (or timeout after 4.5 seconds)
  3. Then proceed to Step 4
// Add this iframe to your payment page
const referenceId = "8feb4e54-0e9e-419a-9098-d09b410f81c5"; // From Step 2
 
function collectDeviceData(referenceId) {
  return new Promise((resolve) => {
    // Create hidden iframe
    const iframe = document.createElement('iframe');
    iframe.src = `https://backendapi.budpay.com/cr6t76y8uijkny/authdevice/${referenceId}`;
    iframe.style.width = '0';
    iframe.style.height = '0';
    iframe.style.border = 'none';
    document.body.appendChild(iframe);
 
    // Listen for completion
    const handler = (event) => {
      if (event.data?.MessageType === 'profile.completed') {
        window.removeEventListener('message', handler);
        clearTimeout(timeout);
        resolve();
      }
    };
    window.addEventListener('message', handler);
 
    // Timeout after 4.5 seconds (Cardinal's standard time)
    const timeout = setTimeout(() => {
      window.removeEventListener('message', handler);
      resolve(); // Continue even if timeout
    }, 4500);
  });
}
 
// Usage
await collectDeviceData(referenceId);
console.log('Device data collected, proceeding to enrollment check...');

The iframe takes about 4.5 seconds to complete. Don't skip this step—it's required for 3D Secure to work properly.


Step 4: Check 3DS Enrollment

Why? This checks if the card requires 3D Secure verification.

Endpoint: POST https://backendapi.budpay.com/api/cr6t76y8uijkny-enrollmentcheck

curl -X POST 'https://backendapi.budpay.com/api/cr6t76y8uijkny-enrollmentcheck' \
  -H 'Content-Type: application/json' \
  -d '{
    "cardnumber": "4000000000001091",
    "expiryMonth": "12",
    "expiryYear": "29",
    "cvv": "484",
    "ref": "ref_21655737521418683"
  }'

Response (3DS Required):

{
  "status": true,
  "alt": "https://3ds-auth-url.com/verify?params..."
}

Response (3DS Not Required):

{
  "status": true,
  "alt": null
}

What to do next:

  • If alt has a URL → Go to Step 5 (open the URL for customer to verify)
  • If alt is null → Skip to Step 6 (check payment status directly)
Try it out

Step 5: Handle 3DS Authentication

Why? If the card requires 3D Secure, the customer must verify their identity.

function handle3DS(enrollmentResponse) {
  if (enrollmentResponse.alt) {
    // 3DS required - open popup for customer to authenticate
    const authWindow = window.open(
      enrollmentResponse.alt,
      'BudPay 3DS',
      'width=500,height=600'
    );
    
    // Start polling for result
    pollPaymentStatus(authWindow);
  } else {
    // No 3DS required - check status directly
    pollPaymentStatus();
  }
}
🔐

The 3DS popup shows the bank's verification page where the customer enters their OTP or approves via their banking app.


Step 6: Verify Payment Status

Why? Poll the transaction status until it completes (success or failure).

Endpoint: GET https://backendapi.budpay.com/api/verify-transaction/{reference}

curl 'https://backendapi.budpay.com/api/verify-transaction/ref_21655737521418683'

Response:

{
  "data": {
    "status": "success",
    "message": "Transaction successful"
  }
}
StatusMeaningWhat to do
pendingStill processingWait 3 seconds and check again
successPayment complete ✅Fulfill the order
failedPayment failed ❌Show error, allow retry
async function pollPaymentStatus(authWindow = null) {
  const reference = "ref_21655737521418683";
  
  const response = await fetch(
    `https://backendapi.budpay.com/api/verify-transaction/${reference}`
  );
  const data = await response.json();
  
  switch (data.data?.status) {
    case 'pending':
      // Still processing, check again in 3 seconds
      setTimeout(() => pollPaymentStatus(authWindow), 3000);
      break;
      
    case 'success':
      if (authWindow) authWindow.close();
      alert('Payment successful! 🎉');
      // Redirect to success page or fulfill order
      break;
      
    case 'failed':
      if (authWindow) authWindow.close();
      alert('Payment failed. Please try again.');
      break;
  }
}

Complete Example

Here's everything together in a working example:

// ============================================
// BUDPAY CARDS V2 - COMPLETE FRONTEND EXAMPLE
// ============================================
 
class BudPayCardsV2 {
  constructor(apiUrl = 'https://backendapi.budpay.com') {
    this.apiUrl = apiUrl;
  }
 
  // Step 3: Collect device data via Cardinal iframe
  async collectDeviceData(referenceId) {
    return new Promise((resolve) => {
      const iframe = document.createElement('iframe');
      iframe.src = `${this.apiUrl}/cr6t76y8uijkny/authdevice/${referenceId}`;
      iframe.style.cssText = 'width:0;height:0;border:none;position:absolute;';
      document.body.appendChild(iframe);
 
      const handler = (e) => {
        if (e.data?.MessageType === 'profile.completed') {
          cleanup();
          resolve();
        }
      };
 
      const cleanup = () => {
        window.removeEventListener('message', handler);
        clearTimeout(timeout);
      };
 
      window.addEventListener('message', handler);
      const timeout = setTimeout(() => { cleanup(); resolve(); }, 4500);
    });
  }
 
  // Step 5: Open 3DS popup if required
  handle3DS(redirectUrl) {
    if (redirectUrl) {
      return window.open(redirectUrl, 'BudPay3DS', 'width=500,height=600');
    }
    return null;
  }
 
  // Step 6: Poll payment status
  async pollStatus(reference, authWindow = null) {
    const response = await fetch(`${this.apiUrl}/api/verify-transaction/${reference}`);
    const data = await response.json();
    const status = data.data?.status;
 
    if (status === 'pending') {
      await new Promise(r => setTimeout(r, 3000));
      return this.pollStatus(reference, authWindow);
    }
 
    if (authWindow) authWindow.close();
    return { status, data: data.data };
  }
}
 
// Usage:
const budpay = new BudPayCardsV2();
 
async function processPayment(referenceId, reference, enrollmentUrl) {
  // Step 3: Collect device data
  await budpay.collectDeviceData(referenceId);
  
  // Step 5: Handle 3DS if needed
  const authWindow = budpay.handle3DS(enrollmentUrl);
  
  // Step 6: Wait for result
  const result = await budpay.pollStatus(reference, authWindow);
  
  if (result.status === 'success') {
    console.log('Payment successful!');
  } else {
    console.log('Payment failed:', result.data?.message);
  }
}

API Reference

StepEndpointMethodDescription
1/api/s2s/test/encryptionPOSTEncrypt card data
2/api/s2s/transaction/initializePOSTInitialize payment
3/cr6t76y8uijkny/authdevice/{referenceId}GETDevice fingerprinting (iframe)
4/api/cr6t76y8uijkny-enrollmentcheckPOSTCheck if 3DS required
6/api/verify-transaction/{reference}GETCheck payment status

Verifying Transactions

Call the Verify Transaction API with the transaction reference:

GET https://api.budpay.com/api/v2/transaction/verify/:reference

Example Request:

curl https://api.budpay.com/api/v2/transaction/verify/BUD_1673600359168063493 \
  -H "Authorization: Bearer YOUR_SECRET_KEY" \
  -X GET

Example Response:

{
  "status": true,
  "message": "Transaction verified successfully",
  "data": {
    "currency": "NGN",
    "amount": "550",
    "reference": "BUD_1673600359168063493",
    "status": "success",
    "message": "Payment successful",
    "customer": {
      "email": "customer@email.com"
    }
  }
}

Always verify transactions on your server before fulfilling orders. Never rely solely on client-side callbacks or redirects.

Verification Workflow:

  1. Receive callback/webhook with transaction reference
  2. Call Verify API (server-side)
  3. Check status === "success"
  4. Verify amount matches expected
  5. Fulfill order/deliver service

Learn more about verifying transactions.


Testing Your Integration

BudPay provides a comprehensive sandbox environment for testing:

Test Card Numbers:
Card TypeNumberCVVExpiry
Visa424242424242424212312/25
Visa (V2)400000000000109148412/29
Mastercard512345000000000810012/25
Verve506099058000021749912312/25
Test Mobile Money Numbers:
  • Kenya (M-Pesa): 254712345678
  • Ghana (MTN): 233244000000

Remember: Always use test credentials in sandbox mode. Switch to live keys only when ready for production.