<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jimit Patel</title>
    <description>The latest articles on DEV Community by Jimit Patel (@jiimt).</description>
    <link>https://dev.to/jiimt</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4220467%2F0cf98b26-57b3-4bc0-83a4-d35f4a26961f.png</url>
      <title>DEV Community: Jimit Patel</title>
      <link>https://dev.to/jiimt</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jiimt"/>
    <language>en</language>
    <item>
      <title>Customer merge failed in Shopify? What the customerMerge docs say about blockers, and where they disagree</title>
      <dc:creator>Jimit Patel</dc:creator>
      <pubDate>Sun, 01 Nov 2026 11:59:28 +0000</pubDate>
      <link>https://dev.to/jiimt/customer-merge-failed-in-shopify-what-the-customermerge-docs-say-about-blockers-and-where-they-2a0i</link>
      <guid>https://dev.to/jiimt/customer-merge-failed-in-shopify-what-the-customermerge-docs-say-about-blockers-and-where-they-2a0i</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why did a customer merge fail in Shopify?
&lt;/h3&gt;

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

&lt;h3&gt;
  
  
  The mutation
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;customerMerge&lt;/code&gt; (&lt;a href="https://shopify.dev/docs/api/admin-graphql/latest/mutations/customerMerge" rel="noopener noreferrer"&gt;reference&lt;/a&gt;) "Merges two customers." You pass &lt;code&gt;customerOneId&lt;/code&gt; and &lt;code&gt;customerTwoId&lt;/code&gt;, and optionally &lt;code&gt;overrideFields&lt;/code&gt;. The page says neither argument decides which customer is kept.&lt;/p&gt;

&lt;p&gt;Which one survives follows an order of rules on the page:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;If &lt;code&gt;overrideFields.customerIdOfEmailToKeep&lt;/code&gt; is given and valid, that customer is kept.&lt;/li&gt;
&lt;li&gt;If only one customer has an email address, that customer is kept.&lt;/li&gt;
&lt;li&gt;If both have email addresses, account state is checked first (an &lt;code&gt;enabled&lt;/code&gt; account beats other states), then some invited-account and email-marketing-consent rules decide.&lt;/li&gt;
&lt;li&gt;If nothing prefers either, or neither has an email, &lt;code&gt;customerTwoId&lt;/code&gt; is kept.&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;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 &lt;code&gt;overrideFields&lt;/code&gt; values that differ between query and variables, so do not copy them without checking.&lt;/p&gt;

&lt;h3&gt;
  
  
  The blockers, as the Help Center states them
&lt;/h3&gt;

&lt;p&gt;Shopify's Help Center (&lt;a href="https://help.shopify.com/en/manual/customers/manage-customers#merge-customer-profiles" rel="noopener noreferrer"&gt;managing customers, merge section&lt;/a&gt;) lists nine cases in which profiles cannot be merged. Either profile:&lt;/p&gt;

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

&lt;p&gt;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."&lt;/p&gt;

&lt;h3&gt;
  
  
  The blockers, as the API states them
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;CustomerMergeErrorFieldType&lt;/code&gt; (&lt;a href="https://shopify.dev/docs/api/admin-graphql/latest/enums/CustomerMergeErrorFieldType" rel="noopener noreferrer"&gt;enum&lt;/a&gt;) describes the hard blockers preventing a merge. It has 11 values:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;What it refers to&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;COMPANY_CONTACT&lt;/td&gt;
&lt;td&gt;A company contact&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CUSTOMER_PAYMENT_METHODS&lt;/td&gt;
&lt;td&gt;Payment methods&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DELETED_AT&lt;/td&gt;
&lt;td&gt;The customer does not exist&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GIFT_CARDS&lt;/td&gt;
&lt;td&gt;Gift cards&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MERGE_IN_PROGRESS&lt;/td&gt;
&lt;td&gt;A merge in progress&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MULTIPASS_IDENTIFIER&lt;/td&gt;
&lt;td&gt;A multipass identifier&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OVERRIDE_FIELDS&lt;/td&gt;
&lt;td&gt;Invalid override fields&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PENDING_DATA_REQUEST&lt;/td&gt;
&lt;td&gt;A pending data request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;REDACTED_AT&lt;/td&gt;
&lt;td&gt;A pending or completed redaction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;STORE_CREDIT&lt;/td&gt;
&lt;td&gt;Store credit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SUBSCRIPTIONS&lt;/td&gt;
&lt;td&gt;Subscription history&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;CustomerMergeErrorCode&lt;/code&gt; (&lt;a href="https://shopify.dev/docs/api/admin-graphql/latest/enums/CustomerMergeErrorCode" rel="noopener noreferrer"&gt;enum&lt;/a&gt;) 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 &lt;code&gt;CUSTOMER_HAS_SUBSCRIPTIONS&lt;/code&gt;, are on neither enum.&lt;/p&gt;

