Testing your integrated OnlinePay Checkout (Full example)

Testing your Checkout integration

The following test scenario can be used to validate your OnlinePay Checkout integration in a test environment. It demonstrates a basic HPP integration with 3D Secure authentication, including server-side code to create a checkout session, handle the return URL, and process webhooks for payment status updates.

⚠️

The values used in this example reflect the test environment endpoints. Ensure that you use CST environment variables if copying this code directly into your project. Alternatively, you can use the replace the test endpoint URLs with production URLs when moving to live.

Prerequisites for testing

  1. Test environment credentials - Ensure you have OnlinePay test account credentials
  2. Node.js - Version 14 or higher
  3. Test card numbers - Use only designated test cards
  4. HTTPS tunnel - Required for webhook testing (ngrok or similar)

Step 1: Set up test environment

Create a new project directory and set up your test files:

mkdir checkout-test
cd checkout-test
npm init -y
npm install express node-fetch dotenv ngrok

Create a .env file with your test credentials:

# This example uses the test environment and associated variables. Ensure you use the correct environment variables based on production or test.
ONLINEPAY_USER_UID=your-test-user-uid
ONLINEPAY_API_KEY=your-test-api-key
ONLINEPAY_ENTITY_ID=your-test-entity-id
ONLINEPAY_PPC_ID=your-test-ppc-id
ONLINEPAY_3DS_ID=your-test-3ds-id

# (Optional)ngrok URL for return_url (set this after starting ngrok)
NGROK_URL=https://your-ngrok-url.ngrok.io
ℹ️

Setting the ngrok URL is optional, and only required if you want to persist the URL for return_url and webhook testing.

Step 2: Create test server

Create a server.js file in the same directory. Copy and paste this exact code, then save:

import 'dotenv/config';
import express from 'express';
import fetch from 'node-fetch';

const app = express();
app.use(express.json());

