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

  1. Start Upload - Request a presigned URL with file metadata
  2. Upload to S3 - PUT the file directly to the presigned URL
  3. Complete Upload - Notify the server the upload finished
  4. Processing - Server processes the file (thumbnails, validation, etc.)
  5. 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/webp
  • video/mp4, video/webm
  • audio/mpeg, audio/ogg
  • application/pdf
  • application/zip, application/x-zip-compressed
  • text/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-compressed
  • application/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 pending or uploaded uploads
  • 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);
}
-- ---