How to convert MOV to MP4 with the Shotstack Ingest API in Python

Contents
Contents

Converting a MOV file to MP4 through an API takes a single request to the Shotstack Ingest API with an mp4 rendition specified. Shotstack pulls the file from a URL or accepts a direct upload, encodes the MP4, and hosts the result. After sending the request, you check the status until the file finishes processing.

We tested the API with an iPhone-style HEVC clip, a portrait phone clip, an H.264 MOV, a browser WebM, and an MKV screen recording, then inspected each resulting MP4. Every file finished in a few seconds as a standard H.264 MP4 with AAC audio.

TL;DR

  • Send POST /sources (for a URL) or POST /upload (for a local file) with outputs.renditions set to [{"format": "mp4"}], and poll GET /sources/{id} until the rendition status reaches ready.
  • Source files in HEVC and H.264 MOV, WebM, and MKV all converted to H.264 High, 8-bit 4:2:0 video with AAC audio at the original dimensions.
  • Every 10 second, 720p clip encoded in less than 4 seconds.
  • A portrait phone clip converted upright at 720 by 1280 without any extra configuration.
  • Include resolution, size, fps, or quality in the rendition settings whenever you want to modify the output file.

Why you need to convert MOV to MP4

MOV is the QuickTime container format from Apple. Starting with iOS 11, iPhones can record video as HEVC using the “High Efficiency” setting under Settings > Camera > Formats, whereas “Most Compatible” records H.264, as noted in Apple’s documentation on HEIF and HEVC media.

Playback problems rarely come from the container itself. The codec inside it is the issue. Browser data on Can I use for September 2026 reports full HEVC support for 19.99% of global users, alongside partial support for another 74.95%.

By contrast, H.264 in an MP4 container has full support for 97.49% of global users. This makes H.264 MP4 the most reliable target when your video needs to play everywhere. Our FFmpeg codecs guide explains the codec details in greater depth.

How file conversion works in the Ingest API

The Ingest API handles files in two parts. A source is the original file that Shotstack stores. A rendition is a derived file generated from that source, using your chosen format and any optional transformations.

You can submit files in two ways:

  • From a URL. Send POST /sources with the file url and an outputs configuration. Shotstack fetches the file directly.
  • From your machine. Send POST /upload with the outputs configuration to get a signed URL. You upload the local file to that address with an HTTP PUT request, and Shotstack starts the rendition once the upload arrives.

Both endpoints return a source id. Calling GET /sources/{id} gives you the source status (queued, importing, ready, failed) along with the status for each rendition (waiting, queued, processing, ready, failed). See the renditions documentation for all supported transformations, including size, frame rate, and quality.

To see the Ingest API at work without writing code, try the MP4 to MOV converter. It runs on the same API in the opposite direction, and its source code is on GitHub.

Python conversion script

This script converts either a local file or a URL to MP4. When run without arguments, it converts the Big Buck Bunny trailer, an open movie from the Blender Foundation licensed under CC BY 3.0 and published as a MOV file. It relies solely on the Python standard library.

Set your SHOTSTACK_API_KEY environment variable first, which you can obtain by registering a free account. Every conversion consumes credits, as detailed on the Shotstack pricing page.

"""Convert a MOV (or WebM, MKV) file to MP4 with the Shotstack Ingest API.

Usage: SHOTSTACK_API_KEY=... python mov_to_mp4.py [path-or-url]
  a local path is uploaded through a signed URL, a URL is fetched by Shotstack,
  no argument converts the Big Buck Bunny trailer (Blender Foundation, CC BY 3.0)
"""

import json
import os
import sys
import time
import urllib.error
import urllib.parse
import urllib.request

API_KEY = os.environ.get("SHOTSTACK_API_KEY") or sys.exit(
    "Set SHOTSTACK_API_KEY first."
)
INGEST = "https://api.shotstack.io/ingest/v1"
EDIT = "https://api.shotstack.io/edit/v1"
SAMPLE = "https://download.blender.org/peach/trailer/trailer_720p.mov"
OUTPUTS = {"renditions": [{"format": "mp4"}]}


def call(method, url, body=None):
    data = json.dumps(body).encode() if body else None
    req = urllib.request.Request(
        url,
        data=data,
        method=method,
        headers={
            "x-api-key": API_KEY,
            "Content-Type": "application/json",
            "Accept": "application/json",
        },
    )
    try:
        with urllib.request.urlopen(req, timeout=30) as r:
            return json.load(r)
    except urllib.error.HTTPError as e:
        sys.exit(f"HTTP {e.code}: {e.read().decode()[:400]}")


src = sys.argv[1] if len(sys.argv) > 1 else SAMPLE
if src.startswith(("http://", "https://")):
    body = {"url": src, "outputs": OUTPUTS}
    source_id = call("POST", f"{INGEST}/sources", body)["data"]["id"]
