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
});
};
}