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

Checkout Form (tag pair)

checkout_form outputs an HTML checkout form that posts to checkout_action, saves customer data, validates required fields, and starts payment processing.

A video tutorial on this tag is available.

See also:

Parameters

In addition to the parameters listed here, this tag also supports Global Form Parameters such as return, secure_return, error_handling, show_errors, required, rules:*, action, secure_action, id, class, name, onsubmit, enctype, and logged_out_redirect.

Amount source and override precedence

Without amount-changing parameters, checkout uses the current product-entry data, cart state, customer information, and store configuration. Mapped product settings—including price, per-product shipping, modifiers, and tax/shipping exemptions—are evaluated again when checkout is finalized; they are not necessarily frozen when the item enters the cart.

Recognized amount and exemption parameters on checkout_form override those calculated settings for this checkout. In particular:

  • price replaces the calculated order subtotal;
  • shipping replaces calculated shipping;
  • tax replaces calculated tax;
  • no_shipping/shipping_exempt removes calculated shipping;
  • no_tax/tax_exempt removes calculated tax;
  • permitted allow_user_shipping input replaces shipping;
  • permitted allow_user_price input replaces the final payable total.

Explicit numeric shipping and tax parameters take precedence over the corresponding exemption parameters. These overrides affect the finalized order calculation; they do not change the product entry or bypass inventory validation.

required

Adds required validation rules for additional fields on this checkout form.

Use a pipe-delimited list:

required="first_name|last_name|email_address"

Gateway-required fields are validated automatically. To exclude specific gateway-required fields, use the not prefix:

required="not first_name|last_name"

cart_empty_redirect

If the cart is empty when the tag renders, the customer is redirected to this path.

cart_empty_redirect="cart/index"

gateway

Sets the payment gateway short name for this checkout form.

  • If gateway selection is disabled in CartThrob settings, this parameter is ignored.
  • You can also post gateway from an input field (for example, from gateway_select).

    gateway=”authorize_net”

gateway_method

Sets an optional gateway-specific method (for gateways that support it).

gateway_method="ideal"

allow_user_price

Allows a posted price form field to set the final payable checkout total.

This is not a subtotal replacement. CartThrob still records the calculated subtotal, discount, shipping, and tax components, but the confirmed order total and gateway charge use the permitted user value. See Checkout amounts and saved orders.

allow_user_price="yes"

price

Sets a fixed subtotal override for this checkout form.

price="100"

This takes precedence over the subtotal calculated from current product prices and cart items. Tax, shipping, and discounts still apply unless separately overridden.

allow_user_shipping

Allows a posted shipping form field to override calculated shipping during checkout.

allow_user_shipping="yes"

The value must be scalar, numeric, and non-negative. This is a trust boundary, not a secure way to preserve a merchant-calculated shipping price: browser fields remain customer-controlled.

shipping

Sets a fixed shipping override for this checkout form.

shipping="15"

An explicit numeric shipping value takes precedence over no_shipping/shipping_exempt.

tax

Sets a fixed tax override for this checkout form.

tax="8.25"

An explicit numeric tax value takes precedence over no_tax/tax_exempt.

no_tax / tax_exempt

Removes tax from the checkout calculation. tax_exempt is retained as a legacy alias. A truthy checkout exemption overrides mapped product tax settings and the normally calculated order tax. An explicit numeric tax parameter has higher precedence.

no_tax="yes"

no_shipping / shipping_exempt

Removes shipping from the checkout calculation. shipping_exempt is retained as a legacy alias. A truthy checkout exemption overrides mapped product shipping settings and normally calculated order shipping. An explicit numeric shipping parameter has higher precedence.

no_shipping="yes"

country_code (legacy note)

country_code is commonly used as an input field value (for example, name="country_code") and saved through customer-info handling.

In current implementation, it is not a dedicated encoded checkout-form parameter like price, shipping, or tax.

expiration_date

When orders are saved as channel entries, sets entry expiration in days from now.

expiration_date="365"

create_user

Enables member creation during checkout.

create_user="yes"

When enabled, include create-member fields in your form (username, email_address, optional screen_name, password, password_confirm).

