Checkout fields
You can edit every checkout field. This page explains how store administrators can customize the default form and how developers can add fields through an add-on.
New fields appear in the existing checkout form.
What a checkout field is
A checkout field is a control the customer fills in while placing an order, with rules about when it appears and what counts as an answer.
A custom field is different: it is a typed value stored on a record, such as a supplier code an operator types on an order afterwards. It is never collected from a shopper.
| Checkout field | Custom field | |
|---|---|---|
| Collected from | The customer, at checkout | An operator or an add-on, on a record |
| Stored in | The order's checkout-field data, or the address itself | The custom field values table |
| Conditions | Required, hidden and valid, per checkout | None |
| Sorts and filters the admin list | No | Yes, when you say so |
A VAT number typed at checkout is a checkout field. A supplier reference added later is a custom field. An answer collected at checkout does not become a filterable column of the order list.
The shipped fields
Every built-in checkout field has a record owned by core. Upgrading preserves the existing fields, their order and their markup.
| Key | Section | Type | Drawn |
|---|---|---|---|
email |
contact | Always required | |
name |
contact | text | Always required |
billing_company |
billing | text | Off by default |
billing_address1, billing_address2 |
billing | text | |
billing_country |
billing | country | |
billing_city |
billing | text | |
billing_region |
billing | region | |
billing_postcode |
billing | text | |
billing_phone |
billing | tel | Off by default |
shipping_name … shipping_phone |
shipping | The delivery address, when it differs from the billing one | |
notes |
order | textarea |
A core field can be reordered, relabelled, required, hidden or switched off. It cannot be deleted: the API answers a delete with a 409 and tells you to switch it off instead. The two company lines and the billing phone ship switched off.
Add a field
- In the admin, open KahunaCart → Checkout and select the Fields panel.
- Select New field. Give it a key, a label key, a type and a section.
- Set Required when, Hidden when and Valid when if the field needs any of them. Each is the same condition builder sales and payment methods use.
- Select Create field.
The field is on the checkout on the next page load.
To hide a built-in field, switch Enabled off. To reorder, change Position: the shipped fields are spaced by ten, so there is room between any two.
The three conditions
required, hidden and validation are written in the same condition language as sales, coupons and payment methods.
A document is a container of all, any or none, holding conditions and other containers. Each condition is an attribute, an operator and a value.
Empty and unset mean different things:
| The document | required |
hidden |
validation |
|---|---|---|---|
| Unset | Never required | Never hidden | Anything is valid |
| An empty container | Always required | Always hidden | Anything is valid |
| A document with conditions | Required when it matches | Hidden when it matches | Valid when it matches |
validation is written as the condition that holds when the answer is good. The refusal is its opposite.
The attributes you can test
| Attribute | Type | Answers |
|---|---|---|
cart_subtotal |
money | What the lines charge, in minor units |
cart_quantity |
number | Every unit in the cart |
cart_line_count |
number | How many lines |
cart_product |
product | The products in the cart, for "the cart contains this product". A free gift does not count |
cart_variant |
variant | The variants in the cart. A free gift does not count |
cart_has_physical |
bool | Whether anything has to be delivered |
billing_country |
text | ISO 3166-1 alpha-2 |
billing_region |
text | The subdivision code, where the country has one |
shipping_country |
text | The delivery country, falling back to the billing one |
shipping_region |
text | The same, for the subdivision |
shipping_method |
text | The chosen method's id |
payment_provider |
text | The chosen provider's slug |
customer_group |
text | The group code of the signed-in customer, or of the customer staff attached to the order. A guest gets the store's default group, and empty on a store with no groups. See Whose customer data a rule reads |
customer_email_domain |
text | Everything after the @ of the signed-in customer's email. Empty for a guest, whatever address they typed. See Whose customer data a rule reads |
customer_logged_in |
bool | Whether an account is signed in |
field_<key> |
The field's own type | What was typed into another field |
The list is closed. An attribute not on it is refused by name when you save the field. GET /kahunacart/checkout-fields serves the list with every label translated, and the admin's condition builder is drawn from it.
A money condition is in a currency. On a store that sells in more than one currency cart_subtotal carries a currency beside its value, the store's own when it is missing, and it is false on an order in any other. A field required over 50.00 in dollars is not required on a euro order unless it also has a condition in euro, in an any container. The checkout field screen is read-only, so write it through the API. See Rules and conditions.
A namespaced key folds its slash to a double underscore, so acme/delivery_date is asked about as field_acme__delivery_date.
A condition on another field reads what was submitted, in one pass, so the answer never depends on field order and cannot loop. A field the customer cannot see reads as empty.
Important
A value posted for a field the checkout is not currently drawing is refused, not ignored, and so is a value under a key nobody declared. Both refusals happen before anything is written, and both apply to the storefront JSON API as well as to the form.
An empty box is not a posting, so a field the resolver hides after the customer changed their country on the same page does not turn a blank into a refused order.
Declare a field from an add-on
Subscribe to onKahunaCartRegisterCheckoutFields and register onto the registry.
public static function getSubscribedEvents(): array
{
return ['onKahunaCartRegisterCheckoutFields' => ['onKahunaCartRegisterCheckoutFields', 0]];
}
public function onKahunaCartRegisterCheckoutFields(Event $event): void
{
$event['registry']->register([
'key' => 'vat_number',
'owner' => 'kahunacart-eu-vat',
'type' => 'text',
'section' => 'billing',
'position' => 65,
'label_key' => 'PLUGIN_KAHUNACART_EU_VAT.NUMBER',
'required' => [
'container' => 'all',
'children' => [
['attribute' => 'billing_country', 'operator' => 'in', 'value' => ['DE', 'FR', 'NL', 'IE']],
['attribute' => 'field_is_business', 'operator' => 'eq', 'value' => '1'],
],
],
], 'kahunacart-eu-vat');
}
The declaration is written into the field table on the first request that reads it. The field survives your add-on being switched off, and a merchant's edits survive it being switched back on.
| Key | Required | Notes |
|---|---|---|
key |
Yes | [a-z][a-z0-9_]*, or vendor/name. Unique per store; the first registration wins and a duplicate is logged |
label_key |
Yes | A PLUGIN_*.* language key. Never English |
owner |
Yes for an add-on | Your plugin's name. core is reserved |
type |
text, textarea, email, tel, select, checkbox, date, number, plus country and region |
|
section |
contact, billing, shipping or order |
|
position |
Order within the section | |
options |
For a select |
A list of {value, label_key} |
required, hidden |
A condition document, or unset | |
validation |
A condition document plus a message_key |
|
enabled |
Whether the form draws it. Defaults to true |
A malformed declaration costs itself and nothing else. It is logged with a sentence explaining what is wrong, and the checkout still renders.
Where a value is stored
A core address field's answer goes into the order's billing or delivery address. Switching billing_company on puts a company on the invoice.
Everything else lands in the order's checkout-field data, one entry per field, each carrying the value and the label key that was on the field when the order was placed:
{
"vat_number": {
"value": "DE123456789",
"label_key": "PLUGIN_ACME_VAT.NUMBER",
"type": "text",
"section": "billing",
"position": 65
}
}
The label travels with the value, so an invoice printed years later still says what the number beside it was, whatever happened to the field since.
In a theme
The checkout template renders every control through one partial, partials/kahunacart-checkout-field.html.twig. Override that file to restyle every checkout control at once. Keep the name and the value: a control posting a name the resolver did not offer is refused.
A theme that overrides the checkout template itself should draw the field model section by section through that partial too, or fields you add here never appear on its checkout. The Kahuna theme does this since 1.0.8, with a Kahuna-styled override of the partial: fields you add, the company line once it is switched on, and fields from add-ons such as the newsletter opt-in all appear, and a field is shown, required or hidden exactly as your rules say for the address the shopper has entered. Before 1.0.8 it drew its own fixed set of inputs.
The checkout template has four named regions, kahunacart_checkout_notices, kahunacart_checkout_express, kahunacart_checkout_fields and kahunacart_checkout_shipping. See Theming.
A section holding exactly one field takes that field's label as its legend and draws the control unlabelled, as the order-notes fieldset does. Autocomplete tokens are declared, not guessed, so a browser never fills a "delivery instructions" box with a street address.
API reference
| Route | Permission | What it does |
|---|---|---|
GET /kahunacart/checkout-fields |
kahunacart.products.manage |
Every field in draw order, with the condition vocabulary beside them |
POST /kahunacart/checkout-fields |
kahunacart.products.manage |
Add a field |
PUT /kahunacart/checkout-fields/{id} |
kahunacart.products.manage |
Change one. Only the keys you send are written |
DELETE /kahunacart/checkout-fields/{id} |
kahunacart.products.manage |
Delete one. A core field answers 409 |
MCP tools: list_checkout_fields, create_checkout_field, update_checkout_field, delete_checkout_field.
Configuration
| Key | Default | Effect |
|---|---|---|
checkout.fields_enabled |
true |
Set to false and the checkout draws the shipped fields and ignores every declaration. An escape hatch, not a setting to run a store on |
Related
- Countries and addresses - the other half of the address form, and where required-by-country comes from
- Rules and conditions - the condition language these three documents are written in
- Theming - the named regions and the field partial
- Storefront - the checkout page itself
- Events - every
onKahunaCart*event - Custom fields and the settings store - the other kind of extra field, and which one you want
- Storefront JSON API - the JSON checkout the same rules apply to