Skip to main content
Mints an upload handle and a URL you PUT a file to. It costs nothing, spends nothing and writes no run row. One thing in the catalog takes a file today: the starting image a video endpoint animates.

Why the handle exists

Tool arguments pass through a model’s context window exactly like tool results do. Sending a photograph as image_base64 therefore means the agent emits the whole file as tool-call arguments, and a 3 MB image is 4 MB of base64, on the order of a million tokens. What happens next is not theoretical: the payload is truncated, the run fails with something that reads like file corruption, and the agent downscales the user’s photo and tries again with a smaller one. A handle is about thirty characters. The bytes go from you straight to storage, so they never pass through the model and never pass through Goro’s API either.

Two roads, and how to pick

The response carries two ways to get the bytes in. They write the same object to the same path and produce the same handle, so nothing downstream can tell them apart. Which one is right depends on one question: can you actually reach the file? If you are an agent without a shell, send the user upload_page_url and wait for them to say it is done. Do not fall back to image_base64: a large inline payload is the failure this whole mechanism exists to prevent, and some clients refuse to render a tool call that size without reporting an error at all.

The three steps

1. Ask for somewhere to put the file

The type you declare decides the object’s extension and the type the store will serve it as. It is a claim rather than evidence, and it is treated as one: the file’s own leading bytes are read when the handle is redeemed, and a pair that disagrees is refused.
Values are illustrative. Treat upload_url as opaque: it addresses the storage provider rather than api.usegoro.ai, and it carries its own credential in the query string. Both upload_page_url and upload_url are capability URLs. Anyone holding one can write that one file to that one path until it expires, so treat them the way you would treat any signed link: hand them to the person they were minted for and nobody else.

2. Either PUT the file, or send the user the page

If you have a shell and the file is on this machine:
Send no Authorization header with it. The URL is the credential, it is good for one write to one path, and it points at the private bucket directly. That is the whole point of the two requests: the file never enters a Goro serverless function, never meets a request body limit, and never enters the model. Otherwise, give the user upload_page_url:
Open this link and choose the photo: https://upload.usegoro.ai/… Tell me when it’s uploaded.
They get a one-field page that checks the file is really the type you asked for and sends it from their browser straight to the same bucket. Nothing is installed, nothing is signed into, and the bytes never travel through you. Wait for them to confirm before step 3: the handle does not resolve until the file has actually landed.
The page checks the file’s leading bytes before sending, so a user who picks an iPhone HEIC when you asked for image/jpeg is told immediately, on the page, instead of failing minutes later inside a paid run.

3. Run the tool with the handle

A handle is upl_, then 22 base64url characters, then .jpg, .png or .webp. Nothing else parses, because the handle becomes part of a storage path. It carries the object id and the extension and nothing else: the workspace comes from the key you authenticated with, so another workspace’s handle looks in your own prefix for an object that is not there.

Three ways to send a starting image

Exactly one of these per call. Sending two is refused, because picking a winner would mean animating an image you did not mean and charging you for it. image_base64 is not going away, but “I have no shell” stopped being a reason to use it: that is what upload_page_url is for. It remains the right answer for a small file when there is no user to ask either. It is the wrong answer for a photograph, and the failure it produces is silent enough to be worth naming twice: a truncated base64 string decodes to fewer, wrong bytes rather than to an error, and some clients will not even render a tool call that large, which looks like the request vanishing.

Why the ceilings differ

The handle path and the link path both stop at 10 MB, which is kie.ai’s own per-image limit, so Goro rejects exactly what the model would reject and nothing more. On the upload path that limit is also declared on the bucket, so an oversized file is refused by the storage layer before a byte of it is stored. The inline path stops at 3 MB for a reason that has nothing to do with images. Base64 inflates by exactly 4/3, so 3 MB of image is 4 MB of request body, and the serverless request body limit behind this API is 4.5 MB. That ceiling is a property of the transport and cannot be raised. An upload does not go through that transport at all, which is the second reason it exists.

The two clocks

The page clock extends nothing. Retention is measured from the moment the bytes land, so a link opened at minute 59 produces a file deleted 30 minutes after that, exactly like one opened immediately. What the hour buys is the gap between an agent posting a link and a person picking up their phone. The second number did not change when uploads were added. The ten minutes are subtracted from the thirty rather than added to them: the signed link handed to the generative provider is worth the twenty that remain, so a link can never outlive the object it points at, and no image lives past thirty minutes. The file is deleted by a scheduled sweep that reads the bucket by age every ten minutes, which is the same job, the same bucket and the same clock the inline intake already used. The practical rule is one line: get the handle immediately before the run, not in advance. A handle from an earlier session is expected to be gone.

What is stored, and what is not

Nothing about the upload path is more exposed than the inline one, and in one respect it is less: the bytes reach storage without passing through our API.
  • The bucket is private. There is no public read and no unsigned address that works.
  • The object’s name is random and sits under a path scoped to your workspace. Nothing about it is derivable from the run record, deliberately, which is why the deletion sweep reads the bucket rather than working backwards from a run.
  • The only link that exists is signed and time limited, and it is handed to the generative provider, not published.
  • An unredeemed handle leaves nothing behind. The object is swept at thirty minutes like every other one.
The Privacy Policy section 4.16 is the authoritative version of this, thirty minutes included. That number is a published claim rather than an implementation detail, which is why the upload path was fitted inside it.

Errors

Minting a handle can fail in two ways. Everything else surfaces on the run that redeems the handle, and all of it is checked before the hold is placed, so none of it charges you.
A handle that does not resolve is settled before the run is quoted, so a bad handle costs nothing: no hold, no run row, nothing to release.

From MCP

The MCP server exposes the same endpoint as create_upload. It takes content_type and returns the same handle, the same page link and the same upload URL, with the exact curl to run next. It needs the tools:read scope rather than tools:run, because it spends nothing. The instruction for an agent is short enough to repeat: never read a file into the conversation in order to send it. Call create_upload. If you have a shell and the file is on that machine, PUT it. Otherwise give the user upload_page_url and wait for them. Either way, pass the handle to run_tool. Do not resize the image to make it fit, because fitting is not the problem the size was causing.

From the CLI

goro run takes --image and does all three steps for you.
The CLI reads the file’s leading bytes to work out what it really is, asks for an upload URL for that type, PUTs the bytes and drops the handle into the input. You never spell image_handle, and a .jpg that is really a PNG still works. See goro run.

Spend it on a run

What a run costs, what it returns, and how the hold settles.