Image and file upload via mutation generateStagedUploads problem

Topic summary

Developers are encountering a SignatureDoesNotMatch error when attempting to upload images using Shopify’s GraphQL stagedUploadsCreate mutation, despite following official documentation.

Core Problem:

  • The mutation response lacks expected S3 parameters (bucket, policy) mentioned in tutorials
  • Direct POST requests to the returned URL fail with signature errors
  • Multiple developers report the same issue, with images specifically failing while videos work

Working Solution (twilson90):
The critical missing piece is adding "httpMethod": "POST" to the input when declaring IMAGE resources:

if (media_type == "IMAGE") input["httpMethod"] = "POST";
else input["fileSize"] = stat.size.toString();

Implementation steps:

  1. Call stagedUploadsCreate with proper input including httpMethod for images
  2. Extract url, resourceUrl, and parameters from response
  3. Create FormData, append all parameters from response
  4. Append the file itself
  5. POST to the staged URL
  6. Use resourceUrl in subsequent productCreateMedia mutation

The discussion remains helpful for others facing identical authentication/upload issues with Shopify’s media API.

Summarized with AI on November 19. AI used: claude-sonnet-4-5-20250929.

Hi Community Developers,

I have failed to upload file via Graphql generateStagedUploads. Can someone help?

I followed this tutorial https://shopify.dev/api/examples/product-media. But the response seems not exactly same. In my response I can not find the bucket name, policy and other fields from parameters: . so I assumed that I can use the “url” in “stagedTargets”: directly. But I always got an SignatureDoesNotMatch error.

`SignatureDoesNotMatch`

The did the following steps:

I made a call:

mutation generateStagedUploads($input: [StagedUploadInput!]!) {
                      stagedUploadsCreate(input: $input) {
                        stagedTargets {
                          url
                          resourceUrl
                          parameters {
                            name
                            value
                          }
                        }
                        
                      }
                    }
variables = {
                  "input": [
                    {
                      "filename": "blank-logo.png",
                      "mimeType": "image/png",
                      "resource": "IMAGE",
                      "fileSize": str(size)

                    }
                  ]
                }

I got data responsed:

{
    "data": {
        "stagedUploadsCreate": {
            "stagedTargets": [
                {
                    "url": "https:\/\/shopify.s3.amazonaws.com\/tmp\/58504741015\/products\/17f18b13-6453-4cd9-a124-5a7c54834df4\/blank-logo.png?x-amz-acl=private\u0026X-Amz-Algorithm=AWS4-HMAC-SHA256\u0026X-Amz-Credential=AKIAJYM555KVYEWGJDKQ%2F20210811%2Fus-east-1%2Fs3%2Faws4_request\u0026X-Amz-Date=20210811T055124Z\u0026X-Amz-Expires=900\u0026X-Amz-SignedHeaders=host\u0026X-Amz-Signature=1e2c67cda90abe7ea6a7c1b2731883f4417badf8139a23807dcb31c129f71e02",
                    "resourceUrl": "https:\/\/shopify.s3.amazonaws.com\/tmp\/58504741015\/products\/17f18b13-6453-4cd9-a124-5a7c54834df4\/blank-logo.png",
                    "parameters": [
                        {
                            "name": "content_type",
                            "value": "image\/png"
                        },
                        {
                            "name": "acl",
                            "value": "private"
                        }
                    ]
                }
            ]
        }
    },
    "extensions": {
        "cost": {
            "requestedQueryCost": 11,
            "actualQueryCost": 11,
            "throttleStatus": {
                "maximumAvailable": 1000.0,
                "currentlyAvailable": 989,
                "restoreRate": 50.0
            }
        }
    }
}

and then i make this call in python:

headers = {
            'x-amz-acl': 'private',
            'Content-Type': 'image/png'
        }
        with open('blank-logo.png', 'rb') as f:
            response = requests.request("POST", url_aws, headers=headers, data=f)

I also tried curl in terminal and postman:

curl -v \
  -F "x-amz-acl=private" \
  -F "Mime-Type=image/png" \
  -F "key=tmp/58504741015/products/a1a28122-03b0-42f0-bb58-1626583177f0/blank-logo.png" \
  -F "Filename=blank-logo.png" \
  -F "file=@/Volumes/Development/backend_project/project-blank-integration/blank-logo.png" \
   "https://shopify.s3.amazonaws.com/tmp/58504741015/products/a1a28122-03b0-42f0-bb58-1626583177f0/blank-logo.png?x-amz-acl=private&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=AKIAJYM555KVYEWGJDKQ/20210811/us-east-1/s3/aws4_request&X-Amz-Date=20210811T055512Z&X-Amz-Expires=900&X-Amz-SignedHeaders=host&X-Amz-Signature=58391e74775af43ec4585ae078e2178fd19c286d830085f53868e456866f696d"

Could you please help me with the correct way to using the aws url?

Thank you very much!

Barrick

3 Likes

I have the same issue

I have the same issue. Did anyone manage to find a solution?

I just posted a question asking the same thing, does anyone have any enlightenment on the subject? :slightly_smiling_face:

Same issue.

Anyone finally worked this out?

Which part is not working?
Are you receiving a response from the StagedUploadsCreate?

Cheers, Matthias

No, I would get a nondescript error when i tried to post an image using this method.

Videos however worked.

I got it working in the end by changing my initial input when declaring the image upload.

I’m not at my PC right now so I cant tell you the exact details, but I can a bit later.

ah if its working :slightly_smiling_face: then congrats

Here’s how I got it working for anyone who might find it useful. Note that I’m using the node api:

var stat = fs.statSync(filepath);
var input = {
  "filename": convert_to_handle(product_title)+"-"+String(++i).padStart(3,"0")+path.extname(m),
  "mimeType": mimetype,
  "resource": media_type,
}
if (media_type == "IMAGE") input["httpMethod"] = "POST";
else input["fileSize"] = stat.size.toString();
inputs.push(input);
var resp = await shopify.graphql(
  `mutation generateStagedUploads($input: [StagedUploadInput!]!) {
    stagedUploadsCreate(input: $input) {
      stagedTargets {
        url
        resourceUrl
        parameters { name, value }
      }
      userErrors {
        field, message
      }
    }
  }`, {
    input: [input]
  }
);

var t = resp.stagedUploadsCreate.stagedTargets[0];
var media = {
  "alt": product_title,
  "mediaContentType": media_type,
  "originalSource": t.resourceUrl,
}
var form_data = new FormData();
for (var p of t.parameters) {
  form_data.append(p.name, p.value);
}
form_data.append("file", fs.createReadStream(filepath));
await axios.post(t.url, form_data);

await shopify.graphql(
  `mutation createProductMedia($id: ID!, $media: [CreateMediaInput!]!) {
    productCreateMedia(productId: $id, media: $media) {
      media {
        ${fields.media}
        mediaErrors {
          code
          details
          message
        }
        mediaWarnings {
          code
          message
        }
      }
      mediaUserErrors {
        code
        field
        message
      }
    }
  }`, {
    id: product_id,
    media: [media]
  }
);

The important part I was missing that was causing the issue I’m pretty sure was declaring the ‘httpMethod’ specifically for images.