curl --request POST \
--url https://apis.viggle.ai/v1/characters \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: multipart/form-data' \
--form image='@example-file' \
--form 'image_url=<string>' \
--form name= \
--form type=render \
--form enhance=false \
--form joint_set=body \
--form model_precision=0.5 \
--form filter_low_quality=false \
--form render_thumbnail=false \
--form 'task_id=<string>'import requests
url = "https://apis.viggle.ai/v1/characters"
files = { "image": ("example-file", open("example-file", "rb")) }
payload = {
"image_url": "<string>",
"name": "",
"type": "render",
"enhance": "false",
"joint_set": "body",
"model_precision": "0.5",
"filter_low_quality": "false",
"render_thumbnail": "false",
"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('image', '<string>');
form.append('image_url', '<string>');
form.append('name', '');
form.append('type', 'render');
form.append('enhance', 'false');
form.append('joint_set', 'body');
form.append('model_precision', '0.5');
form.append('filter_low_quality', 'false');
form.append('render_thumbnail', 'false');
form.append('task_id', '<string>');
const options = {method: 'POST', headers: {Authorization: 'Bearer <token>'}};
options.body = form;
fetch('https://apis.viggle.ai/v1/characters', 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/characters",
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=\"image\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"image_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=\"enhance\"\r\n\r\nfalse\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"joint_set\"\r\n\r\nbody\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"model_precision\"\r\n\r\n0.5\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"filter_low_quality\"\r\n\r\nfalse\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"render_thumbnail\"\r\n\r\nfalse\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/characters"
payload := strings.NewReader("-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"image\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"image_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=\"enhance\"\r\n\r\nfalse\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"joint_set\"\r\n\r\nbody\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"model_precision\"\r\n\r\n0.5\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"filter_low_quality\"\r\n\r\nfalse\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"render_thumbnail\"\r\n\r\nfalse\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/characters")
.header("Authorization", "Bearer <token>")
.body("-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"image\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"image_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=\"enhance\"\r\n\r\nfalse\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"joint_set\"\r\n\r\nbody\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"model_precision\"\r\n\r\n0.5\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"filter_low_quality\"\r\n\r\nfalse\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"render_thumbnail\"\r\n\r\nfalse\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/characters")
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=\"image\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"image_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=\"enhance\"\r\n\r\nfalse\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"joint_set\"\r\n\r\nbody\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"model_precision\"\r\n\r\n0.5\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"filter_low_quality\"\r\n\r\nfalse\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"render_thumbnail\"\r\n\r\nfalse\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>"
}
}{
"error": {
"code": "authentication_required",
"message": "<string>",
"request_id": "<string>"
}
}Create Character (from Image)
Create a reusable Character from an image, optionally extracting a 3D vsplat.
curl --request POST \
--url https://apis.viggle.ai/v1/characters \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: multipart/form-data' \
--form image='@example-file' \
--form 'image_url=<string>' \
--form name= \
--form type=render \
--form enhance=false \
--form joint_set=body \
--form model_precision=0.5 \
--form filter_low_quality=false \
--form render_thumbnail=false \
--form 'task_id=<string>'import requests
url = "https://apis.viggle.ai/v1/characters"
files = { "image": ("example-file", open("example-file", "rb")) }
payload = {
"image_url": "<string>",
"name": "",
"type": "render",
"enhance": "false",
"joint_set": "body",
"model_precision": "0.5",
"filter_low_quality": "false",
"render_thumbnail": "false",
"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('image', '<string>');
form.append('image_url', '<string>');
form.append('name', '');
form.append('type', 'render');
form.append('enhance', 'false');
form.append('joint_set', 'body');
form.append('model_precision', '0.5');
form.append('filter_low_quality', 'false');
form.append('render_thumbnail', 'false');
form.append('task_id', '<string>');
const options = {method: 'POST', headers: {Authorization: 'Bearer <token>'}};
options.body = form;
fetch('https://apis.viggle.ai/v1/characters', 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/characters",
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=\"image\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"image_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=\"enhance\"\r\n\r\nfalse\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"joint_set\"\r\n\r\nbody\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"model_precision\"\r\n\r\n0.5\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"filter_low_quality\"\r\n\r\nfalse\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"render_thumbnail\"\r\n\r\nfalse\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/characters"
payload := strings.NewReader("-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"image\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"image_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=\"enhance\"\r\n\r\nfalse\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"joint_set\"\r\n\r\nbody\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"model_precision\"\r\n\r\n0.5\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"filter_low_quality\"\r\n\r\nfalse\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"render_thumbnail\"\r\n\r\nfalse\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/characters")
.header("Authorization", "Bearer <token>")
.body("-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"image\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"image_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=\"enhance\"\r\n\r\nfalse\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"joint_set\"\r\n\r\nbody\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"model_precision\"\r\n\r\n0.5\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"filter_low_quality\"\r\n\r\nfalse\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"render_thumbnail\"\r\n\r\nfalse\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/characters")
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=\"image\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n<string>\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"image_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=\"enhance\"\r\n\r\nfalse\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"joint_set\"\r\n\r\nbody\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"model_precision\"\r\n\r\n0.5\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"filter_low_quality\"\r\n\r\nfalse\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"render_thumbnail\"\r\n\r\nfalse\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>"
}
}{
"error": {
"code": "authentication_required",
"message": "<string>",
"request_id": "<string>"
}
}status is ready before using its ID in a Render.
type selects what gets produced from the source image: render (default) creates only the render-ready 2D Character, vsplat creates only the 3D Gaussian splat, and all produces both and is billed for both. Once the vsplat extraction is ready, fetch its download URL from Export 3D Character (for 3D/game engines).
type=render costs 1 credit flat. type=vsplat costs 25 credits flat, or 30 credits flat with enhance=true — enhance only affects the vsplat extraction and replaces the 25-credit price rather than adding to it. type=all is billed for both: 26 credits, or 31 credits with enhance=true. See Pricing and retention.Request parameters
Sendmultipart/form-data.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
image | file | One of | — | Character source image uploaded directly. PNG, JPEG, and WebP are accepted; supply exactly one of image or image_url. |
image_url | string | One of | — | Publicly reachable HTTP(S) URL of the character source image. Supply this instead of image and keep it accessible while ingestion starts. |
name | string | No | "" | Optional display label returned by detail and list operations. It helps identify the Character but does not affect generation. |
type | string | No | render | Outputs to create: render produces the reusable 2D Character (1 credit), vsplat produces only the 3D Gaussian-splat asset (25 credits, or 30 with enhance), and all produces both and is billed for both (26 or 31 credits). |
enhance | boolean | No | false | Whether to run an additional AI enhancement pass over the vsplat extraction. Only takes effect when type is vsplat or all — it has no effect and isn’t billed when type=render. Raises the vsplat cost from 25 to 30 credits flat (a replacement, not an add-on). |
image or image_url.
The following fields apply only when type is vsplat or all.
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
model_precision | number | No | — | Optional model-level extraction precision in (0, 1]. Applies upstream, at the pkl extraction stage, before encoding. |
filter_low_quality | boolean | No | false | Master switch for pruning low-importance Gaussian splats out of the extraction before encoding. When true, splats below the extraction service’s quality threshold are removed. |
joint_set | string | No | body | Rig the vsplat is skinned to: full, expression, or body. Doesn’t affect price. |
render_thumbnail | boolean | No | false | Whether to generate the standard Character preview thumbnail with the vsplat extraction. The service selects the render source and pose internally. |
task_id | string | No | — | Caller-generated idempotency ID for type=vsplat. Use a unique, stable value when retrying the same work; for type=all, the service keys the companion extraction with the created Character ID. |
joint_set rigs. full keeps all 441 joints. expression keeps the 119-joint body-plus-facial-expression rig. body keeps the 86-joint body-only rig, dropping every facial joint. Reducing the joint set only changes the joint list and the columns of the skinning matrix W — the splat count itself is untouched, so a body character still has a full-detail face, it just can’t animate it. If you omit joint_set, the V1 API resolves it to body (the extraction service’s own internal default is full, but V1 always fills in body first). A Motion used to drive this Character must be extracted on the same joint_set.Response parameters
Returns200 OK with a Character object.
| Field | Type | Always present | Description |
|---|---|---|---|
id | string | Yes | Public Character ID, for example char_550e8400-e29b-41d4-a716-446655440000. Store it exactly as returned. |
status | string | Yes | Initial status is normally queued. Terminal values are ready and failed. On an all Character, status only reaches ready once both the 2D render and the vsplat extraction have; if either fails, status is failed while vsplat.status shows which one it was. |
name | string | Yes | The 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 Characters include video_render. |
created_at | string or null | Yes | ISO 8601 timestamp with a UTC offset, for example 2026-07-31T09:15:22+00:00. |
completed_at | string or null | Yes | ISO 8601 completion timestamp; null before completion. |
error | object or null | Yes | Structured error when status is failed; otherwise null. |
type | string | Yes | Echoes the requested type: render, vsplat, or all. |
vsplat | object or null | Yes | null for a render-only Character. Otherwise {status, error} — the vsplat extraction’s own status. Never carries a download URL; use Export 3D Character (for 3D/game engines) for that. |
glb | null | Yes | Always null for a Character. |
{
"id": "char_550e8400-e29b-41d4-a716-446655440000",
"status": "queued",
"name": "Presenter",
"progress": 0,
"capabilities": [],
"created_at": "2026-07-31T09:15:22+00:00",
"completed_at": null,
"error": null,
"type": "render",
"vsplat": null,
"glb": null
}
Examples
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, _ := writer.CreateFormFile("image", "character.png")
file, _ := os.Open("character.png")
defer file.Close()
io.Copy(part, file)
writer.WriteField("name", "Presenter")
writer.Close()
req, _ := http.NewRequest("POST", "https://apis.viggle.ai/v1/characters", 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("image", imageFile);
form.append("name", "Presenter");
const response = await fetch("https://apis.viggle.ai/v1/characters", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.VIGGLE_API_KEY}` },
body: form,
});
const character = await response.json();
import os
import requests
with open("character.png", "rb") as image:
response = requests.post(
"https://apis.viggle.ai/v1/characters",
headers={"Authorization": f"Bearer {os.environ['VIGGLE_API_KEY']}"},
files={"image": image},
data={"name": "Presenter"},
)
response.raise_for_status()
character = response.json()
curl -X POST "https://apis.viggle.ai/v1/characters" \
-H "Authorization: Bearer $VIGGLE_API_KEY" \
-F "[email protected]" \
-F "name=Presenter"
-F "type=vsplat" (or all to keep the render too):
curl -X POST "https://apis.viggle.ai/v1/characters" \
-H "Authorization: Bearer $VIGGLE_API_KEY" \
-F "[email protected]" \
-F "type=all" \
-F "model_precision=0.8" \
-F "render_thumbnail=true"
-F "enhance=true" alongside type=vsplat or type=all — enhance has no effect on its own, since it only applies to the vsplat extraction (30 credits flat instead of 25):
curl -X POST "https://apis.viggle.ai/v1/characters" \
-H "Authorization: Bearer $VIGGLE_API_KEY" \
-F "[email protected]" \
-F "type=vsplat" \
-F "enhance=true"
joint_set is body. To keep facial-expression joints (or the full 441-joint rig), add -F "joint_set=expression" (or full) — remember to extract any driving Motion on the same joint_set:
curl -X POST "https://apis.viggle.ai/v1/characters" \
-H "Authorization: Bearer $VIGGLE_API_KEY" \
-F "[email protected]" \
-F "type=vsplat" \
-F "joint_set=expression"
Next step
Use Get Character to poll the returned ID until it is ready, then Export 3D Character (for 3D/game engines) to download the vsplat if requested.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 image as exactly one of image or image_url.
type selects what gets produced from it — see createCharacter.
The fields below type apply only when it is vsplat or all.
This is the complete public contract; workflow paths, output URIs,
multiview details, thumbnail cameras and pose, render source, and
rest mode are service-managed.
Character source image uploaded directly. Supply exactly one of image or image_url; supported formats are validated by the service.
Publicly reachable URL of the character source image. Supply this instead of image, and keep it accessible while the request is processed.
Optional display name for the Character. An omitted value defaults to an empty string.
Outputs to produce: render for the reusable 2D Character (1 credit), vsplat for only the 3D extraction (25 credits, 30 with enhance), or all for both (billed for both — 26 or 31 credits).
render, vsplat, all Whether to run an additional AI enhancement pass over the vsplat
extraction. Only takes effect when type is vsplat or all —
it has no effect and is not billed when type=render. Raises the
vsplat cost from 25 to 30 credits flat, replacing the base
vsplat price rather than adding to it.
Rig the vsplat is skinned to. full keeps all 441 joints;
expression keeps the 119-joint body-plus-facial-expression rig;
body keeps the 86-joint body-only rig, dropping every facial
joint. Reducing the joint set changes only joints and the
columns of the skinning matrix W — the splat count itself is
untouched, so a body character still has a full-detail face,
it just can't animate it. Applies only when type is vsplat
or all, and does not affect price. Omit to get body, the
V1 default (the extraction service's own internal default is
full, but the V1 facade always resolves an omitted value to
body before the request reaches it). A Motion used to drive
this Character must be extracted on the same joint_set.
expression, body, full Optional model-level precision parameter for vsplat extraction, greater than 0 and at most 1. Applies only to type=vsplat or all.
0 < x <= 1Master switch for pruning low-importance Gaussian splats out of
the extraction before encoding. Defaults to false (no
pruning). When true, splats below the extraction service's
quality threshold are removed. Applies only to type=vsplat or
all.
Whether to generate the standard Character thumbnail. The service chooses the render source and pose internally.
Client-supplied idempotency key for type=vsplat. For type=all,
the service keys the companion extraction with the Character ID.
Use a unique, stable non-empty value when retrying the same
creation request; reusing it for separate work causes a conflict.
1Response
Character 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

