How to properly update product variant metafields with Shopify REST API?

Topic summary

A developer is attempting to programmatically sync product sub-collections as metafields on Shopify product variants using the REST Admin API with Node.js and Axios.

Current Approach:

  • Fetches all variants for a product
  • Retrieves custom and smart collections the product belongs to
  • Creates a comma-separated list of collection titles
  • For each variant, checks if a metafield exists (by namespace/key), then either updates or creates it

Problem:
Metafields are not updating properly—they sometimes don’t appear or values don’t persist, with no clear error messages returned.

Key Questions:

  • Is the upsert approach for variant metafields correct?
  • Are the REST endpoints being used appropriately for creating vs. updating?
  • Would GraphQL or bulk mutations be more efficient for handling multiple variants?

Status:
The issue remains unresolved. One commenter suggested posting to the Shopify Developer Community for specialized technical support, as it’s better suited for complex API issues.

Summarized with AI on October 27. AI used: claude-sonnet-4-5-20250929.

I’m trying to programmatically sync the sub-collections of a Shopify product as a metafield on each of its variants using the Shopify REST Admin API and Node.js (Axios).

Here’s what I’m doing:

  1. Fetch all variants for a given product

  2. Fetch custom_collections and smart_collections this product belongs to

  3. Build a comma-separated list of collection titles

  4. For each variant:

    • Check if a metafield with a given namespace/key exists

    • If it exists, update it

    • If not, create it

Here’s a condensed version of my script:

const axios = require('axios');

const SHOPIFY_API_URL        = process.env.SHOPIFY_ADMIN_API_URL.replace(/\/+$/, '');
const SHOPIFY_TOKEN          = process.env.SHOPIFY_ADMIN_ACCESS_TOKEN;
const METAFIELD_NAMESPACE    = process.env.SHOPIFY_METAFIELD_NAMESPACE || 'custom';
const METAFIELD_KEY          = process.env.SHOPIFY_METAFIELD_KEY       || 'sub_collections';
const METAFIELD_TYPE         = 'single_line_text_field';

async function syncSubCollections(productId) {
  if (!productId) throw new Error('syncSubCollections: missing productId');

  // 1) fetch product variants
  const { data: prod } = await axios.get(
    `${SHOPIFY_API_URL}/products/${productId}.json?fields=variants`,
    { headers: { 'X-Shopify-Access-Token': SHOPIFY_TOKEN } }
  );
  const variantIds = prod.product.variants.map(v => v.id);

  // 2) fetch the two REST endpoints for collections associated with the product
  const [customRes, smartRes] = await Promise.all([
    axios.get(
      `${SHOPIFY_API_URL}/custom_collections.json?product_id=${productId}`,
      { headers: { 'X-Shopify-Access-Token': SHOPIFY_TOKEN } }
    ),
    axios.get(
      `${SHOPIFY_API_URL}/smart_collections.json?product_id=${productId}`,
      { headers: { 'X-Shopify-Access-Token': SHOPIFY_TOKEN } }
    ),
  ]);

  // 3) build a comma-separated list of collection titles
  const collections = [
    ...customRes.data.custom_collections.map(c => c.title),
    ...smartRes.data.smart_collections.map(c => c.title),
  ];
  const value = collections.join(', ');

  // 4) upsert the metafield on each variant
  await Promise.all(variantIds.map(async variantId => {
    // list existing metafields
    const { data: mfList } = await axios.get(
      `${SHOPIFY_API_URL}/variants/${variantId}/metafields.json`,
      { headers: { 'X-Shopify-Access-Token': SHOPIFY_TOKEN } }
    );
    const existing = mfList.metafields.find(mf =>
      mf.namespace === METAFIELD_NAMESPACE && mf.key === METAFIELD_KEY
    );

    if (existing) {
      // update
      await axios.put(
        `${SHOPIFY_API_URL}/metafields/${existing.id}.json`,
        { metafield: { id: existing.id, value, type: METAFIELD_TYPE } },
        { headers: {
            'X-Shopify-Access-Token': SHOPIFY_TOKEN,
            'Content-Type': 'application/json'
          }
        }
      );
    } else {
      // create
      await axios.post(
        `${SHOPIFY_API_URL}/metafields.json`,
        { metafield: {
            namespace:     METAFIELD_NAMESPACE,
            key:           METAFIELD_KEY,
            owner_resource:'variant',
            owner_id:      variantId,
            type:          METAFIELD_TYPE,
            value
          }
        },
        { headers: {
            'X-Shopify-Access-Token': SHOPIFY_TOKEN,
            'Content-Type': 'application/json'
          }
        }
      );
    }
  }));

  // return what we synced
  return { productId, collections };
}

module.exports = { syncSubCollections };

The metafields don’t get updated properly — sometimes they don’t appear at all or the value does not persist. No clear error is returned.

Questions:

  • Is this the correct way to upsert metafields for each variant?
  • Am I using the right REST endpoints for updating vs creating variant metafields?
  • Is there a better way (GraphQL or bulk mutation) to do this efficiently for multiple variants?

Hey @clothovia ,

I highly recommend to post this Question on Shopify Developer community which specially created to fix the complex issues. Many developers are waiting for you to fix your issues.

Visit it from here and post the Question: https://community.shopify.dev/