VIDEO_VALIDATION_ERROR when uploading via stagedUpload

Hello all!

Currently having a frustrating error. The goal is to create a system which downloads Video, Model3d and GenericFile files from one Shopify store and then uploads them into another Shopify store. The downloading portion is currently working fine, storing the files onto my local development machine.

The issue comes when I attempt to upload the file up to another store. All of the Graphql APIs are responding successfully, and I am able to upload the files to Google Storage according to the instructions successfully with the following response:

%Req.Response{
  status: 204,
  headers: %{
    "alt-svc" => ["h3=\":443\"; ma=2592000,h3-29=\":443\"; ma=2592000"],
    "content-length" => ["0"],
    "date" => ["Fri, 20 Jun 2025 00:49:55 GMT"],
    "server" => ["UploadServer"],
    "vary" => ["Origin"],
    "x-guploader-uploadid" => ["..."]
  },
  body: ""
}

Annoyingly, videos and 3d models which are uploaded in this way always result in the following errors:

VIDEO_VALIDATION_ERROR & MODEL3D_VALIDATION_ERROR

{
    "node": {
        "id": "gid://shopify/Metafield/31660967559220",
        "key": "file_reference",
        "value": "gid://shopify/Video/26783818317876",
        "namespace": "custom",
        "type": "file_reference",
        "description": null,
        "reference": {
            "__typename": "Video",
            "filename": "e93200e265fb4520afcd9e1d9f144103.mp4",
            "originalSource": null,
            "fileErrors": [
                {
                    "code": "VIDEO_VALIDATION_ERROR",
                    "details": null,
                    "message": "Video failed validation"
                }
            ]
        }
    }
}

Strangely, this upload process seems to work for GenericFile type files. All APIs using 2025-04 versions.

I really have no idea where to go from here! “Video failed validation” isn’t really helping me :frowning:

Thank you for any help you are able to provide!

Eric

Hey @Eric_Froese,

Happy to look in to this with you. To rule out any possible issues with the files themselves, when you add one of these videos or 3d models to your store admin directly do you get any errors?

From there, looking at your response, I can see the validation error is on a metafield. Check the definition settings to make sure the media meets the set requirements

Hi @KyleG-Shopify ,

Thank you for your assistance!

I was able to upload these same video & 3d model files directly into the store without issue. I hope this rules out any issues with the files themselves.

The metafield which these video files is associated with is (and was) set to allow all file types.

Could it be that I am performing these tests from a development store? Is it possible to upload video and 3d model files programatically into a development store?

Eric

@KyleG-Shopify Another quick update: I’ve created a new, paid Shopify store and the video file uploads still display VIDEO_VALIDATION_ERROR.

The video files can be uploaded to the store through the Shopify Admin.

What are the different criteria which might trigger the VIDEO_VALIDATION_ERROR code? What validation steps are being applied to the video?

Eric

Thanks for doing those tests Eric.

If the error was a specific plan limitation, the error would reference that (although trial plans are more restrictive).

Can you share the mutation you are using to add the media and the x-request-id from the response headers? I’ll see if I can replicate.

@KyleG-Shopify Thank you for your attention to this matter :slight_smile:

Here are the steps which I am taking:

  1. Create the staged upload
mutation stagedUploadsCreate($input: [StagedUploadInput!]!) {
  stagedUploadsCreate(input: $input) {
    stagedTargets {
      url
      resourceUrl
      parameters {
        name
        value
      }
    }
  }
}

with input:

{
  "input": [
    {
      "filename": "video-test.mp4",
      "mimeType": "video/mp4",
      "fileSize": "6110491",
      "resource": "VIDEO",
      "httpMethod": "POST"
    }
  ]
}

I’ve confirmed the file size here is accurate:

➜  ~ stat -f %z ~/Downloads/video-test.mp4
6110491

This returns the following payload:

{
    "data": {
        "stagedUploadsCreate": {
            "stagedTargets": [
                {
                    "url": "https://shopify-video-production-core-originals.storage.googleapis.com",
                    "resourceUrl": "https://shopify-video-production-core-originals.storage.googleapis.com?external_video_id=49753950",
                    "parameters": [
                        {
                            "name": "GoogleAccessId",
                            "value": "video-production@video-production-225115.iam.gserviceaccount.com"
                        },
                        {
                            "name": "key",
                            "value": "c/o/v/cc3f187e8d42445f83c80e304ce76235.mp4"
                        },
                        {
                            "name": "policy",
                            "value": "eyJjb25kaXRpb25zIjpbWy..."
                        },
                        {
                            "name": "signature",
                            "value": "myIjP3t9LuVtQ7Un+R3HfVZjJ7kG/mbd4LviUE++2Fw..."
                        }
                    ]
                }
            ]
        }
    }
}

