Asset APIs enable you to create, update, retrieve, and manage experience assets including models, images, audio, and more through OpenCloud.
Overview
Asset operations include:
- Create and upload new assets
- Update existing assets
- Retrieve asset metadata
- Manage asset versions
- Archive and restore assets
- Roll back to previous versions
Import
import { fetchApi } from 'rozod';
import { v1 } from 'rozod/lib/opencloud';
Asset APIs are only available in v1. There is no v2 alternative yet.
Authentication
Asset operations require specific scopes:
asset:read - Read asset metadata and versions
asset:write - Create, update, and manage assets
import { configureServer } from 'rozod';
configureServer({
cloudKey: 'your_api_key_with_asset_scopes'
});
Supported asset types
OpenCloud supports the following asset types:
- Model - 3D models (.fbx, .obj)
- Image - Images (.png, .jpg, .bmp, .tga)
- Audio - Audio files (.mp3, .ogg)
- Video - Video files (.mp4)
- Font Family - Font files
Each type has specific size limits and requirements. See Roblox’s documentation for details.
Create an asset
Upload a new asset with metadata:
import { fetchApi } from 'rozod';
import { v1 } from 'rozod/lib/opencloud';
import * as fs from 'fs';
const assetFile = fs.readFileSync('MyModel.fbx');
const operation = await fetchApi(
v1.assets.postAssets,
{},
{
request: {
assetType: 'Model',
displayName: 'Epic Sword',
description: 'A legendary sword for brave warriors',
creationContext: {
creator: {
userId: 156, // Or groupId for group-owned assets
},
expectedPrice: 0,
},
},
fileContent: assetFile,
}
);
console.log('Operation ID:', operation.path);
console.log('Done:', operation.done);
if (operation.done) {
console.log('Asset ID:', operation.response.assetId);
}
Check operation status
Asset creation returns an operation that may complete asynchronously:
import { fetchApi } from 'rozod';
import { v1 } from 'rozod/lib/opencloud';
async function waitForAssetCreation(operationId: string) {
let operation;
do {
operation = await fetchApi(v1.assets.getOperationsOperationId, {
operationId: operationId,
});
if (!operation.done) {
console.log('Still processing...');
await new Promise(resolve => setTimeout(resolve, 2000));
}
} while (!operation.done);
if (operation.error) {
throw new Error(`Operation failed: ${operation.error.message}`);
}
return operation.response;
}
const asset = await waitForAssetCreation('operation-id-here');
console.log('Asset created:', asset.assetId);
Retrieve information about an existing asset:
import { fetchApi } from 'rozod';
import { v1 } from 'rozod/lib/opencloud';
const asset = await fetchApi(v1.assets.getAssetsAssetId, {
assetId: '123456789',
readMask: 'description,displayName,icon,previews',
});
console.log(asset.displayName);
console.log(asset.description);
console.log(asset.assetType);
console.log(asset.state); // Active or Archived
console.log(asset.revisionId); // Current revision
console.log(asset.moderationResult); // Moderation status
console.log(asset.creationContext);
// Social links
if (asset.socialLink) {
console.log(asset.socialLink.title);
console.log(asset.socialLink.uri);
}
// Previews
for (const preview of asset.previews || []) {
console.log(preview.asset, preview.altText);
}
Use readMask to request specific fields and reduce response size.
Update an asset
Update asset metadata or upload a new version:
import { fetchApi } from 'rozod';
import { v1 } from 'rozod/lib/opencloud';
import * as fs from 'fs';
// Update metadata only
const operation = await fetchApi(
v1.assets.patchAssetsAssetId,
{
assetId: '123456789',
updateMask: 'description,displayName',
},
{
request: {
description: 'Updated description',
displayName: 'Epic Sword v2',
},
}
);
// Update with new file
const newFile = fs.readFileSync('MyModel_v2.fbx');
const fileOperation = await fetchApi(
v1.assets.patchAssetsAssetId,
{
assetId: '123456789',
updateMask: 'description',
},
{
request: {
description: 'Version 2 with improved textures',
},
fileContent: newFile,
}
);
List asset versions
Get version history for an asset:
import { fetchApi } from 'rozod';
import { v1 } from 'rozod/lib/opencloud';
const versions = await fetchApi(v1.assets.getAssetsAssetIdVersions, {
assetId: '123456789',
maxPageSize: 20,
});
for (const version of versions) {
console.log('Version:', version.path);
console.log('Published:', version.published);
console.log('Moderation:', version.moderationResult);
}
// Pagination
if (versions.nextPageToken) {
const nextPage = await fetchApi(v1.assets.getAssetsAssetIdVersions, {
assetId: '123456789',
pageToken: versions.nextPageToken,
});
}
Get specific version
Retrieve a particular version of an asset:
import { fetchApi } from 'rozod';
import { v1 } from 'rozod/lib/opencloud';
const version = await fetchApi(v1.assets.getAssetsAssetIdVersionsVersionNumber, {
assetId: '123456789',
versionNumber: '2',
});
console.log(version.path);
console.log(version.published);
console.log(version.moderationResult);
Rollback to previous version
Restore an earlier version of an asset:
import { fetchApi } from 'rozod';
import { v1 } from 'rozod/lib/opencloud';
const rolledBack = await fetchApi(
v1.assets.postAssetsAssetIdVersionsRollback,
{
assetId: '123456789',
},
{
assetVersion: 'assets/123456789/versions/2',
}
);
console.log('Rolled back to version:', rolledBack.path);
Archive an asset
Hide an asset from the catalog:
import { fetchApi } from 'rozod';
import { v1 } from 'rozod/lib/opencloud';
const archived = await fetchApi(v1.assets.postAssetsAssetIdArchive, {
assetId: '123456789',
});
console.log('Asset state:', archived.state); // Should be 'Archived'
Archived assets are not visible in Roblox experiences and cannot be used until restored.
Restore archived asset
Make an archived asset active again:
import { fetchApi } from 'rozod';
import { v1 } from 'rozod/lib/opencloud';
const restored = await fetchApi(v1.assets.postAssetsAssetIdRestore, {
assetId: '123456789',
});
console.log('Asset state:', restored.state); // Should be 'Active'
Asset privacy settings
Control who can use your assets:
import { fetchApi } from 'rozod';
import { v1 } from 'rozod/lib/opencloud';
const operation = await fetchApi(
v1.assets.postAssets,
{},
{
request: {
assetType: 'Model',
displayName: 'Private Model',
description: 'For my game only',
creationContext: {
creator: { userId: 156 },
assetPrivacy: 'restricted', // default, restricted, or openUse
expectedPrice: 0,
},
},
fileContent: modelFile,
}
);
Privacy options:
default - Follows standard Roblox privacy settings
restricted - Only you/your group can use
openUse - Anyone can use
Complete workflow example
import { fetchApi, configureServer } from 'rozod';
import { v1 } from 'rozod/lib/opencloud';
import * as fs from 'fs';
configureServer({ cloudKey: process.env.ROBLOX_CLOUD_KEY });
class AssetManager {
async uploadAsset(filePath: string, metadata: any) {
const fileContent = fs.readFileSync(filePath);
// Create asset
const operation = await fetchApi(
v1.assets.postAssets,
{},
{
request: metadata,
fileContent: fileContent,
}
);
// Wait for completion
return await this.waitForOperation(operation.path.split('/').pop()!);
}
async waitForOperation(operationId: string) {
let operation;
let attempts = 0;
const maxAttempts = 30;
do {
operation = await fetchApi(v1.assets.getOperationsOperationId, {
operationId: operationId,
});
if (!operation.done) {
if (++attempts >= maxAttempts) {
throw new Error('Operation timed out');
}
await new Promise(resolve => setTimeout(resolve, 2000));
}
} while (!operation.done);
if (operation.error) {
throw new Error(`Operation failed: ${operation.error.message}`);
}
return operation.response;
}
async updateAssetVersion(assetId: string, filePath: string) {
const fileContent = fs.readFileSync(filePath);
const operation = await fetchApi(
v1.assets.patchAssetsAssetId,
{
assetId: assetId,
updateMask: 'description',
},
{
request: {
description: `Updated ${new Date().toISOString()}`,
},
fileContent: fileContent,
}
);
return await this.waitForOperation(operation.path.split('/').pop()!);
}
async getAssetInfo(assetId: string) {
return await fetchApi(v1.assets.getAssetsAssetId, {
assetId: assetId,
readMask: 'displayName,description,state,revisionId',
});
}
}
// Usage
const manager = new AssetManager();
const asset = await manager.uploadAsset('sword.fbx', {
assetType: 'Model',
displayName: 'Legendary Sword',
description: 'A powerful weapon',
creationContext: {
creator: { userId: 156 },
assetPrivacy: 'restricted',
expectedPrice: 0,
},
});
console.log('Created asset:', asset.assetId);
// Update it later
await manager.updateAssetVersion(asset.assetId, 'sword_v2.fbx');
// Get current info
const info = await manager.getAssetInfo(asset.assetId);
console.log('Current state:', info.state);
Best practices
Validate files before upload
import * as fs from 'fs';
function validateAssetFile(filePath: string, assetType: string) {
const stats = fs.statSync(filePath);
const sizeInMB = stats.size / (1024 * 1024);
const limits = {
'Model': 100,
'Image': 30,
'Audio': 20,
'Video': 500,
};
if (sizeInMB > limits[assetType]) {
throw new Error(`File too large: ${sizeInMB}MB (max ${limits[assetType]}MB)`);
}
}
Handle moderation
import { fetchApi } from 'rozod';
import { v1 } from 'rozod/lib/opencloud';
async function checkModeration(assetId: string) {
const asset = await fetchApi(v1.assets.getAssetsAssetId, {
assetId: assetId,
});
if (asset.moderationResult?.moderationState === 'Rejected') {
console.warn('Asset was rejected by moderation');
return false;
}
return true;
}
Version tracking
import { fetchApi } from 'rozod';
import { v1 } from 'rozod/lib/opencloud';
async function trackVersions(assetId: string) {
const versions = await fetchApi(v1.assets.getAssetsAssetIdVersions, {
assetId: assetId,
maxPageSize: 50,
});
const versionLog = versions.map((v, idx) => ({
number: versions.length - idx,
path: v.path,
published: v.published,
}));
console.table(versionLog);
}
Error handling
import { fetchApi, isAnyErrorResponse } from 'rozod';
import { v1 } from 'rozod/lib/opencloud';
async function safeUploadAsset(filePath: string, metadata: any) {
try {
// Validate file
validateAssetFile(filePath, metadata.assetType);
// Upload
const fileContent = fs.readFileSync(filePath);
const operation = await fetchApi(
v1.assets.postAssets,
{},
{
request: metadata,
fileContent: fileContent,
}
);
if (isAnyErrorResponse(operation)) {
throw new Error(`Upload failed: ${operation.message}`);
}
// Wait for completion
// ... (operation polling logic)
return operation;
} catch (error) {
console.error('Asset upload error:', error);
throw error;
}
}