else:  # local file: ask for a signed URL, then PUT the bytes to it
    upload = call("POST", f"{INGEST}/upload", {"outputs": OUTPUTS})["data"]
    source_id = upload["id"]
    with open(src, "rb") as f:
        put = urllib.request.Request(
            upload["attributes"]["url"], data=f.read(), method="PUT"
        )
    urllib.request.urlopen(put, timeout=600).close()
print(f"source {source_id}")

last = None
while True:
    source = call("GET", f"{INGEST}/sources/{source_id}")["data"]["attributes"]
    rendition = ((source.get("outputs") or {}).get("renditions") or [{}])[0]
    status = (source["status"], rendition.get("status"))
    if status != last:
        print(f"  source {status[0]:<10} mp4 {status[1]}")
        last = status
    if "failed" in status:  # stop on a failed source or rendition
        sys.exit(f"conversion failed: {source.get('error') or rendition.get('error')}")
    if status == ("ready", "ready"):
        break
    time.sleep(3)

print(f"mp4 ready: {rendition['width']}x{rendition['height']}, {rendition['fps']} fps")
print(rendition["url"])

# Read the streams back with the Edit API's probe endpoint (ffprobe, no download).
probe = call("GET", f"{EDIT}/probe/" + urllib.parse.quote(rendition["url"], safe=""))
for s in probe["response"]["metadata"]["streams"]:
    if s["codec_type"] not in ("video", "audio"):
        continue
    detail = s.get("pix_fmt") or f"{s.get('sample_rate')} Hz, {s.get('channels')} ch"
    print(f"  {s['codec_type']}: {s['codec_name']} {s.get('profile', '')}, {detail}")

Three details in the script are worth keeping in your own code:

  1. A single minimal rendition. Passing {"format": "mp4"} is all you need for a direct format conversion. Only include resolution, size, fps, or quality when you want to alter the output.
  2. Monitoring both statuses. Because the source must finish importing before rendition encoding begins, the polling loop prints every status change and stops if either one reports failed.
  3. Inspecting the finished MP4. The probe endpoint in the Edit API runs ffprobe against the hosted output and returns its stream details, letting you verify the codec, profile, and pixel format without downloading the file. Our guide to inspecting media metadata explains the rest of that response.

Script output

Below are the results from two runs: the default sample fetched from a URL, and a local iPhone-style HEVC clip that carries a portrait rotation flag.

source zzz01m64-ayay0-xcme6-a14s1-s3qs6c
  source importing  mp4 waiting
  source ready      mp4 processing
  source ready      mp4 ready
mp4 ready: 1280x720, 25 fps
https://shotstack-ingest-api-v1-renditions.s3.ap-southeast-2.amazonaws.com/35tqpmb0ya/zzz01m64-ayay0-xcme6-a14s1-s3qs6c/zzz01m64-ayb04-rqk26-866m0-d4f8r4.mp4
  video: h264 High, yuv420p
  audio: aac LC, 48000 Hz, 6 ch
source zzz01m64-ayvr5-4fmep-zz1f0-bsbm9w
  source importing  mp4 waiting
  source ready      mp4 processing
  source ready      mp4 ready
mp4 ready: 720x1280, 24 fps
https://shotstack-ingest-api-v1-renditions.s3.ap-southeast-2.amazonaws.com/35tqpmb0ya/zzz01m64-ayvr5-4fmep-zz1f0-bsbm9w/zzz01m64-ayvs1-2j568-303c6-knbwer.mp4
  video: h264 High, yuv420p
  audio: aac LC, 48000 Hz, 2 ch

Both output MP4s use H.264 High with 8-bit 4:2:0 color, which is the profile expected by web browsers and phones. The trailer preserved its 5.1 surround audio as six AAC channels. The portrait clip came out upright at 720 by 1280.

Test results across five source files

We extracted the same 10 second, 1280 by 720, 24 fps section from the Sintel trailer (Blender Foundation, CC BY 3.0) into the formats developers most frequently need to convert. We uploaded each file with an mp4 rendition and probed the output. Encode time reflects the rendition’s reported executionTime.

Source fileInputMP4 videoMP4 audioMP4 sizeEncode time
iPhone-style MOV: HEVC, AAC1.24 MBH.264 High, 4:2:0AAC stereo1.93 MB3.8 s
Same MOV, portrait rotation flag1.24 MBH.264 High, 4:2:0, 720 x 1280AAC stereo1.95 MB3.8 s
Camera MOV: H.264, AAC2.92 MBH.264 High, 4:2:0AAC stereo2.09 MB3.6 s
WebM from a browser: VP9, Opus2.19 MBH.264 High, 4:2:0AAC stereo2.26 MB3.4 s
MKV screen recording: H.264, AAC2.35 MBH.264 High, 4:2:0AAC stereo2.07 MB3.2 s