// Test page
app.get('/', (req, res) => {
    res.send(`
        <!DOCTYPE html>
        <html>
        <head>
            <title>Checkout Test</title>
        </head>
        <style>
            body {
                font-family: Arial, sans-serif;
                margin: 50px;
                .form-group { margin-bottom: 15px; }
                label { display: block; margin-bottom: 5px; }
                input, select { width: 100%; padding: 8px; box-sizing: border-box; }
                button { padding: 10px 15px; background-color: #28a745; color: white; border: none; cursor: pointer; }
                button:hover { background-color: #218838; }
                .test-info { margin-top: 20px; padding: 10px; background-color: #f8f9fa; border: 1px solid #ddd; }
            }
        </style>
        </head>
        <body>
            <h1>Checkout Test Page</h1>

            <div class="test-info">
                <h3>Test Card Numbers:</h3>
                <p><strong>Success:</strong><ul><li>Card no: 4601842090094756</li><li>Expiry: 08/26</li><li>CVV: 490</li></ul></p>
                <p><strong>Declined:</strong><ul><li>Card no: 400000000000002</li><li>Expiry: Any future date</li><li>CVV: Any 3 digits</li></ul></p>
            </div>

            <form id="checkoutForm">
                <div class="checkoutForm">
                    <h3>Shopping Cart</h3>
                    <div class="form-group">
                        <label><input type="checkbox" class="item-checkbox" data-price="1000"> T-Shirt - $10.00</label>
                        <input type="number" class="item-qty" min="0" max="10" value="0" style="width: 60px; margin-left: 10px;">
                    </div>
                    <div class="form-group">
                        <label><input type="checkbox" class="item-checkbox" data-price="2500"> Hoodie - $25.00</label>
                        <input type="number" class="item-qty" min="0" max="10" value="0" style="width: 60px; margin-left: 10px;">
                    </div>
                    <div class="form-group">
                        <label><input type="checkbox" class="item-checkbox" data-price="5000"> Jacket - $50.00</label>
                        <input type="number" class="item-qty" min="0" max="10" value="0" style="width: 60px; margin-left: 10px;">
                    </div>
                    <div class="form-group">
                        <label><input type="checkbox" class="item-checkbox" data-price="10000"> Premium Coat - $100.00</label>
                        <input type="number" class="item-qty" min="0" max="10" value="0" style="width: 60px; margin-left: 10px;">
                    </div>
                    <div class="form-group" style="background-color: #e9ecef; padding: 10px; border-radius: 5px;">
                        <strong>Total: $<span id="totalAmount">0.00</span></strong>
                        <input type="hidden" id="amount" value="0">
                    </div>
                    <h3>Customer Information</h3>
                    <div class="form-group">
                        <label>Email:</label>
                        <input type="email" id="email" value="[email protected]" required />
                    </div>
                    <div class="form-group">
                        <label>First name:</label>
                        <input type="text" id="firstName" value="John" required />
                    </div>
                    <div class="form-group">
                        <label>Last name:</label>
                        <input type="text" id="lastName" value="Smith" required />
                    </div>
                    <div class="form-group">
                        <label>Address:</label>
                        <input type="text" id="address" value="123 Collins Street" required />
                    </div>
                    <div class="form-group">
                        <label>City:</label>
                        <input type="text" id="city" value="Melbourne" required />
                    </div>
                    <div class="form-group">
                        <label>State:</label>
                        <input type="text" id="state" value="VIC" required />
                    </div>
                    <div class="form-group">
                        <label>Postcode:</label>
                        <input type="text" id="postalCode" value="3000" required />
                    </div>
                    <div class="form-group">
                        <label>Country Code:</label>
                        <input type="text" id="countryCode" value="AU" required />
                    </div>
                    <button type="submit">Checkout & Pay</button>
            </form>

            <script>
                // Calculate total when checkboxes or quantities change
                function updateTotal() {
                    let total = 0;
                    const checkboxes = document.querySelectorAll('.item-checkbox');
                    const quantities = document.querySelectorAll('.item-qty');
                    
                    checkboxes.forEach((checkbox, index) => {
                        if (checkbox.checked) {
                            const price = parseInt(checkbox.dataset.price);
                            const qty = parseInt(quantities[index].value) || 1;
                            total += price * qty;
                        }
                    });
                    
                    document.getElementById('totalAmount').textContent = (total / 100).toFixed(2);
                    document.getElementById('amount').value = total;
                }

                // Add event listeners
                document.querySelectorAll('.item-checkbox').forEach(checkbox => {
                    checkbox.addEventListener('change', (e) => {
                        const qtyInput = e.target.parentElement.nextElementSibling;
                        if (e.target.checked && qtyInput.value === '0') {
                            qtyInput.value = '1';
                        }
                        updateTotal();
                    });
                });

                document.querySelectorAll('.item-qty').forEach(input => {
                    input.addEventListener('input', updateTotal);
                });

                document.getElementById('checkoutForm').addEventListener('submit', async (e) => {
                    e.preventDefault();

                const total = parseInt(document.getElementById('amount').value);
                if (total === 0) {
                    alert('Please select at least one item');
                    return;
                }

                const formData = {
                    cartTotalCents: total,
                    customerData: {
                        email: document.getElementById('email').value,
                        billing: {
                            firstName: document.getElementById('firstName').value,
                            lastName: document.getElementById('lastName').value,
                            address: document.getElementById('address').value,
                            city: document.getElementById('city').value,
                            state: document.getElementById('state').value,
                            postalCode: document.getElementById('postalCode').value,
                            countryCode: document.getElementById('countryCode').value,
                        }
                    }
                };

                try {
                    const response = await fetch('/api/checkout', {
                        method: 'POST',
                        headers: { 'Content-Type': 'application/json' },
                        body: JSON.stringify(formData)
                    });

                    if (!response.ok) {
                        const error = await response.text();
                        alert('Error: ' + error);
                        return;
                    }

                    const { checkoutUrl } = await response.json();
                    console.log('Redirecting to:', checkoutUrl);
                    window.location.href = checkoutUrl;
                } catch (error) {
                    console.error('Checkout failed:', error);
                    alert('Checkout failed: ' + error.message);
                }
            });
            </script> 
        </body>
        </html>
        `)
});

