curl --request POST \
--url https://apis.viggle.ai/v1/motions \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: multipart/form-data' \
--form motion_video='@example-file' \
--form 'motion_video_url=<string>' \
--form name= \
--form type=render \
--form enable_smoothing=false \
--form target_fps=30 \
--form 'task_id=<string>'import requests
url = "https://apis.viggle.ai/v1/motions"
files = { "motion_video": ("example-file", open("example-file", "rb")) }
payload = {
"motion_video_url": "<string>",
"name": "",
"type": "render",
"enable_smoothing": "false",
"target_fps": "30",
"task_id": "<string>"
}
headers = {"Authorization": "Bearer <token>"}
response = requests.post(url, data=payload, files=files, headers=headers)
print(response.text)const form = new FormData();
form.append('motion_video', '<string>');
form.append('motion_video_url', '<string>');
form.append('name', '');
form.append('type', 'render');
form.append('enable_smoothing', 'false');
form.append('target_fps', '30');
form.append('task_id', '<string>');
const options = {method: 'POST', headers: {Authorization: 'Bearer <token>'}};
options.body = form;
fetch('https://apis.viggle.ai/v1/motions', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://apis.viggle.ai/v1/motions",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => "-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"motion_video\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"motion_video_url\"\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\n\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"type\"\r\n\r\nrender\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"enable_smoothing\"\r\n\r\nfalse\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"target_fps\"\r\n\r\n30\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"task_id\"\r\n\r\n<string>\r\n-----011000010111000001101001--",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: multipart/form-data"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://apis.viggle.ai/v1/motions"
payload := strings.NewReader("-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"motion_video\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"motion_video_url\"\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\n\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"type\"\r\n\r\nrender\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"enable_smoothing\"\r\n\r\nfalse\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"target_fps\"\r\n\r\n30\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"task_id\"\r\n\r\n<string>\r\n-----011000010111000001101001--")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://apis.viggle.ai/v1/motions")
.header("Authorization", "Bearer <token>")
.body("-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"motion_video\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"motion_video_url\"\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\n\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"type\"\r\n\r\nrender\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"enable_smoothing\"\r\n\r\nfalse\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"target_fps\"\r\n\r\n30\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"task_id\"\r\n\r\n<string>\r\n-----011000010111000001101001--")
.asString();require 'uri'
require 'net/http'
url = URI("https://apis.viggle.ai/v1/motions")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request.body = "-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"motion_video\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"motion_video_url\"\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\n\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"type\"\r\n\r\nrender\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"enable_smoothing\"\r\n\r\nfalse\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"target_fps\"\r\n\r\n30\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"task_id\"\r\n\r\n<string>\r\n-----011000010111000001101001--"
response = http.request(request)
puts response.read_body{
"id": "<string>",
"status": "queued",
"name": "<string>",
"progress": 50,
"capabilities": [
"<string>"
],
"created_at": "<string>",
"completed_at": "<string>",
"error": {
"code": "<string>",
"message": "<string>",
"request_id": "<string>"
},
"type": "render",
"vsplat": {
"status": "queued",
"error": {
"code": "<string>",
"message": "<string>",
"request_id": "<string>"
}
},
"glb": {
"status": "queued",
"skeletons": [
"mixamo"
],
"error": {
"code": "<string>",
"message": "<string>",
"request_id": "<string>"
}
}
}{
"error": {
"code": "authentication_required",
"message": "<string>",
"request_id": "<string>"
}
}{
"error": {
"code": "authentication_required",
"message": "<string>",
"request_id": "<string>"
}
}{
"error": {
"code": "authentication_required",
"message": "<string>",
"request_id": "<string>"
}
}{
"error": {
"code": "authentication_required",
"message": "<string>",
"request_id": "<string>"
}
}{
"error": {
"code": "authentication_required",
"message": "<string>",
"request_id": "<string>"
}
}{
"error": {
"code": "authentication_required",
"message": "<string>",
"request_id": "<string>"
}
}{
"error": {
"code": "authentication_required",
"message": "<string>",
"request_id": "<string>"
}
}Create Motion (from Text)
Generate a reusable 3D Motion from a text prompt.
curl --request POST \
--url https://apis.viggle.ai/v1/motions \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: multipart/form-data' \
--form motion_video='@example-file' \
--form 'motion_video_url=<string>' \
--form name= \
--form type=render \
--form enable_smoothing=false \
--form target_fps=30 \
--form 'task_id=<string>'import requests
url = "https://apis.viggle.ai/v1/motions"
files = { "motion_video": ("example-file", open("example-file", "rb")) }
payload = {
"motion_video_url": "<string>",
"name": "",
"type": "render",
"enable_smoothing": "false",
"target_fps": "30",
"task_id": "<string>"
}
headers = {"Authorization": "Bearer <token>"}
response = requests.post(url, data=payload, files=files, headers=headers)
print(response.text)const form = new FormData();
form.append('motion_video', '<string>');
form.append('motion_video_url', '<string>');
form.append('name', '');
form.append('type', 'render');
form.append('enable_smoothing', 'false');
form.append('target_fps', '30');
form.append('task_id', '<string>');
const options = {method: 'POST', headers: {Authorization: 'Bearer <token>'}};
options.body = form;
fetch('https://apis.viggle.ai/v1/motions', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://apis.viggle.ai/v1/motions",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => "-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"motion_video\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"motion_video_url\"\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\n\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"type\"\r\n\r\nrender\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"enable_smoothing\"\r\n\r\nfalse\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"target_fps\"\r\n\r\n30\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"task_id\"\r\n\r\n<string>\r\n-----011000010111000001101001--",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: multipart/form-data"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://apis.viggle.ai/v1/motions"
payload := strings.NewReader("-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"motion_video\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"motion_video_url\"\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\n\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"type\"\r\n\r\nrender\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"enable_smoothing\"\r\n\r\nfalse\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"target_fps\"\r\n\r\n30\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"task_id\"\r\n\r\n<string>\r\n-----011000010111000001101001--")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://apis.viggle.ai/v1/motions")
.header("Authorization", "Bearer <token>")
.body("-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"motion_video\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"motion_video_url\"\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\n\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"type\"\r\n\r\nrender\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"enable_smoothing\"\r\n\r\nfalse\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"target_fps\"\r\n\r\n30\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"task_id\"\r\n\r\n<string>\r\n-----011000010111000001101001--")
.asString();require 'uri'
require 'net/http'
url = URI("https://apis.viggle.ai/v1/motions")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request.body = "-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"motion_video\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"motion_video_url\"\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"name\"\r\n\r\n\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"type\"\r\n\r\nrender\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"enable_smoothing\"\r\n\r\nfalse\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"target_fps\"\r\n\r\n30\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"task_id\"\r\n\r\n<string>\r\n-----011000010111000001101001--"
response = http.request(request)
puts response.read_body{
"id": "<string>",
"status": "queued",
"name": "<string>",
"progress": 50,
"capabilities": [
"<string>"
],
"created_at": "<string>",
"completed_at": "<string>",
"error": {
"code": "<string>",
"message": "<string>",
"request_id": "<string>"
},
"type": "render",
"vsplat": {
"status": "queued",
"error": {
"code": "<string>",
"message": "<string>",
"request_id": "<string>"
}
},
"glb": {
"status": "queued",
"skeletons": [
"mixamo"
],
"error": {
"code": "<string>",
"message": "<string>",
"request_id": "<string>"
}
}
}{
"error": {
"code": "authentication_required",
"message": "<string>",
"request_id": "<string>"
}
}{
"error": {
"code": "authentication_required",
"message": "<string>",
"request_id": "<string>"
}
}{
"error": {
"code": "authentication_required",
"message": "<string>",
"request_id": "<string>"
}
}{
"error": {
"code": "authentication_required",
"message": "<string>",
"request_id": "<string>"
}
}{
"error": {
"code": "authentication_required",
"message": "<string>",
"request_id": "<string>"
}
}{
"error": {
"code": "authentication_required",
"message": "<string>",
"request_id": "<string>"
}
}{
"error": {
"code": "authentication_required",
"message": "<string>",
"request_id": "<string>"
}
}application/json to generate a 3D skeletal animation from a text prompt. Text-generated Motions always use type: glb because they do not have a render-ready 2D video counterpart.
This uses the same POST /v1/motions endpoint as Create Motion (from Video), but the content type and request fields are different. It replaces the old, now-removed POST /v1/animations/generate endpoint.
duration_seconds. This is a different rate from the per-second glb extraction on Create Motion (from Video). See Pricing and retention.Request parameters
Sendapplication/json.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
text | string (1–400 characters) | Yes | — | Natural-language description of the movement to generate. Include the action, direction, pace, and relevant body parts for a more specific result. |
duration_seconds | number | No | 5 | Requested animation length in seconds. It is rounded to the nearest frame at 30 FPS; values outside [3, 60] return 400 instead of being clamped. Does not affect the flat 10-credit price. |
guidance_scale | number | No | 5 | Controls how strongly generation follows the text prompt. Values must be between 0 and 30; out-of-range input returns 400. |
smooth | boolean | No | true | Whether to smooth the generated animation to reduce abrupt frame-to-frame joint changes. |
name | string | No | "" | Optional display label returned by detail and list operations. It does not affect motion generation. |
task_id | string | No | — | Optional caller-generated idempotency ID for safe retries. Use the same stable value for the same intended generation and a new value for separate work. |
Response parameters
Returns200 OK with a queued Motion object.
| Field | Type | Always present | Description |
|---|---|---|---|
id | string | Yes | Public Motion ID beginning with mot_. Store it exactly as returned. |
status | string | Yes | Normally queued; terminal values are ready and failed. |
name | string | Yes | Supplied label, or an empty string. |
progress | integer or null | Yes | Progress from 0 to 100 when available. |
capabilities | string[] | Yes | Empty for a text-generated Motion because it has no 2D render capability. |
created_at / completed_at | string or null | Yes | ISO 8601 timestamps. |
error | object or null | Yes | Structured failure details when status is failed. |
type | string | Yes | Always glb. |
glb | object or null | Yes | Contains {status, skeletons, error} while generating and after completion. Ready Motions provide both mixamo and metahuman skeletons. |
vsplat | null | Yes | Always null for a Motion. |
{
"id": "mot_550e8400-e29b-41d4-a716-446655440000",
"status": "queued",
"name": "Cartwheel",
"progress": 0,
"capabilities": [],
"created_at": "2026-07-21T10:00:00+00:00",
"completed_at": null,
"error": null,
"type": "glb",
"vsplat": null,
"glb": {
"status": "queued",
"skeletons": [],
"error": null
}
}
Examples
payload := strings.NewReader(`{"text":"a person doing a cartwheel","duration_seconds":5,"guidance_scale":7.5,"smooth":true,"name":"Cartwheel","task_id":"motion-cartwheel-001"}`)
req, _ := http.NewRequest("POST", "https://apis.viggle.ai/v1/motions", payload)
req.Header.Set("Authorization", "Bearer "+os.Getenv("VIGGLE_API_KEY"))
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil { panic(err) }
defer resp.Body.Close()
const response = await fetch("https://apis.viggle.ai/v1/motions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.VIGGLE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
text: "a person doing a cartwheel",
duration_seconds: 5,
guidance_scale: 7.5,
smooth: true,
name: "Cartwheel",
task_id: "motion-cartwheel-001",
}),
});
const motion = await response.json();
import os
import requests
response = requests.post(
"https://apis.viggle.ai/v1/motions",
headers={"Authorization": f"Bearer {os.environ['VIGGLE_API_KEY']}"},
json={
"text": "a person doing a cartwheel",
"duration_seconds": 5,
"guidance_scale": 7.5,
"smooth": True,
"name": "Cartwheel",
"task_id": "motion-cartwheel-001",
},
)
response.raise_for_status()
motion = response.json()
curl -X POST "https://apis.viggle.ai/v1/motions" \
-H "Authorization: Bearer $VIGGLE_API_KEY" \
-H "Content-Type: application/json" \
-d '{"text":"a person doing a cartwheel","duration_seconds":5,"guidance_scale":7.5,"smooth":true,"name":"Cartwheel","task_id":"motion-cartwheel-001"}'
Next step
Use Get Motion to poll the returned ID untilstatus is ready, then Export 3D Motion (for 3D/game engines) to retrieve the mixamo or metahuman glb.
Quick Start: Text to Motion
Authorizations
Server-side SDK clients use a project API key. Remote MCP clients use an OAuth access token. Never expose a project API key in browser code.
Headers
Optional caller-supplied correlation ID, up to 128 characters. The service returns the effective value in the response header for tracing and support.
1 - 128Optional source-channel label, up to 128 characters, used to attribute traffic to an SDK, integration, product surface, or internal workflow.
1 - 128Body
Supply the source video as exactly one of motion_video or
motion_video_url. type selects what gets produced from it — see
createMotion. The fields below type apply only when it is
glb or all.
This is the complete public contract; staged paths, output locations,
extraction templates, character PKLs, joint configuration, and
tracking masks are service-managed.
Driving video uploaded directly. Supply exactly one of motion_video or motion_video_url.
Publicly reachable URL of the driving video. Supply this instead of motion_video, and keep it accessible during ingestion.
Optional label for render or all. A glb-only Motion
currently returns an empty name.
Outputs to produce: render for the reusable 2D Motion (no separate preprocessing charge), glb for only the 3D animation (5 credits per second of source video, rounded up), or all for both.
render, glb, all Whether to apply motion smoothing during 3D extraction. Applies only to type=glb or all and may reduce frame-to-frame jitter.
Target extraction and GLB timeline frame rate. Omit to use the
extraction service's 30 FPS default. The value must be greater
than zero and applies only to type=glb or all.
x > 0Client-supplied idempotency key for type=glb. For type=all,
the service keys the companion extraction with the Motion ID.
Use a unique, stable non-empty value when retrying the same
creation request; reusing it for separate work causes a conflict.
1Response
Motion queued.
A character or motion asset. Both resources share one wire shape.
progress is null in list responses. error is always null, including
on a failed asset: read status to detect failure.
type reflects what was requested at creation. For characters it is
render, vsplat, or all; for motions it is render, glb,
or all. Assets created before type existed report render.
vsplat and glb carry that extraction's own status — never
download URLs, which come from GET /v1/characters/{character_id}/export
or GET /v1/motions/{motion_id}/export instead — and are null unless
type requested that extraction. On an all asset, the top-level
status only reaches ready once both the 2D render and the 3D
extraction have; if either fails, the top-level status is failed
while the sub-object's own status still shows which one it was.
Public asset identifier. Character IDs normally begin with char_; Motion IDs normally begin with mot_.
1Overall lifecycle of the asset. Use an asset for rendering or export only after the required capability becomes ready.
queued, processing, ready, failed, cancelled Display name supplied at creation or derived during import; it may be empty when a workflow does not accept a name.
Best-effort processing percentage from 0 through 100 on detail responses; null in list responses or when unavailable.
0 <= x <= 100What the asset can be used for. video_render appears once the
asset is ready.
ISO 8601 timestamp with a UTC offset, for example 2026-07-31T09:15:22+00:00.
ISO 8601 timestamp with a UTC offset when all requested processing reached a terminal state; null while work is active.
Reserved top-level failure detail. It is currently always null; detect failure from status and inspect extraction sub-objects when applicable.
Show child attributes
Show child attributes
Work requested when the asset was created. render produces the 2D render-ready asset, vsplat/glb requests 3D output, and all requests both supported outputs.
render, vsplat, glb, all Character vsplat extraction status. Null for motions, and for
characters whose type is render. The request parameters
(model_precision, etc.) submitted at creation are not echoed
back here — this is status only.
Show child attributes
Show child attributes
Motion 3D animation extraction/generation status. Null for
characters, and for motions whose type is render.
Show child attributes
Show child attributes

