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 Video)
Create a reusable Motion from a driving video.
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>"
}
}multipart/form-data with a driving video uploaded as a file or supplied by URL. type selects what gets produced: render (default) creates only the render-ready 2D Motion, glb creates only the 3D skeletal animation, and all produces both and is billed for both.
This uses the same POST /v1/motions endpoint as Create Motion (from Text), but the content type and request fields are different.
type=render has no separate preprocessing charge. type=glb costs 5 credits per second of source video, rounded up. type=all is billed for both. See Pricing and retention.Request parameters
Sendmultipart/form-data.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
motion_video | file | One of | — | Driving video uploaded directly as the motion source. Supply exactly one of motion_video or motion_video_url; visible body movement is extracted for reuse. |
motion_video_url | string | One of | — | Publicly reachable HTTP(S) URL of the driving video. Supply this instead of motion_video and keep it accessible while ingestion starts. |
name | string | No | "" | Optional label for render or all. A glb-only Motion currently returns an empty name. |
type | string | No | render | Outputs to create: render produces the reusable 2D Motion (no charge), glb produces only the 3D animation (5 credits per second of source video, rounded up), and all produces both and is billed for both. |
motion_video or motion_video_url. For reusable Motions, the service generates and stores the thumbnail internally from the source video.
The following fields apply only when type is glb or all.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
enable_smoothing | boolean | No | false | Whether to smooth extracted joint motion to reduce frame-to-frame jitter. Applies only to type=glb or all and may slightly soften abrupt movements. |
target_fps | number | No | 30 | Target extraction and GLB timeline frame rate. Must be greater than 0; omit to use the extraction service’s 30 FPS default. |
task_id | string | No | — | Caller-generated idempotency ID for type=glb. Use a unique, stable value when retrying the same work; for type=all, the service keys the companion extraction with the created Motion ID. |
Response parameters
Returns200 OK with a 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 until ready; ready render-capable Motions include video_render. |
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 | Echoes the requested type: render, glb, or all. |
glb | object or null | Yes | null for a render-only Motion. Otherwise {status, skeletons, error} — skeletons is empty until ready, then [mixamo, metahuman] (both variants are always generated together). |
vsplat | null | Yes | Always null for a Motion. |
{
"id": "mot_550e8400-e29b-41d4-a716-446655440000",
"status": "queued",
"name": "Dance loop",
"progress": 0,
"capabilities": [],
"created_at": "2026-07-21T10:00:00+00:00",
"completed_at": null,
"error": null,
"type": "render",
"vsplat": null,
"glb": null
}
Examples
file, _ := os.Open("dance.mp4"); defer file.Close()
body := &bytes.Buffer{}; writer := multipart.NewWriter(body)
part, _ := writer.CreateFormFile("motion_video", "dance.mp4"); io.Copy(part, file)
writer.WriteField("name", "Dance loop"); writer.Close()
req, _ := http.NewRequest("POST", "https://apis.viggle.ai/v1/motions", body)
req.Header.Set("Authorization", "Bearer "+os.Getenv("VIGGLE_API_KEY")); req.Header.Set("Content-Type", writer.FormDataContentType())
resp, err := http.DefaultClient.Do(req); if err != nil { panic(err) }; defer resp.Body.Close()
const form = new FormData(); form.append("motion_video", videoFile); form.append("name", "Dance loop");
const response = await fetch("https://apis.viggle.ai/v1/motions", {method:"POST", headers:{Authorization:`Bearer ${process.env.VIGGLE_API_KEY}`}, body:form});
const motion = await response.json();
import os, requests
with open("dance.mp4", "rb") as video:
response = requests.post("https://apis.viggle.ai/v1/motions", headers={"Authorization": f"Bearer {os.environ['VIGGLE_API_KEY']}"}, files={"motion_video": video}, data={"name": "Dance loop"})
response.raise_for_status(); motion = response.json()
curl -X POST "https://apis.viggle.ai/v1/motions" -H "Authorization: Bearer $VIGGLE_API_KEY" -F "[email protected]" -F "name=Dance loop"
type=glb (or all to keep the render too):
curl -X POST "https://apis.viggle.ai/v1/motions" \
-H "Authorization: Bearer $VIGGLE_API_KEY" \
-F "[email protected]" \
-F "type=all" \
-F "enable_smoothing=true" \
-F "target_fps=30"
Next step
Use Get Motion to poll the ID, then Export 3D Motion (for 3D/game engines) once itsglb.status is ready.
Create Motion (from Text)
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