app.post('/api/checkout', async (req, res) => {
  try {
    const { cartTotalCents, customerData } = req.body;
    const auth = Buffer.from(`${process.env.ONLINEPAY_USER_UID}:${process.env.ONLINEPAY_API_KEY}`).toString('base64');
    const orderId = `TEST-${Date.now()}`;

    const body = {
        entity_id: process.env.ONLINEPAY_ENTITY_ID,
        currency_code: 'AUD',
        amount: cartTotalCents,
        merchant_reference: `ORD-${Date.now()}`,
        interaction_type: 'HPP',
        return_url: `${process.env.NGROK_URL || 'http://localhost:3000'}/payment-complete?order=${orderId}`,

        customer_details: {
            entity_id: process.env.ONLINEPAY_ENTITY_ID,
            email_address: customerData.email,
            billing: {
                first_name: customerData.billing.firstName,
                last_name: customerData.billing.lastName,
                address_1: customerData.billing.address,
                city: customerData.billing.city,
                state: customerData.billing.state,
                postal_code: customerData.billing.postalCode,
                country_code: customerData.billing.countryCode,
            }
        },
        
        configurations: {
            card: {
                shopper_interaction: 'ECOMMERCE',
                payment_contract_id: process.env.ONLINEPAY_PPC_ID,
                threed_secure: {
                    threeds_contract_id: process.env.ONLINEPAY_3DS_ID,
                    enabled: true,
                    transaction_mode: 'S'
                }
            },
            google_pay: {
                card: {
                    threeds_secure: {
                        threeds_contract_id: process.env.ONLINEPAY_3DS_ID,
                        transaction_mode: 'P',
                },
                sca_compliance_level: 'WALLET'
                shopper_interation: 'ECOMMERCE'
                payment_contract_id: process.env.ONLINEPAY_PPC_ID,
                }
            },
            apple_pay: {
                card: {
                    sca_compliance_level: 'NONE'
                    shopper_interation: 'ECOMMERCE'
                    payment_contract_id: process.env.ONLINEPAY_PPC_ID,
                }
            },
        }
    }

    console.log('Creating checkout for:', orderId);
    console.log('Amount:', cartTotalCents / 100, 'AUD');

    const resp = await fetch('https://cst.test-gsc.vfims.com/oidc/checkout-service/v2/checkout', { // This example uses the test environment endpoint. Merchants using production should change the URL accordingly.
        method: 'POST',
        headers: {
            'Authorization': `Basic ${auth}`,
            'Content-Type': 'application/json',
            'Accept': 'application/json'
        },
        body: JSON.stringify(body)
    });

    if (!resp.ok) {
        const error = await resp.text();
        console.error('Checkout API error:', error);
        return res.status(resp.status).json({ error: error });
    }

    const data = await resp.json();
    console.log('Checkout created:', data.id);
    console.log('Redirect URL:', data.url);

    res.json({ checkoutUrl: data.url, checkoutId: data.id });

    } catch (error) {
        console.error('Server error:', error);
        res.status(500).json({ error: error.message });
    }
});

// Payment completion page
app.get('/payment-complete', (req, res) => {
    const { order, transaction_id, checkout_id, error_code, transaction_status } = req.query;

    console.log('Payment return received:');
    console.log('Order ID:', order);
    console.log('Transaction ID:', transaction_id);
    console.log('Checkout ID:', checkout_id);
    console.log('Error Code:', error_code);
    console.log('Transaction Status:', transaction_status);
    console.log('All query params:', req.query);

    // Determine if payment was successful
    const isSuccess = transaction_id && !error_code && transaction_status !== 'DECLINED' && transaction_status !== 'FAILED';
    const isFailed = error_code || transaction_status === 'DECLINED' || transaction_status === 'FAILED';

    res.send(`
        <!DOCTYPE html>
        <html>
        <head>
            <title>Payment Complete</title>
        <style>
            body { font-family: Arial, sans-serif; margin: 50px; }
            .success { color: green; }
            .error { color: red; }
            .processing { color: orange; }
            .details { margin-top: 20px; padding: 10px; background-color: #f8f9fa; border: 1px solid #ddd; }
        </style>
        </head>
        <body>
            <h1 Payment Status</h1>

            ${isSuccess ? 
                `<div class="success">
                    <h2>Payment Successful!</h2>
                    <p>Your payment has been processed successfully.</p>
                </div>` :
                isFailed ?
                `<div class="error">
                    <h2>Payment Failed</h2>
                    <p>Your payment couldn't be processed.</p>
                    ${transaction_status ? `<p><strong>Status:</strong> ${transaction_status}</p>` : ''}
                </div>` :
                `<div class="processing">
                    <h2>Payment Processing</h2>
                    <p>Your payment is being processed. Please wait...</p>
                </div>`
            }

            <div class="details">
                <h3>Payment Details:</h3>
                <p><strong>Order ID:</strong> ${order || 'N/A'}</p>
                <p><strong>Checkout ID:</strong> ${checkout_id || 'N/A'}</p>
                ${transaction_id ? `<p><strong>Transaction ID:</strong> ${transaction_id}</p>` : ''}
                ${transaction_status ? `<p><strong>Status:</strong> ${transaction_status}</p>` : ''}
                ${error_code ? `<p><strong>Error Code:</strong> ${error_code}</p>` : ''}
            </div>

            <p><a href="/">Return to Test Page</a></p>
        </body>
        </html>
    `);
});

// Webhook endpoint
app.post('/webhook/onlinepay', (req, res) => {
    console.log('Webhook received:', req.body);
    
    if (req.body.eventType === 'Checkout - Transaction succeeded') {
        console.log('Payment SUCCESS');
    } else if (req.body.eventType === 'Checkout - Transaction failed') {
        console.log('Payment FAILED');
    }
    
    res.status(200).send('OK');
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
    console.log(`Server running on http://localhost:${PORT}`);
    console.log(`Webhook endpoint: http://localhost:${PORT}/webhook/onlinepay`);
    console.log(`Open http://localhost:${PORT} to start testing.`);
});

