How to upload a file to an S3 bucket from a URL in Python
Stream the URL's response straight into boto3's upload_fileobj, with no temp file. For a generated video, copy on the webhook and check the bytes.

To upload a file to an S3 bucket from a URL, download it in your own code and stream the response body straight into the upload, so nothing lands on disk. In Python, open the URL with requests.get(url, stream=True) and pass its raw stream to boto3's upload_fileobj, a managed transfer that switches to a multipart upload when needed. For an AI-generated video, copy from the artifact's media.sume.com URL when the run's webhook arrives, before you mark the record ready.
AWS facts come from the boto3 Uploading files guide and upload_fileobj reference and the Amazon S3 Uploading objects page; Requests facts from its Quickstart and Advanced usage pages; Sume facts from Runs and results, Embed a Format, and the Sume API reference. All were read on 2026-09-27. Sume has no S3 connector: your code makes the copy.
How do I stream a file from a URL into S3 with Python?
With stream=True, Requests downloads only the headers up front and leaves the body to be read, and Response.raw hands over the bytes exactly as sent, untransformed. upload_fileobj takes a file-like object that, at a minimum, implements read and returns bytes, so a small wrapper can count and hash the bytes as boto3 pulls them. ContentType is one of the allowed ExtraArgs; when you have no type to pass, the code falls back to the response's own Content-Type header. The with block closes the connection even when the upload fails.
S3 takes up to 5 GB in a single PUT and up to 50 TB through a multipart upload.
import hashlib
import boto3
import requests
s3 = boto3.client("s3")
class HashingReader:
"""Counts and hashes the bytes as boto3 reads them."""
def __init__(self, raw):
self.raw, self.sha256, self.size = raw, hashlib.sha256(), 0
def read(self, *args):
chunk = self.raw.read(*args)
self.sha256.update(chunk)
self.size += len(chunk)
return chunk
def copy_url_to_s3(url, bucket, key, content_type=None):
with requests.get(url, stream=True, timeout=30) as resp:
resp.raise_for_status()
reader = HashingReader(resp.raw)
ctype = content_type or resp.headers.get("content-type", "application/octet-stream")
s3.upload_fileobj(reader, bucket, key, ExtraArgs={"ContentType": ctype})
return reader.size, reader.sha256.hexdigest()Which URL and fields should I copy for a generated video?
A finished Sume Format run lists every file it made in artifacts[], and primary_output_url names the one to show. These URLs are durable media.sume.com URLs that do not expire and are public to anyone holding them, so no API key is needed to read them; copy the files when your product needs per-customer access control. Store the Sume URL, never a raw provider URL. A job from POST /v1/videos is different: its unsigned_urls point at an API route that needs your key, which Download a generated video from the API covers.
| Artifact field | Use it for |
|---|---|
url | The source URL to stream from |
content_type | The object's ContentType in S3; nullable |
size_bytes | Compare with the bytes you copied, when it is set; nullable |
checksum_sha256 | Nullable, so don't count on it; keep your own digest |
id | Part of your S3 key, so a repeated copy overwrites the same object |
When should the copy run?
On the webhook, before you mark the record ready: Sume's integration cookbook gives that order for anyone who wants copies that would survive leaving Sume. The webhook itself must get a 2xx within 10 seconds, so record the event durably, answer, and then run the copy in a background job that is not bound by that window.
- Retries repeat the delivery's
request_id, up to 10 attempts, so dedupe on it before starting a copy. - Build the S3 key from ids, such as
videos/{run_id}/{artifact_id}.mp4, so a copy that runs twice writes the same object. - Do Sume video URLs expire? covers whether to copy at all or just store the link.
How do I know the copy is complete?
Check what the wrapper counted before you flip the record to ready:
- Compare the byte count with the artifact's
size_byteswhen the receipt sets it. - Store the SHA-256 digest with the record. The API reference marks
checksum_sha256nullable, so a receipt may not carry one; your own digest, taken from the exact bytes you uploaded, is the one to keep. If an artifact does carry a checksum, compare the two. - On a mismatch or an exception, leave the record not ready and run the copy again. The source URL does not expire, so a later retry reads the same file.
Sources
- Runs and results
- Embed a Format in your product
- Media inputs
- Video generation
- API reference
- Sume API reference
- Boto3: Uploading files (read 2026-09-27)
- Boto3: S3.Client.upload_fileobj (read 2026-09-27)
- Boto3: S3 customization reference (read 2026-09-27)
- Amazon S3: Uploading objects (read 2026-09-27)
- Requests: Quickstart (read 2026-09-27)
- Requests: Advanced usage (read 2026-09-27)
Related posts
More in Integrations
- Vercel AI SDK: generate video with a Sume API tool call
Generate video from the Vercel AI SDK with a tool() that calls Sume's POST /v1/videos on your server, returns the job id, and polls for the clip.
- Vercel Cron Jobs: call the Sume API daily without duplicates
A Vercel cron job sends a GET to your route, which calls the Sume API with a date-based Idempotency-Key, so a duplicate invocation cannot bill twice.
- Vercel function timeout on video generation: use a webhook
A Vercel Function stops at 300 seconds by default; a Sume video run takes minutes. Submit with a webhook_url, return, then verify the signed POST.
- Visual Studio MCP server: add Sume's hosted MCP in .mcp.json
Add Sume's hosted MCP server to Copilot agent mode in Visual Studio with one .mcp.json entry, sign in through CodeLens, and confirm tool calls.
Written by Sume