You call Sora or Veo, wait for the job to finish, and get back a URL. It plays. The clip looks done. It is not done — it’s sitting in the model provider’s temporary output bucket, on a clock that started the moment the job completed. Nobody tells you this loudly, because it’s usually one line in the docs, easy to skim past the first time you’re just excited the video generated at all.
Then a few hours later someone re-opens the job, or an agent tries to embed the clip in a page it built yesterday, and the URL 404s. The video isn’t lost — it was never actually stored anywhere that intended to keep it.
Sora, Veo, and fal.ai all return a download URL for generated video, and
none of them are meant to be a permanent home for the file. OpenAI’s Sora
documentation tells you outright to copy the output to your own storage —
the download URL expires roughly an hour after generation. Google’s Veo,
via Vertex AI, auto-deletes generated output after about two days. fal.ai
returns fal.media URLs with retention that isn’t publicly documented at
all. If nothing moves the bytes to durable, streamable storage inside that
window, the clip is gone — not archived, not recoverable, gone.
Three models, three clocks, one pattern
The specifics differ, but the shape of the problem is identical across providers: the URL you get back is a pointer into infrastructure built for handing off a finished job, not for serving video long-term.
- Sora (OpenAI). The download URL is short-lived — on the order of an hour — and the documentation explicitly instructs you to copy the file to your own storage before it expires. This isn’t a bug you can route around; it’s the documented contract.
- Veo (Google, via Vertex AI). Generated output persists in Google’s storage for about two days, then it’s auto-deleted. Two days sounds generous until you consider a queue, a review step, or a retry loop — any of which can eat that window without anyone noticing.
- fal.ai. Output comes back as a
fal.mediaURL. There’s no published retention policy, which is arguably worse than a documented one-hour window: you don’t know the deadline, so you can’t safely wait to find out.
None of these providers are doing anything wrong. Generation infrastructure and delivery infrastructure are different jobs, and conflating them would make the generation API slower and more expensive for everyone. But it does mean the output URL a model hands you is a receipt, not a home.
What actually breaks when nobody copies the file
The failure mode is quiet, which is what makes it common. Nothing errors at generation time — the job succeeds, the URL works, the demo looks great. The break happens downstream, usually in one of three ways:
- The clip vanishes from a pipeline that assumed it was permanent. A job queue, a review dashboard, or a “regenerate thumbnail” retry that runs hours later finds a dead link where a video used to be.
- It’s never embeddable in the first place. Ephemeral provider URLs
frequently don’t support range requests or CORS the way a real HLS player
expects, so even inside the expiry window, dropping the raw URL into a
<video>tag on your own site is unreliable. - Nobody owns the copy. If the only reference to a generated clip is the model provider’s URL, there’s no video id, no thumbnail, no place to attach captions or analytics — it’s not really an asset in your product, it’s a link that happens to still resolve.
A generated video that only exists at a model provider’s temporary URL isn’t an asset yet — it’s a countdown.
The fix: import the URL before it expires, not after
The mechanism that solves this is the same one that solves migrating an existing catalog off S3: server-side import by URL. Instead of downloading the model’s output to a client and re-uploading it somewhere durable — burning bandwidth twice and adding a step that can fail — you hand the provider’s URL straight to rehelios and let it do the fetch:
npx @rehelios/cli import https://sora.chatgpt.com/download/abc123 \
--collection col_generated \
--wait \
--json
Or call the REST endpoint directly, right after the generation job returns:
const res = await fetch("https://api.rehelios.com/v1/videos/import", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.REHELIOS_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
url: soraOutputUrl, // or the Veo / fal.media URL — same call
collectionId: "col_generated",
}),
});
const { id, playbackUrl } = await res.json();
rehelios fetches the file from that URL server-side — no client re-upload —
transcodes it to adaptive HLS, and stores it behind the Cloudflare edge.
GET /v1/videos/{id} returns the metadata once it’s ready, including the
playbackUrl your player reads, and a video.ready webhook (HMAC-signed)
fires so a pipeline doesn’t have to poll. The whole thing is the same
POST /v1/videos/import call used for migrating an S3 video library
— importing generated output before it expires and importing existing files
you already own are the same operation, just on a tighter clock. Encoding is
free, so importing every clip a model produces costs storage and delivery
only, never a per-encode line item.
The practical rule: call import in the same job that receives the generation result, before anything else touches the URL. Don’t queue it, don’t wait for a review step, don’t let it sit in a “process later” bucket. An hour is not a lot of runway once you account for retries and cold starts.
Why this matters more for agents than for a script you write once
A human developer testing Sora manually will probably remember to save the good clips. An agent generating video autonomously — as part of a content pipeline, a course-builder, a video-ad generator — has no such instinct unless it’s built in. That’s the actual stakes here: as more of this generation happens inside agent loops instead of a person clicking “download,” the ephemeral-URL problem stops being an edge case and becomes the default failure mode of the whole pipeline.
This is why the import step is exposed as an MCP tool, not just a REST
endpoint. An agent running inside Claude Desktop, Claude Code, or Cursor can
call rehelios_import with the model’s output URL the moment generation
finishes — one tool call, no human in the loop, no separate download-then-upload
step for the agent to forget. The agent that generated the clip is the same
agent that puts it live, durably, before the provider’s clock runs out. See
the MCP server for the full tool list, or
AI-generated video for how this fits into a
generation pipeline end to end — and give an agent upload and hosting
tools for the broader case
of agent-driven video without a human gate.
Where this leaves you
If you’re building anything on top of Sora, Veo, or fal.ai, treat the output
URL as a receipt with an expiration date, not a video host. The fix is one
API call — server-side import, right after generation, before the URL goes
cold — and it turns a temporary link into a permanent, edge-delivered
playbackUrl with none of the bandwidth cost of downloading and re-uploading
the file yourself. For an agent, that call is the difference between
generating a video and actually shipping one.