Hosted Payments Page

Secure Payment Processing Integration Guide

Best Practices

Integration Best Practices

1. Token Management

  • Generate tokens server-side with appropriate expiration times
  • Never expose token generation secrets in client-side code
  • Include only necessary claims in the token (merchant ID, application ID, permissions)
  • Implement token refresh logic for long-lived sessions
// Server-side token generation (Node.js example)
const jwt = require('jsonwebtoken');

function generatePaymentToken(merchantId, userId) {
    const payload = {
        merchantId: merchantId,
        userId: userId,
        appid: process.env.APP_ID,
        permissions: ['payment.process', 'payment.save']
    };

    const options = {
        expiresIn: '30m',  // Token expires in 30 minutes
        issuer: 'your-company',
        audience: 'hpp-service'
    };

    return jwt.sign(payload, process.env.JWT_SECRET, options);
}

// Client-side: Fetch token from your server
fetch('/api/payment-token', {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify({orderId: orderId})
})
.then(response => response.json())
.then(data => {
    // Use token with HPP
    var paymentWidget = new hpp({
        Token: data.token,
        // ... other config
    });
});

2. Error Handling

var paymentWidget = new hpp({
    // ... configuration ...

    OnError: function(error) {
        // Log error for debugging
        console.error("Payment error:", error);

        // Send error to monitoring service
        logErrorToMonitoring({
            type: 'payment_error',
            error: error,
            timestamp: new Date().toISOString(),
            orderId: currentOrderId
        });

        // Show user-friendly message
        var userMessage = "We're sorry, but we couldn't process your payment. ";

        if (error.includes("network")) {
            userMessage += "Please check your internet connection and try again.";
        } else if (error.includes("timeout")) {
            userMessage += "The request timed out. Please try again.";
        } else {
            userMessage += "Please try again or contact support.";
        }

        showErrorMessage(userMessage);

        // Provide fallback options
        showAlternativePaymentOptions();
    },

    OnDecline: function(response) {
        // Log decline reason
        console.warn("Payment declined:", response.ResponseMessage);

        // Track decline for analytics
        trackEvent("payment_declined", {
            reason: response.ResponseMessage,
            orderId: currentOrderId
        });

        // Provide actionable feedback to user
        var message = "Your payment was declined. ";

        if (response.ResponseMessage.includes("insufficient funds")) {
            message += "Please check your account balance or use a different payment method.";
        } else if (response.ResponseMessage.includes("expired")) {
            message += "Your card has expired. Please use a different card.";
        } else if (response.ResponseMessage.includes("cvv")) {
            message += "The CVV code was incorrect. Please verify and try again.";
        } else {
            message += "Please verify your payment information or contact your bank.";
        }

        showErrorMessage(message);

        // Offer to try different payment method
        if (confirm(message + "\n\nWould you like to try a different payment method?")) {
            paymentWidget.Clear();
            showPaymentMethodSelector();
        }
    }
});

3. Amount Management

// Always use decimal precision for currency
var baseAmount = 99.99;
var surcharge = 2.50;

// Initialize with precise amounts
paymentWidget.Initialize(baseAmount, surcharge);

// When updating amounts, recalculate with precision
function updatePaymentAmount(newItems) {
    var newBaseAmount = calculateCartTotal(newItems);
    var newSurcharge = calculateSurcharge(newBaseAmount);

    // Ensure proper decimal precision
    newBaseAmount = Number(newBaseAmount.toFixed(2));
    newSurcharge = Number(newSurcharge.toFixed(2));

    paymentWidget.SetAmount(newBaseAmount, newSurcharge);
}

// Calculate surcharge based on amount
function calculateSurcharge(amount) {
    // Example: 2.5% surcharge with $0.50 minimum
    var surcharge = amount * 0.025;
    if (surcharge < 0.50) {
        surcharge = 0.50;
    }
    return Number(surcharge.toFixed(2));
}

4. Field Pre-population

OnLoaded: function(event) {
    // Wait for form to load before setting values
    var customerData = getCustomerData();

    if (customerData && customerData.hasShippingAddress) {
        // Pre-fill with shipping address
        paymentWidget.SetFormValues({
            "Request_Name": customerData.name,
            "Request_Email": customerData.email,
            "Request_Phone": customerData.phone,
            "Request_Address": customerData.shippingAddress.street,
            "Request_City": customerData.shippingAddress.city,
            "Request_Region": customerData.shippingAddress.state,
            "Request_Postal": customerData.shippingAddress.zip
        });

        // Ask if billing address is different
        if (!confirm("Use shipping address as billing address?")) {
            // Clear address fields for manual entry
            paymentWidget.SetFormValues({
                "Request_Address": "",
                "Request_City": "",
                "Request_Region": "",
                "Request_Postal": ""
            });
        }
    }
}

