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:
- Your Secret Key from the BudPay Dashboard (opens in a new tab)
- A unique reference for each transaction (minimum 16 characters)
- Customer's card details (number, CVV, expiry)
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.
| Field | Description | Example |
|---|---|---|
data.number | Card number | 4000000000001091 |
data.expiryMonth | Expiry month (MM) | 12 |
data.expiryYear | Expiry year (YY) | 29 |
data.cvv | CVV/CVC | 484 |
reference | Your 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.
| Field | Description | Example |
|---|---|---|
email | Customer email | customer@email.com |
amount | Amount to charge | 10 |
currency | USD, NGN, GHS, or KES | USD |
reference | Same reference from Step 1 | ref_21655737521418683 |
card | Encrypted card from Step 1 | 740a75e2d40b... |
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:
- Create an iframe pointing to BudPay's device collection URL
- Wait for the
profile.completedmessage (or timeout after 4.5 seconds) - 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
althas a URL → Go to Step 5 (open the URL for customer to verify) - If
altisnull→ 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"
}
}| Status | Meaning | What to do |
|---|---|---|
pending | Still processing | Wait 3 seconds and check again |
success | Payment complete ✅ | Fulfill the order |
failed | Payment 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
| Step | Endpoint | Method | Description |
|---|---|---|---|
| 1 | /api/s2s/test/encryption | POST | Encrypt card data |
| 2 | /api/s2s/transaction/initialize | POST | Initialize payment |
| 3 | /cr6t76y8uijkny/authdevice/{referenceId} | GET | Device fingerprinting (iframe) |
| 4 | /api/cr6t76y8uijkny-enrollmentcheck | POST | Check if 3DS required |
| 6 | /api/verify-transaction/{reference} | GET | Check payment status |
Verifying Transactions
Call the Verify Transaction API with the transaction reference:
GET https://api.budpay.com/api/v2/transaction/verify/:referenceExample Request:
curl https://api.budpay.com/api/v2/transaction/verify/BUD_1673600359168063493 \
-H "Authorization: Bearer YOUR_SECRET_KEY" \
-X GETExample 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:
- Receive callback/webhook with transaction reference
- Call Verify API (server-side)
- Check
status === "success" - Verify amount matches expected
- Fulfill order/deliver service
Learn more about verifying transactions.
Testing Your Integration
BudPay provides a comprehensive sandbox environment for testing:
Test Card Numbers:
| Card Type | Number | CVV | Expiry |
|---|---|---|---|
| Visa | 4242424242424242 | 123 | 12/25 |
| Visa (V2) | 4000000000001091 | 484 | 12/29 |
| Mastercard | 5123450000000008 | 100 | 12/25 |
| Verve | 5060990580000217499 | 123 | 12/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.