Upload API
Uploads are two calls: ask for a signed PUT for one object key, then put the bytes there. The signing service never sees your file, and the file never touches the Studio API.
Open Appsinity StudioRequest a signed URL
One endpoint, two aliases. Both paths do the same thing, and the legacy path exists so older clients keep working.
POST https://<upload-worker>/api/storage/presign-upload
POST https://<upload-worker>/upload-url (alias)
Authorization: Bearer <studio token>
Content-Type: application/json
{
"type": "game-build", // game-build | game-web | app-file | app-media
"fileName": "my-game-1.4.0.zip", // sanitised to [\w.-], 140 chars
"gameId": "my-game", // required for game-* types
"appId": "my-game", // required for app-* types
"version": "1.4.0", // defaults to "draft", else Date.now()
"size": 734003200, // bytes, must be > 0
"contentType": "application/zip", // optional, capped at 120 chars
"contentEncoding": "gzip" // optional
}
200 {
"uploadUrl": "<presigned PUT, valid 10800s>",
"fileUrl": "https://storage.appsinity.com/public/games/my-game/builds/1.4.0/my-game-1.4.0.zip",
"key": "games/my-game/builds/1.4.0/my-game-1.4.0.zip",
"objectKey": "same as key",
"bucket": "public",
"contentType": "application/zip",
"contentEncoding": "",
"expiresIn": 10800
}/public/ will not resolve for a player, and this is the single most common self-inflicted broken download link.Object key layout
| Type | Key template | Notes |
|---|---|---|
game-build | games/{gameId}/builds/{version}/{fileName} | One directory per version |
game-web | games/{gameId}/web/[{version}/]{fileName} | The version segment is dropped when version is draft |
app-file | apps/{appId}/files/{version}/{fileName} | Same versioning as a build |
app-media | apps/{appId}/media/{fileName} | No version segment at all |
Date.now(), uploading the same file twice creates two distinct build directories rather than overwriting the first.The validation gates
Every rule below is enforced by the service, after the sanitisers have already run on your name. The client repeats several of them for a faster message, and the service decides.
| Rule | Detail | Error you get |
|---|---|---|
| Type must be known | One of the four | Invalid upload type. |
| Blocked extensions | 16 extensions, always | Blocked file extension: .exe |
| Allow-listed extensions | 13, for all types except game-web | File extension not allowed: .xyz |
| Size present | A finite number above zero | Valid file size is required. |
| Size ceiling | 5 GiB, or 250 MiB for app-media | File is too large for this upload category. |
| File name usable | After sanitising, a name and extension survive | Valid fileName is required. |
| Media is media | app-media must be png, jpg, jpeg, webp or mp4 | App media must be an image or MP4 video. |
| Web is not an APK | game-web refuses .apk and .aab | Game web uploads cannot be APK or AAB files. |
| Identifiers | gameId or appId present per type | gameId is required. |
blocked (16): .php .exe .sh .bat .cmd .ps1 .msi .dmg
.jar .com .scr .vbs .app .xapk .ipa .rar
allowed (13): .apk .aab .zip .html .js .wasm .data
.json .png .jpg .jpeg .webp .mp4
caps: game-build 5 GiB | game-web 5 GiB | app-file 5 GiB
app-media 250 MiB
current object key sanitiser: NFKD, keep [\w.-], collapse runs,
trim, slice to 140application/octet-stream and an .html becomes text/html; charset=utf-8. A wrong explicit value is respected, so set it deliberately for a web bundle.Sending the bytes
- PUT to uploadUrl. Use the URL exactly as returned, including its query signature. Any edit invalidates it.
- Match Content-Type. Send the returned
contentTypeverbatim; a mismatch is a failed signature check. - Send the body raw. No multipart envelope and no wrapping field.
- Do not exceed three hours. A large upload on a slow connection can outlive the URL; the gate is total elapsed time, not transfer time.
- Record fileUrl. That is the value your listing's build or web URL must take, including
/public/. - Re-request on failure. A new presign is cheap; a retried PUT against an expired URL never succeeds.
Confirming an object exists
- No credential is required for this call, because it returns metadata about an object in a public bucket.
- Only the
publicbucket is accepted; anything else is Invalid bucket. - The check is a HEAD, so it tells you size and type but nothing about contents.
- Use it after an upload to confirm the object landed with the byte count you sent.
POST https://<upload-worker>/api/storage/file-info
{ "bucket": "public", "key": "games/my-game/builds/1.4.0/my-game-1.4.0.zip" }
200 {
"bucket": "public",
"key": "games/...",
"sizeBytes": 734003200,
"sizeFormatted": "700.00 MB",
"contentType": "application/zip",
"contentEncoding": ""
}
400 { "error": "File info lookup failed (404)." }Frequently asked questions
Can I upload without a Studio login?
No. Presigning requires a verifiable bearer token, and the two failure messages are Missing login token. and Invalid or expired login token.
Why is my 4 GiB file fine and my 5.5 GiB file refused?
5 GiB is the ceiling for every build type; the client checks first and the service checks again. Split assets or reduce compression rather than retrying.
Is there a delete API?
Not on the upload service. Removal happens through Studio's delete flows and the account-level app deletion OTP route.
Can I list what I have uploaded?
No listing endpoint exists on the storage service. Your build records in Studio are the inventory, see Upload an app.
Developer publishing support: [email protected]
General support: [email protected]
Partnerships: [email protected]
APPSINITY