Fix Instagram Reels API Error 2207052 (Media Upload Has Failed)

Your n8n workflow builds the Reels container, the Graph API returns a creation ID, and then the publish call comes back with Media upload has failed and the code 2207052. The response body carries no field telling you what about the media failed. Nine times out of ten the file is fine everywhere else you play it, which is why people retry three times and give up.
Quick answer: Instagram Graph API error 2207052 ("Media upload has failed") means Meta's transcoder could not ingest the video at thevideo_urlyou passed. The causes are almost always encode-level: an HDR source from an iPhone, video bitrate above 25 Mbps, audio that isn't AAC at 128 kbps, a frame rate below 23 fps, a clip shorter than 3 seconds, or a URL Meta's fetcher can't read. Normalize the file first withffmpeg -vf "scale=1080:1920:force_original_aspect_ratio=decrease,pad=1080:1920:(ow-iw)/2:(oh-ih)/2,format=yuv420p,fps=30" -c:v libx264 -b:v 8M -maxrate 10M -c:a aac -b:a 128k -ar 48000 -movflags +faststart out.mp4, or run the same normalize as one API call with FFmpeg Micro before you hand the URL to Meta.
Error 2207052 is a format rejection wearing a network error's clothes
Common reading of 2207052: it's a flaky upload, so retry with backoff. That's partly fair: Meta's own error text says "Please try reuploading your video," and a small share really are transient fetch timeouts on large files. But retrying an unchanged file that Meta already refused gives you the same code forever, and the failure isn't in your HTTP client. It's in the media.
Meta doesn't accept your bytes and store them. It fetches the URL server-side, runs the file through its own transcoder, and 2207052 is what comes back when that transcoder bails. The publish endpoint has no idea why it bailed, so it hands you a generic string. The Make community thread titled "Media upload has failed with error code 2207052 (Instagram reels)" is full of people trading retry advice for this reason: the error names a symptom, and the cause sits in the file's stream metadata.
The fix is to treat the video as a spec you enforce, not an input you accept. One normalize pass before the container call turns 2207052 into a class of error you stop seeing.
Poll the container status before you touch anything else
The publish error is the least informative thing Instagram will tell you. The container status is more useful, and most pipelines never read it. After POST /{ig-user-id}/media, poll the container ID directly:
curl -s "https://graph.facebook.com/v21.0/${CONTAINER_ID}?fields=status_code,status&access_token=${TOKEN}"
You'll get back:
{
"status_code": "ERROR",
"status": "Error: 2207052, Media upload has failed. Please try reuploading your video.",
"id": "17900000000000000"
}
status_code moves through IN_PROGRESS, then lands on FINISHED or ERROR. Only call /media_publish after you see FINISHED. Publishing immediately after container creation throws errors that look like media failures but are really race conditions. Containers expire 24 hours after creation, so a workflow retrying a day-old creation ID gets a failure that has nothing to do with the file. Poll every 3 to 5 seconds and cap it at around 60 seconds for a short Reel.
Instagram Graph API video requirements, mapped to the ffprobe check
Meta publishes the Reels spec but not a way to test against it, so the help-center pages for this error list numbers and stop. Here's each requirement next to the field that proves it, so you can check a file in one command.
| Requirement | Instagram's limit | Where it shows up in ffprobe |
|---|---|---|
| Container | MP4 or MOV, moov atom at the front | `format_name`, plus faststart |
| Video codec | H.264 High profile or HEVC, progressive, closed GOP | `codec_name`, `field_order` |
| Chroma | 4:2:0 | `pix_fmt` (want `yuv420p`) |
| Video bitrate | 25 Mbps maximum | `stream.bit_rate` |
| Frame rate | 23 to 60 fps | `r_frame_rate` |
| Frame size | 1920 px max on the long edge; 1080x1920 avoids cropping | `width`, `height` |
| Color | SDR only | `color_transfer`, `color_primaries` |
| Audio codec | AAC, 48 kHz, mono or stereo | `codec_name`, `sample_rate`, `channels` |
| Audio bitrate | 128 kbps | audio `stream.bit_rate` |
| Duration | 3 seconds minimum | `format.duration` |
| File size | 1 GB maximum | `format.size` |
One command dumps everything you need:
ffprobe -v error -select_streams v:0 \
-show_entries stream=codec_name,width,height,r_frame_rate,pix_fmt,bit_rate,color_transfer,color_primaries \
-show_entries format=duration,size \
-of default=noprint_wrappers=1 input.mp4
Run it with -select_streams a:0 and stream=codec_name,sample_rate,channels,bit_rate for the audio side. A screen recording with no audio track at all is a frequent 2207052: Instagram wants an audio stream even on a silent Reel.
The two fields that catch people are color_transfer and the video bit_rate. An iPhone shooting in HDR writes arib-std-b67 (HLG) or smpte2084 there, and ProRes or high-bitrate HEVC straight off the phone routinely runs past 25 Mbps. Both play perfectly in QuickTime. Both get refused.
One normalize pass clears every item on that list
For an SDR source, one FFmpeg command puts the file inside every limit:
ffmpeg -i input.mp4 \
-vf "scale=1080:1920:force_original_aspect_ratio=decrease,\
pad=1080:1920:(ow-iw)/2:(oh-ih)/2:color=black,format=yuv420p,fps=30" \
-c:v libx264 -profile:v high -level 4.1 \
-b:v 8M -maxrate 10M -bufsize 16M \
-g 60 -keyint_min 60 -sc_threshold 0 \
-c:a aac -b:a 128k -ar 48000 -ac 2 \
-movflags +faststart \
reel.mp4
Every flag maps to a rejection. scale plus pad forces 1080x1920 without distorting a 16:9 source. format=yuv420p gives Meta the 4:2:0 chroma it wants instead of the 4:2:2 that a ProRes or 10-bit source carries. fps=30 rescues anything shot as a 12 or 15 fps timelapse, which falls under the 23 fps floor. -b:v 8M -maxrate 10M keeps you far under the 25 Mbps ceiling. -g 60 -keyint_min 60 -sc_threshold 0 produces a closed GOP with a keyframe every two seconds. -movflags +faststart moves the moov atom to the front so Meta's fetcher can read the header without pulling the whole file, the same flag that decides whether an MP4 plays in a browser at all.
If the probe showed color_transfer=arib-std-b67 or smpte2084, prepend a tonemap to that filter chain:
-vf "zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,\
tonemap=hable:desat=0,zscale=t=bt709:m=bt709:r=tv,format=yuv420p,\
scale=1080:1920:force_original_aspect_ratio=decrease,\
pad=1080:1920:(ow-iw)/2:(oh-ih)/2:color=black,fps=30"
Apply that branch only when the source is actually HDR. Running a tonemap on an already-SDR file crushes contrast, and zscale needs an FFmpeg build compiled with libzimg, so a plain apt binary fails with "No such filter: 'zscale'". Why iPhone footage washes out and which builds carry the filter is covered in FFmpeg HDR to SDR: stop iPhone uploads coming out gray.
Wiring the normalize into the n8n or Make branch that posts the Reel
Running that command inside an automation platform is where this gets awkward. n8n 2.0 disabled the Execute Command node by default, the distroless image ships without an FFmpeg binary, and Make and Zapier never had a shell. So the normalize step either lives on a box you maintain or it doesn't happen, and most teams discover this while debugging 2207052.
This is the step FFmpeg Micro exists for. Send the source URL and your target settings to the API, poll the job or take the webhook, and you get back an MP4 URL that already satisfies Instagram's spec, which you drop straight into video_url on the container call. No FFmpeg install, no zimg build, no server to run. The API docs cover the job shape and the parameters for each operation.
The branch order that works: fetch the source, normalize, create the container with the normalized URL, poll status_code until FINISHED, then publish. Three nodes become one HTTP call in the middle.
Pitfalls that still return 2207052 after a clean encode
A correctly encoded file can still fail if Meta can't fetch it. These burn the most time:
- The URL isn't a direct file. Meta's fetcher needs a raw video response, not an HTML viewer page. Google Drive share links, Dropbox preview links, and Notion-hosted files all fail here. Passing a real file URL instead of a Drive link fixes it.
- The signed URL expires mid-fetch. A 5-minute S3 presigned URL can die while Meta is still downloading a 400 MB file. Give it at least an hour.
- The response has the wrong content type. Serve
video/mp4, notapplication/octet-stream. - The clip is under 3 seconds. A trimmed hook that lands at 2.8 seconds gets refused with no mention of duration.
- The file has no audio stream. Add a silent one with
-f lavfi -i anullsrc=r=48000:cl=stereo -shortest. - You hit the posting cap. The Instagram Graph API allows 25 published posts per 24 hours per account, and a burst-scheduling workflow can trip it.
Meta's Reels ceiling has moved up to 15 minutes, but plenty of scheduler docs still list 90 seconds, the old cap. If you're normalizing anyway, trimming to 90 seconds removes one more variable.
When normalizing isn't the answer
Normalizing fixes format rejections, and 2207052 is not always a format rejection. If the same normalized file fails on one Instagram account and publishes on another, you're looking at an account or permissions problem: the account needs to be a Business or Creator account connected to a Facebook Page, and your app needs instagram_content_publish. No encode changes that.
Reels published through the API also can't use Instagram's licensed music catalog, can't be posted to a personal account, and can't be scheduled by Meta for later. If your job is really a template-driven render where a designer owns the layout, a template-editor service is closer to that shape than an encode pipeline is. The encode-normalize path fits when you already have finished videos from a workflow that need to pass a platform's ingest check, the same problem as YouTube Shorts settings that survive the re-encode with a different set of numbers.
FAQ
Why does error 2207052 happen on videos that play fine everywhere else?
Error 2207052 fires because Instagram's transcoder has a narrower spec than your media player. QuickTime and VLC happily play 10-bit HDR HEVC at 40 Mbps with 4:2:2 chroma. Instagram's ingest wants 8-bit SDR, 4:2:0, under 25 Mbps, AAC audio at 48 kHz. Playback success tells you nothing about ingest acceptance.
Can I just retry the upload when I get error 2207052?
Retrying an unchanged file almost never clears error 2207052, because the same transcoder applies the same rules to the same bytes. Retry is worth one attempt if the file is large and the origin is slow, since a fetch timeout produces the same code. If attempt two fails, probe the file rather than adding a third retry.
What aspect ratio does the Instagram Reels API actually require?
The Instagram Reels API accepts a wide range of aspect ratios but crops anything that isn't 9:16 to fit the Reels player. Encoding to 1080x1920 with pad for letterboxing or crop for a center cut means you control what gets cut instead of Meta deciding. A 1920x1080 landscape video will publish and then lose most of its frame.
Does the video_url have to be publicly accessible?
The video_url must be reachable by Meta's servers without authentication, because Instagram downloads the file rather than accepting an upload from you. Presigned URLs work as long as they stay valid through the fetch. Anything behind a login, a viewer page, or an IP allowlist returns error 2207052.
How do I tell a format failure from a permissions failure?
Poll GET /{container-id}?fields=status_code,status after creating the container. A format failure shows status_code: ERROR with 2207052 in the status string, while a permissions problem fails earlier at container creation with an OAuth error naming the missing scope. They fail at different steps, so the step that failed tells you which one you have.
If your workflow is already posting Reels and the only missing piece is a reliable normalize between the render and the container call, that's one API call away. Sign up free and run a clip through the spec above before Meta sees it.
About Javid Jamae
Founder & CEO at FFmpeg Micro
Javid is a software engineer, author, and entrepreneur with over 25 years of professional software development experience across enterprise, startup, and consulting environments. He founded FFmpeg Micro to make video processing accessible to developers through a simple, automation-first REST API.
You might also like

Check Your Encode Didn't Wreck the Video: VMAF Scores with FFmpeg
Run ffmpeg vmaf to score an encode before it ships: the input order everyone gets backwards, what 90 means for social, and a pass/fail gate for CI.

Fix "Output File Is Empty, Nothing Was Encoded" in FFmpeg
An ffmpeg output file empty result is usually a warning, not an error: FFmpeg exits 0 with nothing encoded. Every cause, plus the ffprobe guard to catch it.

MediaRecorder WebM Isn't Broken. Convert It to MP4 Server Side
Chrome's MediaRecorder omits duration and cues, so blobs report Infinity. Convert MediaRecorder WebM to MP4 server side and get a seekable, indexed file.
Ready to process videos at scale?
Start using FFmpeg Micro's simple API today. No infrastructure required.
Get Started Free