Hosted Payments Page

Secure Payment Processing Integration Guide

Handling Timeouts

This guide explains how to handle slow or unresponsive payment requests when integrating with the Hosted Payments Page. Because the HPP client library does not impose its own request timeout, your integrating page is responsible for implementing any timeout UX.

Important: The HPP client library (hpp.js) does not abort or time out the underlying fetch call that loads the payment form, and it does not fire OnError after any particular wait. If you want users to see a "this is taking too long" message, you must implement the timer in your own code.

Where Timeouts Can Occur

Client-Side

  • Initial form load: Initialize(amount, surcharge) issues an authenticated GET to the HPP's Pay/Embed endpoint and injects the returned HTML via srcdoc. The browser's default network timeout applies; the library adds none.
  • BIN lookup: While the user types the card number, the iframe calls the merchant's BIN service to detect the card brand and surcharge eligibility.
  • Payment submission: When the iframe submits the form, the server posts to the backend integration service, which in turn talks to the payment processor.

Server-Side

Server-to-processor calls use HttpClient, which defaults to a 100-second timeout. If the upstream processor is slow, the HPP request can stall until that timeout elapses, after which the iframe renders a "We're Sorry" result page and fires the OnError or OnDecline event via postMessage.

Recommended user-facing budget: 30–60 seconds is reasonable before showing a "taking longer than expected" message. Users should not have to wait more than a minute without feedback.

Detecting Slow Responses

Since the library does not fire a timeout event on its own, the pattern is:

  1. Start a setTimeout when you call Initialize or when the user clicks submit.
  2. Cancel the timer in OnSuccess, OnDecline, OnError, and OnCancel.
  3. If the timer fires first, show a timeout message and let the user decide what to do next.

Example: Parent-Side Timeout Timer

var PAYMENT_TIMEOUT_MS = 60000;
var timeoutId = null;

var paymentWidget = new hpp({
    Target: "payment-container",
    HPPUrl: "https://your-payment-domain.com/",
    Token: getAuthToken(),
    MerchantId: "merchant-123",
    ShowPaymentTypeCc: true,
    InitialPaymentType: "CC",

    OnSubmit: function (amount) {
        // Start a parent-side timer when the user submits
        clearTimeout(timeoutId);
        timeoutId = setTimeout(displayTimeoutMessage, PAYMENT_TIMEOUT_MS);

        showLoadingSpinner();

        // Allow the iframe to actually submit
        paymentWidget.SubmitOk();
    },

    OnSuccess: function (response) {
        clearTimeout(timeoutId);
        hideLoadingSpinner();
        showSuccess(response);
    },

    OnDecline: function (response) {
        clearTimeout(timeoutId);
        hideLoadingSpinner();
        showDecline(response);
    },

    OnError: function (error) {
        clearTimeout(timeoutId);
        hideLoadingSpinner();
        showError(error);
    },

    OnCancel: function () {
        clearTimeout(timeoutId);
        hideLoadingSpinner();
    }
});

paymentWidget.Initialize(100.00, 0);
Note: OnError is also fired for non-timeout conditions (PCI compliance violations, amount validation errors, server-side exceptions). When it fires, the detail argument is a string message — inspect it to decide whether the situation should be treated as a timeout or something else.

Displaying Timeout Messages to Users

User-Friendly Message Examples

Best Practice Messages:
  • Friendly: "Sorry, this is taking longer than expected. Please wait a moment, or click below to try again."
  • Clear: "The payment request timed out. Your card has NOT been charged. Please try submitting your payment again."
  • Helpful: "We're having trouble connecting to our payment processor. Please check your internet connection and try again in a moment."

Example: Simple Timeout Message Display

