Working with product media

You can use the GraphQL Admin API to manage the various types of media associated with merchants' products. This guide explains the different types of product media, and how to use Shopify's API to work with them.

Only the GraphQL Admin API supports working with all types of product media. The REST Admin API supports only product images.

Required access

read_products, write_products access scopes.

To test using product media in the Shopify admin, create a development store and enable a developer preview from your Partner Dashboard.

Product media types

A product can have the following media types:

GraphQL type Description
MediaImage An image in one of the following formats: PNG, GIF, or JPG. The maximum image size is 4472 x 4472 px, or 20 megapixels. For square images, 2048 x 2048 px looks best.
Video An MP4 video that's hosted by Shopify. The video runtime must be 1 minute or shorter. The maximum file size is 1 GB.
ExternalVideo A video that's hosted on YouTube, specified by its embed URL.
Model3d A 3D model provided in the GLB format. The asset can be retrieved in GLB and USDZ formats. The file size must be 15 MB or smaller.

Uploading media to Shopify

Before you add a piece of media to a product, you can upload it and host it on Shopify. There are two parts to uploading an asset to Shopify:

  1. Generate the upload URL and parameters
  2. Upload the asset

The number of media assets that a shop can upload varies, depending on the shop's Shopify plan. To learn more, see Plan-based limits for product media.

Generate the upload URL and parameters

You can use the stagedUploadsCreate mutation to generate the values that you need to authenticate multiple uploads. The mutation returns the stagedTargets array. Each staged target has the following properties:

  • params — The parameters that you use to authenticate an upload request
  • url — The signed URL where you can upload the media asset
  • resourceUrl — The URL that you pass to productCreateMedia in the originalSource field after the asset has been uploaded

The mutation accepts an input of type stagedUploadInput, which has the following fields:

Field Type Description
resource StagedUploadTargetGenerateUploadResource Specifies the resource type to upload. Valid media values: VIDEO, MODEL_3D, IMAGE.
filename String The name of the file to upload.
mimeType String The Media type of the file to upload. Use one of the following values:
  • video/mp4
  • video/quicktime
  • image/png
  • image/gif
  • image/jpeg
  • model/gltf-binary
httpMethod StagedUploadHttpMethodType The HTTP method to be used by the staged upload. Valid values: POST (all media types), PUT (images only).
fileSize UnsignedInt64 The size in bytes of the file to upload.

The following example uses the stagedUploadsCreate mutation to generate the upload values required to upload an MP4 video and a 3D model.

POST /admin/api/unstable/graphql.json

mutation generateStagedUploads($input: [StagedUploadInput!]!) {
  stagedUploadsCreate(input: $input) {
    stagedTargets {
      url
      resourceUrl
      parameters {
        name
        value
      }
    }
    userErrors { field, message }
  }
}

Variables

{
  "input" : [
    {
      "filename": "watches_comparison.mp4",
      "mimeType": "video/mp4",
      "resource": "VIDEO",
      "fileSize": "899765"
    },
    {
      "filename": "another_watch.glb",
      "mimeType": "model/gltf-binary",
      "resource": "MODEL_3D",
      "fileSize": "456"
    }
  ]
}

View response

JSON response

