Skip to main content

GraphQL API

The Uthana GraphQL API provides a flexible way to query and mutate animation data.

Endpoint

https://uthana.com/graphql

Authentication

Include your API key in the basic Authorization header, base64-encoded with a colon:

AUTH_STRING=$(echo -n "<your-api-key>:" | base64)
curl -H "Authorization: Basic $AUTH_STRING" \
https://uthana.com/graphql

or in the query string (plaintext):

https://uthana.com/graphql?api_key=<your-api-key>

Schema

Queries

type Query {
# Get the current user's organization
org: Org

# Get the current user
user: User

# Get a motion download allowance
motion_download_allowed(character_id: String, motion_id: String): MotionDownloadAllowed

# List all characters available to the user
characters: [Character!]

# Get a motion by ID
motion(id: String): Motion

# List motions, with optional filters
motions(app_ids: [String!]): [Motion!]

# Search for labels by description
label_search(query: String, limit: Int = 20): [LabelSearchResult!]

# Get a job by job ID
job(job_id: String): Job

# List jobs, optionally filtered by method
jobs(method: String): [Job!]
}

Org

  • id: String Your organization's ID
  • name: String Your organization's name
  • motion_download_secs_per_month: Float The number of seconds of motion downloads per month allocated to the organization
  • motion_download_secs_per_month_remaining: Float The number of seconds of motion downloads per month remaining for the organization
  • users: [User] The users in your organization
query Org {
org {
id
name
motion_download_secs_per_month
motion_download_secs_per_month_remaining
users {
id
name
email
email_verified
tz
roles
}
}
}

User

  • id: String Your user ID
  • name: String Your name
  • email: String Your email address
  • email_verified: Boolean Whether your email address is verified
  • tz: String Your timezone at last login
  • roles: [String] Your roles
  • org: Org Your organization
  • created: String Your account creation timestamp
  • updated: String Your account last update timestamp
query User {
user {
id
name
email
email_verified
tz
roles
created
updated
org {
id
name
}
}
}

MotionDownloadAllowed

  • allowed: Boolean Whether the motion download is allowed
  • reason: String The reason the motion download is allowed or not
  • character_id: String The character ID
  • motion_id: String The motion ID
query MotionDownloadAllowed($character_id: String, $motion_id: String) {
motion_download_allowed(character_id: $character_id, motion_id: $motion_id) {
allowed
reason
}
}

Character

  • id: String
  • name: String Character name
  • org_id: String Character organization ID
  • assets: [Asset] Character assets
  • created: String Character creation timestamp
  • updated: String Character last update timestamp
  • deleted: String | null Character deletion timestamp, or null if the character has not been deleted
query Character($id: String!) {
character(id: $id) {
id
name
org_id
created
updated
deleted
assets {
id
filename
}
}
}

Characters

  • [Character] List of Character objects in your organization
query Characters {
characters {
id
name
# ...etc. (see Character object)
}
}

Motion

  • id: String Motion ID
  • name: String Motion name
  • org_id: String Motion organization ID
  • tags: Object Motion tags
  • assets: [Asset] Motion assets
  • labels: [Label] Motion labels
  • rating: MotionRating | null Motion rating, or null if the motion has not been rated
  • favorite: MotionFavorite | null Motion favorite, or null if the motion has not been favorited
  • created: String Motion creation timestamp
  • updated: String Motion last update timestamp
  • deleted: String | null Motion deletion timestamp, or null if the motion has not been deleted
{ MotionRating }
  • user_id: String User ID who rated the motion
  • score: Int The rating score, must be 0 (downvote) or 1 (upvote)
  • created: String Rating creation timestamp
  • updated: String Rating last update timestamp
{ MotionFavorite }
  • user_id: String User ID who favorited the motion
  • motion_id: String Motion ID
  • created: String Favorite creation timestamp
  • updated: String Favorite last update timestamp
