Upload media in a session
POST /api/developer/uploads/ — declare a game's media, upload each file, then import or release with the session.
A game's media doesn't have to travel inside the ZIP. Declare it in an upload session, PUT each file to storage, then import or release with the session. This is how a game gets past the 10 MiB ZIP limit.
POST /api/developer/uploads/
Content-Type: application/json{
"project_id": "6f1d2c8e-...",
"files": [
{ "path": "assets/hero.png", "size": 48213, "md5": "0b4e7a0e5fe84ad35fb5f95b9ceeac79" },
{ "path": "assets/boom.wav", "size": 5, "md5": "5d41402abc4b2a76b9719d911017c592" }
]
}| Field | Details |
|---|---|
files | Required, may be empty, at most 2,000 entries. Each path is a media path under assets/ in canonical form, size is the byte length, and md5 is 32 lowercase hex digits. |
project_id | Optional. Send it with Authorization: Bearer <agent_token> for an update session. Leave it out for a new game: with a login token (games:write) the session is that account's, otherwise anonymous. |
Anonymous sessions are rate limited: over the limit the answer is HTTP 429 with Retry-After.
Response
HTTP 201:
{
"upload_id": "9a4b2f0e-...",
"upload_token": "<upload_token>",
"expires_at": "2026-10-28T18:30:00Z",
"linked": ["assets/hero.png"],
"uploads": [
{
"path": "assets/boom.wav",
"url": "<signed upload URL>",
"method": "PUT",
"headers": {
"Content-Type": "audio/wav",
"Content-MD5": "XUFAKrxLKna5cZ2REBfFkg==",
"x-goog-content-length-range": "5,5"
}
}
]
}linkedlists declared paths Rosebud already holds, with the same MD5 and size. They aren't uploaded again. An update session links the owner's files and the files the game already serves at the same path; a login session links the account's files; an anonymous session links nothing this way.- A linked path skips the media type and folder rules but must still be a safe, canonical name. Editor-only media such as an MP4 can only be kept this way, unchanged at the same path.
uploadshas one entry per path that isn't linked.PUTthe file's bytes tourlwith exactly the returnedheaders, all of them. Storage rejects bytes whose MD5 or length differs from the declaration.upload_tokenis shown once. Keep it private.- Each signed URL works for one hour, and the session for 24 hours (
expires_at).
While Rosebud is still checking files the game already serves, it can answer 409 upload_retry with a remaining count and Retry-After: 1. Repeat the identical request: remaining falls until the session is created. Stop if it stops falling.
Use the session
Send upload_id and upload_token as two more multipart fields, both or neither, with an import or a release.
- A release is a full snapshot: the game becomes the ZIP's files plus every path the session declared, linked or uploaded. Anything else is removed, so declare all of the game's media, not only the changes.
- A path in both the ZIP and the session is
duplicate_path. The ZIP and the session count together toward the file-count and total-size limits. - New (not linked) media in one release is at most 128 MiB (
413 release_upload_limit, with the exactlimit). Publish part of it, then release the rest; later sessions declare the earlier media again and link it. - An update session releases only its game. A new-game session can't update a game, and is spent by the import that succeeds with it. A login session works only for that login's account.
- Retries use the release's
operation_id. After the session expires, create a new one that declares the same paths and MD5s and retry with the sameoperation_id; its files needn't be uploaded.
Errors
| Code | Status | When |
|---|---|---|
upload_retry | 409 | Rosebud is still checking the game's existing files. Repeat the identical request. |
invalid_upload | 404 | No session has this upload_id with this token. |
upload_expired | 410 | The session expired, or a new-game session was already used. |
upload_mismatch | 403 | The session was made for another game, another account, or the other of import and update. |
upload_missing | 400 | A declared file was never uploaded. filename is the first missing path. |
upload_corrupt | 400 | The stored bytes differ from the declared size or MD5. |
upload_stale | 409 | A linked file is no longer available. Create a new session. |
release_upload_limit | 413 | New media exceeds 128 MiB. limit is the exact number of bytes. |
invalid_md5 | 400 | md5 isn't 32 lowercase hex digits. |
duplicate_path | 400 | A path was declared twice, or is in both the ZIP and the session. |
busy | 503 | No room for the release's new media right now. Nothing changed; send it again after Retry-After. |
sync_unavailable | 503 | This server has no file-by-file uploads. Nothing changed. |
The endpoint also answers the connection, login, limit and path errors on Errors. A failed release changes nothing.