How to upload a product image with GraphQL?

Hello,

I want to migrate a REST call that is being used to upload product images to GraphQL, and I am a little bit confused about which mutations to use to perform this operation.

I am using the following REST API call

$image = [
  "filename" => $imageName,
  "attachment" => base64_encode($imageContent),
  "position" => $position,
  "alt" => $alt,
  "variant_ids" => $variantIds,
]; 


this->shopifyClient->call('POST', "/admin/api/2024-01/products/$productId/images.json", [
  'json' => [
  "image" => $image,
 ]]);

This call is basically doing the following operations for me

  • Upload image to Shopify by using base64_encoded image content string
  • Assign it to product
  • Attach it to provided variant_ids
  • Set position (order) of image
  • Set alt text of the image

I think to perform this operation with graphQL I will need to do multiple calls.

  1. Use base64_encoded string and store it in a URL which can be uploaded to Shopify because an image can not be provided as a base64 encoded string. We have to provide a URL.

  2. productUpdate => upload image to product, here I can also add alt_tag so it will solve two steps.

  3. productReorderMedia => To set position of the image I will need to make a call to reorder media.

  4. productVariantDetachMedia => I will need to detach existing variant media by using this call before attaching new media.

  5. productVariantAppendMedia => I will need to use this mutation to attach media to variant.

It seems like I will need to do 4 different calls to achieve what I was able to achieve with REST API, I hope I just missed something and it is not true.

Can someone please help to figure out if I can do it with less calls?

Hey @farid
That looks right to me!
I can understand that making multiple calls isn’t ideal, but I can understand on Shopify side the need to make these changes to support the extensible variant model so we can have all the variants.

There are a couple of benefits to the GraphQL calls, which I want to highlight.

  1. In the previous REST call it only supported one image at a time. With the GraphQL methods, you could upload multiple images etc in the same productUpdate call.
  2. You can also stage your media so multiple images, videos and 3D models can be added at the same time so it would be more efficient for adding multiple media items at once.
  3. You can also use bulk imports with productUpdate, if you wanted affect mutliple products and media.
  4. If you are doing multiple media/images at once in a single GraphQL, you will be better off with the rate limit as a mutation only costs 10 points.

There is a great tutorial here around any media operations. Manage media for products

You can also combine multiple mutations into one request, it doesn’t change any rate limiting. But it might make the variant detach/attached easier for you, something like this:

mutation productVariantManageMedia(
  $productId: ID!, 
  $detach: [ProductVariantDetachMediaInput!]!,
  $append: [ProductVariantAppendMediaInput!]!) {
  productVariantDetachMedia(productId: $productId, variantMedia: $variantMedia) {
    userErrors {
      code
      field
      message
    }
  }
  productVariantAppendMedia(productId: $productId, variantMedia: $variantMedia) {
    userErrors {
      code
      field
      message
    }
  }
}

Jordan, I think you do a great job helping us understand why we need so many different calls to add images, it still seems like there are so many different ways to go about it which is still a bit confusing. That said, going to spend some time tonight working to figure it out. Will report back.

1 Like

Thanks, Jordan, for sharing a detailed answer. Our app is uploading thousands of images in a single job, so error handling here will be very critical and challenging. Also, one of the challenges I see is that previously, it was possible to upload images by providing a base64_encoded string, but now I will have to provide a URL, which means I will need to store the data on my server or third-party service and then provide a URL, which makes things extra complicated in my case.

Hey Farid,

Yes, I know it’s going to be a lot of processing for you!
In my head I’m thinking of it this way:

  1. Group images by product that you need to add them to, then for each:
  2. Take images and upload either to your own blob storage or you can stage it in Shopify you can do that in chunks of up 250 at a time I believe (there are some size limits here just to flag)
  3. Do a product update to add the media (again you can do multiple media in one call), OR if you want to do it for all products at the same time you could do a bulk productUpdate.
  4. Detach/Attach from each variant after that again you can do multiple media here for each variant.

I’d maybe push these onto a queue and churn through them, so you can retry if you get a 503 or anything

1 Like

Hey guys, so after attaching the media did you then have to publish the product? or is there a way to do it within these calls?

I am in the process of trying to publish the product while doing this. I am using the publishablePublish mutation but getting the error that I need the “read_product_listings” scope but the documentation says I only need the “write_publications” scope which I have. @Liam-Shopify any chance you have insight in to this?

Below is the publication query I am doing:

mutation PublishProduct($productId: ID!, $publicationIds: [PublicationInput!]!) {
  publishablePublish(id: $productId, input: $publicationIds) {
    publishable{
      publishedOnCurrentPublication
    }
}
  }

Here is the error:

{
  "errors": [
    {
      "message": "Access denied for publishedOnCurrentPublication field. Required access: `read_product_listings` access scope.",
      "locations": [
        {
          "line": 152,
          "column": 7
        }
      ],
      "path": [
        "publishablePublish",
        "publishable",
        "publishedOnCurrentPublication"
      ],
      "extensions": {
        "code": "ACCESS_DENIED",
        "documentation": "https://shopify.dev/api/usage/access-scopes",
        "requiredAccess": "`read_product_listings` access scope."
      }
    }
  ],
  "data": {
    "publishablePublish": {
      "publishable": null
    }
  },
  "extensions": {
    "cost": {
      "requestedQueryCost": 10,
      "actualQueryCost": 10,
      "throttleStatus": {
        "maximumAvailable": 2000,
        "currentlyAvailable": 1990,
        "restoreRate": 100
      }
    }
  }
}

To my knowledge “read_product_listings” is an unauthorized scope so no clue why I would need that for this mutation.

If there is a way to do this without the mutation I’d take that as well.

Not doing anything regarding publishing in my case unfortunately.