group_id

Sets the member group for create_user="yes".

  • Values below 5 are forced to 5.
  • role_id is accepted as an alias in current code.

    group_id=”6”

force_processing

Skips immediate card charge and marks order as processing.

force_processing="yes"

force_vault

Forces vault creation even when no subscription is present.

force_vault="yes"

vault_id

Uses an existing vault record for payment.

  • Vault ownership is validated against the current member.
  • When vault_id is used, render {vault_fields} instead of {gateway_fields}.

    vault_id=”1”

member_id

Admin checkout parameter: checkout on behalf of another member (allowed admin groups only).

member_id="123"

order_id

Completes an existing order in processing status. This parameter is the order entry ID (ExpressionEngine entry_id), not the invoice ID contained in the title. If this is set, member_id is ignored.

authorized_redirect, failed_redirect, declined_redirect, processing_redirect

Optional status-specific return paths used after checkout completion.

authorized_redirect="store/order/success"
declined_redirect="store/order/declined"
failed_redirect="store/order/failed"
processing_redirect="store/order/processing"

The Processing destination can receive an offline order awaiting payment or, with the CartThrob 10 payment-reconciliation update, a payment requiring merchant review. On the result page, check {if payment_review} before generic Processing/success wording and display the message/reference. See submitted_order_info.

custom_js

Set to yes/true to disable gateway-provided auto-injected JavaScript (form_extra) so you can provide your own implementation.

custom_js="yes"

That is not a styling switch. Custom code owns validation, quote finalization, changed-total confirmation, remote payment preparation, and final CartThrob submission, in that order. Built-in quotes do not cover this path. See Custom checkout JavaScript, Upgrade custom checkout JavaScript for CartThrob 10, and Built-in checkout vs custom JavaScript.

price_change_handling

Controls what the customer sees when CartThrob finalizes checkout and the authoritative total differs from the preview rendered with the form. Valid values are:

  • none — the default when the parameter is omitted. CartThrob dispatches the price-change events and then continues with the finalized amount without requiring purchaser confirmation.
  • confirm — displays a browser confirmation with the previous total, updated total, changed components, and current subtotal/discount/shipping/tax breakdown.
  • review — displays an accessible same-page review panel and keeps supported hosted payment fields mounted. This is the recommended built-in presentation for most stores.
  • inline — returns to the checkout form and provides the inline price-change variables documented below. The template must display the notice and the customer submits again after reviewing it.
  • template — loads the marked content from the internal EE template configured with price_change_template into a same-page modal. The checkout and supported hosted payment fields remain mounted.

Only a changed final total interrupts checkout. A redistribution among subtotal, discount, shipping, and tax that leaves the total unchanged is informational and does not require confirmation.

price_change_handling is independent of error_handling. For example, error_handling="inline" without an explicit price_change_handling still uses the silent none default.

price_change_handling="review"

price_change_template

Required when price_change_handling="template". Use an internal ExpressionEngine template name in group/template form:

price_change_handling="template"
price_change_template="store/checkout_price_changed"

The target template reads CartThrob’s opaque session-bound state with {exp:cartthrob:checkout_price_change}. Wrap the modal content in data-ct-checkout-price-change-modal, and mark one button with data-ct-checkout-price-change-confirm and another with data-ct-checkout-price-change-cancel. CartThrob loads that marked content over the existing checkout and owns the confirm/cancel lifecycle. See the checkout_price_change example.

For compatibility, a target template without the modal marker keeps the older full-page transition. Hosted gateway fields such as Stripe Elements and PayPal Hosted Fields cannot persist their entered values through that navigation, so the customer must enter payment details again after returning. The same warning applies to any custom flow that navigates away from or re-renders the checkout page.

AJAX can also apply coupons or save address/shipping changes and refresh the visible total before the final checkout submission.

Variables

In addition to the variables below, you can use Global Tag Variables, including cart/customer/custom-data values and form error values when inline error handling is used.

{gateway_fields}

Outputs the HTML fields required by the active gateway.

If gateway selection is enabled, this reflects the selected gateway context.

