Skip to Content

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.

emailstring

The customer’s email. Unique per shop. Used as the primary login identity.

"email": email
phonestring

Shopify 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": country

Every 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]/, "")
firstNamestring
lastNamestring
localestring

The 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.

address1string

Street address line 1. Setting this is what triggers an addresses entry on the Shopify customer — without address1, no address is created.

"address1": billing.street
address2string

Apartment, suite, unit, etc.

citystring
provincestring

State / province name or code. Shopify accepts both.

countrystring

Country name or ISO 3166-1 alpha-2 code (e.g. US, DE). Codes are safer.

"country": country_id
zipstring

Postal code.

companystring

Company 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_marketingboolean

If true, sets the customer’s email marketing consent state to SUBSCRIBED. Has no effect unless email is also set.

"accepts_email_marketing": newsletter_subscribed = true
accepts_sms_marketingboolean

If true, sets SMS marketing consent to SUBSCRIBED. Requires phone to be set.

email_marketing_opt_in_levelstring

How 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_levelstring

Same values and default as email_marketing_opt_in_level, applied to SMS consent.

Customer attributes

Tax exempt, internal notes, tags.

tagsstring[] | string

Searchable tags. Either an array of strings or a single comma-separated string.

"tags": $append(customer_groups.name, ["lifetime-value-" & $string($floor(ltv / 100) * 100)])
taxExemptboolean

Whether the customer is tax-exempt (e.g. wholesale customer with a resale certificate).

notestring

Internal 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 country alongside phone and 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: true without email silently produces no consent (the staging model checks both). Same for SMS + phone.
Last updated on