5. Correlation ID Tracking

// Generate or retrieve correlation ID from your system
function getOrCreateCorrelationId(orderId) {
    // Check if correlation ID exists for this order
    var existingId = sessionStorage.getItem('correlationId_' + orderId);

    if (existingId) {
        return existingId;
    }

    // Generate new correlation ID
    var correlationId = 'ORD-' + orderId + '-' + Date.now() + '-' +
                        Math.random().toString(36).substr(2, 9);

    // Store for reuse
    sessionStorage.setItem('correlationId_' + orderId, correlationId);

    return correlationId;
}

var correlationId = getOrCreateCorrelationId(currentOrderId);

var paymentWidget = new hpp({
    // ... other configuration ...
    CorrelationID: correlationId,

    OnSuccess: function(response) {
        // Use correlation ID to link payment to your order
        linkPaymentToOrder(currentOrderId, correlationId, response.TransactionId);

        // Store for customer service reference
        saveTransactionRecord({
            orderId: currentOrderId,
            correlationId: correlationId,
            transactionId: response.TransactionId,
            amount: response.AuthAmount,
            timestamp: new Date().toISOString(),
            customerId: getCurrentCustomerId()
        });
    }
});

User Experience Best Practices

1. Loading State

<style>
.loading-overlay {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: rgba(255, 255, 255, 0.9);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 1000;
}

.spinner {
    border: 4px solid #f3f3f3;
    border-top: 4px solid #3498db;
    border-radius: 50%;
    width: 40px;
    height: 40px;
    animation: spin 1s linear infinite;
}

@keyframes spin {
    0% { transform: rotate(0deg); }
    100% { transform: rotate(360deg); }
}
</style>

<div id="payment-container" style="min-height: 400px; position: relative;">
    <div class="loading-overlay" id="loading-spinner">
        <div class="spinner"></div>
    </div>
</div>

<script>
var paymentWidget = new hpp({
    // ... configuration ...

    OnLoaded: function() {
        // Hide loading spinner
        document.getElementById('loading-spinner').style.display = 'none';

        // Track load time
        var loadTime = Date.now() - startTime;
        trackMetric('payment_form_load_time', loadTime);
    }
});

var startTime = Date.now();
paymentWidget.Initialize(amount, surcharge);
</script>

2. Custom Styling

Tip: Use the Style parameter to inject custom CSS that matches your brand:
var customCSS = `
    body {
        font-family: 'Your Brand Font', -apple-system, BlinkMacSystemFont, sans-serif;
        background-color: #f8f9fa;
    }

    .payment-form {
        max-width: 500px;
        margin: 0 auto;
        padding: 30px;
        background: white;
        border-radius: 8px;
        box-shadow: 0 2px 10px rgba(0,0,0,0.1);
    }

    .field {
        margin-bottom: 20px;
    }

    .field label {
        display: block;
        color: #333;
        font-weight: 600;
        margin-bottom: 5px;
    }

    .field input,
    .field select {
        width: 100%;
        border: 1px solid #ddd;
        border-radius: 4px;
        padding: 10px;
        font-size: 14px;
        transition: border-color 0.3s;
    }

    .field input:focus,
    .field select:focus {
        outline: none;
        border-color: #007bff;
        box-shadow: 0 0 0 3px rgba(0,123,255,0.1);
    }

    .field input:invalid.was-touched {
        border-color: #dc3545;
    }

    .btn-primary {
        background-color: #007bff;
        border-color: #007bff;
        color: white;
        padding: 12px 24px;
        font-size: 16px;
        font-weight: 600;
        border-radius: 4px;
        cursor: pointer;
        transition: background-color 0.3s;
        width: 100%;
    }

    .btn-primary:hover {
        background-color: #0056b3;
    }

    .btn-primary:disabled {
        background-color: #6c757d;
        cursor: not-allowed;
    }

    #CardLogos {
        display: flex;
        gap: 10px;
        margin-top: 10px;
    }
`;

var paymentWidget = new hpp({
    Style: encodeURIComponent(customCSS),  // Encode CSS for URL
    // ... other configuration ...
});

3. Responsive Design