Gateway-generated fields and inline errors

{gateway_fields} can output standard customer fields as well as the gateway’s payment UI. In an ordinary checkout, treat that output as the authoritative set of customer and payment fields. Do not also add handwritten inputs with the same name or id; duplicate fields can make validation messages appear beside one input while a different value is submitted.

error_handling="inline" works with gateway-generated fields. Inline handling means CartThrob renders the same checkout template again and exposes the form error variables. It does not automatically insert each error beside the matching generated input. A checkout template can display:

  • an error summary with {errors}...{/errors};
  • a specific message such as {error:first_name};
  • custom gateway-field markup that places errors beside its inputs; or
  • JavaScript that maps each {field} value to the generated field’s ID or name.

Some gateways have an Enable Fields setting. If customer fields are disabled there, add any fields required by your checkout exactly once in the checkout template, or remove validation rules for fields the customer cannot edit. A configured Gateway Fields Template is also a full replacement for the default gateway output and must include every customer field and gateway integration element the checkout needs.

{vault_fields}

When vault_id is used, this outputs gateway vault fields for tokenized payment flows.

{captcha}

When guest checkout CAPTCHA is enabled, outputs ExpressionEngine’s local image CAPTCHA for logged-out customers.

Before enabling CartThrob’s setting:

  • turn off EE reCAPTCHA v3, which is not supported by CartThrob’s current word-and-IP validator;
  • configure EE’s CAPTCHA filesystem path and public URL;
  • make sure the configured directory exists and is writable by PHP; and
  • enable the PHP GD extension.

EE’s global Require CAPTCHA? setting may remain off. CartThrob independently decides when guest checkout requires the challenge.

CartThrob validates these prerequisites when its CAPTCHA setting is enabled. If an already-enabled site later loses the image backend, guest checkout fails closed: the form still requires a valid local CAPTCHA answer even if EE cannot create visible image markup.

The setting does not inject markup into an existing custom checkout template. Every checkout template must include the {if captcha} block, or logged-out customers will have no challenge available to complete.

Checkout Amount Variables

The tag calculates a checkout preview from the current cart and supported checkout-form overrides. Render this breakdown next to the payment controls:

  • {checkout_subtotal} / {checkout_subtotal_numeric}
  • {checkout_discount} / {checkout_discount_numeric}
  • {checkout_shipping} / {checkout_shipping_numeric}
  • {checkout_tax} / {checkout_tax_numeric}
  • {checkout_total_adjustment} / {checkout_total_adjustment_numeric}
  • {checkout_total} / {checkout_total_numeric}
  • {checkout_item_tax} / {checkout_item_tax_numeric}
  • {checkout_shipping_tax} / {checkout_shipping_tax_numeric}
  • {checkout_discount_tax} / {checkout_discount_tax_numeric}
  • {checkout_discount_total} / {checkout_discount_total_numeric}
  • {checkout_has_amount_overrides}
  • {checkout_total_differs_from_cart}
  • {checkout_has_tax_breakdown} and {checkout_tax_allocation}
  • {checkout_subtotal_source}, {checkout_shipping_source}, {checkout_tax_source}, and {checkout_total_source}
  • {checkout_currency} and {checkout_precision}

The formatted variables are appropriate for customer-facing output. The numeric variants are unformatted decimal values.

The six primary amounts reconcile at the configured currency precision:

checkout_total = checkout_subtotal - checkout_discount + checkout_shipping + checkout_tax + checkout_total_adjustment

checkout_total_adjustment is a signed, display-only reconciliation amount. It is normally zero and does not add another charge. It can be nonzero when an allowed customer-entered total or an extension supplies a final total that the other components cannot describe.

checkout_item_tax is net item tax after the tax reduction represented by checkout_discount_tax; it excludes shipping tax. checkout_discount_total is the pre-tax discount plus that tax reduction. Test checkout_has_tax_breakdown before displaying these details. They are blank, rather than estimated, when CartThrob cannot allocate the aggregate tax exactly. checkout_tax remains available, and checkout_tax_allocation is exact or unallocated.

