Uploads
Presigned upload flow for files, images, and game assets
The Uploads API provides a presigned URL flow for uploading files to S3. All file uploads go through this flow.
Upload Flow
- Start Upload - Request a presigned URL with file metadata
- Upload to S3 - PUT the file directly to the presigned URL
- Complete Upload - Notify the server the upload finished
- Processing - Server processes the file (thumbnails, validation, etc.)
- Ready - File is ready to use
Upload Types
| Type | Max Size | Description |
|---|---|---|
file |
100 MB | General file upload |
profile_pic |
10 MB | Profile picture |
emote |
1 MB | Custom emote |
chat_attachment |
25 MB | Chat message attachment |
game_asset |
500 MB | World, avatar, or prop |
Allowed Content Types
file
image/jpeg,image/png,image/gif,image/webpvideo/mp4,video/webmaudio/mpeg,audio/oggapplication/pdfapplication/zip,application/x-zip-compressedtext/plain
profile_pic
image/jpeg,image/png,image/gif,image/webp
emote
image/gif,image/png,image/webp
chat_attachment
- Same as
file
game_asset
application/zip,application/x-zip-compressedapplication/octet-stream
Start Upload
Request a presigned upload URL.
POST /v1/uploads
Authorization: Bearer {token}
General File:
{
"upload_type": "file",
"filename": "document.pdf",
"content_type": "application/pdf",
"size_bytes": 1048576,
"parent_id": "123456789"
}
Profile Picture:
{
"upload_type": "profile_pic",
"filename": "avatar.png",
"content_type": "image/png",
"size_bytes": 524288,
"crop_x": 100,
"crop_y": 50,
"crop_w": 400,
"crop_h": 400
}
Emote:
{
"upload_type": "emote",
"filename": "cool_emote.gif",
"content_type": "image/gif",
"size_bytes": 262144,
"shortcode": "cool_emote"
}
Chat Attachment:
{
"upload_type": "chat_attachment",
"filename": "screenshot.png",
"content_type": "image/png",
"size_bytes": 2097152,
"channel_id": "123456789"
}
Game Asset (New):
{
"upload_type": "game_asset",
"filename": "world.zip",
"content_type": "application/zip",
"size_bytes": 52428800,
"asset_type": "world"
}
Game Asset (New Version):
{
"upload_type": "game_asset",
"filename": "world_v2.zip",
"content_type": "application/zip",
"size_bytes": 55574528,
"asset_id": "123456789"
}
Response (201 Created):
{
"upload_id": "999888777",
"presigned_url": "https://upload.ocvr.net/...",
"signed_headers": {
"Content-Type": "application/pdf",
"Content-Length": "1048576"
},
"expires_at": 1703521800
}
Error Codes:
| Code | Description |
|---|---|
UPLOAD_INVALID_TYPE |
Invalid upload_type |
UPLOAD_INVALID_CONTENT_TYPE |
Content type not allowed |
UPLOAD_FILE_TOO_LARGE |
Exceeds size limit |
UPLOAD_QUOTA_EXCEEDED |
Storage quota exceeded |
Upload to S3
After receiving the presigned URL, upload the file directly to S3:
PUT {presigned_url}
Content-Type: application/pdf
Content-Length: 1048576
[file bytes]
Important: Include all headers from signed_headers exactly as provided.
Get Upload Status
Check the status of an upload.
GET /v1/uploads/{uploadID}
Authorization: Bearer {token}
Response:
{
"id": "999888777",
"upload_type": "file",
"filename": "document.pdf",
"content_type": "application/pdf",
"size_bytes": 1048576,
"state": "pending",
"error_message": null,
"result_file_id": null,
"created_at": 1703520000,
"expires_at": 1703521800
}
Upload States:
| State | Description |
|---|---|
pending |
Awaiting S3 upload |
uploaded |
S3 upload complete, processing |
completed |
Processing complete |
failed |
Processing failed |
Complete Upload
Notify the server that the S3 upload is finished.
POST /v1/uploads/{uploadID}/complete
Authorization: Bearer {token}
Response:
{
"upload": {
"id": "999888777",
"upload_type": "file",
"filename": "document.pdf",
"content_type": "application/pdf",
"size_bytes": 1048576,
"state": "uploaded",
"error_message": null,
"result_file_id": null,
"created_at": 1703520000,
"expires_at": 1703521800
},
"file_id": null
}
After completion, the server processes the file asynchronously:
- Images: Generate thumbnails
- Profile pics: Crop and resize
- Game assets: Validate and create version
Poll GET /v1/uploads/{id} to check when state becomes completed.
Error Codes:
| Code | Description |
|---|---|
UPLOAD_NOT_FOUND |
Upload doesn't exist |
UPLOAD_EXPIRED |
Upload URL expired |
UPLOAD_INVALID_STATE |
File not in S3 yet |
Cancel Upload
Cancel a pending upload.
DELETE /v1/uploads/{uploadID}
Authorization: Bearer {token}
Response: 204 No Content
Notes:
- Can only cancel
pendingoruploadeduploads - Cannot cancel completed or failed uploads
Example: Full Upload Flow
// 1. Start upload
const startRes = await fetch('/v1/uploads', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
upload_type: 'file',
filename: 'photo.jpg',
content_type: 'image/jpeg',
size_bytes: file.size
})
});
const { upload_id, presigned_url, signed_headers } = await startRes.json();
// 2. Upload to S3
await fetch(presigned_url, {
method: 'PUT',
headers: signed_headers,
body: file
});
// 3. Complete upload
await fetch(`/v1/uploads/${upload_id}/complete`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}` }
});
// 4. Poll for completion
let upload;
do {
await sleep(1000);
const res = await fetch(`/v1/uploads/${upload_id}`, {
headers: { 'Authorization': `Bearer ${token}` }
});
upload = await res.json();
} while (upload.state === 'uploaded');
// 5. Use the file
if (upload.state === 'completed') {
console.log('File ID:', upload.result_file_id);
}