with x-request-id: 0f12cbc5-eb18-4b8b-b8d0-b3bcf0d420f2-1750432491

  1. Create the file with the fileCreate mutation, using the resourceUrl from the stagedTarget
mutation fileCreate($files: [FileCreateInput!]!) {
  fileCreate(files: $files) {
    files {
      id
      fileErrors {
        details
      }
      fileStatus
    }
  }
}

With input:

{
    "files": [
        {
            "alt": null,
            "contentType": "VIDEO",
            "originalSource": "https://shopify-video-production-core-originals.storage.googleapis.com?external_video_id=49753950"
        }
    ]
}

(Note here, if you provide the filename parameter in the files input, the request will fail and return files: [], but that’s not my current issue)

This returns the following payload:

{
    "data": {
        "fileCreate": {
            "files": [
                {
                    "id": "gid://shopify/Video/33339025162402",
                    "fileErrors": [],
                    "fileStatus": "UPLOADED"
                }
            ]
        }
    }
}

with x-request-id: 8b0fb991-9386-439d-8cdf-8e50d1a8ab4c-1750432225

  1. Upload the file to the staged upload endpoint with cURL
curl -v \
  -F "GoogleAccessId=video-production@video-production-225115.iam.gserviceaccount.com" \
  -F "key=c/o/v/cc3f187e8d42445f83c80e304ce76235.mp4" \
  -F "policy=eyJjb25kaXRpb25zIjpbWyJlc..." \
  -F "signature=myIjP3t9LuVtQ7Un+..." \
  -F "file=@/Users/my-username/Downloads/video-test.mp4" \
  "https://shopify-video-production-core-originals.storage.googleapis.com"

Which returns

*   Trying 172.217.165.27:443...
* Connected to shopify-video-production-core-originals.storage.googleapis.com (172.217.165.27) port 443 (#0)
* ALPN: offers h2
* ALPN: offers http/1.1
*  CAfile: /etc/ssl/cert.pem
*  CApath: none
* (304) (OUT), TLS handshake, Client hello (1):
* (304) (IN), TLS handshake, Server hello (2):
* (304) (IN), TLS handshake, Unknown (8):
* (304) (IN), TLS handshake, Certificate (11):
* (304) (IN), TLS handshake, CERT verify (15):
* (304) (IN), TLS handshake, Finished (20):
* (304) (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / AEAD-CHACHA20-POLY1305-SHA256
* ALPN: server accepted h2
* Server certificate:
*  subject: CN=*.storage.googleapis.com
*  start date: Jun  2 08:36:24 2025 GMT
*  expire date: Aug 25 08:36:23 2025 GMT
*  subjectAltName: host "shopify-video-production-core-originals.storage.googleapis.com" matched cert's "*.storage.googleapis.com"
*  issuer: C=US; O=Google Trust Services; CN=WR2
*  SSL certificate verify ok.
* Using HTTP2, server supports multiplexing
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* h2h3 [:method: POST]
* h2h3 [:path: /]
* h2h3 [:scheme: https]
* h2h3 [:authority: shopify-video-production-core-originals.storage.googleapis.com]
* h2h3 [user-agent: curl/7.86.0]
* h2h3 [accept: */*]
* h2h3 [content-length: 6111819]
* h2h3 [content-type: multipart/form-data; boundary=------------------------73bb7c0ac999999f]
* Using Stream ID: 1 (easy handle 0x12880c600)
> POST / HTTP/2
> Host: shopify-video-production-core-originals.storage.googleapis.com
> user-agent: curl/7.86.0
> accept: */*
> content-length: 6111819
> content-type: multipart/form-data; boundary=------------------------73bb7c0ac999999f
>
* We are completely uploaded and fine
< HTTP/2 204
< x-guploader-uploadid: ABgVH8-4Mu2LkpZU6xZZtfuAK4hujInzuSHs-hy0_8jugfahu4cWZ1f86JiYUE2iwEqo16_M
< vary: Origin
< date: Fri, 20 Jun 2025 15:21:06 GMT
< server: UploadServer
< alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000
<
* Connection #0 to host shopify-video-production-core-originals.storage.googleapis.com left intact

I suspect that the issue might be the order of steps here. According to the documentation, it seems that you can call fileCreate with the staged upload URL, then separately you can upload the file. This approach is convenient for me because it means that I can perform the upload step asynchronously.

As I was typing this, I tried to perform these steps with #3 and #2 swapped (upload first, create file second) and it seemed to work properly… Is this the expected behaviour?

This exact set of steps works properly for GenericFile type metafields. Uploading a PDF in this order works properly. Seems not to work for Video and Model3d

Eric

Thanks for those details Eric.

Yes, reversing steps 2 and 3 is the correct process.

Step 1: StagedUploadCreate
Step 2: CURL request to upload the media
Step 3+: use that staged media in Graphql mutations to add to products, etc.