Back to Support
Troubleshooting

403 Forbidden on Transcode

Where a 403 in the upload and transcode flow comes from, and how to fix it

The FFmpeg Micro API does not answer a transcode request with a 403. If you see one, it came from Google Cloud Storage, on the signed link you upload to. The quickest way to tell is the response body: storage answers in XML, and the API answers in JSON.


Upload Content Type


A mismatched content type on the upload is what causes a 403 in this flow. When you ask for an upload link, you name the file's contentType. The link is signed for that exact type. If the Content-Type header on your upload is anything else, storage refuses it:


<Error>
  <Code>SignatureDoesNotMatch</Code>
  <Message>Access denied.</Message>
</Error>

Send the same value in both places:


# 1. Ask for the upload link, naming the content type
curl -X POST "https://api.ffmpeg-micro.com/v1/upload/presigned-url" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "filename": "video.mp4", "contentType": "video/mp4", "fileSize": 12345678 }'

# 2. Upload with the SAME content type
curl -X PUT "$UPLOAD_URL" \
  -H "Content-Type: video/mp4" \
  --data-binary @video.mp4

Tools that set the header for you are the usual culprit. An HTTP client that sends application/octet-stream or multipart/form-data by default will be refused. Set the header yourself, and send the file as the raw request body.



An expired link is a 400, not a 403. Storage answers with the code ExpiredToken.


  • An upload link works for 15 minutes. Ask for a new one with POST /v1/upload/presigned-url.
  • A download link works for 10 minutes. Ask for a new one with GET /v1/transcodes/:id/download. You can do this as often as you like.

  • Errors Mistaken for a 403


    These come from the API, in JSON, and each has its own status.


    400 "Placeholder bucket name": the input address uses a bucket name copied from an example, such as gs://your-bucket/video.mp4.


    400 "Invalid input": the input address names a bucket that is not your upload bucket.


    For both, use the fileUrl that POST /v1/upload/confirm returned, exactly as it came back. Do not build the address yourself.


    401: the API key is missing, mistyped or revoked. See Getting Started with Your API Key.


    402 Payment Required: the account has nothing left to process with. See How to Check Your Usage and Quota.


    Upload and Transcode Flow


    The whole sequence, with the file's address taken from the confirm response:


    # The file's real size in bytes. The API checks it.
    FILE_SIZE=$(wc -c < video.mp4 | tr -d ' ')
    
    # 1. Get an upload link
    UPLOAD_RESPONSE=$(curl -s -X POST "https://api.ffmpeg-micro.com/v1/upload/presigned-url" \
      -H "Authorization: Bearer YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d "{ \"filename\": \"video.mp4\", \"contentType\": \"video/mp4\", \"fileSize\": $FILE_SIZE }")
    
    UPLOAD_URL=$(echo $UPLOAD_RESPONSE | jq -r '.result.uploadUrl')
    FILENAME=$(echo $UPLOAD_RESPONSE | jq -r '.result.filename')
    
    # 2. Upload the file, with the same content type
    curl -X PUT "$UPLOAD_URL" \
      -H "Content-Type: video/mp4" \
      --data-binary @video.mp4
    
    # 3. Confirm the upload and keep the address it returns
    FILE_URL=$(curl -s -X POST "https://api.ffmpeg-micro.com/v1/upload/confirm" \
      -H "Authorization: Bearer YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d "{ \"filename\": \"$FILENAME\", \"fileSize\": $FILE_SIZE }" | jq -r '.result.fileUrl')
    
    # 4. Create the transcode job with that address
    curl -X POST "https://api.ffmpeg-micro.com/v1/transcodes" \
      -H "Authorization: Bearer YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d "{ \"inputs\": [{ \"url\": \"$FILE_URL\" }], \"outputFormat\": \"mp4\", \"preset\": { \"quality\": \"medium\" } }"
    

    Support


    If a 403 does not fit the case above, write to javid@ffmpeg-micro.com with:


  • the exact response body, XML or JSON
  • which request returned it: the upload, the transcode request, or the download
  • the job ID, if there is one

  • Do not send your API key.