Download a motion
Download generated motions in FBX, GLB, or BVH using the motion download endpoints. (For integrating Uthana motions with a Unitree G1 robot, see Unitree G1).
Step 1: Identify your character and motion IDs
You need both the characterId and motionId to download files.
Note: The filename in the URL is customizable for /motion/file/ and /motion/animation/ endpoints. You can use any filename you want (e.g., motion.fbx, walking-animation.glb). The /motion/bundle/ endpoint requires the fixed filename character.glb or character.fbx.
Retargeting: When you download a motion with a specific character ID, retargeting happens automatically—the motion is adapted to work with that character's skeleton and proportions. This applies to FBX, GLB, and BVH character-animated exports (the Unitree G1 CSV format does not include model meshes, so retargeting is not applicable).
Step 2: Download the motion file
- Shell
- Python
- TypeScript
- React
- C#
API_KEY="{{apiKey}}"
CHARACTER_ID="cXi2eAP19XwQ"
MOTION_ID="your-motion-id"
# FBX (includes character mesh)
curl -L "https://uthana.com/motion/file/motion_viewer/$CHARACTER_ID/$MOTION_ID/fbx/motion.fbx" \
-u $API_KEY: \
-o motion.fbx
# GLB (includes character mesh)
curl -L "https://uthana.com/motion/file/motion_viewer/$CHARACTER_ID/$MOTION_ID/glb/motion.glb" \
-u $API_KEY: \
-o motion.glb
import asyncio
from uthana import Uthana
client = Uthana("{{apiKey}}")
CHARACTER_ID = "cXi2eAP19XwQ"
MOTION_ID = "your-motion-id"
async def main():
# GLB (includes character mesh)
glb_data = await client.motions.download(CHARACTER_ID, MOTION_ID, output_format="glb")
with open("motion.glb", "wb") as f:
f.write(glb_data)
# FBX
fbx_data = await client.motions.download(CHARACTER_ID, MOTION_ID, output_format="fbx")
with open("motion.fbx", "wb") as f:
f.write(fbx_data)
asyncio.run(main())
import { UthanaClient } from "@uthana/client";
const client = new UthanaClient(process.env.UTHANA_API_KEY!);
const CHARACTER_ID = "cXi2eAP19XwQ";
const MOTION_ID = "your-motion-id";
// GLB (includes character mesh)
const glbBuffer = await client.motions.download(CHARACTER_ID, MOTION_ID, { output_format: "glb" });
// FBX
const fbxBuffer = await client.motions.download(CHARACTER_ID, MOTION_ID, { output_format: "fbx" });
import { useUthanaClient } from "@uthana/react";
function DownloadButton({ characterId, motionId }: { characterId: string; motionId: string }) {
const client = useUthanaClient();
async function download(format: "glb" | "fbx") {
const buffer = await client.motions.download(characterId, motionId, { output_format: format });
const mime = format === "glb" ? "model/gltf-binary" : "application/octet-stream";
const url = URL.createObjectURL(new Blob([buffer], { type: mime }));
const a = document.createElement("a");
a.href = url;
a.download = `motion.${format}`;
a.click();
}
return (
<div>
<button onClick={() => download("glb")}>Download GLB</button>
<button onClick={() => download("fbx")}>Download FBX</button>
</div>
);
}
var apiKey = "{{apiKey}}";
var characterId = "cXi2eAP19XwQ";
var motionId = "your-motion-id";
var downloadUrl = $"https://uthana.com/motion/file/motion_viewer/{characterId}/{motionId}/fbx/motion.fbx";
var authValue = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{apiKey}:"));
_httpClient.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", authValue);
var bytes = await _httpClient.GetByteArrayAsync(downloadUrl);
Download options
Frame rate (FPS)
You can specify the frame rate for downloaded motions using the fps query parameter. Supported values are 24, 30, and 60.
- Shell
- Python
- TypeScript
- React
- C#
# Download at 30 FPS
curl -L "https://uthana.com/motion/file/motion_viewer/$CHARACTER_ID/$MOTION_ID/fbx/motion.fbx?fps=30" \
-u $API_KEY: \
-o motion-30fps.fbx
# Download at 60 FPS
curl -L "https://uthana.com/motion/file/motion_viewer/$CHARACTER_ID/$MOTION_ID/fbx/motion.fbx?fps=60" \
-u $API_KEY: \
-o motion-60fps.fbx
async def main():
data = await client.motions.download(CHARACTER_ID, MOTION_ID, output_format="fbx", fps=30)
with open("motion-30fps.fbx", "wb") as f:
f.write(data)
asyncio.run(main())
const buffer = await client.motions.download(CHARACTER_ID, MOTION_ID, {
output_format: "fbx",
fps: 30,
});
const buffer = await client.motions.download(characterId, motionId, {
output_format: "fbx",
fps: 30,
});
public async Task<byte[]> DownloadMotionAtFpsAsync(string motionId, string format, int fps, string filename = "motion")
{
var downloadUrl = $"https://uthana.com/motion/file/motion_viewer/{CHARACTER_ID}/{motionId}/{format}/{filename}.{format}?fps={fps}";
var response = await _httpClient.GetAsync(downloadUrl);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsByteArrayAsync();
}
// Usage
var motion30fps = await DownloadMotionAtFpsAsync(motionId, "fbx", 30);
Exclude character mesh
Use the no_mesh query parameter to download animation data without the character mesh. Set no_mesh=true to exclude the mesh, or no_mesh=false to include it (default). For .glb downloads only, there is an additonal option no_mesh=minimal, which creates a skeleton-like mesh for easier viewing.
- Shell
- Python
- TypeScript
- React
- C#
# Download FBX without character mesh
curl -L "https://uthana.com/motion/file/motion_viewer/$CHARACTER_ID/$MOTION_ID/fbx/motion.fbx?no_mesh=true" \
-u $API_KEY: \
-o motion-no-mesh.fbx
async def main():
data = await client.motions.download(CHARACTER_ID, MOTION_ID, output_format="fbx", no_mesh=True)
with open("motion-no-mesh.fbx", "wb") as f:
f.write(data)
asyncio.run(main())
const buffer = await client.motions.download(CHARACTER_ID, MOTION_ID, {
output_format: "fbx",
no_mesh: true,
});
const buffer = await client.motions.download(characterId, motionId, {
output_format: "fbx",
no_mesh: true,
});
public async Task<byte[]> DownloadMotionNoMeshAsync(string motionId, string format, string filename = "motion")
{
var downloadUrl = $"https://uthana.com/motion/file/motion_viewer/{CHARACTER_ID}/{motionId}/{format}/{filename}.{format}?no_mesh=true";
var response = await _httpClient.GetAsync(downloadUrl);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsByteArrayAsync();
}
In-place motion
Use the in_place query parameter to remove horizontal root motion from the animation. Set in_place=true to keep the character in place with no horizontal translation (default: false). This applies to FBX, GLB, and BVH retargeted exports, not to Unitree G1 CSV.
- Shell
- Python
- TypeScript
- React
- C#
# Download motion with character in place
curl -L "https://uthana.com/motion/file/motion_viewer/$CHARACTER_ID/$MOTION_ID/fbx/motion.fbx?in_place=true" \
-u $API_KEY: \
-o motion-in-place.fbx
async def main():
data = await client.motions.download(CHARACTER_ID, MOTION_ID, output_format="fbx", in_place=True)
with open("motion-in-place.fbx", "wb") as f:
f.write(data)
asyncio.run(main())
const buffer = await client.motions.download(CHARACTER_ID, MOTION_ID, {
output_format: "fbx",
in_place: true,
});
const buffer = await client.motions.download(characterId, motionId, {
output_format: "fbx",
in_place: true,
});
public async Task<byte[]> DownloadMotionInPlaceAsync(string motionId, string format, string filename = "motion")
{
var downloadUrl = $"https://uthana.com/motion/file/motion_viewer/{CHARACTER_ID}/{motionId}/{format}/{filename}.{format}?in_place=true";
var response = await _httpClient.GetAsync(downloadUrl);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsByteArrayAsync();
}
Torso-isolated motion
Use the torso_only query parameter to isolate motion to the torso. Set torso_only=true to keep the character lower body immobile (default: false). The same retargeting pipeline applies to FBX, GLB, and BVH; it is not used for Unitree G1 CSV exports.
- Shell
- Python
- TypeScript
- React
- C#
# Download torso-isolated motion
curl -L "https://uthana.com/motion/file/motion_viewer/$CHARACTER_ID/$MOTION_ID/fbx/motion.fbx?torso_only=true" \
-u $API_KEY: \
-o motion-torso-only.fbx
async def main():
data = await client.motions.download(CHARACTER_ID, MOTION_ID, output_format="fbx", torso_only=True)
with open("motion-torso-only.fbx", "wb") as f:
f.write(data)
asyncio.run(main())
const buffer = await client.motions.download(CHARACTER_ID, MOTION_ID, {
output_format: "fbx",
torso_only: true,
});
const buffer = await client.motions.download(characterId, motionId, {
output_format: "fbx",
torso_only: true,
});
public async Task<byte[]> DownloadMotionTorsoOnlyAsync(string motionId, string format, string filename = "motion")
{
var downloadUrl = $"https://uthana.com/motion/file/motion_viewer/{CHARACTER_ID}/{motionId}/{format}/{filename}.{format}?torso_only=true";
var response = await _httpClient.GetAsync(downloadUrl);
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsByteArrayAsync();
}
Roblox-compatible FBX
ExperimentalUse the roblox_compatible query parameter to export FBX optimized for Roblox Studio. Set roblox_compatible=true to enable Roblox-compatible export.
Caveats:
- Only applies to FBX format (not GLB); using with GLB returns an error
- Only supported for character IDs:
cFB7NoFCUCvf,cg1RuTM77HXu,c8EaC2nVbPS8; using with any other character ID returns an error
- Shell
- Python
- TypeScript
- React
- C#
# Download Roblox-compatible FBX (use a character with engine "roblox" in metadata)
CHARACTER_ID="cFB7NoFCUCvf"
curl -L "https://uthana.com/motion/file/motion_viewer/$CHARACTER_ID/$MOTION_ID/fbx/motion.fbx?roblox_compatible=true" \
-u $API_KEY: \
-o motion-roblox.fbx
ROBLOX_CHARACTER_ID = "cFB7NoFCUCvf"
async def main():
# Use a character with engine "roblox" in metadata
data = await client.motions.download(
ROBLOX_CHARACTER_ID,
MOTION_ID,
output_format="fbx",
roblox_compatible=True,
)
with open("motion-roblox.fbx", "wb") as f:
f.write(data)
asyncio.run(main())
// Use a character with engine "roblox" in metadata
const buffer = await client.motions.download("cFB7NoFCUCvf", MOTION_ID, {
output_format: "fbx",
roblox_compatible: true,
});
const buffer = await client.motions.download("cFB7NoFCUCvf", motionId, {
output_format: "fbx",
roblox_compatible: true,
});
// Download Roblox-compatible FBX (use a character with engine "roblox" in metadata)
var characterId = "cFB7NoFCUCvf";
var downloadUrl = $"https://uthana.com/motion/file/motion_viewer/{characterId}/{motionId}/fbx/motion.fbx?roblox_compatible=true";
var response = await _httpClient.GetAsync(downloadUrl);
response.EnsureSuccessStatusCode();
var bytes = await response.Content.ReadAsByteArrayAsync();
Speed multiplier
Use the speed_multiplier query parameter to scale the playback speed of the downloaded motion. Accepts a float value greater than 0 and at most 2.0. The default is 1.0 (no change). Not supported for Unitree G1 CSV exports.
- Shell
- Python
- TypeScript
- React
- C#
# Download at half speed
curl -L "https://uthana.com/motion/file/motion_viewer/$CHARACTER_ID/$MOTION_ID/fbx/motion.fbx?speed_multiplier=0.5" \
-u $API_KEY: \
-o motion-half-speed.fbx
# Download at double speed
curl -L "https://uthana.com/motion/file/motion_viewer/$CHARACTER_ID/$MOTION_ID/fbx/motion.fbx?speed_multiplier=2.0" \
-u $API_KEY: \
-o motion-double-speed.fbx
async def main():
# Half speed
data = await client.motions.download(CHARACTER_ID, MOTION_ID, output_format="fbx", speed_multiplier=0.5)
with open("motion-half-speed.fbx", "wb") as f:
f.write(data)
asyncio.run(main())
const buffer = await client.motions.download(CHARACTER_ID, MOTION_ID, {
output_format: "fbx",
speed_multiplier: 0.5,
});
const buffer = await client.motions.download(characterId, motionId, {
output_format: "fbx",
speed_multiplier: 0.5,
});
var downloadUrl = $"https://uthana.com/motion/file/motion_viewer/{CHARACTER_ID}/{motionId}/fbx/motion.fbx?speed_multiplier=0.5";
var response = await _httpClient.GetAsync(downloadUrl);
response.EnsureSuccessStatusCode();
var bytes = await response.Content.ReadAsByteArrayAsync();
Combining options
You can combine fps, no_mesh, in_place, torso_only, roblox_compatible, and speed_multiplier parameters (some parameters are not supported for certain formats, e.g. Unitree G1).
# Download at 30 FPS without character mesh
curl -L "https://uthana.com/motion/file/motion_viewer/$CHARACTER_ID/$MOTION_ID/fbx/motion.fbx?fps=30&no_mesh=true" \
-u $API_KEY: \
-o motion-30fps-no-mesh.fbx
# Download at 30 FPS, in-place, with Roblox-compatible FBX (use a character with engine "roblox" in metadata)
CHARACTER_ID="cFB7NoFCUCvf"
curl -L "https://uthana.com/motion/file/motion_viewer/$CHARACTER_ID/$MOTION_ID/fbx/motion.fbx?fps=30&no_mesh=false&in_place=true&roblox_compatible=true" \
-u $API_KEY: \
-o motion-30fps-roblox.fbx
# Download at 30 FPS, half speed
curl -L "https://uthana.com/motion/file/motion_viewer/$CHARACTER_ID/$MOTION_ID/fbx/motion.fbx?fps=30&speed_multiplier=0.5" \
-u $API_KEY: \
-o motion-30fps-half-speed.fbx
Motion-only GLB
To download only animation data without the character mesh, you can use the motion-only endpoint or the no_mesh=true parameter:
- Shell
- Python
- TypeScript
- React
- C#
# Using the motion-only endpoint
curl -L "https://uthana.com/motion/animation/motion_viewer/$CHARACTER_ID/$MOTION_ID/glb/motion.glb" \
-u $API_KEY: \
-o motion-only.glb
# Or using no_mesh parameter
curl -L "https://uthana.com/motion/file/motion_viewer/$CHARACTER_ID/$MOTION_ID/glb/motion.glb?no_mesh=true" \
-u $API_KEY: \
-o motion-only.glb
async def main():
data = await client.motions.download(CHARACTER_ID, MOTION_ID, output_format="glb", no_mesh=True)
with open("motion-only.glb", "wb") as f:
f.write(data)
asyncio.run(main())
const buffer = await client.motions.download(CHARACTER_ID, MOTION_ID, {
output_format: "glb",
no_mesh: true,
});
const buffer = await client.motions.download(characterId, motionId, {
output_format: "glb",
no_mesh: true,
});
var motionOnlyUrl = $"https://uthana.com/motion/animation/motion_viewer/{CHARACTER_ID}/{motionId}/glb/motion.glb";
var response = await _httpClient.GetAsync(motionOnlyUrl);
response.EnsureSuccessStatusCode();
var bytes = await response.Content.ReadAsByteArrayAsync();
Preview WebM
Download a short preview video (WebM) for a motion. Preview downloads do not count against your download quota.
- Shell
- Python
- TypeScript
- React
- C#
# Preview WebM is available via the API reference — see /graphql for the preview query
async def main():
preview_bytes = await client.motions.preview(CHARACTER_ID, MOTION_ID)
with open("preview.webm", "wb") as f:
f.write(preview_bytes)
asyncio.run(main())
const previewBuffer = await client.motions.preview(CHARACTER_ID, MOTION_ID);
// Browser: display in a video element
const url = URL.createObjectURL(new Blob([previewBuffer], { type: "video/webm" }));
// <video src={url} autoPlay loop muted />
import { useUthanaMotionPreview } from "@uthana/react";
function MotionPreview({ characterId, motionId }: { characterId: string; motionId: string }) {
const { preview } = useUthanaMotionPreview(characterId, motionId);
const url = preview
? URL.createObjectURL(new Blob([preview], { type: "video/webm" }))
: undefined;
return url ? <video src={url} autoPlay loop muted /> : null;
}
Preview WebM is available via the GraphQL API. See the API reference for the preview query.
Check download quota
Check whether a motion download is allowed before consuming quota.
- Shell
- Python
- TypeScript
- React
- C#
# Check download quota via GraphQL — see the API reference for the motionDownloads query
async def main():
allowed = await client.motion_downloads.is_allowed(CHARACTER_ID, MOTION_ID)
if allowed:
data = await client.motions.download(CHARACTER_ID, MOTION_ID, output_format="glb")
asyncio.run(main())
const allowed = await client.motionDownloads.isAllowed(CHARACTER_ID, MOTION_ID);
if (allowed) {
const buffer = await client.motions.download(CHARACTER_ID, MOTION_ID, { output_format: "glb" });
}
import { useUthanaIsMotionDownloadAllowed, useUthanaMotionDownloads } from "@uthana/react";
function DownloadButton({ characterId, motionId }: { characterId: string; motionId: string }) {
const { isAllowed } = useUthanaIsMotionDownloadAllowed(characterId, motionId);
const client = useUthanaClient();
async function download() {
const buffer = await client.motions.download(characterId, motionId, { output_format: "glb" });
// save or display buffer...
}
return (
<button onClick={download} disabled={!isAllowed}>
{isAllowed ? "Download" : "Download limit reached"}
</button>
);
}
// Check download quota via GraphQL — see the API reference for the motionDownloads query
Error handling
These endpoints return binary file data on success. On error, they return an HTTP error status. Check response.status (or response.status_code) in your code—the response body may contain error details, but for file downloads the status code is the primary signal.
| Condition | Status code |
|---|---|
Invalid fps (not 24, 30, or 60) | 400 |
Invalid no_mesh (not true, false, or minimal) | 400 |
no_mesh=minimal with non-GLB format | 400 |
Invalid in_place (not true or false) | 400 |
Invalid torso_only (not true or false) | 400 |
Invalid roblox_compatible (not true or false) | 400 |
roblox_compatible=true with GLB format | 400 |
roblox_compatible=true with a non-FBX format (including BVH, CSV) | 400 |
roblox_compatible=true with a character that does not have "engine": "roblox" in bundle metadata | 400 |
speed_multiplier is not a valid number | 400 |
speed_multiplier out of range (must be > 0 and ≤ 2) | 400 |
| Invalid motion or character ID, or no access | 404 |
| Permission denied (quota, etc.) | 403 |
| Not logged in | 401 |
Related docs
- Unitree G1 for robot (
.csv) exports - Asset management for listing motions and metadata