Addresses

Commerce manages shipping and billing addresses using Address (opens new window) models that are relevant to Orders and Customers.

In the control panel, you’ll find addresses within the context of Orders and Customers. A Store Location may also be entered at CommerceStore SettingsStore Location.

You can also manage any customer’s addresses from their user account. Navigate to Users, choose a user to visit their edit page, and see the Addresses section of the Customer Info tab:

A user’s addresses seen from their Craft user account page

# Managing Addresses

Your front end can work with addresses by way of the cart and via customer accounts.

Every order may have a shipping and billing address, and customers with accounts can save and re-use addresses when placing new orders. How you collect and validate addresses on the front end is up to you. Commerce provides tools that help streamline address management:

An Addresses service (opens new window) also provides methods for working directly with address data. We can use it here, for example, to get the store location address:

{% set storeAddress = craft.commerce.addresses.storeLocationAddress %}

If you flattened storeAddress into an array, this is what it might look like:

array (
  'id' => '5',
  'isStoreLocation' => true,
  'attention' => '',
  'title' => '',
  'firstName' => '',
  'lastName' => '',
  'fullName' => '',
  'address1' => '1234 Balboa Towers Circle',
  'address2' => '#100',
  'address3' => '',
  'city' => 'Los Angeles',
  'zipCode' => '92662',
  'phone' => '(555) 555-5555',
  'alternativePhone' => '',
  'label' => '',
  'businessName' => 'Gobias Industries',
  'businessTaxId' => '',
  'businessId' => '',
  'stateName' => '',
  'countryId' => '236',
  'stateId' => '26',
  'notes' => '',
  'custom1' => '',
  'custom2' => '',
  'custom3' => '',
  'custom4' => '',
  'isEstimated' => false,
  'stateValue' => '26',
  'countryIso' => 'US',
  'countryText' => 'United States',
  'stateText' => 'California',
  'abbreviationText' => 'CA',
  'addressLines' => array (
    'address1' => '1234 Balboa Towers Circle',
    'address2' => '#100',
    'city' => 'Los Angeles',
    'zipCode' => '92662',
    'phone' => '(555) 555-5555',
    'businessName' => 'Gobias Industries',
    'stateText' => 'California',
    'countryText' => 'United States',
  ),
  'country' => array (
    'id' => '236',
    'name' => 'United States',
    'iso' => 'US',
    'isStateRequired' => NULL,
    'enabled' => '1',
  ),
  'state' => array (
    'id' => '26',
    'name' => 'California',
    'abbreviation' => 'CA',
    'countryId' => '236',
    'enabled' => '1',
    'sortOrder' => '5',
  ),
)
expand

Several Events may also be used, when extending Commerce, to provide custom functionality as addresses are changed in the system:

# Address Lines

The address model has a read-only addressLines parameter that returns a key-value array with every line of the address. By default it consists of most of the items in the example above:

  • attention
  • name
  • fullName
  • address1
  • address2
  • address3
  • city
  • zipCode
  • phone
  • alternativePhone
  • label
  • notes
  • businessName
  • businessTaxId
  • stateText
  • countryText
  • custom1
  • custom2
  • custom3
  • custom4

This parameter is designed to allow consistency when displaying a customer’s address on the front end and in the control panel.

Address lines are used, for example, on the order edit page in the control panel. There are examples for displaying an address (opens new window) in the example templates.

You can customize this array using the defineAddressLines event.

# Cart Addresses

# Fetching Cart Addresses

Once you have a cart object, you can access order addresses via cart.shippingAddress and cart.billingAddress:

{% if cart.shippingAddress %}
  {{ cart.shippingAddress.firstName }}
  {# ... #}
{% endif %}

{% if cart.billingAddress %}
  {{ cart.billingAddress.firstName }}
  {# ... #}
{% endif %}

If present, the address will be an Address (opens new window) object like the store location example above. Otherwise the shippingAddress or billingAddress property will return null if an address is not set.

You don’t need to add your own logic for checking shippingAddressSameAsBilling or billingAddressSameAsShipping; Commerce will return the correct address taking those options into account.

# Updating Cart Addresses

Any post to the commerce/cart/update-cart action can include parameters for modifying shipping and billing address information in two formats:

  1. A shippingAddress and/or billingAddress array with details to be added, along with an ID (i.e. shippingAddress[id]) to update an existing address.
  2. A shippingAddressId and/or billingAddressId parameter for designating an existing addresses by its ID.

With either approach, you can also use shippingAddressSameAsBilling or billingAddressSameAsShipping parameters to synchronize addresses and not have to send the same information in two places.

If you provide a shippingAddress or shippingAddressId, for example, and the order’s billing address should be identical, you can simply pass billingAddressSameAsShipping with a value of 1 rather than supplying the same billingAddress or billingAddressId.

If you provide conflicting information for an address, like passing shippingAddress fields and shippingAddressId for example, Commerce will ignore the shippingAddress information and honor shippingAddressId.

Commerce doesn’t require an order to have a shipping address, but providing one is important for the tax and shipping engine to calculate more accurate options and costs.

# Cart Address Examples

The following are a handful of approaches you can take updating cart addresses.

# Use an Existing Address by Default

This creates a form for adding the customer’s first saved address as the shippingAddressId and using it for the billing address with the billingAddressSameAsShipping parameter. The fields are hidden so the user doesn’t have any fields to populate.

{% set address = craft.commerce.customers.customer.addresses|first %}

<form method="post">
  {{ csrfInput() }}
  {{ actionInput('commerce/cart/update-cart') }}
  {{ hiddenInput('shippingAddressId', address.id) }}
  {{ hiddenInput('billingAddressSameAsShipping', '1') }}
  <button type="submit">Submit</button>
</form>

You could achieve the inverse providing the first address as the billingAddressId and setting the shippingAddressSameAsBilling param.

The same thing could also be done explicitly setting each address ID:

{% set address = craft.commerce.customers.customer.addresses|first %}

<form method="post">
  {{ csrfInput() }}
  {{ actionInput('commerce/cart/update-cart') }}
  {{ hiddenInput('shippingAddressId', address.id) }}
  {{ hiddenInput('billingAddressId', address.id) }}
  <button type="submit">Submit</button>
</form>

# Submit New Addresses During Checkout

The customer, especially if they’re a guest, need to enter an address at checkout.

This example creates a form for collecting the customer’s name and country, which are applied for the shipping and billing addresses.

<form method="post">
  {{ csrfInput() }}
  {{ actionInput('commerce/cart/update-cart') }}
  {{ hiddenInput('successMessage', 'Updated shipping address.'|hash) }}

  {# You can send a blank string here or omit this form parameter #}
  {{ hiddenInput('shippingAddressId', '') }}

  <input type="text" name="shippingAddress[firstName]" value="">
  <input type="text" name="shippingAddress[lastName]" value="">
  <select name="shippingAddress[countryId]">
    {% for id, name in craft.commerce.countries.getAllCountriesAsList() %}
      <option value="{{ id }}">{{ name }}</option>
    {% endfor %}
  </select>
  {{ hiddenInput('billingAddressSameAsShipping', '1') }}

  <button type="submit">Add to Cart</button>
</form>

The example sends the shippingAddressId parameter with an empty string, which behaves the same as omitting the parameter. When shippingAddressId is an integer, the address form data is ignored and the form action attempts to set the shipping address to that of the ID.

If we were updating an existing address, we could use this same form but simply populate shippingAddressId with the ID of the address we’d like to update.

# Select an Existing Address

If your customers have saved multiple addresses, you can display radio buttons allowing the a customer to select the proper shippingAddressId and billingAddressId, or create a new address on the fly:

{% set cart = craft.commerce.carts.cart %}
{% set customerAddresses = craft.commerce.customers.customer.addresses %}

<form method="post">
  {{ csrfInput() }}
  {{ actionInput('commerce/cart/update-cart') }}
  {{ hiddenInput('successMessage', 'Updated addresses.'|hash) }}

  {# Display saved addresses as options if we have them #}
  {% if customerAddresses | length %}
    <div class="shipping-address">
      {% for address in customerAddresses %}
        <label>
          <input type="radio"
            name="shippingAddressId"
            value="{{ address.id }}" {{- cart.shippingAddressId ? ' checked' : null }}
          >
          {# Identifying address information, up to you! #}
          {# ... #}
        </label>
      {% endfor %}
    </div>

    <div class="billing-address">
      {% for address in customerAddresses %}
        <label>
          <input type="radio"
            name="billingAddressId"
            value="{{ address.id }}" {{- cart.billingAddressId ? ' checked' : null }}
          >
          {# Identifying address information, up to you! #}
          {# ... #}
        </label>
      {% endfor %}
    </div>
  {% else %}
    {# No existing addresses; provide forms to add new ones #}
    <div class="new-billing-address">
      <label>
        First Name
        <input type="text" name="billingAddress[firstName]">
      </label>
      {# Remaining address fields #}
      {# ... #}
    </div>

    <div class="new-shipping-address">
      <label>
        First Name
        <input type="text" name="shippingAddress[firstName]">
      </label>
      {# Remaining address fields #}
      {# ... #}
    </div>
  {% endif %}
</form>

You may need to create custom routes to allow customers to manage these addresses, or introduce logic in the browser to hide and show new address forms based on the type(s) of addresses you need.

# Update an Existing Address

An address in the cart may be updated by passing the id part of the address model e.g. shippingAddress[id].

If the ID of the address belongs to a customer, the customer’s address will also be updated at the same time.

This example starts a form that could be used to update the shipping address attached to the cart:

{% set cart = craft.commerce.carts.getCart() %}
{% set address = cart.shippingAddress %}

<form method="post">
  {{ csrfInput() }}
  {{ actionInput('commerce/cart/update-cart') }}
  {{ hiddenInput('successMessage', 'Updated addresses.'|hash) }}

  {{ hiddenInput('shippingAddress[id]', address.id) }}
  <input type="text" name="shippingAddress[firstName]" value="{{ address.firstName }}">
  <input type="text" name="shippingAddress[lastName]" value="{{ address.lastName }}">

  {# ... #}

  <button type="submit">Update Address</button>
</form>

If a cart’s billing and shipping address IDs are the same, updating the shipping address will have no effect as the billing address takes precedence.

# Estimate Cart Addresses

It’s common to provide a shipping or tax cost estimate before a customer has entered full address details.

To help with this, the cart can use estimated shipping and billing addresses for calculations before complete addresses are available.

An estimated address is an Address model (opens new window) with its isEstimated property set to true. This simple differentiation prevents any confusion between estimated and final calculations.

# Adding a Shipping Estimate Address to the Cart

You can add or update an estimated addresses on the order with the same commerce/cart/update-cart form action.

In this example we’ll first check for existing estimate addresses with the estimatedShippingAddressId and estimatedBillingAddressId attributes on the cart (opens new window) object, displaying a form to collect only the shipping country, state, and zip code if we don’t already have them:

{% set cart = craft.commerce.carts.cart %}

<form method="post">
  {{ csrfInput() }}
  {{ actionInput('commerce/cart/update-cart') }}
  {{ hiddenInput('estimatedBillingAddressSameAsShipping', '1') }}

  {% if not cart.estimatedShippingAddressId %}
    {# Display country selection dropdown #}
    <select name="estimatedShippingAddress[countryId]">
      {% for key, option in craft.commerce.countries.allCountriesAsList %}
        <option value="{{ key }}">{{ option }}</option>
      {% endfor %}
    </select>

    {# Display state selection dropdown #}
    <select name="estimatedShippingAddress[stateValue]">
      {% for states in craft.commerce.states.allStatesAsList %}
        {% for key, option in states %}
          <option value="{{ key }}">{{ option }}</option>
        {% endfor %}
      {% endfor %}
    </select>

    {# Display a zip code input #}
    <input type="text" name="estimatedShippingAddress[zipCode]" value="">
  {% endif %}


  {% if cart.availableShippingMethodOptions|length and cart.estimatedShippingAddressId %}
    {# Display name+price selection for each available shipping method #}
    {% for handle, method in cart.availableShippingMethodOptions %}
      {% set price = method.priceForOrder(cart)|commerceCurrency(cart.currency) %}
      <label>
        <input type="radio"
          name="shippingMethodHandle"
          value="{{ handle }}"
          {% if handle == cart.shippingMethodHandle %}checked{% endif %}
        />
        {{ method.name }} - {{ price }}
      </label>
    {% endfor %}
  {% endif %}

  <button type="submit">Submit</button>
</form>

Tax adjusters (opens new window) and shipping adjusters (opens new window) will include an isEsimated attribute when their calculations were based on estimated address data.

A full example of this can be seen in the example templates on the cart page.

# Customer Addresses

Your front end modify customer addresses indepdendently of the cart.

When a customer is logged in and checks out with a new address, that address is saved to their address book. (This is not true for guests since they don’t have any need for an address book.)

Customers can only add and remove addresses from the front end while they’re logged in.

See the Customer model (opens new window) to learn about the methods available to retrieve customer address data e.g. Customer::getPrimaryBillingAddress() (opens new window), Customer::getPrimaryShippingAddress() (opens new window) and Customer::getAddressById() (opens new window).

# Get All Current Customer Addresses

{% set addresses = craft.commerce.customers.customer.addresses %}

# Get Current Customer Address by ID

{% set address = craft.commerce.customers.customer.getAddressById(id) %}

# Add or Update a Customer Address

The form action for adding or updating a customer’s address is commerce/customer-addresses/save.

This example would add a new address for the customer with the details in the address form field:

<form method="post">
  {{ csrfInput() }}
  {{ actionInput('commerce/customer-addresses/save') }}
  {{ redirectInput('commerce/customer/addresses') }}
  <input type="text" name="address[firstName]" value="{{ address is defined ? address.firstName : '' }}">
  <input type="text" name="address[lastName]" value="{{ address is defined ? address.lastName : '' }}">
  {# ... #}
  <button type="submit">Save</button>
</form>

To update an existing address, include its ID for the value of a address[id] parameter:





 






<form method="post">
  {{ csrfInput() }}
  {{ actionInput('commerce/customer-addresses/save') }}
  {{ redirectInput('commerce/customer/addresses') }}
  <input type="text" name="address[id]" value="{{ address.id }}">
  <input type="text" name="address[firstName]" value="{{ address.firstName }}">
  <input type="text" name="address[lastName]" value="{{ address.lastName }}">
  {# ... #}
  <button type="submit">Save</button>
</form>

# Delete a Customer Address

The form action for deleting a customer address is commerce/customer-addresses/delete. All that’s needed is the address ID:

<form method="post">
  {{ csrfInput() }}
  {{ actionInput('commerce/customer-addresses/delete') }}
  {{ redirectInput('commerce/customer/addresses') }}
  {{ hiddenInput('id', address.id) }}
  <button type="submit">Delete</button>
</form>

If an address is designated for shipping or billing in a cart, edits will carry over to the cart before checkout. Deleting an address will remove it from the cart and require further user action to complete the order.

# Validating Addresses

Commerce saves customer address data without any validation. If you’d like to provide your own validation rules, you can either do that on the front end or use a custom plugin or module to provide server-side validation.

If you’d like to provide your own server-side validation, make sure you’re comfortable creating a plugin or module for Craft CMS (opens new window). Take a look at this Knowledge Base article for a complete example: craftcms.com/knowledge-base/custom-module-events (opens new window)

If you write your own plugin or module, you’ll want to use its init() method to subscribe to the event that’s triggered when the Address model collects it rules prior to attempting validation. Your event listener can add additional validation rules (opens new window) for the Address model.

In this example, a new rule is added to make firstName and lastName required:

use craft\commerce\models\Address;
use craft\base\Model;
use craft\events\DefineRulesEvent;

Event::on(
    Address::class,
    Model::EVENT_DEFINE_RULES,
    function(DefineRulesEvent $event) {
        $rules = $event->rules;
        $rules[] = [['firstName', 'lastName'], 'required'];
        $event->rules = $rules;
    }
);

In any of the above examples that post to the commerce/customer-addresses/save action, you would access validation errors in your template like any other model:

{% if address %}
  {# Get all validation errors for the address #}
  {% set errors = address.getErrors() %}

  {# Get the `firstName` error for the address #}
  {% set firstNameErrors = address.getErrors('firstName') %}
{% endif %}

For a complete template example that outputs individual field validation errors, see shop/_includes/addresses/form.twig (opens new window) in the example templates.