// Container should be responsive
<style>
.payment-wrapper {
    width: 100%;
    max-width: 600px;
    margin: 0 auto;
    padding: 20px;
}

@media (max-width: 768px) {
    .payment-wrapper {
        padding: 10px;
    }
}
</style>

<div class="payment-wrapper">
    <div id="payment-container"></div>
</div>

<script>
// Widget will auto-adjust height based on content
var paymentWidget = new hpp({
    Width: "100%",
    Height: "100%",  // Will auto-resize
    // ... other configuration ...

    OnSize: function({width, height}) {
        // Optional: Add custom responsive behavior
        console.log("Form size:", width, "x", height);

        // Adjust parent container if needed
        if (width < 400) {
            document.querySelector('.payment-wrapper').classList.add('compact-mode');
        } else {
            document.querySelector('.payment-wrapper').classList.remove('compact-mode');
        }
    }
});
</script>

4. Payment Type Selection

// Allow users to choose payment type if multiple are available
var paymentWidget = new hpp({
    ShowPaymentTypeCc: true,
    ShowPaymentTypeAch: true,
    AllowPaymentTypeToggle: true,
    InitialPaymentType: "CC",  // Default to credit card

    OnChangePaymentType: function(paymentType) {
        // Update UI to reflect payment type
        var messageElement = document.getElementById('payment-type-message');

        if (paymentType === "ACH") {
            messageElement.textContent = "ACH payments typically take 3-5 business days to process";
            messageElement.className = "alert alert-info";

            // Show save option for ACH
            document.getElementById('save-payment-option').style.display = 'block';
        } else if (paymentType === "CC") {
            messageElement.textContent = "Credit card payments are processed immediately";
            messageElement.className = "alert alert-success";
        }

        // Track payment type selection
        trackEvent('payment_type_selected', {type: paymentType});

        // Recalculate amount if surcharge rules differ
        updateAmountForPaymentType(paymentType);
    }
});

Performance Best Practices

1. Load Widget Asynchronously

// Load HPP library asynchronously to avoid blocking page load
<script src="https://your-payment-domain.com/scripts/hpp.js" async></script>

<script>
function initializePayment() {
    if (typeof hpp !== 'undefined') {
        // Initialize widget
        var paymentWidget = new hpp({
            /* ... configuration ... */
        });
        paymentWidget.Initialize(amount, surcharge);
    } else {
        // Wait for library to load
        setTimeout(initializePayment, 100);
    }
}

// Initialize after page load
if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', initializePayment);
} else {
    initializePayment();
}
</script>

2. Reuse Widget Instances

// Create widget once, update as needed
var paymentWidget = null;

function showPaymentForm(amount, surcharge) {
    if (!paymentWidget) {
        // Create widget first time
        paymentWidget = new hpp({
            Target: "payment-container",
            HPPUrl: "https://your-payment-domain.com/",
            Token: getAuthToken(),
            MerchantId: getMerchantId(),
            ShowPaymentTypeCc: true,
            InitialPaymentType: "CC",
            OnSuccess: handlePaymentSuccess,
            OnDecline: handlePaymentDecline
        });

        paymentWidget.Initialize(amount, surcharge);
    } else {
        // Reuse existing widget, just update amount
        paymentWidget.SetAmount(amount, surcharge);
    }
}

// Later, update amount without recreating widget
showPaymentForm(75.00, 0);  // First call creates widget
showPaymentForm(100.00, 2.50);  // Second call reuses widget

3. Clean Up When Done

// When navigating away or showing different content
function closePaymentForm() {
    if (paymentWidget) {
        paymentWidget.Clear();  // Removes iframe and event listeners
        paymentWidget = null;
    }

    document.getElementById('payment-container').style.display = 'none';
}

// Clean up on page unload
window.addEventListener('beforeunload', function() {
    if (paymentWidget) {
        paymentWidget.Clear();
    }
});

// Clean up when hiding payment section
document.getElementById('close-payment-btn').addEventListener('click', function() {
    closePaymentForm();
    showMainContent();
});

Testing Best Practices

1. Use Test Credentials

  • Always use test merchant IDs and test tokens during development
  • Use test card numbers provided by your payment processor
  • Never test with real payment information
// Test card numbers (examples - use your processor's test cards)
const TEST_CARDS = {
    visa: {
        approved: '4111111111111111',
        declined: '4000000000000002',
        expired: '4000000000000069'
    },
    mastercard: {
        approved: '5555555555554444',
        declined: '5105105105105100'
    },
    amex: {
        approved: '378282246310005'
    }
};