{
  "data": {
    "stagedUploadsCreate": {
      "stagedTargets": [
        {
          "url": "https://shopify-video-production-core-originals.s3.amazonaws.com/",
          "resourceUrl": "https://shopify-video-production-core-originals.s3.amazonaws.com?external_video_id=4635",
          "parameters": [
            {
              "name": "bucket",
              "value": "shopify-video-production-core-originals"
            },
            {
              "name": "key",
              "value": "c/o/v/7827ebe111a24a09869ec90f3412768f.mp4"
            },
            {
              "name": "policy",
              "value": "eyJjb25kaXRpb25zIjpbWyJlcSIsIiRidWNrZXQiLCJzaG9waWZ5LXZpZGVvLXByb2R1Y3Rpb24tY29yZS1vcmlnaW5hbHMiXSxbImVxIiwiJGtleSIsImMvby92Lzc4MjdlYmUxMTFhMjRhMDk4NjllYzkwZjM0MTI3NjhmLm1wNCJdLFsiY29udGVudC1sZW5ndGgtcmFuZ2UiLDg5OTc2NSw4OTk3NjVdLFsiZXEiLCIkY2FjaGUtY29udHJvbCIsInB1YmxpYywgbWF4LWFnZT0zMTUzNjAwMCJdLFsiZXEiLCIkeC1hbXotY3JlZGVudGlhbCIsIkFLSUFZT0k1S1o2MkpRQ1c2M0xVLzIwMTkwOTE3L3VzLWVhc3QtMS9zMy9hd3M0X3JlcXVlc3QiXSxbImVxIiwiJHgtYW16LWFsZ29yaXRobSIsIkFXUzQtSE1BQy1TSEEyNTYiXSxbImVxIiwiJHgtYW16LWRhdGUiLCIyMDE5MDkxN1QyMDQ4MzhaIl1dLCJleHBpcmF0aW9uIjoiMjAxOS0wOS0xN1QyMTo0ODozOFoifQ=="
            },
            {
              "name": "cache-control",
              "value": "public, max-age=31536000"
            },
            {
              "name": "x-amz-signature",
              "value": "16bd494f09d3739f428777e44b2a1f8de96f9545b83db4a58cf027503833c9fc"
            },
            {
              "name": "x-amz-credential",
              "value": "AKIAYOI5KZ62JQCW63LU/20190917/us-east-1/s3/aws4_request"
            },
            {
              "name": "x-amz-algorithm",
              "value": "AWS4-HMAC-SHA256"
            },
            {
              "name": "x-amz-date",
              "value": "20190917T204838Z"
            }
          ]
        },
        {
          "url": "https://storage.googleapis.com/threed-models-production/models/60d55fd9e8cba091/another_watch.glb?external_model3d_id=bW9kZWwzZC0xNTk3",
          "resourceUrl": "https://storage.googleapis.com/threed-models-production/models/60d55fd9e8cba091/another_watch.glb?external_model3d_id=bW9kZWwzZC0xNTk3",
          "parameters": [
            {
              "name": "GoogleAccessId",
              "value": "threed-model-service--6bgx7cbe@shopify-applications.iam.gserviceaccount.com"
            },
            {
              "name": "key",
              "value": "models/60d55fd9e8cba091/another_watch.glb"
            },
            {
              "name": "policy",
              "value": "eyJleHBpcmF0aW9uIjoiMjAxOS0wOS0xN1QyMTowMzozOFoiLCJjb25kaXRpb25zIjpbWyJlcSIsIiRidWNrZXQiLCJ0aHJlZWQtbW9kZWxzLXByb2R1Y3Rpb24iXSxbImVxIiwiJGtleSIsIm1vZGVscy82MGQ1NWZkOWU4Y2JhMDkxL2Fub3RoZXJfd2F0Y2guZ2xiIl0sWyJjb250ZW50LWxlbmd0aC1yYW5nZSIsNDU2LDQ1Nl1dfQ=="
            },
            {
              "name": "signature",
              "value": "Tp5B5OXpE2bWEnvQ7F1JvozQPGloujwMJ9CMbLWUwM1LmnUpL3tAh6eJ9d61laiwLqGRgTUOdTkMZTofFi5n/af9sCDZAHXeDKzukrohWYX8M+Ui7VvDflA/l0CIv5uzhMgbfRmqK9XCBK61/WkSuMZblEP0AAmqMtkKZhuxTzj8JMH83JYRqVb0r9k9IE+TEJluc5eSJ6Y+NrrGFV+nbh7T/lexCc02v8NVNNw2I0xNyQ8sZWW53c0WJIuQ4F+oagMGSXeyhxNvjKVPC1NVv5NqFM4A/d7eGLwBmlz4lE4GyUonxyH//Iww7zq+Klyr0+mP8IcYnGJQpT3uJDow+w=="
            }
          ]
        }
      ],
      "userErrors": []
    }
  }
}

Upload the asset

After generating the parameters and URL for an upload, you need to upload the asset by using a POST or PUT request. The request is formatted differently depending on the media type and the HTTP method that you're using.

Videos, 3D models: POST request

Use a multipart form, and include all parameters as form inputs in the request body:

curl -v \
  -F "bucket=shopify-video-production-core-originals" \
  -F "key=c/o/v/7827ebe111a24a09869ec90f3412768f.mp4" \
  -F "policy=eyJjb25kaXRpb25zIjpbWyJlcSIsIiRidWNrZXQiLCJzaG9waWZ5LXZpZGVvLXByb2R1Y3Rpb24tY29yZS1vcmlnaW5hbHMiXSxbImVxIiwiJGtleSIsImMvby92Lzc4MjdlYmUxMTFhMjRhMDk4NjllYzkwZjM0MTI3NjhmLm1wNCJdLFsiY29udGVudC1sZW5ndGgtcmFuZ2UiLDg5OTc2NSw4OTk3NjVdLFsiZXEiLCIkY2FjaGUtY29udHJvbCIsInB1YmxpYywgbWF4LWFnZT0zMTUzNjAwMCJdLFsiZXEiLCIkeC1hbXotY3JlZGVudGlhbCIsIkFLSUFZT0k1S1o2MkpRQ1c2M0xVLzIwMTkwOTE3L3VzLWVhc3QtMS9zMy9hd3M0X3JlcXVlc3QiXSxbImVxIiwiJHgtYW16LWFsZ29yaXRobSIsIkFXUzQtSE1BQy1TSEEyNTYiXSxbImVxIiwiJHgtYW16LWRhdGUiLCIyMDE5MDkxN1QyMDQ4MzhaIl1dLCJleHBpcmF0aW9uIjoiMjAxOS0wOS0xN1QyMTo0ODozOFoifQ==" \
  -F "cache-control=public, max-age=31536000" \
  -F "x-amz-signature=16bd494f09d3739f428777e44b2a1f8de96f9545b83db4a58cf027503833c9fc" \
  -F "x-amz-algorithm=AWS4-HMAC-SHA256" \
  -F "x-amz-date=20190917T204838Z"
  -F "file=@/Users/shopifyemployee/watches_comparison.mp4" \
  "https://shopify-video-production-core-originals.s3.amazonaws.com/"

Images: POST request

Use a multipart form, and include all parameters as form inputs in the request body:

curl -v \
  -F "AWSAccessKeyId=AKIAJYM555KVYEWGJDKQ" \
  -F "key=tmp/17681717/products/ec24e5f6-ba91-43d7-bb43-4c7174770ac1/watches_comparison.png" \
  -F "policy=eyJleHBpcmF0aW9uIjoiMjAxOS0wNi0xNFQxNjo0OTowM1oiLCJjb25kaXRpb25zIjpbeyJidWNrZXQiOiJzaG9waWZ5In0seyJrZXkiOiJ0bXBcLzE3NjgxNzE3XC9wcm9kdWN0c1wvZWMyNGU1ZjYtYmE5MS00M2Q3LWJiNDMtNGM3MTc0NzcwYWMxXC9JR1EucG5nIn0seyJGaWxlbmFtZSI6IklHUS5wbmcifSx7Ik1pbWUtVHlwZSI6ImltYWdlXC9wbmcifSx7IkZpbGUtU2l6ZSI6IjMzNjQ2MSJ9LHsic3VjY2Vzc19hY3Rpb25fc3RhdHVzIjoiMjAxIn0seyJhY2wiOiJwcml2YXRlIn0sWyJjb250ZW50LWxlbmd0aC1yYW5nZSIsMSwyMDk3MTUyMF1dfQ==" \
  -F "signature=yZsEky4DDqbbcNnOVFNAagZnlcI=" \
  -F "Filename=watches_comparison.png" \
  -F "Mime-Type=image/png" \
  -F "File-Size=336461" \
  -F "success_action_status=201" \
  -F "acl=private" \
  -F "file=@/Users/shopifyemployee/Desktop/watches_comparison.png" \
   "https://shopify.s3.amazonaws.com/"

Images: PUT request

Include the parameters as request headers. Additional parameters are already included in the URL:

curl -X PUT -T ~/Desktop/watches_comparison.png \
  -H 'content_type:image/png' \
  -H 'x-aws-acl:private' \
  -H 'Content-Type:image/png' \
  "https://shopify.s3.amazonaws.com/tmp/17681717/products/35c29fff-a86e-4ca4-a8aa-f725b2b6fdcb/watches_comparison.png?AWSAccessKeyId=AKIAJYM555KVYEWGJDKQ&Expires=1560534810&Signature=zUu%2BAmDsgkUsMhE%2BixRwRdXCNMY%3D"

Plan-based limits for product media

There is a limit to the number of videos and 3D models that a Shopify store can upload. The limit depends on the Shopify plan that the store is on:

Basic Shopify Shopify Advanced Shopify Shopify Plus
250 1000 5000 Contact Plus Support

Adding media to a product

You can add new media to a product by using the productCreateMedia mutation. The mutation takes two arguments:

For each CreateMediaInput object, include the following fields:

Field Type Description
originalSource String The original source of the media object. Can be an external URL for images and videos hosted outside of Shopify, or a signed upload URL for images, videos, and 3D models hosted by Shopify. For assets hosted by Shopify, use the `resourceUrl` value returned by the `stagedUploadsCreate` mutation.
alt String The alt text associated to the media.
mediaContentType MediaContentType The content type of the asset that you're adding. Valid values: IMAGE, VIDEO, EXTERNAL_VIDEO, MODEL_3D.

The following example adds a Shopify-hosted video to a product.

POST /admin/api/unstable/graphql.json

mutation createProductMedia(
  $id: ID!
  $media: [CreateMediaInput!]!
) {
  productCreateMedia(productId: $id, media: $media) {
    media {
      ... fieldsForMediaTypes
      mediaErrors {
        code
        details
        message
      }
    }
    product {
      id
    }
    userErrors {
      field
      message
    }
  }
}

fragment fieldsForMediaTypes on Media {
  alt
  mediaContentType
  position
  previewImage {
    id
  }
  status
  ... on Video {
    id
    sources {
      format
      height
      mimeType
      url
      width
    }
  }
  ... on ExternalVideo {
    id
    embeddedUrl
  }
  ... on Model3d {
    sources {
      format
      mimeType
      url
    }
  }
}

Variables

{
  "id": "gid://shopify/Product/1",
  "media": [
    {
      "originalSource": "https://storage.googleapis.com/shopify-video-production-core-originals/c/o/v/af64d230f6bc40cbba40a87be950a1a2.mp4?external_video_id=1730",
      "alt": "Comparison video showing the different models of watches.",
      "mediaContentType": "VIDEO"
    }
  ]
}

View response

JSON response

{
  "data": {
    "productCreateMedia": {
      "media": [
        {
          "mediaContentType": "VIDEO",
          "alt": "Comparison video showing the different models of watches.",
          "status": "UPLOADED",
          "id": "gid://shopify/Video/3113016",
          "sources": [
            {
              "format": "mp4",
              "height": 360,
              "mimeType": "video/mp4",
              "url": "https://videos.shopifycdn.com/c/vp/2a82811738ca41e7a508e6744028d169/SD-360p.mp4?Expires=1560534951&KeyName=core-signing-key-1&Signature=T_aftXCRfamwlXZNlCwSAOq1sg8=",
              "width": 640
            }
          ]
        }
      ],
      "userErrors": []
    }
  }
}

Retrieving media objects

You can retrieve a product's media of all types by using the media connection on the Product type. The connection returns nodes that implement the Media interface.

The media connection includes the mediaContentType field, which you can use to check the media type of each node. Because each media type can return different fields, you can specify the return fields for each type by using fragments:

POST /admin/api/unstable/graphql.json

{
  product(id:"gid://shopify/Product/1") {
    title
    media(first:5) {
      edges {
        node {
          ... fieldsForMediaTypes
        }
      }
    }
  }
}

fragment fieldsForMediaTypes on Media {
  alt
  mediaContentType
  position
  previewImage {
    id
    altText
    width
    height
    originalSrc
  }
  status
  ... on Video {
    id
    sources {
      format
      height
      mimeType
      url
      width
    }
    originalSource {
      format
      height
      mimeType
      url
      width
    }
  }
  ... on ExternalVideo {
    id
    embeddedUrl
  }
  ... on Model3d {
    sources {
      format
      mimeType
      url
    }
    originalSource {
      format
      mimeType
      url
    }
  }
  ... on MediaImage {
    id
    image {
      altText
      width
      height
      originalSrc
    }
  }
}

View response

JSON response