The source variables explain why an amount has its current value:

  • calculated: current cart/store calculation;
  • fixed: a price=, shipping=, or tax= parameter on this form;
  • submitted: a scalar price or shipping value permitted by allow_user_price="yes" or allow_user_shipping="yes"; or
  • extension: the checkout-options hook changed that component.

Checkout parameters configure this rendered form. They do not rewrite the cart or become state on cart_info. An allowed submitted price or shipping amount belongs to that submission and finalized quote; a fresh GET returns to the saved cart plus the parameters declared on the newly rendered form.

The preview is not authoritative. A later address, shipping option, coupon, permitted customer amount, or checkout-options hook can change the finalized quote. The same breakdown is available to custom JavaScript through the quote response and the ct.checkout.quote-finalized and ct.checkout.amount-changed browser events. See Custom checkout JavaScript.

Inline Price-Change Variables

These variables are populated after a changed total returns to a form configured with price_change_handling="inline":

  • {checkout_price_changed} (1 when a notice is present)
  • {checkout_price_change_handling} (inline)
  • {checkout_changed_components} (pipe-delimited component names)
  • {checkout_previous_subtotal}, {checkout_previous_discount}, {checkout_previous_shipping}, {checkout_previous_tax}, {checkout_previous_total_adjustment}, and {checkout_previous_total}
  • {checkout_current_subtotal}, {checkout_current_discount}, {checkout_current_shipping}, {checkout_current_tax}, {checkout_current_total_adjustment}, and {checkout_current_total}
  • matched previous/current item-tax, shipping-tax, discount-tax, and discount-total formatted/numeric variables
  • previous/current {checkout_*_has_tax_breakdown} values, allocation values, and amount-source variables

A changed total is not a field-validation failure. Render validation variables such as {errors}, {field_errors}, {global_errors}, and {error:field} independently from this notice.

Checkout Action URLs

{checkout_quote_url} and {checkout_confirm_url} are available inside the tag pair for custom payment JavaScript. Standard gateway JavaScript uses these automatically. Most templates should not post to them directly. See Custom checkout JavaScript.

Error Variables

When using inline error handling, these are available inside the form:

  • errors_exist
  • field_errors_exist
  • global_errors_exist
  • {errors}...{/errors}
  • {field_errors}...{/field_errors}
  • {global_errors}...{/global_errors}
  • error:field_name (per-field message)

Conditionals

{if captcha}

True when captcha is required for this checkout context.

{if captcha}
    <p>Please enter the word in the image:</p>
    <p>{captcha}</p>
    <input type="text" name="captcha" autocomplete="off" />
{/if}

For Twig/Coilpack, use captcha_required and output the trusted image markup with the raw filter:

{% if checkout_form.captcha_required %}
    <p>Please enter the word in the image:</p>
    <p>{{ checkout_form.captcha | raw }}</p>
    <input type="text" name="captcha" autocomplete="off">
{% endif %}

The current CartThrob sample checkout templates include this conditional. Existing site templates are not rewritten during an update and must be checked manually.

{if errors_exist}

True when the previous submit failed validation/processing and errors are available.

{if errors_exist}
    {errors}
        <p>{if field_error}<strong>{field}:</strong>{/if} {error}</p>
    {/errors}
{/if}

{if checkout_has_amount_overrides}

True when the checkout tag contains a recognized amount-changing parameter.

{if checkout_total_differs_from_cart}

True when the checkout-aware preview total differs from the current cart total.

{if no_results} (legacy compatibility)

Legacy docs referenced this conditional, but checkout form behavior is currently controlled by cart_empty_redirect or your own outer cart checks (for example with total_items_count).

Use cart_empty_redirect when you want an automatic redirect for empty carts.

Form Fields

use_billing_info

If truthy, posted shipping fields are copied from billing fields during customer-info save.

<input type="checkbox" name="use_billing_info" value="1" />

price (form field)

Posted price is used only when allow_user_price is enabled.

<input type="text" name="price" value="11" />

shipping (form field)

Posted shipping is used only when allow_user_shipping is enabled.

<input type="hidden" name="shipping" value="10" />

tax (form field)