// Test environment configuration
const isTestMode = process.env.NODE_ENV === 'development';

var paymentWidget = new hpp({
    HPPUrl: isTestMode ?
        "https://test-payments.example.com/" :
        "https://payments.example.com/",
    Token: isTestMode ? getTestToken() : getProductionToken(),
    MerchantId: isTestMode ? "test-merchant-123" : getProductionMerchantId(),
    // ... other configuration ...
});

2. Test All Payment Types

// Test with each payment type enabled
const testConfigurations = [
    {
        name: "Credit Card Only",
        config: {
            ShowPaymentTypeCc: true,
            InitialPaymentType: "CC"
        }
    },
    {
        name: "ACH Only",
        config: {
            ShowPaymentTypeAch: true,
            InitialPaymentType: "ACH"
        }
    },
    {
        name: "Credit Card and ACH",
        config: {
            ShowPaymentTypeCc: true,
            ShowPaymentTypeAch: true,
            AllowPaymentTypeToggle: true,
            InitialPaymentType: "CC"
        }
    }
];

// Run automated tests
testConfigurations.forEach(testConfig => {
    describe(testConfig.name, () => {
        it('should load payment form', () => {
            testPaymentFormLoad(testConfig.config);
        });

        it('should process successful payment', () => {
            testSuccessfulPayment(testConfig.config);
        });

        it('should handle declined payment', () => {
            testDeclinedPayment(testConfig.config);
        });
    });
});

3. Test Error Scenarios

  • Test with declined cards (specific test card numbers trigger declines)
  • Test with expired cards
  • Test with invalid CVV codes
  • Test with invalid bank routing numbers
  • Test network timeouts and server errors
  • Test PCI compliance validation
  • Test duplicate transaction detection

Troubleshooting Common Issues

Issue: Iframe Not Loading

  • Verify the HPPUrl is correct and accessible
  • Check that the authentication token is valid and not expired
  • Ensure CORS is configured correctly on the HPP server
  • Check browser console for CSP or mixed content errors
  • Verify the Target element exists in the DOM

Issue: Form Submission Fails

  • Verify merchant is active and configured to accept the payment type
  • Check that required fields are filled and valid
  • Ensure PCI compliance validation is passing
  • Check for network errors or timeouts
  • Verify token has necessary permissions

Issue: Events Not Firing

  • Ensure callback functions are defined before calling Initialize()
  • Check for JavaScript errors that might be blocking execution
  • Verify postMessage communication is not blocked by browser security policies
  • Check that OnResponse event shows messages are being received

Issue: Surcharge Not Applied

  • Verify merchant is configured to allow surcharges
  • Check that EnforceSurchargeCompliance is set correctly
  • Ensure the card BIN is eligible for surcharges (via OnBinLookupResponse)
  • Verify payment type is credit card (surcharges don't apply to ACH)
  • Check that surcharge amount is passed correctly to Initialize() or SetAmount()

Monitoring and Analytics

// Track key metrics
function trackPaymentMetrics(widget) {
    var metrics = {
        startTime: Date.now(),
        loadTime: null,
        submitTime: null,
        completionTime: null,
        paymentType: null,
        amount: null,
        result: null
    };

    widget.OnLoaded = function(event) {
        metrics.loadTime = Date.now() - metrics.startTime;
        trackMetric('payment_form_load_time', metrics.loadTime);
    };

    widget.OnChangePaymentType = function(type) {
        metrics.paymentType = type;
        trackEvent('payment_type_changed', {type: type});
    };

    widget.OnSubmit = function(amount) {
        metrics.submitTime = Date.now();
        metrics.amount = amount;
        trackEvent('payment_submit', {amount: amount});
        widget.SubmitOk();
    };

    widget.OnSuccess = function(response) {
        metrics.completionTime = Date.now() - metrics.startTime;
        metrics.result = 'success';

        trackMetric('payment_completion_time', metrics.completionTime);
        trackEvent('payment_success', {
            transactionId: response.TransactionId,
            amount: response.AuthAmount,
            duration: metrics.completionTime
        });
    };

    widget.OnDecline = function(response) {
        metrics.result = 'declined';
        trackEvent('payment_declined', {
            reason: response.ResponseMessage,
            amount: metrics.amount
        });
    };

    widget.OnError = function(error) {
        metrics.result = 'error';
        trackEvent('payment_error', {
            error: error,
            amount: metrics.amount
        });
    };
}