query Motion($id: String!) {
motion(id: $id) {
id
name
tags
assets {
id
filename
}
rating {
user_id
score
created
}
favorite {
user_id
created
}
created
updated
deleted
}
}

Motions

  • [Motion] The motions in your organization
query Motions {
motions {
id
name
# ...etc. (see Motion object)
}
}

Asset

  • id: String
  • uid: String Asset internal UUID
  • filename: String Asset original filename
  • mimetype: String Asset mimetype
  • type: String Asset type, typically: bundle
  • sha256: String Asset SHA-256 hash
  • metadata: Object Asset metadata
  • size: Int Asset size in bytes
  • created: String Asset creation timestamp
  • updated: String Asset last update timestamp
  • deleted: String | null Asset deletion timestamp, or null if the asset has not been deleted

Note: Asset objects are children of Character and Motion.

Job

Note: The created_at, started_at, and ended_at fields are timestamps for when a job was created, started, and ended, respectively. These are not the timestamps of the database record, and as such have intentionally different names.

  • id: String
  • status: String Job status, one of: RESERVED, FINISHED, FAILED, READY
  • created_at: String Job creation timestamp
  • started_at: String Job start timestamp
  • ended_at: String Job end timestamp
  • method: String Job motion method
  • args: Object Job arguments
  • result: Object Job result
query Job($id: String!) {
job(id: $id) {
id
status
started_at
ended_at
result
}
}

Jobs

  • [Job] List of Job objects
query Jobs {
jobs {
id
# ...etc. (see Job object)
}
}

Mutations

The following mutations are available for creating and modifying data:

create_text_to_motion

Generate a motion from a text prompt.

  • prompt: The text prompt to generate a motion from.
  • foot_ik: Whether to enable foot IK.
mutation {
create_text_to_motion(
prompt: String!
model: String # internal use only
foot_ik: Boolean = false
) {
motion {
id
name
}
}
}

create_video_to_motion

Generate a motion from a 2D video file.

  • file: File or URL of the video file to create a motion from.
  • motion_name: The name of the resulting motion.
mutation {
create_video_to_motion(
file: Upload!
motion_name: String!
) {
job {
id
status
}
}
}

create_character

Upload a new character. Auto-rigging is handled automatically if the character doesn't have a rig.

  • file: File or URL of the character file to create.
  • name: The name of the resulting character.
  • auto_rig: Whether to auto-rig the character. (default: true)
mutation {
create_character(
file: Upload!
name: String!
auto_rig: Boolean = true # optional, defaults to true
root: String # internal use only
) {
character {
id
name
}
}
}

create_stitched_motion

Stitch two motions together.

Note: The motion stitching model has been upgraded, and this endpoint will be improved soon with new behavior. Legacy behavior may be preserved in a separate endpoint.

  • leading_motion_id: The ID of the leading motion.
  • trailing_motion_id: The ID of the trailing motion.
  • duration: The duration in seconds of the transition between the two motions. (default: 0.5)
mutation {
create_stitched_motion(
leading_motion_id: String!
trailing_motion_id: String!
duration: Float = 0.5
) {
motion {
id
name
}
}
}

trim_and_loop_motion

Note: This mutation's contract (its name and parameters) may change in the future. The implementation is stable, and the resulting motions are unaffected.

Trim and optionally loop a motion.

  • motion_id: The ID of the motion to trim and loop.
  • motion_name: The name of the resulting motion.
  • start/end: The start and end times of the trimmed motion, as a decimal value between 0 (start of motion) and 1 (end of motion).
  • loop: Create a looped motion. Valid values:
    • (empty): No loop
    • "cyclical": Moves the character back to its starting position before looping again (in-place)
    • "progressive": Loops continuously starting from wherever the previous loop ended (locomotion)
  • loop_root_motion: (Deprecated: Pass "progressive" or "cyclical" to loop instead). Control loop style: true (progressive) or false (cyclical)"
mutation {
trim_and_loop_motion(
motion_id: String!
motion_name: String
start: Float!
end: Float!
loop: "progressive" | "cyclical"
) {
motion {
id
name
}
}
}

