DEV Community

Jimit Patel
Jimit Patel

Posted on Fully Autonomous

Customer merge failed in Shopify? What the customerMerge docs say about blockers, and where they disagree

If you build anything that merges Shopify customers, the documentation is spread across a Help Center page, a mutation page, two enums, an object and a webhook topic. This post puts what those pages say in one place, flags where they disagree, and says what they leave out. Everything below is from Shopify's own pages, read on 2026-11-01 against Admin GraphQL version 2026-10.

Why did a customer merge fail in Shopify?

When a customer merge fails in Shopify, check three places: userErrors on the mutation, Customer.mergeable with its errorFields and reason, and the customers/merge webhook's status and errors. Shopify's docs list hard blockers such as subscriptions, store credit and company contacts. They do not map every error code to a blocker.

The mutation

customerMerge (reference) "Merges two customers." You pass customerOneId and customerTwoId, and optionally overrideFields. The page says neither argument decides which customer is kept.

Which one survives follows an order of rules on the page:

  1. If overrideFields.customerIdOfEmailToKeep is given and valid, that customer is kept.
  2. If only one customer has an email address, that customer is kept.
  3. If both have email addresses, account state is checked first (an enabled account beats other states), then some invited-account and email-marketing-consent rules decide.
  4. If nothing prefers either, or neither has an email, customerTwoId is kept.

The payload returns a job (the merge runs asynchronously), resultingCustomerId, and userErrors. The docs say of resultingCustomerId: "Treat this ID as authoritative." The mutation needs the write_customer_merge access scope. The page also points to customerMergePreview for confirming which customer will be kept before you merge.

Two practical notes from the page. First, the job is asynchronous, so a successful mutation response is a request accepted and not a finished merge. Second, the page's own examples have some overrideFields values that differ between query and variables, so do not copy them without checking.

The blockers, as the Help Center states them

Shopify's Help Center (managing customers, merge section) lists nine cases in which profiles cannot be merged. Either profile:

  • has, or has ever had, a subscription contract
  • is a B2B customer actively linked to a company, or still linked to past B2B orders
  • has a vaulted credit card
  • has a store credit account
  • has a multipass login
  • has been deleted
  • has been redacted, or a data redaction request is in progress

And, at the store level, a customer profile merge is in progress, or a customer data request is in progress. The page also states that merging is permanent: "After the profiles are merged, you can't reverse the process."

The blockers, as the API states them

CustomerMergeErrorFieldType (enum) describes the hard blockers preventing a merge. It has 11 values:

Value What it refers to
COMPANY_CONTACT A company contact
CUSTOMER_PAYMENT_METHODS Payment methods
DELETED_AT The customer does not exist
GIFT_CARDS Gift cards
MERGE_IN_PROGRESS A merge in progress
MULTIPASS_IDENTIFIER A multipass identifier
OVERRIDE_FIELDS Invalid override fields
PENDING_DATA_REQUEST A pending data request
REDACTED_AT A pending or completed redaction
STORE_CREDIT Store credit
SUBSCRIPTIONS Subscription history

My reading, which is interpretation and not something Shopify states: most values line up with a Help Center case (SUBSCRIPTIONS with the subscription contract, STORE_CREDIT with the store credit account, MULTIPASS_IDENTIFIER with multipass, and so on). The enum adds OVERRIDE_FIELDS, which concerns your own input, and GIFT_CARDS, which has no counterpart in the Help Center list. CUSTOMER_PAYMENT_METHODS ("payment methods") is broader in wording than "vaulted credit card", and the docs do not say whether they mean the same set.

CustomerMergeErrorCode (enum) has only six values: CUSTOMER_HAS_GIFT_CARDS, INTERNAL_ERROR, INVALID_CUSTOMER, INVALID_CUSTOMER_ID, MISSING_OVERRIDE_ATTRIBUTE and OVERRIDE_ATTRIBUTE_INVALID. INVALID_CUSTOMER is described as "The customer cannot be merged." The pages I read do not say which code comes back for each blocker other than gift cards, so do not hard-code a mapping from error code to blocker. Some codes that circulate in forum posts and blog posts, such as CUSTOMER_HAS_SUBSCRIPTIONS, are on neither enum.

The gift card conflict

The Help Center lists "Gift cards" among the information that is combined in a merge and does not list them among the nine cases. The API reference lists GIFT_CARDS as a hard blocker and CUSTOMER_HAS_GIFT_CARDS as an error code. Neither page mentions the other or explains the difference. I could not find a statement of which one applies to a given store, so if gift cards matter to your flow, treat the API as the check to run and report what it returns for a real pair.

Customer.mergeable

Instead of discovering a blocker by failing a merge, the Customer object has a mergeable field of type CustomerMergeable!. Its fields (object):

  • isMergeable (Boolean!): whether the customer can be merged with another customer.
  • errorFields ([CustomerMergeErrorFieldType!]!): the fields preventing the merge.
  • reason (String, nullable): the reason the customer cannot be merged.
  • mergeInProgress (CustomerMergeRequest, nullable): the merge request if one is in progress.

Scopes are a place where the pages differ in what they say. The CustomerMergeable page says the read_customer_merge permission is needed, and the Customer object page says it requires the read_customers access scope. I did not test which combination a query needs, so request both while you develop and trim later.

A query assembled from those fields (not run against a store):

query CheckMergeable($id: ID!) {
  customer(id: $id) {
    id
    mergeable {
      isMergeable
      errorFields
      reason
      mergeInProgress {
        resultingCustomerId
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Note what this does not tell you. mergeable is a property of one customer. The docs do not describe a pair-level check beyond customerMergePreview, so a pair can still fail on something that depends on both, such as invalid override fields.

The customers/merge webhook

Shopify's webhook reference (webhooks) has a customers/merge topic. The sample payload shows these fields:

{
  "admin_graphql_api_customer_kept_id": "gid://shopify/Customer/1",
  "admin_graphql_api_customer_deleted_id": "gid://shopify/Customer/2",
  "admin_graphql_api_job_id": null,
  "status": "failed",
  "errors": [
    {
      "customer_ids": [1],
      "field": "merge_in_progress",
      "message": "John Doe is currently being merged."
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Two things are worth noticing. The sample is a failure, with job_id null and a lowercase merge_in_progress field name that does not match the uppercase enum values, so do not assume the webhook errors use the GraphQL enum strings. And the pages I read do not describe when the topic fires, for example whether it fires for every merge or only for ones from certain sources, nor what other status values exist, so check the topic's own section for scopes and trigger text before you rely on either.

Merge customer button missing, or a permission problem?

For people working in the admin, Shopify's store permissions page lists a Merge permission that allows users to merge customer profiles. So if the merge option is missing for a staff user, that permission is the first thing to check. That step is my inference: the pages I read do not mention a missing button. For apps, the scope is write_customer_merge.

What I have not tested

I did not run any of this against a store, including the gift card case. Everything here is what the docs state and where they stop.

Checklist from the above

  • Treat resultingCustomerId from the mutation as the kept customer, and wait for the job to finish before you read the result.
  • Check Customer.mergeable before offering a merge to a user, and show reason when it says no.
  • Do not map error codes to blockers; the docs do not.
  • Test gift cards on a real pair, since the two Shopify sources disagree.
  • Handle webhook status values you have not seen. Only "failed" appears in the sample.
  • Remember that the merge is permanent, so confirm that two profiles are one person before calling it. Household members are different people.

This article was created with the help of AI, and every statement was checked against the Shopify pages linked above.

Top comments (0)