Key takeaways from the table:

  • Consistent output specs. The HEVC, VP9, and H.264 sources all converted into H.264 High MP4 files, while both AAC and Opus audio tracks became AAC at 48 kHz and roughly 128 kbps.
  • Original dimensions are preserved. When resolution and size are omitted, every MP4 retains the source’s 1280 by 720 dimensions, rotated upright for the portrait clip.
  • Converting HEVC to H.264 trades file size for reach. Because H.264 requires more bits than HEVC for the same visual quality, the HEVC clip increased from 1.24 MB to 1.93 MB. The tradeoff is playback for the 97% of users whose browsers fully support H.264.
  • Most of the wait is the upload. Every MP4 finished between 8 and 16 seconds after its upload completed, which includes the import step and a 3 to 4 second encode.

Adjust dimensions, frame rate, or quality during conversion

You can also reshape the video within the same conversion request using these documented rendition options:

  • resolution accepts a preset: preview (512 by 288), mobile (640 by 360), sd (1024 by 576), hd (1280 by 720), or fhd (1920 by 1080).
  • size defines a custom width and height, while fit controls whether to crop, stretch, or fit the whole frame when the aspect ratio changes.
  • fps sets the output frame rate, supporting values from 12 to 60.
  • quality balances file size against sharpness on a scale of 1 to 100. The reference recommends 50 to 75.

The rendition below converts a MOV file into a 720p MP4 at 30 fps, with the quality set to 60:

{
  "format": "mp4",
  "resolution": "hd",
  "fps": 30,
  "quality": 60
}

Running this on the Big Buck Bunny trailer produced a 7.47 MB file, compared to 5.96 MB for a plain mp4 rendition at the original 25 fps. Omitting quality gives an encode that the API reference describes as optimized for speed, size, and quality, so test a fixed value on your own footage before relying on it.

If you also need to crop footage for social formats, our tutorial on how to resize and crop videos with the Edit and Ingest API uses this same rendition block.

Automate converting phone and browser videos to web-ready MP4 files

Across our tests, the Ingest API converted iPhone, camera, browser, and screen-recording files into standard H.264 MP4s in a few seconds, kept their dimensions and orientation, and hosted the output. The workflow takes one request per file, a status check, and a probe to confirm the output specs.

Sign up for a free API key and test the script on your own footage. Because the converted MP4 is immediately usable as a video asset in the Edit API, a user upload can go straight into a rendered video.

Frequently asked questions (FAQs)

Can the Ingest API convert files to formats other than MP4?

Yes. For video files, the rendition format also supports webm, mov, avi, mkv, ogv, wmv, avif, and gif, along with mp3 and wav for audio and jpg, png, webp, and tif for images. The API reference notes that each file is encoded to the same media type, so a video source converts into another video.

Can you generate multiple sizes from one upload?

Yes. Include one rendition per size in the renditions array, such as one hd and one mobile. Assign each entry a filename so you can identify outputs by name rather than by id. Shotstack adds the file extension automatically.

How do you fix audio drifting out of sync on phone recordings?

Phones frequently record at a variable frame rate, which can cause audio and video tracks to drift apart. Set fixOffset: true on the rendition. The API reference describes this setting as an attempt to fix audio and video sync issues caused by variable frame rate recording.

Should you poll for status or use a webhook?

Polling works well for a standalone script. For a production pipeline, pass a callback URL in the request so Shotstack sends a status notification when the source arrives and when each rendition finishes processing. Read the polling versus webhooks guide for a comparison of both approaches.

Get started with Shotstack's video editing API in two steps:

  1. Sign up for free to get your API key.
  2. Send an API request to create your video:
    curl --request POST 'https://api.shotstack.io/v1/render' \
    --header 'x-api-key: YOUR_API_KEY' \
    --data-raw '{
      "timeline": {
        "tracks": [
          {
            "clips": [
              {
                "asset": {
                  "type": "video",
                  "src": "https://shotstack-assets.s3.amazonaws.com/footage/beach-overhead.mp4"
                },
                "start": 0,
                "length": "auto"
              }
            ]
          }
        ]
      },
      "output": {
        "format": "mp4",
        "size": {
          "width": 1280,
          "height": 720
        }
      }
    }'
Peace Aisosa

BY PEACE AISOSA

Published October 29, 2026

Peace Aisosa is a Customer Success Specialist at Shotstack. She writes about video automation when she's not troubleshooting customers' video pipelines. More from Peace

Studio Real Estate
Experience Shotstack for yourself.
SIGN UP FOR FREE