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": emailphonestringE.164 format (+15555550123). Shopify will reject loosely-formatted
phones like (555) 123-4567 — normalize before emitting.
"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.
Marketing consent
Two booleans. Only set 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.
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. The staging
model only sets marketingState: "SUBSCRIBED" — it doesn’t carry
a consentUpdatedAt. 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 rejects malformed phones. Normalize to E.164 in JSONata.
- 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.