Posts a tax override value for this checkout request.

<input type="hidden" name="tax" value="2.50" />

coupon_code

Adds a coupon code during checkout submission.

<input type="text" name="coupon_code" />

shipping_option and shipping[key]

These are saved as shipping info during checkout save flow.

<input type="hidden" name="shipping_option" value="ground" />
<input type="hidden" name="shipping[service_code]" value="ground" />

custom_data[key]

Stores custom cart data values on submit.

<input type="hidden" name="custom_data[source]" value="newsletter" />

meta:some_key

For Stripe gateways, posted fields prefixed with meta: are forwarded as Stripe metadata.

<input type="hidden" name="meta:ReferredBy" value="Wanda Sellalot" />
<input type="text" name="meta:GiftFor" placeholder="Gift Recipient Name" />

Behavior and Constraints

  1. Checkout form validates inventory before rendering. If inventory is invalid, the tag errors immediately.
  2. If cart_empty_redirect is provided and cart is empty, customer is redirected.
  3. Gateway selection logic:
    • uses configured default gateway by default
    • respects gateway only when gateway selection is allowed
    • can use customer-selected posted gateway
  4. save_customer_info() runs during checkout action unless bypassed by admin/order-specific flows.
  5. Required fields come from gateway-required fields plus your required/rules:* settings.
  6. coupon_code is applied before payment processing.
  7. Redirect behavior:
    • return is standard fallback
    • status redirects (authorized_redirect, processing_redirect, declined_redirect, failed_redirect) are supported.
  8. vault_id is ownership-checked; invalid vault IDs are ignored.
  9. custom_js="yes" disables auto gateway JS injection so custom integration can control gateway scripts.
  10. Subscription-compatible subscription_* encoded parameters are accepted by checkout form/action and are processed during checkout subscription handling.
  11. When built-in payment JavaScript is used, omit id or set id="checkout_form". CartThrob supplies that ID by default. Use a class for styling instead of replacing this functional ID.
  12. Explicit numeric tax and shipping values take precedence over the corresponding exemption flags.
  13. Current mapped product data is evaluated during finalization. Recognized checkout amount/exemption parameters take precedence over the corresponding calculated product, cart, and store values. Inventory validation remains authoritative.
  14. Non-subscription Stripe Payments, PayPal Standard, and PayPal Advanced use the finalized-quote flow. Do not assume the same amount-changing behavior for subscription checkout or custom payment JavaScript.
  15. An expired, superseded, consumed, or state-mismatched quote stops checkout rather than authorizing an obsolete amount.

Checkout and Payment Validation Events

Deferred Stripe Payments can dispatch:

ct.payment-form.validation-error

The event detail can include:

  • fieldErrors
  • globalErrors
  • error
  • errorHandling
  • source

A custom listener can call event.preventDefault() and take responsibility for displaying the error. CartThrob field validation, gateway-element validation, quote-integrity errors, and remote payment errors are separate layers; not every payment failure belongs beside one form field.

Checkout Amounts and Saved Orders

CartThrob distinguishes four related stages:

  1. Cart total — the cart’s current products, discounts, shipping, and tax. Existing cart_* variables report this state.
  2. Checkout preview — the current cart with known amount-changing checkout_form parameters applied. The checkout_* variables report this state.
  3. Finalized checkout quote — the server-authoritative calculation after current product settings, submitted customer information, coupon, shipping selection, checkout overrides, and permitted user values have been processed.
  4. Gateway charge — the amount sent to the payment gateway. In supported built-in gateway flows this must match the confirmed finalized quote.

The saved order’s order-level subtotal, discount, shipping, tax, and total reflect the finalized checkout calculation. Purchased-item rows continue to record the underlying cart item prices.

As a result, a fixed price or a permitted user final total can make the order total intentionally differ from the sum of ordinary product prices. The gateway is not charged the old cart total: it receives the confirmed finalized order total.

For normal stores, prefer this sequence:

  1. collect and save billing and shipping information;
  2. select and save the shipping method;
  3. apply coupons;
  4. recalculate and display the cart;
  5. show the customer a final review;
  6. collect or confirm payment.

