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_subtotalcart_discountcart_shippingcart_taxcart_totaltotal_itemstotal_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.