Tom Sailors
Brief · Anonymized case study

Legacy Customer Import with Loyalty Preservation

I'd build this as a four-stage migration flow. First, validate the legacy export for format, duplicates, and Shopify compatibility. Then batch-import customers and apply tags through the Admin API. Third, move loyalty balances into metafields or a third-party loyalty app. Finally, audit the whole import against the original file to catch gaps or errors before the merchant goes live.

A mid-market e-commerce merchant needed to migrate tens of thousands of customer records from a legacy CRM into Shopify while preserving loyalty point balances, lifetime value metrics, and customer segmentation tags. The challenge was ensuring data integrity during the bulk import—catching duplicates and format mismatches upfront, mapping legacy fields to Shopify's structure, and storing historical loyalty data in a way that remained accessible post-migration.
Four pieces
Migration

Data Mapper & Validator

Reads the legacy CRM export file, checks that emails and addresses match Shopify's format, flags duplicates or invalid records, and prepares a clean file ready for bulk import.

Admin dashboard + validation service
Admin GraphQL explorer graphql
# Admin GraphQL — test customer creation + metafield attachment
mutation CreateCustomerWithLoyalty($input: CustomerInput!) {
  customerCreate(input: $input) {
    customer {
      id
      email
      firstName
      lastName
      tags
      metafields(first: 10) { edges { node { namespace key value } } }
    }
    userErrors { field message }
  }
}

# Variables example:
# {
#   "input": {
#     "email": "customer@example.com",
#     "firstName": "Jane",
#     "lastName": "Smith",
#     "tags": ["gold-member", "high-value"]
#   }
# }
Changed CustomerCreateInput to CustomerInput per Admin API schema.
Migration

Bulk Import Job

Processes the validated customer file in batches, creates or updates customers in Shopify, applies tags for segmentation, and stores loyalty balances as metafields behind the scenes.

Backend service + job queue
Admin GraphQL explorer graphql
# Admin GraphQL — batch upsert customers with metafields
mutation UpsertCustomerWithMetafields($input: CustomerInput!, $metafields: [MetafieldsSetInput!]!) {
  customerCreate(input: $input) {
    customer { id email }
    userErrors { field message }
  }
  metafieldsSet(metafields: $metafields) {
    metafields { id namespace key value }
    userErrors { field message }
  }
}

# Variables example (one customer with loyalty data):
# {
#   "input": {
#     "email": "customer@example.com",
#     "firstName": "Jane",
#     "lastName": "Doe"
#   },
#   "metafields": [
#     {
#       "ownerId": "gid://shopify/Customer/12345",
#       "namespace": "loyalty",
#       "key": "lifetime_value",
#       "type": "decimal",
#       "value": "2450.00"
#     }
#   ]
# }
Fixed: customerCreate takes single CustomerInput, not array. For batch processing, loop this mutation per customer in backend job queue.
Operations

Loyalty Points Transfer

Moves or recreates loyalty point balances from the legacy system into the target loyalty platform—whether that is Shopify metafields for custom storage, a third-party app like Smile, or Shopify Subscriptions.

Integration service + app API
Admin GraphQL explorer graphql
# Admin GraphQL — store loyalty balance as metafield for retrieval later
mutation SetLoyaltyBalance($input: MetafieldsSetInput!) {
  metafieldsSet(metafields: [$input]) {
    metafields {
      id
      namespace
      key
      value
    }
    userErrors { field message }
  }
}

# Variables:
# {
#   "input": {
#     "ownerId": "gid://shopify/Customer/12345",
#     "namespace": "loyalty",
#     "key": "points_balance",
#     "type": "integer",
#     "value": "1250"
#   }
# }
If using Smile or Swell, replace with their respective APIs; this stores raw balance in Shopify for now.
Operations

Post-Import Audit Dashboard

Shows how many customers imported successfully, which records failed and why, tag distribution, loyalty balance totals, and drift between the legacy export and Shopify—so gaps or errors are caught before go-live.

Custom admin dashboard
Admin GraphQL explorer graphql
# Admin GraphQL — count imported customers and fetch sample metafields
query AuditImport {
  customers(first: 250, query: "created:>2024-01-01") {
    edges {
      node {
        id
        email
        tags
        createdAt
        metafields(first: 10) {
          edges {
            node {
              namespace
              key
              value
            }
          }
        }
      }
    }
    pageInfo { hasNextPage endCursor }
  }
  shop { name }
}

# Run this query to verify customer count, tags applied, and loyalty metafields synced.
Paginate with cursor to scan all 50K records; build a summary report offline.

Got a similar problem?

Sketch your build in 30 seconds — voice, type, or attach a screenshot.

Sketch the build →