Location-sensitive shipping and tax can change when checkout receives a new address. A total displayed before that address reaches the server is an estimate, even when it accurately reflects the current cart.

Reviewing Dynamic Amount Changes

For built-in Authorize.net, PayPal Advanced, PayPal Standard, and non-subscription Stripe Payments flows, CartThrob finalizes a server quote before preparing or completing payment. If the finalized total differs from the amount rendered by checkout_form, CartThrob dispatches the price-change events before continuing with the finalized amount.

If price_change_handling is omitted, the none default does not interrupt the purchaser. Use confirm for the browser confirmation, review for the built-in same-page panel, inline for template variables on a re-rendered form, or template for a modal whose content comes from a dedicated internal template.

Two browser events support custom review interfaces:

  • ct.checkout.quote-finalized fires whenever a quote is finalized. Its detail includes quote, previous, current, formattedPrevious, formattedCurrent, changedComponents, requiresConfirmation, and priceChangeHandling.
  • ct.checkout.amount-changed fires when the finalized total changed. It is cancelable. Call event.preventDefault() to replace the configured or silent default behavior, then call event.detail.confirm() or event.detail.cancel().

Full event payloads, the tokenizer quote path, and custom_js="yes" examples are in Custom checkout JavaScript.

Raw component amounts in these event payloads are integer minor units. Customer-facing formatted values are also available as formattedPrevious and formattedCurrent. A custom listener must eventually confirm or cancel; otherwise checkout remains paused.

A same-page modal is often the smoothest option for hosted gateway fields: the original page and secure payment frames remain mounted while the customer reviews the new amount. The built-in template handling provides this when its target uses the documented modal markers. By contrast, navigating to another page or re-rendering the full page requires provider-controlled payment details to be entered again.

Examples

Standard checkout with gateway fields:

{exp:cartthrob:checkout_form
    id="checkout_form"
    class="form-horizontal"
    return="store/order_info"
}
    {gateway_fields}
    <input type="submit" value="Complete Checkout" />
{/exp:cartthrob:checkout_form}

Recommended same-page price-change review:

{exp:cartthrob:checkout_form
    price_change_handling="review"
    return="store/order_info"
}
    <dl>
        <dt>Subtotal</dt><dd>{checkout_subtotal}</dd>
        <dt>Discount</dt><dd>-{checkout_discount}</dd>
        <dt>Shipping</dt><dd>{checkout_shipping}</dd>
        <dt>Tax</dt><dd>{checkout_tax}</dd>
        <dt>Expected total</dt><dd><strong>{checkout_total}</strong></dd>
    </dl>
    {gateway_fields}
    <input type="submit" value="Continue to payment" />
{/exp:cartthrob:checkout_form}

Checkout with inline errors and coupon field:

{exp:cartthrob:checkout_form
    error_handling="inline"
    required="first_name|last_name|email_address"
    return="store/order_info"
}
    {if errors_exist}
        {errors}
            <p>
                {if field_error}<a href="#{field}"><strong>{field}:</strong></a>{/if}
                {error}
            </p>
        {/errors}
    {/if}

    {gateway_fields}
    {if error:first_name}
        <p class="error"><a href="#first_name">First name:</a> {error:first_name}</p>
    {/if}
    <input type="text" name="coupon_code" />
    <input type="submit" value="Checkout" />
{/exp:cartthrob:checkout_form}

Create member during checkout:

{exp:cartthrob:checkout_form
    create_user="yes"
    group_id="6"
    return="store/order_info"
}
    {gateway_fields}
    <input type="text" name="username" />
    <input type="text" name="email_address" />
    <input type="text" name="screen_name" />
    <input type="password" name="password" />
    <input type="password" name="password_confirm" />
    <input type="submit" value="Checkout" />
{/exp:cartthrob:checkout_form}

Vault checkout example:

{exp:cartthrob:checkout_form
    vault_id="1"
    force_vault="yes"
    return="store/order_info"
}
    {vault_fields}
    <input type="submit" value="Charge Saved Method" />
{/exp:cartthrob:checkout_form}