docs/developer/core-concepts/addresses.mdx
Geography is central to how Spree handles checkout, pricing, taxes, and shipping. The system works through a chain of related concepts:
Markets → Countries → Zones → Tax Rates & Shipping Methods
↘ States
↘ Addresses
An address represents a shipping or billing location. Every order has a shipping address and a billing address, and customers can save multiple addresses in their address book.
| Field | Description |
|---|---|
first_name, last_name | Contact name |
address1, address2 | Street address |
city | City |
postal_code | Postal code (not required for all countries) |
phone | Phone number |
company | Company name (optional) |
label | User-facing label like "Home" or "Office" (optional, unique per customer) |
country_iso | Country (ISO alpha-2 code, e.g., US) |
state_abbr | State/province abbreviation (required for some countries, e.g., CA) |
Whether a state or zipcode is required depends on the country. For example, the US requires both, while Hong Kong requires neither. The API returns states_required and zipcode_required on each country so your frontend can adapt the form dynamically.
Customers can save multiple addresses and set a default for shipping and billing. When a customer completes checkout, the selected addresses are cloned onto the order — so editing an address later doesn't change past orders.
<CodeGroup>// List addresses
const { data: addresses } = await client.customer.addresses.list()
// Create an address
const address = await client.customer.addresses.create({
first_name: 'John',
last_name: 'Doe',
address1: '123 Main St',
city: 'Los Angeles',
country_iso: 'US',
state_abbr: 'CA',
postal_code: '90001',
phone: '555-0100',
})
// Update an address
await client.customer.addresses.update('addr_xxx', { city: 'Brooklyn' })
// Delete an address
await client.customer.addresses.delete('addr_xxx')
// Set as default shipping or billing address
await client.customer.addresses.markAsDefault('addr_xxx', 'shipping')
# List addresses
curl 'https://api.mystore.com/api/v3/store/customer/addresses' \
-H 'Authorization: Bearer <jwt_token>'
# Create an address
curl -X POST 'https://api.mystore.com/api/v3/store/customer/addresses' \
-H 'Authorization: Bearer <jwt_token>' \
-H 'Content-Type: application/json' \
-d '{
"first_name": "John",
"last_name": "Doe",
"address1": "123 Main St",
"city": "Los Angeles",
"country_iso": "US",
"state_abbr": "CA",
"postal_code": "90001",
"phone": "555-0100"
}'
# Update an address
curl -X PATCH 'https://api.mystore.com/api/v3/store/customer/addresses/addr_xxx' \
-H 'Authorization: Bearer <jwt_token>' \
-H 'Content-Type: application/json' \
-d '{ "city": "Brooklyn" }'
# Delete an address
curl -X DELETE 'https://api.mystore.com/api/v3/store/customer/addresses/addr_xxx' \
-H 'Authorization: Bearer <jwt_token>'
During checkout, you can either reference a saved address by ID or pass a new address inline:
<CodeGroup>// Use saved addresses
await client.carts.update(cartId, {
shipping_address_id: 'addr_xxx',
billing_address_id: 'addr_yyy',
})
// Or pass new addresses inline
await client.carts.update(cartId, {
shipping_address: {
first_name: 'John',
last_name: 'Doe',
address1: '123 Main St',
city: 'Los Angeles',
country_iso: 'US',
state_abbr: 'CA',
postal_code: '90001',
phone: '555-0100',
},
})
# Use a saved address
curl -X PATCH 'https://api.mystore.com/api/v3/store/carts/cart_xxx' \
-H 'Authorization: Bearer pk_xxx' \
-H 'X-Spree-Token: order_token' \
-H 'Content-Type: application/json' \
-d '{ "shipping_address_id": "addr_xxx", "billing_address_id": "addr_yyy" }'
# Or pass a new address inline
curl -X PATCH 'https://api.mystore.com/api/v3/store/carts/cart_xxx' \
-H 'Authorization: Bearer pk_xxx' \
-H 'X-Spree-Token: order_token' \
-H 'Content-Type: application/json' \
-d '{
"shipping_address": {
"first_name": "John",
"last_name": "Doe",
"address1": "123 Main St",
"city": "Los Angeles",
"country_iso": "US",
"state_abbr": "CA",
"postal_code": "90001",
"phone": "555-0100"
}
}'
Countries are the foundation of Spree's geographic system. They connect to Markets, contain states, and belong to zones.
Each country includes metadata that drives address form behavior:
| Field | Description | Example |
|---|---|---|
iso | ISO 3166-1 alpha-2 code | US |
iso3 | ISO 3166-1 alpha-3 code | USA |
name | Country name | United States |
states_required | Whether the address form should show a state/province picker | true |
zipcode_required | Whether the address form should require a postal code | true |
Only countries assigned to a Market are available during checkout. This lets you control exactly where you sell.
<CodeGroup>// List all countries available in this store
const { data: countries } = await client.countries.list()
// Get a country with its states (for address form dropdowns)
const usa = await client.countries.get('US', { include: 'states' })
// usa.states => [{ name: "Alabama", abbr: "AL" }, { name: "Alaska", abbr: "AK" }, ...]
// Get a country with its market (for currency/locale resolution)
const germany = await client.countries.get('DE', { include: 'market' })
// germany.market => { currency: "EUR", default_locale: "de", tax_inclusive: true }
# List all countries
curl 'https://api.mystore.com/api/v3/store/countries' \
-H 'Authorization: Bearer pk_xxx'
# Get a country with states
curl 'https://api.mystore.com/api/v3/store/countries/US?include=states' \
-H 'Authorization: Bearer pk_xxx'
# Get a country with its market
curl 'https://api.mystore.com/api/v3/store/countries/DE?include=market' \
-H 'Authorization: Bearer pk_xxx'
Use the states_required and zipcode_required fields to build adaptive address forms — show a state picker only when needed, and skip the zipcode field for countries that don't use them.
States (provinces, regions) belong to a country and are used for address validation and zone matching. Countries like the US, Canada, Australia, and India have predefined states — for these countries, customers must select a state from the list rather than typing a name.
States are fetched via the country endpoint using ?include=states:
const usa = await client.countries.get('US', { include: 'states' })
// Build a state picker from the response
usa.states.forEach(state => {
console.log(state.abbr, state.name) // "AL", "Alabama"
})
curl 'https://api.mystore.com/api/v3/store/countries/US?include=states' \
-H 'Authorization: Bearer pk_xxx'
For countries without predefined states, addresses accept a free-text state_name field instead of state_abbr.
Zones group countries or states together for tax and shipping rules. A zone is either country-based or state-based.
Examples:
When a customer enters their address at checkout, Spree matches it against zones to determine:
Zones are configured in the admin dashboard — storefront developers don't interact with them directly via the API.
Each Market resolves a tax zone from its default country. This means product prices display the correct tax treatment (inclusive or exclusive) before the customer enters an address — just from knowing their market.
Here's how geography flows through a typical checkout:
states_required and zipcode_required