WordPress payment form redirects failing with plain permalinks when processing checkout submissions

I’m working on a PayPal integration for my WordPress site and running into issues with form redirects. My payment form has an action attribute that should send users to a confirmation page after they submit their payment details. The confirmation page uses a custom template.

The problem happens when I use plain permalinks in WordPress. After someone submits the payment form, they get a “page not found” error instead of reaching the success page. If I manually type the URL in the browser, the page loads fine.

When I change permalinks to custom structure with /index.php/%postname%, most pages work correctly but the form submission still fails to redirect properly.

Here’s my payment form code:

<?php /*Template Name: PayPal Checkout*/ get_header(); ?>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css">
<script src="https://js.paypal.com/sdk/js"></script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script type="text/javascript">
PayPal.configure({
    environment: 'sandbox',
    client_id: 'AeB5Q7xKjRnF8vC2mP9wL3tY1sN4kD6gH'
});

function handlePaymentResponse(status, data) {
    if (data.error) {
        $('#submitBtn').prop('disabled', false);
        $('.error-message').text(data.error.details);
    } else {
        var formElement = $('#checkoutForm');
        var paymentId = data.id;
        formElement.append('<input type="hidden" name="paymentId" value="' + paymentId + '" />');
        formElement[0].submit();
    }
}

$(function() {
    $('#checkoutForm').on('submit', function(e) {
        $('#submitBtn').prop('disabled', true);
        
        PayPal.createPayment({
            amount: $('.total-amount').val(),
            currency: 'USD',
            description: $('.item-description').val()
        }, handlePaymentResponse);
        
        return false;
    });
});

paymentRequest.on('approval', function(event) {
    fetch('/process-payment', {
        method: 'POST',
        body: JSON.stringify({payment_id: event.payment.id}),
        headers: {'content-type': 'application/json'}
    })
    .then(function(result) {
        if (result.ok) {
            event.complete('success');
        } else {
            event.complete('fail');
        }
    });
});
</script>

<div class="container">
    <div class="row justify-content-center">
        <div class="col-lg-8">
            <h2>Complete Your Purchase</h2>
            
            <div class="error-message text-danger"></div>
            
            <form action="<?php echo esc_url(get_permalink(542)); ?>" method="POST" id="checkoutForm">
                <div class="form-group">
                    <label>Full Name</label>
                    <input type="text" name="customer_name" class="form-control" required />
                </div>
                <div class="form-group">
                    <label>Email Address</label>
                    <input type="email" name="customer_email" class="form-control" required />
                </div>
                <div class="form-group">
                    <label>Card Number</label>
                    <input type="text" name="card_number" class="form-control card-number" autocomplete="off" />
                </div>
                <div class="form-group">
                    <label>Security Code</label>
                    <input type="text" name="security_code" class="form-control card-cvc" autocomplete="off" />
                </div>
                <div class="form-row">
                    <div class="col">
                        <label>Month</label>
                        <input type="text" name="exp_month" class="form-control card-expiry-month" placeholder="MM" />
                    </div>
                    <div class="col">
                        <label>Year</label>
                        <input type="text" name="exp_year" class="form-control card-expiry-year" placeholder="YYYY" />
                    </div>
                </div>
                <button type="submit" id="submitBtn" class="btn btn-primary btn-lg">Process Payment</button>
            </form>
        </div>
    </div>
</div>

<?php get_footer(); ?>

Had this exact same issue last year building a custom payment gateway. The problem is WordPress doesn’t recognize your form endpoint with plain permalinks, especially on custom page templates. Ditch get_permalink(542) in your form action. Use admin_url('admin-post.php') instead and handle everything through WordPress’s admin-post hook system. Don’t forget wp_nonce_field() for security and create a handler function that processes payment data before redirecting. What worked for me was adding a custom rewrite rule in functions.php to explicitly handle the payment endpoint. This way WordPress recognizes the URL no matter what permalink structure you’re using. The trick is running your payment logic before any template redirects happen - otherwise you’ll keep getting 404s even with correct permalinks.

I’ve hit the same redirect issues with payment forms on WordPress. The problem is WordPress handles POST requests differently than GET requests, especially with custom templates and plain permalinks.

Ditch get_permalink() for your form action. Use home_url('/payment-confirmation/') instead and set up a proper rewrite rule. Drop this in your functions.php:

function add_payment_rewrite_rules() {
    add_rewrite_rule('^payment-confirmation/?', 'index.php?pagename=payment-confirmation', 'top');
}
add_action('init', 'add_payment_rewrite_rules');

Another fix that worked for me: catch the form submission earlier using the template_redirect action. This grabs the POST data before WordPress messes with the permalink structure. Process your payment data there and manually redirect to your success page with wp_redirect().

Always flush your rewrite rules after making changes - fixes those weird 404 errors that pop up when switching permalink structures.

check if your .htaccess file is writable - plain permalinks will break post handling. use wp_ajax hooks instead of direct form submission. set up handlers with wp_ajax_process_payment and wp_ajax_nopriv_process_payment, then point your form action to admin_url('admin-ajax.php'). much cleaner than dealing with permalink rewrites.