&lt;h3&gt;
  
  
  The gift card conflict
&lt;/h3&gt;

&lt;p&gt;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 &lt;code&gt;GIFT_CARDS&lt;/code&gt; as a hard blocker and &lt;code&gt;CUSTOMER_HAS_GIFT_CARDS&lt;/code&gt; 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.&lt;/p&gt;

&lt;h3&gt;
  
  
  Customer.mergeable
&lt;/h3&gt;

&lt;p&gt;Instead of discovering a blocker by failing a merge, the Customer object has a &lt;code&gt;mergeable&lt;/code&gt; field of type &lt;code&gt;CustomerMergeable!&lt;/code&gt;. Its fields (&lt;a href="https://shopify.dev/docs/api/admin-graphql/latest/objects/CustomerMergeable" rel="noopener noreferrer"&gt;object&lt;/a&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;isMergeable&lt;/code&gt; (&lt;code&gt;Boolean!&lt;/code&gt;): whether the customer can be merged with another customer.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;errorFields&lt;/code&gt; (&lt;code&gt;[CustomerMergeErrorFieldType!]!&lt;/code&gt;): the fields preventing the merge.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;reason&lt;/code&gt; (&lt;code&gt;String&lt;/code&gt;, nullable): the reason the customer cannot be merged.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mergeInProgress&lt;/code&gt; (&lt;code&gt;CustomerMergeRequest&lt;/code&gt;, nullable): the merge request if one is in progress.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;A query assembled from those fields (not run against a store):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight graphql"&gt;&lt;code&gt;&lt;span class="k"&gt;query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CheckMergeable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;!)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;mergeable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="n"&gt;isMergeable&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="n"&gt;errorFields&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="n"&gt;mergeInProgress&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="n"&gt;resultingCustomerId&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;h3&gt;
  
  
  The customers/merge webhook
&lt;/h3&gt;

&lt;p&gt;Shopify's webhook reference (&lt;a href="https://shopify.dev/docs/api/webhooks" rel="noopener noreferrer"&gt;webhooks&lt;/a&gt;) has a &lt;code&gt;customers/merge&lt;/code&gt; topic. The sample payload shows these fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"admin_graphql_api_customer_kept_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gid://shopify/Customer/1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"admin_graphql_api_customer_deleted_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"gid://shopify/Customer/2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"admin_graphql_api_job_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"failed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"errors"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"customer_ids"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"field"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"merge_in_progress"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"John Doe is currently being merged."&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things are worth noticing. The sample is a failure, with &lt;code&gt;job_id&lt;/code&gt; null and a lowercase &lt;code&gt;merge_in_progress&lt;/code&gt; 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 &lt;code&gt;status&lt;/code&gt; values exist, so check the topic's own section for scopes and trigger text before you rely on either.&lt;/p&gt;

&lt;h3&gt;
  
  
  Merge customer button missing, or a permission problem?
&lt;/h3&gt;

&lt;p&gt;For people working in the admin, Shopify's &lt;a href="https://help.shopify.com/en/manual/your-account/users/roles/permissions/store-permissions" rel="noopener noreferrer"&gt;store permissions page&lt;/a&gt; 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 &lt;code&gt;write_customer_merge&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I have not tested
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;h3&gt;
  
  
  Checklist from the above
&lt;/h3&gt;

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

&lt;p&gt;This article was created with the help of AI, and every statement was checked against the Shopify pages linked above.&lt;/p&gt;

</description>
      <category>shopify</category>
      <category>graphql</category>
      <category>api</category>
      <category>ecommerce</category>
    </item>
  </channel>
</rss>