Step 3: Set up ngrok for return URL and webhooks

⚠️

The return URL must be HTTPS for OnlinePay to redirect customers back to your site.

Setting up ngrok (requires free account)

  1. Create free ngrok account. Go to ngrok.com and sign up.

  2. From your ngrok dashboard, copy your auth token.

  3. Install and configure ngrok:

    # Install ngrok globally
    npm install -g ngrok
    
    # Authenticate with your token (one-time setup)
    ngrok config add-authtoken YOUR_AUTH_TOKEN_HERE
    
    # Start your server first
    node server.js
    
    # In another terminal, create HTTPS tunnel
    ngrok http 3000

    Alternatively, use ngrok without installing globally

    # Use npx to run ngrok without global install
    npx ngrok http 3000
    # You'll still need to authenticate: npx ngrok config add-authtoken YOUR_TOKEN

Configure your ngrok URL

  1. Copy your ngrok URL (e.g., https://abc123.ngrok.io).
  2. Update your .env file with the ngrok URL:
    NGROK_URL=https://abc123.ngrok.io
  3. Restart your server so that it picks up the new environment variable.

Set up webhook in OnlinePay dashboard

  1. Navigate to: Administration > Advanced Settings > Notifications.
  2. Create notification:
    • Name: "Test Checkout Webhooks"
    • Events: Select "Checkout - Transaction succeeded" and "Checkout - Transaction failed"
    • URL: https://your-ngrok-url.ngrok.io/webhook/onlinepay
    • Payload: "Full event payload"

Step 4: Run tests

  1. Start your server: node server.js
  2. In a separate terminal, start ngrok: ngrok http 3000
  3. Open your browser and navigate to http://localhost:3000
  4. Select a payment amount from the drop-down list of amounts. The default is $10.00 AUD.
  5. Test successful payment:
    • Use card number: 4601 8420 9009 4756
    • Expiry: 08/26, CVV: 490
    • Complete the payment
    • Check console for webhook confirmation
  6. Test failed payment:
    • Use card number: 4564 7100 0000 0004
    • Expiry: 02/29, CVV: 222
    • Check error handling

Step 5: Monitor results

In your server console, you should see the following logs for a successful payment:

All query params: [Object: null prototype] {
  order: 'TEST-1699123456789',
  checkout_id: 'abc123-def456-ghi789',
  transaction_id: '0a2b3c4d5e-6f7g-8h9i-j0k1-l2m3n4o5p6q7',
  authentication_id: 'z9y8x7w6v5-u4t3-s2r1-q0p9-o8n7m6l5k4j3',
}

Failed payments show the above details as well as an error_code.

You can also view your webhook logs in the OnlinePay Dashboard under Administration > Advanced Settings > Notifications > Test Checkout Webhooks.

Test card numbers

Test cards for payments

You can use the following test card numbers to simulate different payment scenarios in your test environment. These cards will not process real transactions but will help you test various payment flows.

Cards that are not 3D Secure will not trigger the 3D Secure flow, so you can test transactions without additional authentication steps.

Card TypeCard NumberExpiry DateCVV3D Secure
Visa4564 7100 0000 000402/29847No
Visa4601 8420 9009 475608/26490Yes
Mastercard5163 2000 0000 000808/30070No

Card Verification Value (CVV) check simulator

The OnlinePay test environment simulates the Card Verification Value (CVV) check. The following CVV values will trigger different responses:

card.cvvcvv_resultMeaning
1111Matched
2222Not matched
3333Not checked
any other value0Unavailable

Troubleshooting

Common issues:

  • "Missing parameters" error: Ensure all required customer billing fields are provided.
  • Webhook not received: Check your ngrok server is running and that the webhook URL is correct in the OnlinePay dashboard.
  • Payment failed: Verify you're using test credentials and test card numbers.
  • HTTPS errors: Ensure your ngrok tunnel is active and HTTPS URL is used for webhooks.

Debug tips:

  • Check server console for detailed logs.
  • Use in-browser developer tools to inspect network requests
  • Verify environment variables are loaded correctly in your .env file.
  • Test with different card numbers for various scenarios.


Terms and conditions Website requirements Other fees and charges

This information is a general statement for information purposes only and should only be used as a guide. While all care has been taken in preparation of this document, no member of the Westpac Group, nor any of their employees or directors gives any warranty of accuracy or reliability nor accepts any liability in any other way, including by reason of negligence for any errors or omissions contained herein, to the extent permitted by law. Unless otherwise specified, the products and services described are available only in Australia.

© Westpac Banking Corporation ABN 33 007 457 141 AFSL and Australian credit licence 233714.