function displayTimeoutMessage() {
    hideLoadingSpinner();

    var messageBox = document.getElementById('payment-message');
    messageBox.innerHTML = `
        <div class="timeout-message">
            <h3>Payment Request Timed Out</h3>
            <p>
                We didn't receive a response from the payment processor
                in time. We aren't sure yet whether the payment went through;
                please wait a moment before trying again.
            </p>
            <p><strong>What you can do:</strong></p>
            <ul>
                <li>Wait a moment and try again</li>
                <li>Check your internet connection</li>
                <li>Contact support if the problem continues</li>
            </ul>
            <button onclick="retryPayment()" class="retry-button">Try Again</button>
        </div>
    `;
    messageBox.style.display = 'block';
    messageBox.scrollIntoView({ behavior: 'smooth' });
}

function retryPayment() {
    document.getElementById('payment-message').style.display = 'none';
    // The form is still live inside the iframe; the user can resubmit.
}

Example: Styled Timeout Message CSS

<style>
.timeout-message {
    background-color: #fff3cd;
    border: 2px solid #ffc107;
    border-radius: 8px;
    padding: 20px;
    margin: 20px 0;
    box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}

.timeout-message h3 { color: #856404; margin-top: 0; margin-bottom: 15px; }
.timeout-message p  { color: #856404; line-height: 1.6; margin-bottom: 10px; }
.timeout-message ul { color: #856404; margin: 10px 0 20px 20px; }

.retry-button {
    background-color: #0693e3;
    color: white;
    border: none;
    padding: 12px 24px;
    font-size: 16px;
    border-radius: 4px;
    cursor: pointer;
    font-weight: 500;
    transition: background-color 0.3s;
}

.retry-button:hover { background-color: #007cba; }
</style>

Complete Example: Full Timeout Handling

A complete working example combining the concepts above:

<!DOCTYPE html>
<html>
<head>
    <title>Payment with Timeout Handling</title>
    <script src="https://your-payment-domain.com/scripts/hpp.js"></script>
</head>
<body>
    <div id="payment-container"></div>
    <div id="payment-message" style="display:none;"></div>
    <div id="loading-spinner" style="display:none;">Processing your payment...</div>

    <script>
    var PAYMENT_TIMEOUT_MS = 60000;
    var timeoutTimer = null;

    var paymentWidget = new hpp({
        Target: "payment-container",
        HPPUrl: "https://your-payment-domain.com/",
        Token: "YOUR_TOKEN",
        MerchantId: "YOUR_MERCHANT_ID",
        ShowPaymentTypeCc: true,
        InitialPaymentType: "CC",

        OnSubmit: function (amount) {
            document.getElementById('loading-spinner').style.display = 'block';
            clearTimeout(timeoutTimer);
            timeoutTimer = setTimeout(handleTimeout, PAYMENT_TIMEOUT_MS);

            // Required — without SubmitOk() the iframe will not submit.
            paymentWidget.SubmitOk();
        },

        OnSuccess: function (response) {
            clearTimeout(timeoutTimer);
            document.getElementById('loading-spinner').style.display = 'none';
            showMessage('success', 'Payment Successful!',
                'Your payment has been processed. Thank you!');
        },

        OnDecline: function (response) {
            clearTimeout(timeoutTimer);
            document.getElementById('loading-spinner').style.display = 'none';
            showMessage('error', 'Payment Declined',
                response.ResponseMessage || 'Please check your payment information and try again.');
        },

        OnError: function (errorMessage) {
            clearTimeout(timeoutTimer);
            document.getElementById('loading-spinner').style.display = 'none';
            showMessage('error', 'Payment Error',
                'An error occurred: ' + errorMessage);
        },

        OnCancel: function () {
            clearTimeout(timeoutTimer);
            document.getElementById('loading-spinner').style.display = 'none';
        }
    });

    paymentWidget.Initialize(100.00, 0);

    function handleTimeout() {
        document.getElementById('loading-spinner').style.display = 'none';

        var messageDiv = document.getElementById('payment-message');
        messageDiv.innerHTML = `
            <div class="message-box timeout">
                <h3>Request Timed Out</h3>
                <p>
                    The payment request took too long to complete.
                    We aren't sure yet whether the charge went through,
                    so please wait a moment before retrying.
                </p>
                <p><strong>What to do next:</strong></p>
                <ul>
                    <li>Check your internet connection</li>
                    <li>Wait a moment for the system to settle</li>
                    <li>Verify with your server whether the transaction completed before retrying</li>
                    <li>Contact support if this keeps happening</li>
                </ul>
            </div>
        `;
        messageDiv.style.display = 'block';
        messageDiv.scrollIntoView({ behavior: 'smooth' });
    }

    function showMessage(type, title, message) {
        var messageDiv = document.getElementById('payment-message');
        messageDiv.innerHTML =
            '<div class="message-box ' + type + '">' +
            '<h3>' + title + '</h3><p>' + message + '</p></div>';
        messageDiv.style.display = 'block';
        messageDiv.scrollIntoView({ behavior: 'smooth' });
    }
    </script>
</body>
</html>

Testing Timeout Scenarios

  1. Reduce the timeout in your code to something short (e.g., 5 seconds) during testing.
  2. Throttle the network with browser dev tools ("Slow 3G" in Chrome/Edge) to force slow responses.
  3. Submit a test payment and confirm your timeout message appears.
  4. Test the retry path after the message appears.
  5. Return the timeout to production value (typically 30–60 seconds) when finished.
Testing Warning: Use small test amounts ($1.00) and a non-production merchant. A request that appears to time out on the client may still complete on the processor — always verify transaction state server-side before assuming it failed.

Important Considerations

Don't Automatically Retry

Never auto-retry a payment after a timeout. The original request may have succeeded; an automatic retry can double-charge the customer.

Don't do this:
function handleTimeout() {
    submitPaymentAgain(); // dangerous — may double-charge
}
Do this instead:
function handleTimeout() {
    displayTimeoutMessage();
    enableRetryButton(); // let the user choose
}

Verify State Server-Side Before Retrying

After a suspected timeout, the safest action is to ask your own backend whether the transaction completed — relying on the HPP's correlation ID to tie the original request to its eventual outcome.

function handleTimeout() {
    displayTimeoutMessage();

    // Optional: after a short delay, check with your server whether
    // the transaction actually completed using the correlation ID
    // that was passed to the HPP.
    setTimeout(function () {
        checkTransactionStatus(correlationId)
            .then(function (status) {
                if (status === 'completed') {
                    showMessage('success',
                        'Payment Completed',
                        'Your payment went through successfully.');
                }
            });
    }, 5000);
}

Log Timeout Events

function handleTimeout() {
    console.error('Payment timeout', {
        timestamp: new Date().toISOString(),
        merchantId: 'YOUR_MERCHANT_ID',
        amount: currentAmount,
        timeoutMs: PAYMENT_TIMEOUT_MS,
        correlationId: correlationId
    });

    if (window.analytics) {
        analytics.track('Payment Timeout', {
            amount: currentAmount,
            duration: PAYMENT_TIMEOUT_MS
        });
    }

    displayTimeoutMessage();
}

Troubleshooting Common Issues

Problem Possible Cause Solution
Frequent timeouts Network issues or upstream processor latency Check connectivity; contact Procare support with correlation IDs if the issue persists
Timeout message never appears setTimeout not started, or the timer isn't being cleared in other callbacks correctly Verify the timer is started in OnSubmit (or Initialize) and cleared in every terminal callback
User charged twice after timeout Automatic retry on timeout Remove auto-retry; always require explicit user action and verify state server-side first
Timeout fires too eagerly Timer too short for real-world processor latency Use 30–60 seconds for payment submission

Summary Checklist

Implementing Timeout Handling
  • Set a parent-side timer of 30–60 seconds — the library does not provide one
  • Start the timer in OnSubmit (after calling SubmitOk()) or around Initialize for load timeouts
  • Clear the timer in OnSuccess, OnDecline, OnError, and OnCancel
  • Display a clear, user-friendly timeout message
  • Never auto-retry — require explicit user action
  • Verify transaction state server-side before retrying
  • Log timeouts with the correlation ID for later investigation
  • Test your timeout UX with network throttling before going live