{
  "data": {
    "product": {
      "title": "Polaris Watch",
      "media": {
        "edges": [
          {
            "node": {
              "alt": "Comparison video showing the different models of watches.",
              "mediaContentType": "VIDEO",
              "position": 3,
              "previewImage": {
                "id": "gid://shopify/Image/1",
                "altText": "Comparison video showing the different models of watches..",
                "width": 640,
                "height": 360,
                "originalSrc": "https://cdn.shopify.com/s/files/1/1768/1717/products/31f1438669864f4f91847f07e39e3835.jpg?v=1234567890"
              },
              "status": "READY",
              "id": "gid://shopify/Video/3113016",
              "sources": [
                {
                  "format": "mp4",
                  "height": 360,
                  "mimeType": "video/mp4",
                  "url": "https://videos.shopifycdn.com/c/vp/2a82811738ca41e7a508e6744028d169/SD-360p.mp4?Expires=1560956269&KeyName=core-signing-key-1&Signature=MYq_eEWGB-2Ww-oN58j-TbxwDYw=",
                  "width": 640
                }
              ]
            }
          },
          {
            "node": {
              "alt": "Comparison video showing the different models of watches.",
              "mediaContentType": "EXTERNAL_VIDEO",
              "position": 2,
              "previewImage": {
                "id": "gid://shopify/Image/2",
                "altText": "Comparison video showing the different models of watches..",
                "width": 640,
                "height": 360,
                "originalSrc": "https://cdn.shopify.com/s/files/1/1768/1717/products/31f1438669864f4f91847f07e39e3835.jpg?v=2234567890"
              },
              "status": "READY",
              "id": "gid://shopify/ExternalVideo/8486968",
              "embeddedUrl": "https://youtu.be/z7RLjNOael0"
            }
          }
        ]
      }
    }
  }
}

Checking whether media is ready to display

When you add a piece of media to a product, Shopify needs to process the media before it can be displayed. The Media interface includes the status field, which you can use to check whether the media has been processed. The status field can return the following values:

Value Description
UPLOADED The media has been uploaded but not processed.
PROCESSING The media is being processed.
READY The media is ready to be displayed.
FAILED The media processing failed.

Reordering media objects

You can reorder a product’s media by using the productReorderMedia mutation. The mutation accepts two arguments:

  • id — The ID of the product whose media you’re reordering.
  • moves — An array of tuples consisting of a media object’s ID and its new position in the list. For example, the following array would move the media objects to the front of the product’s media list:

    [
      {
        "id": "gid://shopify/MediaImage/37"
        "newPosition": "0"
      },
      {
        "id": "gid://shopify/Video/2",
        "newPosition": "1"
      }
    ]

The following example adjusts the order of a products first three media assets.

POST /admin/api/unstable/graphql.json

mutation reorderProductMedia($id:ID!, $moves: [MoveInput!]!) {
  productReorderMedia(id: $id, moves: $moves) {
     job {
       id
       done
     }
    userErrors {
      field
      message
    }
  }
}

Variables

{
  "id": "gid://shopify/Product/1",
  "moves": [
    {
      "id": "gid://shopify/MediaImage/37",
      "newPosition": "0"
    }, {
      "id": "gid://shopify/Video/2",
      "newPosition": "1"
    }, {
      "id": "gid://shopify/ExternalVideo/1",
      "newPosition": "2"
    }
  ]
}

View response

JSON response

{
  "data": {
    "productReorderMedia": {
      "job": {
        "id": "gid://shopify/Job/e1104bd6-4234-4b3e-b6d6-173c1d7e89f5",
        "done": false
      },
      "userErrors": []
    }
  }
}

You can use the returned ID for the job to poll for when the reordering job is complete.

Deleting media objects

To delete media assets from a product, use the productDeleteMedia mutation. The mutation accepts two arguments:

  • productId — The ID of the product whose media you’re deleting.
  • mediaIds — An array of IDs for the media that you’re deleting.

The following example deletes two media assets from a product.

POST /admin/api/unstable/graphql.json

mutation deleteProductMedia($id: ID!, $mediaIds: [ID!]!) {
  productDeleteMedia(productId: $id, mediaIds: $mediaIds) {
    deletedMediaIds
    product {
      id
    }
    userErrors {
      field
      message
    }
  }
}

Variables

{
  "id": "gid://shopify/Product/1",
  "mediaIds": [
    "gid://shopify/ExternalVideo/1",
    "gid://shopify/Video/2"
  ]
}

View response

JSON response

{
  "data": {
    "productDeleteMedia": {
      "deletedMediaIds": [
        "gid://shopify/ExternalVideo/1",
        "gid://shopify/Video/2"
      ],
      "product": {
        "id": "gid://shopify/Product/1"
      },
      "userErrors": []
    }  
  }
}

Sign up for a Partner account to get started.

Sign up