Customer mapping
StagingCustomer is intentionally minimal — flat name and contact
fields, one address (not an array), and two boolean flags for
marketing consent. Anything more goes in dict_metafields.
Quick reference — minimum loadable customer
{
"original_id": entity_id,
"email": email,
"firstName": firstname,
"lastName": lastname
}A customer needs at minimum an email or phone to be useful (those
are the login identities). Neither is enforced by the staging model
— enforce in your mapping if your source allows blanks.
Identity
Login fields and display name.
emailstringThe customer’s email. Unique per shop. Used as the primary login identity.
"email": emailphonestringShopify only accepts E.164 (+15555550123). Map the raw source value
— a local number is converted for you when country is also mapped,
so 12 34 56 78 with country: "DK" becomes +4512345678.
"phone": phone,
"country": countryEvery country is covered, and each country’s own dialling rules are
applied — an Italian landline keeps the 0 that belongs to its
number, a Dutch mobile drops it. country can be an ISO code or a
country name.
Placeholders that hold no number (0, 00, -, blank) are treated
as no phone at all. A number that can’t be resolved — because no
country was mapped alongside it, or because it’s the wrong length
for the country it claims — fails validation with INVALID_FORMAT on
phone. Fix it in the mapping:
"phone": "+" & country_dial_code & $replace(phone, /[^0-9]/, "")firstNamestringlastNamestringlocalestringThe customer’s preferred language locale, e.g. en, fr-CA.
Determines what language Shopify uses for transactional emails.
Address
One flat address. For multiple addresses on a single customer, store the extras in dict_metafields.
address1stringStreet address line 1. Setting this is what triggers an addresses
entry on the Shopify customer — without address1, no address is
created.
"address1": billing.streetaddress2stringApartment, suite, unit, etc.
citystringprovincestringState / province name or code. Shopify accepts both.
countrystringCountry name or ISO 3166-1 alpha-2 code (e.g. US, DE).
Codes are safer.
"country": country_idzipstringPostal code.
companystringCompany name on the address. Common on B2B customers.
list_addressesobject[]Every saved address, when the customer has more than one. The flat fields above describe a single address; anything else the source holds is lost unless you map it here.
Each entry takes the same keys as the flat form — address1,
address2, city, province, country, zip, firstName,
lastName, company, phone.
"list_addresses": [addresses.{
"address1": street,
"city": city,
"country": country_code,
"zip": postcode,
"company": company
}]The first entry becomes the customer’s default address, so put the source’s default first. When you map this, the flat fields are ignored.
Marketing consent
A boolean per channel, plus an optional opt-in level. Only set the booleans true if the source can prove the customer opted in.
accepts_email_marketingbooleanIf true, sets the customer’s email marketing consent state to
SUBSCRIBED. Has no effect unless email is also set.
"accepts_email_marketing": newsletter_subscribed = trueaccepts_sms_marketingbooleanIf true, sets SMS marketing consent to SUBSCRIBED. Requires
phone to be set.
email_marketing_opt_in_levelstringHow the customer opted in: SINGLE_OPT_IN, CONFIRMED_OPT_IN
(they confirmed via a follow-up email — “double opt-in”), or
UNKNOWN. Defaults to SINGLE_OPT_IN, and is only used when
accepts_email_marketing is true.
Map it when the source records the distinction — it’s part of the consent record a merchant may need to stand behind under GDPR or CASL.
"email_marketing_opt_in_level": double_opt_in_confirmed_at ? "CONFIRMED_OPT_IN" : "SINGLE_OPT_IN"sms_marketing_opt_in_levelstringSame values and default as email_marketing_opt_in_level, applied
to SMS consent.
Customer attributes
Tax exempt, internal notes, tags.
tagsstring[] | stringSearchable tags. Either an array of strings or a single comma-separated string.
"tags": $append(customer_groups.name, ["lifetime-value-" & $string($floor(ltv / 100) * 100)])taxExemptbooleanWhether the customer is tax-exempt (e.g. wholesale customer with a resale certificate).
notestringInternal admin note. Not shown to the customer.
Patterns
Magento customer with billing address
{
"original_id": entity_id,
"email": email,
"firstName": firstname,
"lastName": lastname,
"phone": telephone,
"address1": default_billing.street,
"address2": default_billing.street_2,
"city": default_billing.city,
"province": default_billing.region.region_code,
"country": default_billing.country_id,
"zip": default_billing.postcode,
"accepts_email_marketing": is_subscribed = true
}Storing extra addresses as metafields
The staging model takes one address. To preserve the full address book on the source side:
{
"original_id": entity_id,
"email": email,
"firstName": firstname,
"lastName": lastname,
"address1": default_billing.street,
"city": default_billing.city,
"country": default_billing.country_id,
"zip": default_billing.postcode,
"dict_metafields": {
"additional_addresses": $count(addresses) > 1
? addresses[address_id != default_billing.address_id]
: null
}
}The auto-detected metafield type for an array is json — perfect
for an address book the merchant can post-process later.
Tagging by customer group
{
"original_id": entity_id,
"email": email,
"firstName": firstname,
"lastName": lastname,
"tags": [
"group-" & $lowercase(group.code),
is_b2b ? "b2b" : "b2c"
]
}Skipping guest checkouts
If your source allows orders by non-registered customers, those shouldn’t migrate as customers:
{
"original_id": entity_id,
"exclude": is_guest = true,
"email": email,
…
}Gotchas
Marketing consent without timestamp is suspicious. Consent
carries a state and an opt-in level, but no date of consent. If
audit trail matters for the merchant (GDPR, CASL), capture the
original opt-in date in dict_metafields so it’s preserved.
- Email uniqueness is enforced by Shopify. Two source customers with the same email collide. Either dedupe in your mapping or accept that the second load fails for that row.
- Phone format. Shopify only accepts E.164. Map
countryalongsidephoneand local numbers are converted for you; whatever’s left over fails validation instead of the load, so you see it before the run. - Country/province codes. ISO codes work everywhere; full names work in most cases but break for unusual spellings. Default to codes.
- One address only in the staging model. If the merchant needs multi-address customers preserved exactly, store the extras as JSON metafields and post-process after migration.
accepts_email_marketing: truewithoutemailsilently produces no consent (the staging model checks both). Same for SMS + phone.