Tom Sailors
· Refreshed
Brief · Anonymized case study

Bulk Customer Import with Loyalty Preservation

Migrating a large customer base from a legacy system into Shopify requires a four-step approach: validate and map the source data to Shopify's schema, batch-import customers with tags and metafields, transfer loyalty balances to the target system, and audit the result to catch drift or missing records before going live. Each step reduces risk and makes the handoff verifiable.

A mid-market DTC merchant needed to migrate tens of thousands of customer records from a legacy CRM into Shopify while preserving loyalty balances, lifetime value metadata, and existing tag-based audience segmentation. The challenge was maintaining data integrity across format differences, handling duplicate detection, and ensuring loyalty data remained queryable post-migration.
Four pieces
Migration

Data Mapper & Validator

Reads a CRM export file, checks that emails and addresses match Shopify's format requirements, flags duplicates and malformed records, and produces a clean file ready for bulk import. Validation catches upstream issues before they create customer records.

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 customer records in Shopify, applies segmentation tags, and stores loyalty balances as metafields. Built as a background job to handle high volume without blocking the admin.

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 CRM into the target loyalty system—whether Shopify Subscriptions, a third-party app such as Smile or Swell, or a custom points ledger. Balances become queryable and actionable in the new system.

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

Scans imported customers to verify count, tag distribution, metafield completeness, and loyalty balance totals. Surfaces failed records, validation errors, and drift between the CRM export and live Shopify data before going 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 →