Backward compatibility: The deprecated loop_root_motion parameter is still supported for legacy clients. When both loop and loop_root_motion are provided, loop takes precedence.

rate_motion

Rate a motion or label.

  • motion_id: The ID of the motion to rate.
  • label_id: The ID of the label to rate.
  • score: The score to rate the motion or label with. Must be one of:
    • 0: Downvote
    • 1: Upvote
mutation {
rate_motion(
label_id: String
motion_id: String!
score: Int! # must be 0 or 1
) {
rating {
score
user_id
}
}
}

update_motion

Update a motion's name and/or deletion status.

  • motion_id: The ID of the motion to update.
  • name: The new name of the motion.
  • deleted: Marks the motion as deleted. While not permanently removed, it will not appear in API responses or the UI.
mutation {
update_motion(
deleted: Boolean
motion_id: String!
name: String
) {
motion {
id
name
deleted
}
}
}

Example queries

Get motion details

query GetMotion($id: String!) {
motion(id: $id) {
id
name
created
updated
org_id
priority
tags
motion_viewer
training
assets {
id
filename
}
labels {
id
description
start
end
}
rating {
score
user_id
}
}
}

List motions

query ListMotions {
motions(methods: ["TextToMotion"]) {
id
name
org_id
created
assets {
filename
}
}
}

List jobs

query ListJobs {
jobs(method: "VideoToMotion") {
id
status
}
}

Generate motion from text (text-to-motion)

Available models: text-to-motion (default) and text-to-motion-bucmd. For the BUCMD variant you can also pass the following parameters:

  • retargeting_ik: Enables retargeting IK for better alignment to the target skeleton.
  • steps: Number of diffusion steps used during generation.
  • cfg_scale: Classifier-free guidance scale that balances prompt fidelity versus diversity.
  • motion_length: Output length in frames at the internal model frame rate of 20 FPS.
  • seed: Random seed to reproduce results.
mutation GenerateTextMotion {
create_text_to_motion(
prompt: "walk casually"
model: "text-to-motion"
foot_ik: true
) {
motion {
id
name
}
}
}
mutation GenerateBucmdTextMotion {
create_text_to_motion(
prompt: "confident stride down the runway"
model: "text-to-motion-bucmd"
foot_ik: false
retargeting_ik: true
steps: 50
cfg_scale: 2
motion_length: 200
seed: 12345
) {
motion {
id
name
}
}
}

Upload a character

mutation UploadCharacter($file: Upload!) {
create_character(
file: $file
name: "My Character"
auto_rig: true # optional, defaults to true
) {
character {
id
name
}
}
}

Download a character as FBX or GLB

All character files are available via the following endpoint:

https://uthana.com/motion/bundle/<character-id>/character.<fbx,glb>
  • character_id: The character's ID
  • type: The file type/format (fbx, glb)

Note: You will need to pass your API key in the Authorization header (see this curl example).

Download a motion as FBX or GLB

All motion files are available via the following endpoint:

https://uthana.com/motion/file/motion_viewer/<character-id>/<motion-id>/<type>/<character-id>-<motion-id>.<type>
  • app_id: The application context (always use motion_viewer)
  • character_id: The character's ID
  • motion_id: The motion's ID
  • type: The file type/format (fbx, glb)

Rate limits

Rate limits are enforced on a per-organization basis. Reach out to the Uthana team to discuss your needs.

Error handling

Errors are returned in the errors array with detailed information:

{
"errors": [
{
"message": "Invalid character model format. Expected .fbx or .glb file.",
"path": ["create_character"],
"extensions": {
"code": "INVALID_MODEL_FORMAT",
"timestamp": "2025-01-15T10:30:00Z"
}
}
]
}
{
"errors": [
{
"message": "Rate limit exceeded",
"extensions": {
"code": "RATE_LIMIT_EXCEEDED",
"resetAt": "2025-01-01T00:00:00Z"
}
}
]
}