curl --request POST \
--url https://apis.viggle.ai/v1/renders/prepare \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"character": {
"kind": "upload",
"filename": "<string>",
"content_type": "<string>",
"bytes": 2
},
"motion": {
"kind": "upload",
"filename": "<string>",
"content_type": "<string>",
"bytes": 2
},
"output": {}
}
'import requests
url = "https://apis.viggle.ai/v1/renders/prepare"
payload = {
"character": {
"kind": "upload",
"filename": "<string>",
"content_type": "<string>",
"bytes": 2
},
"motion": {
"kind": "upload",
"filename": "<string>",
"content_type": "<string>",
"bytes": 2
},
"output": {}
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
character: {kind: 'upload', filename: '<string>', content_type: '<string>', bytes: 2},
motion: {kind: 'upload', filename: '<string>', content_type: '<string>', bytes: 2},
output: {}
})
};
fetch('https://apis.viggle.ai/v1/renders/prepare', 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/renders/prepare",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'character' => [
'kind' => 'upload',
'filename' => '<string>',
'content_type' => '<string>',
'bytes' => 2
],
'motion' => [
'kind' => 'upload',
'filename' => '<string>',
'content_type' => '<string>',
'bytes' => 2
],
'output' => [
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$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/renders/prepare"
payload := strings.NewReader("{\n \"character\": {\n \"kind\": \"upload\",\n \"filename\": \"<string>\",\n \"content_type\": \"<string>\",\n \"bytes\": 2\n },\n \"motion\": {\n \"kind\": \"upload\",\n \"filename\": \"<string>\",\n \"content_type\": \"<string>\",\n \"bytes\": 2\n },\n \"output\": {}\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
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/renders/prepare")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"character\": {\n \"kind\": \"upload\",\n \"filename\": \"<string>\",\n \"content_type\": \"<string>\",\n \"bytes\": 2\n },\n \"motion\": {\n \"kind\": \"upload\",\n \"filename\": \"<string>\",\n \"content_type\": \"<string>\",\n \"bytes\": 2\n },\n \"output\": {}\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://apis.viggle.ai/v1/renders/prepare")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"character\": {\n \"kind\": \"upload\",\n \"filename\": \"<string>\",\n \"content_type\": \"<string>\",\n \"bytes\": 2\n },\n \"motion\": {\n \"kind\": \"upload\",\n \"filename\": \"<string>\",\n \"content_type\": \"<string>\",\n \"bytes\": 2\n },\n \"output\": {}\n}"
response = http.request(request)
puts response.read_body{
"draft_id": "<string>",
"state": "awaiting_uploads",
"uploads": [
{
"slot": "character",
"upload_handle": "<string>",
"method": "PUT",
"url": "<string>",
"required_headers": {},
"expires_at": "2023-11-07T05:31:56Z"
}
]
}{
"error": {
"code": "UNAUTHENTICATED",
"message": "<string>",
"retryable": true,
"request_id": "<string>",
"details": {},
"remediation": {
"action": "<string>",
"retry_after_ms": 1
}
}
}{
"error": {
"code": "UNAUTHENTICATED",
"message": "<string>",
"retryable": true,
"request_id": "<string>",
"details": {},
"remediation": {
"action": "<string>",
"retry_after_ms": 1
}
}
}{
"error": {
"code": "UNAUTHENTICATED",
"message": "<string>",
"retryable": true,
"request_id": "<string>",
"details": {},
"remediation": {
"action": "<string>",
"retry_after_ms": 1
}
}
}{
"error": {
"code": "UNAUTHENTICATED",
"message": "<string>",
"retryable": true,
"request_id": "<string>",
"details": {},
"remediation": {
"action": "<string>",
"retry_after_ms": 1
}
}
}{
"error": {
"code": "UNAUTHENTICATED",
"message": "<string>",
"retryable": true,
"request_id": "<string>",
"details": {},
"remediation": {
"action": "<string>",
"retry_after_ms": 1
}
}
}{
"error": {
"code": "UNAUTHENTICATED",
"message": "<string>",
"retryable": true,
"request_id": "<string>",
"details": {},
"remediation": {
"action": "<string>",
"retry_after_ms": 1
}
}
}{
"error": {
"code": "UNAUTHENTICATED",
"message": "<string>",
"retryable": true,
"request_id": "<string>",
"details": {},
"remediation": {
"action": "<string>",
"retry_after_ms": 1
}
}
}Prepare Render
Prepare a render and receive direct-upload plans for any media that needs uploading.
curl --request POST \
--url https://apis.viggle.ai/v1/renders/prepare \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"character": {
"kind": "upload",
"filename": "<string>",
"content_type": "<string>",
"bytes": 2
},
"motion": {
"kind": "upload",
"filename": "<string>",
"content_type": "<string>",
"bytes": 2
},
"output": {}
}
'import requests
url = "https://apis.viggle.ai/v1/renders/prepare"
payload = {
"character": {
"kind": "upload",
"filename": "<string>",
"content_type": "<string>",
"bytes": 2
},
"motion": {
"kind": "upload",
"filename": "<string>",
"content_type": "<string>",
"bytes": 2
},
"output": {}
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
character: {kind: 'upload', filename: '<string>', content_type: '<string>', bytes: 2},
motion: {kind: 'upload', filename: '<string>', content_type: '<string>', bytes: 2},
output: {}
})
};
fetch('https://apis.viggle.ai/v1/renders/prepare', 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/renders/prepare",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'character' => [
'kind' => 'upload',
'filename' => '<string>',
'content_type' => '<string>',
'bytes' => 2
],
'motion' => [
'kind' => 'upload',
'filename' => '<string>',
'content_type' => '<string>',
'bytes' => 2
],
'output' => [
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$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/renders/prepare"
payload := strings.NewReader("{\n \"character\": {\n \"kind\": \"upload\",\n \"filename\": \"<string>\",\n \"content_type\": \"<string>\",\n \"bytes\": 2\n },\n \"motion\": {\n \"kind\": \"upload\",\n \"filename\": \"<string>\",\n \"content_type\": \"<string>\",\n \"bytes\": 2\n },\n \"output\": {}\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
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/renders/prepare")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"character\": {\n \"kind\": \"upload\",\n \"filename\": \"<string>\",\n \"content_type\": \"<string>\",\n \"bytes\": 2\n },\n \"motion\": {\n \"kind\": \"upload\",\n \"filename\": \"<string>\",\n \"content_type\": \"<string>\",\n \"bytes\": 2\n },\n \"output\": {}\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://apis.viggle.ai/v1/renders/prepare")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"character\": {\n \"kind\": \"upload\",\n \"filename\": \"<string>\",\n \"content_type\": \"<string>\",\n \"bytes\": 2\n },\n \"motion\": {\n \"kind\": \"upload\",\n \"filename\": \"<string>\",\n \"content_type\": \"<string>\",\n \"bytes\": 2\n },\n \"output\": {}\n}"
response = http.request(request)
puts response.read_body{
"draft_id": "<string>",
"state": "awaiting_uploads",
"uploads": [
{
"slot": "character",
"upload_handle": "<string>",
"method": "PUT",
"url": "<string>",
"required_headers": {},
"expires_at": "2023-11-07T05:31:56Z"
}
]
}{
"error": {
"code": "UNAUTHENTICATED",
"message": "<string>",
"retryable": true,
"request_id": "<string>",
"details": {},
"remediation": {
"action": "<string>",
"retry_after_ms": 1
}
}
}{
"error": {
"code": "UNAUTHENTICATED",
"message": "<string>",
"retryable": true,
"request_id": "<string>",
"details": {},
"remediation": {
"action": "<string>",
"retry_after_ms": 1
}
}
}{
"error": {
"code": "UNAUTHENTICATED",
"message": "<string>",
"retryable": true,
"request_id": "<string>",
"details": {},
"remediation": {
"action": "<string>",
"retry_after_ms": 1
}
}
}{
"error": {
"code": "UNAUTHENTICATED",
"message": "<string>",
"retryable": true,
"request_id": "<string>",
"details": {},
"remediation": {
"action": "<string>",
"retry_after_ms": 1
}
}
}{
"error": {
"code": "UNAUTHENTICATED",
"message": "<string>",
"retryable": true,
"request_id": "<string>",
"details": {},
"remediation": {
"action": "<string>",
"retry_after_ms": 1
}
}
}{
"error": {
"code": "UNAUTHENTICATED",
"message": "<string>",
"retryable": true,
"request_id": "<string>",
"details": {},
"remediation": {
"action": "<string>",
"retry_after_ms": 1
}
}
}{
"error": {
"code": "UNAUTHENTICATED",
"message": "<string>",
"retryable": true,
"request_id": "<string>",
"details": {},
"remediation": {
"action": "<string>",
"retry_after_ms": 1
}
}
}prepare returns short-lived direct-upload URLs; upload the character/motion bytes straight to those URLs, then call the application/json form of Render Video (from Character and/or Motion) with the returned draft_id.
Request parameters
Sendapplication/json.
| Parameter | Type | Required | Description |
|---|---|---|---|
character | object | Yes | Character source descriptor. Choose exactly one form: direct-upload metadata, a public HTTPS URL, or the ID of a ready reusable Character. |
motion | object | Yes | Motion source descriptor. Choose direct-upload metadata, a public HTTPS URL, a ready reusable Motion ID, or an official Viggle motion template ID. |
character.kind / motion.kind | string | Yes | Source discriminator: upload, url, or asset; the motion source also accepts official_motion. The selected value determines which sibling fields are required. |
character.filename / motion.filename | string | With kind=upload | Original filename including its extension. It is used to identify and validate the media in the subsequent direct upload. |
character.content_type / motion.content_type | string | With kind=upload | MIME type such as image/png or video/mp4. Send the same value in the direct-upload request’s Content-Type header. |
character.bytes / motion.bytes | integer | With kind=upload | Exact file size in bytes, greater than zero. The uploaded object’s size is checked against this declaration. |
character.url / motion.url | string | With kind=url | Publicly reachable HTTPS URL of the source media. It must remain accessible while the draft is prepared and consumed. |
character.asset_id / motion.asset_id | string | With kind=asset | ID of a ready reusable asset owned by the caller. Use a Character ID in character and a Motion ID in motion. |
motion.motion_id | string | With kind=official_motion | Identifier of the official Viggle motion template to use as the driving motion. This form is available only for the motion slot. |
output | object | No | Optional output configuration. Omit it to use the pipeline’s default background treatment and source aspect ratio. |
output.background_mode | string | No | original, green, white, or transparent. green/white are translated to the pipeline’s solid mode with the matching colour. |
output.aspect_ratio | string | No | Only source is served today; 16:9, 9:16, and 1:1 are accepted values but currently rejected rather than silently ignored, since the render pipeline does not reframe yet. |
kind: "upload" declares an upload slot without sending bytes yet — supply the file’s filename, content_type, and bytes size, then PUT the actual bytes to the URL returned below. Omitting output entirely uses the pipeline’s default background and aspect ratio, same as omitting it on the direct multipart/form-data form of Render Video (from Character and/or Motion).
Response parameters
Returns200 OK.
| Field | Type | Always present | Description |
|---|---|---|---|
draft_id | string | Yes | Pass this to the JSON form of Render Video (from Character and/or Motion). |
state | string | Yes | awaiting_uploads if any kind: "upload" source was declared, otherwise ready_to_create. |
uploads | array | Yes | One entry per declared upload slot. Empty when every source was url/asset/official_motion. |
uploads[].slot | string | Yes | character or motion. |
uploads[].upload_handle | string | Yes | Echo this in upload_completions when creating the render. |
uploads[].method | string | Yes | Always PUT. |
uploads[].url | string | Yes | Short-lived direct-upload URL. |
uploads[].required_headers | object | Yes | Headers to send on the PUT request, as {name: value}. |
uploads[].expires_at | string | Yes | ISO 8601 expiry of the upload URL. |
{
"draft_id": "draft_abc123",
"state": "awaiting_uploads",
"uploads": [
{
"slot": "character",
"upload_handle": "up_1",
"method": "PUT",
"url": "https://uploads.viggle.ai/draft_abc123/character?sig=...",
"required_headers": { "Content-Type": "image/png" },
"expires_at": "2026-07-31T09:30:00+00:00"
}
]
}
Examples
curl -X POST "https://apis.viggle.ai/v1/renders/prepare" \
-H "Authorization: Bearer $VIGGLE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"character": {"kind":"upload","filename":"character.png","content_type":"image/png","bytes":204800},
"motion": {"kind":"asset","asset_id":"mot_456def"}
}'
import os, requests
response = requests.post(
"https://apis.viggle.ai/v1/renders/prepare",
headers={"Authorization": f"Bearer {os.environ['VIGGLE_API_KEY']}"},
json={
"character": {"kind": "upload", "filename": "character.png", "content_type": "image/png", "bytes": 204800},
"motion": {"kind": "asset", "asset_id": "mot_456def"},
},
)
response.raise_for_status()
draft = response.json()
for upload in draft["uploads"]:
with open("character.png", "rb") as f:
put = requests.put(upload["url"], data=f, headers=upload["required_headers"])
put.raise_for_status()
Next step
PUT the raw bytes of each declared source to its uploads[].url with uploads[].required_headers, before expires_at. Then call Render Video (from Character and/or Motion) with Content-Type: application/json, the returned draft_id, and an upload_completions entry per uploaded slot.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
Character input for the render. Choose exactly one source form—direct upload metadata, a public HTTPS URL, or a reusable asset ID.
- Option 1
- Option 2
- Option 3
Show child attributes
Show child attributes
Motion input for the render. Choose a direct upload, public HTTPS URL, reusable Motion ID, or official motion template ID.
- Option 1
- Option 2
- Option 3
- Option 4
Show child attributes
Show child attributes
Optional output settings. Omitted fields use the service defaults described by RenderOutputOptions.
Show child attributes
Show child attributes
Response
Draft and any uploads required before render creation.
Opaque prepared-render draft identifier. Pass it unchanged to POST /v1/renders after completing any required uploads.
1Whether media uploads must be completed before creation. ready_to_create means the draft can be submitted immediately.
awaiting_uploads, ready_to_create Direct-upload plans for inputs declared with kind: upload; empty when every source is already available to the service.
Show child attributes
Show child attributes

