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

Discounts & Coupons/Vouchers

A variety of discount & coupons/vouchers can be created and used with CartThrob. These discounts are stored as channel entries and are configured using a special custom field. The discounts system differs very little from the coupons system except that discounts do not require the customer to enter a code to apply the discount.

Available discount plugins

CartThrob currently supports the following discount & coupon types:

  • Amount off over x
  • Amount off product
  • Amount off entire cart
  • Buy x get x for discounted price
  • Free order
  • Free shipping
  • Limit by quantity
  • Percentage off categories
  • Percentage off product
  • Percentage off entire cart

Discounts Setup and Configuration

You create discounts by adding entries to a Discounts channel. There are some specific requirements for setting up your discounts channel, so please take note of the directions below.

Discounts channel setup

Create the discounts channel. At a minimum, the discounts channel requires a “discount settings” field. When you create a custom field for this discount settings field, you must use the CartThrob Discount Settings field type. This field type will allow you to select from one of the coupon plugins (current plugins are listed above.) The CartThrob Discount Settings custom field type will output additional required coupon code fields in your channel.

When you choose a coupon type in your discounts channel, you will notice additional fields specific to that coupon type. Not all discounts require the same data, so this custom field type makes it easy to display all necessary settings for that coupon code.

Backend Configuration

Configure the CartThrob backend. Go to Add-ons › CartThrob. Click on Discounts. Once on this page, map the discount channel & then the discount field to the custom field you just created.

As our users request additional discount types we will add them, so please contact us if the discount type you require is not currently available.

Coupons/Vouchers Setup and Configuration

You create coupon codes by adding entries to a coupons channel. There are some specific requirements for setting up your Coupons channel, so please take note of the directions below.

Coupons channel setup

Create the Coupons channel. If you used the “Auto-Install Templates” option of the CartThrob extension, your coupons channel should already be correctly configured. At a minimum, the coupons channel requires a “coupon settings” field. When you create a custom field for this coupon settings field, you must use the “CartThrob Discount Settings” field type. This field type will allow you to select from one of the coupon plugins (current plugins are listed above). The Coupon Code Settings custom field type will output additional required coupon code fields in your channel.

The following fields are used by CartThrob’s coupon system. The Coupon Code Field is optional:

  • Coupon Code Field: you can set the Title field as the code field in the coupon settings if you’d like.
  • Coupon Settings: described above.

When you choose one of these coupon types in your coupon channel you will notice additional fields specific to that coupon type. Not all coupons require the same data, so this custom field type makes it easy to display all necessary settings for that coupon code.

Backend Configuration

Configure the CartThrob backend. Go to Add-ons › CartThrob. Click on Discounts. Once on this page, map the coupon fields to the fields you just created.

As our users request additional coupon types we will add them, so please contact us if the coupon type you require is not currently available.

Coupon timing and final payment review

For the clearest checkout, apply the coupon before the payment step and show the discounted cart amount in a final review. A coupon field may be placed inside checkout_form, but its result is not known to the server when the form first renders.

In supported built-in payment flows, CartThrob finalizes the checkout quote before preparing payment. If a checkout-time coupon changes the rendered amount, payment pauses and the customer must confirm the updated subtotal, discount, shipping, tax, and total. The saved order and gateway charge then use that confirmed finalized total.

Apply a coupon without reloading the checkout page

You can submit add_coupon_form with JavaScript and update a checkout summary from the JSON response. This lets the customer see the discount before submitting payment, without reloading the page.

add_coupon_form and checkout_form must remain separate forms. HTML does not support nesting one form inside another. They can still appear together in the page layout.

The following example is for a normal checkout whose displayed summary comes from the cart. It uses native browser JavaScript and does not require jQuery or another JavaScript library.

Keep id="checkout_form" on checkout_form. Built-in Authorize.net, PayPal, and Stripe JavaScript bind that id. Use a CSS class if you need a styling hook. This coupon script does not replace the checkout quote path; see Custom checkout JavaScript.

{exp:cartthrob:add_coupon_form
    id="coupon-form"
    json="yes"
    return="checkout"
}
    <label for="coupon-code">Coupon code</label>
    <input
        id="coupon-code"
        name="coupon_code"
        type="text"
        autocomplete="off"
    >

    <button type="submit">Apply coupon</button>
    <p id="coupon-message" role="status" aria-live="polite"></p>
{/exp:cartthrob:add_coupon_form}

{exp:cartthrob:checkout_form
    id="checkout_form"
    return="order/complete"
    error_handling="inline"
}
    <div id="checkout-preview">
        <p>Subtotal: <span class="js-cart-subtotal">{cart_subtotal}</span></p>
        <p>Discount: <span class="js-cart-discount">{cart_discount}</span></p>
        <p>Shipping: <span class="js-cart-shipping">{cart_shipping}</span></p>
        <p>Tax: <span class="js-cart-tax">{cart_tax}</span></p>
        <p>Total: <strong class="js-cart-total">{cart_total}</strong></p>
    </div>

    <!-- Add customer, billing, shipping, and gateway fields here. -->
    {gateway_fields}

    <button type="submit">Place order</button>
{/exp:cartthrob:checkout_form}

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

    if (!couponForm || !message) {
        return;
    }

    const submitButton = couponForm.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)] : [];
    };

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

        try {
            const response = await fetch(couponForm.action, {
                method: 'POST',
                body: new FormData(couponForm),
                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 coupon could not be applied.');
            }

            // CartThrob returns formatted cart amounts in the AJAX response.
            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);

            // Secure-form installations may return a replacement CSRF token.
            const csrf = data.csrf_token || data.CSRF_TOKEN;
            if (csrf) {
                document
                    .querySelectorAll('input[name="csrf_token"]')
                    .forEach((input) => {
                        input.value = csrf;
                    });
            }

            message.textContent = 'Coupon applied. Please review the updated total.';
        } catch (error) {
            message.textContent = error.message || 'The coupon could not be applied.';
        } finally {
            submitButton.disabled = false;
        }
    });
})();
</script>

On success, the coupon action saves the updated cart and returns these formatted values:

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

It can also return CSRF_TOKEN. Copying that onto csrf_token fields is safe. Front-end AJAX to CartThrob actions does not require a new token for the AJAX post itself; leave ExpressionEngine secure forms on. See Ajax.

The JSON response contains cart amounts, not the finalized checkout_* amounts produced by amount-changing checkout_form parameters such as price, shipping, tax, no_shipping, or no_tax. Do not relabel data.cart_total as the final checkout charge when using those parameters. Render the checkout-aware preview with the corresponding checkout_* variables, and let CartThrob finalize and confirm the checkout quote before payment is prepared.

Address changes can also recalculate tax or shipping. Applying a coupon with JavaScript does not remove the need to refresh those dynamic amounts or provide a final review step. The server-side finalized quote, saved order total, and gateway charge remain authoritative.