CartThrob 9 documentation for ExpressionEngine commerce teams Visit CartThrob.com
CartThrob.com

Ajax

CartThrob form actions can return JSON instead of redirecting. Use that to save customer info, apply a coupon, or update the cart, then redraw totals without reloading the page.

This is for cart and customer-info forms (save_customer_info_form, update_cart_form, add_coupon_form). Checkout payment — quotes, checkout_quote_id, Stripe or PayPal prepare, custom_js="yes" — is a different contract. See Custom checkout JavaScript.

The current pattern is the CartThrob 10 live coupon checkout: native fetch, one named form, FormData, and X-Requested-With: XMLHttpRequest. No jQuery.form.js. Bind only the form you mean to submit.

How CartThrob returns JSON

Send the form as an AJAX POST. ExpressionEngine treats a request as AJAX when it includes the X-Requested-With: XMLHttpRequest header. On that path CartThrob does not redirect: it prints the flashdata as JSON, including formatted cart amounts when the action saved the cart.

Typical success keys:

  • success
  • cart_subtotal
  • cart_discount
  • cart_shipping
  • cart_tax
  • cart_total
  • total_items
  • total_unique_items

Those are cart amounts. They are not {checkout_total}. If this checkout uses price=, no_tax=, or other checkout-level overrides, print {checkout_*} in the template and let checkout finalize the quote before payment. See Checkout Amounts and Payment Review.

add_coupon_form also accepts json="yes". That is the parameter used on the CartThrob 10 coupon demo. save_customer_info_form and update_cart_form do not take that parameter; the AJAX header is enough.

CSRF: leave secure forms on

Do not turn off ExpressionEngine’s “Process form data in Secure Mode” setting to make AJAX work.

CartThrob forms already include a csrf_token field. FormData posts it. Front-end AJAX posts to CartThrob actions are not run through ExpressionEngine’s CSRF check (EE skips that check for non-CP AJAX unless the action class opts into strict mode). CartThrob does not. A later full-page submit — checkout_form Place order — still needs csrf_token on the form.

If the JSON includes CSRF_TOKEN, copying it onto csrf_token fields is safe. It is not what makes the AJAX post succeed. The CartThrob 10 live coupon template does not copy it.

Example: save location and refresh cart totals

Bind this form only. Do not attach a change handler to every input on the page; that would also submit checkout_form.

<p>Subtotal: <span class="js-cart-subtotal">{exp:cartthrob:cart_subtotal}</span></p>
<p>Shipping: <span class="js-cart-shipping">{exp:cartthrob:cart_shipping}</span></p>
<p>Tax: <span class="js-cart-tax">{exp:cartthrob:cart_tax}</span></p>
<p>Total: <strong class="js-cart-total">{exp:cartthrob:cart_total}</strong></p>

{exp:cartthrob:save_customer_info_form
    id="save-customer-form"
    return=""
    error_handling="inline"
}
    <label for="zip">Zip</label>
    <input id="zip" type="text" name="zip" value="{customer_zip}">

    <label for="state">State</label>
    <input id="state" type="text" name="state" value="{customer_state}">

    <button type="submit">Update totals</button>
    <p id="save-customer-message" role="status" aria-live="polite"></p>
{/exp:cartthrob:save_customer_info_form}

<script>
(() => {
    const form = document.querySelector('#save-customer-form');
    const message = document.querySelector('#save-customer-message');

    if (!form || !message || !window.fetch || !window.FormData) {
        return;
    }

    const submitButton = form.querySelector('[type="submit"]');

    const updateText = (selector, value) => {
        if (typeof value === 'undefined' || value === null) {
            return;
        }

        document.querySelectorAll(selector).forEach((element) => {
            element.textContent = value;
        });
    };

    const flattenErrors = (value) => {
        if (Array.isArray(value)) {
            return value.flatMap(flattenErrors);
        }

        if (value && typeof value === 'object') {
            return Object.values(value).flatMap(flattenErrors);
        }

        return value ? [String(value)] : [];
    };

    form.addEventListener('submit', async (event) => {
        event.preventDefault();
        message.textContent = 'Updating…';
        submitButton.disabled = true;

        try {
            const response = await fetch(form.action, {
                method: 'POST',
                body: new FormData(form),
                headers: {
                    'Accept': 'application/json',
                    'X-Requested-With': 'XMLHttpRequest'
                }
            });

            const data = await response.json();

            if (!response.ok || !data.success) {
                const errors = flattenErrors(data.errors);
                throw new Error(errors.join(' ') || 'The cart could not be updated.');
            }

            updateText('.js-cart-subtotal', data.cart_subtotal);
            updateText('.js-cart-discount', data.cart_discount);
            updateText('.js-cart-shipping', data.cart_shipping);
            updateText('.js-cart-tax', data.cart_tax);
            updateText('.js-cart-total', data.cart_total);
            message.textContent = 'Totals updated.';
        } catch (error) {
            message.textContent = error.message || 'The cart could not be updated.';
        } finally {
            submitButton.disabled = false;
        }
    });
})();
</script>

Coupons

Applying a coupon without reloading checkout uses the same fetch pattern on a separate add_coupon_form (json="yes"). Do not nest that form inside checkout_form. Keep id="checkout_form" on the payment form.

The full example — including the CartThrob 10 live coupon checkout that redraws the summary from JSON, then still runs the checkout quote on Place order — is on Discounts.

Same-page gateway field replacement is a different job (HTML, not cart JSON). See Allow the customer to select a gateway and gateway_fields_url.

Live-rate shipping forms can use this same fetch pattern to refresh cart_shipping / cart_total. See get_live_rates_form.

What not to do

  • Do not submit checkout_form with this AJAX pattern.
  • Do not bind every input and select on the page.
  • Do not disable ExpressionEngine secure forms.
  • Do not treat data.cart_total as the gateway charge when the checkout form has amount overrides.