Skip to main content

Character Generation API

Generate a complete character (persona, voice, appearance) from a single image, then optionally generate a 3D model from the resulting appearance description.

Base path: /api/generate

The generation pipeline has two steps:

  1. Image-to-Character -- Upload an image, receive a fully populated character with persona, voice description, and appearance description. Persona only -- no 3D model is generated.
  2. Generate Model (optional) -- Trigger 3D model generation for an existing character, then poll for status.

Authentication & Player Scoping

Same authentication as the Characters API.

HeaderRequiredDescription
X-API-KeyYesDeveloper API key (est_...)
X-Player-IdNoIf present, the created character is scoped to that player.

Image to Character

POST /api/generate/image-to-character

Generate a character persona from an uploaded image. The response includes a fully populated character with persona, voice description, and appearance description. modelStatus is null -- no 3D model is generated by this endpoint.

Content Types

This endpoint accepts two payload formats:

multipart/form-data (preferred)

Send the image as a file upload. Use this whenever the client can send multipart/form-data.

FieldTypeRequiredDescription
imagefileYesImage to analyze (jpeg/png/webp)
appearance_promptstringNoOverride appearance instruction (max 500 chars). Output is still capped at 750 chars.
voice_promptstringNoOverride voice instruction (max 300 chars). Output is still capped at 200 chars.
persona_promptstringNoOverride personality instruction (max 500 chars). Output is still capped at 400 chars.

application/json

Send the image inline as a base64-encoded string. Useful for clients that cannot send multipart bodies (e.g., Snap Spectacles).

{
"image": "<base64-encoded image>",
"mime_type": "image/jpeg",
"appearance_prompt": "Optional override (max 500 chars)",
"voice_prompt": "Optional override (max 300 chars)",
"persona_prompt": "Optional override (max 500 chars)"
}
FieldTypeRequiredDescription
imagestringYesBase64-encoded image data
mime_typestringYesMIME type, e.g. "image/jpeg"
appearance_promptstringNoSame as multipart variant
voice_promptstringNoSame as multipart variant
persona_promptstringNoSame as multipart variant

:::info JSON+base64 size limit Maximum base64 payload is approximately 7.5 MB, which encodes to roughly a 10 MB image. Prefer multipart/form-data whenever your client supports it. :::

Response

Status: 200 OK

Returns an AgentResponse (camelCase), which includes the standard character fields plus generation-specific fields:

{
"agentId": "550e8400-e29b-41d4-a716-446655440000",
"name": "Generated name",
"tagline": "Short tagline",
"personality": "Generated personality...",
"appearance": "Generated appearance description...",
"voiceDescription": "Generated voice description...",
"playerId": null,
"modelStatus": null,
"modelPreviewUrl": null,
"modelUrl": null,
"thumbnailUrl": null,
"sourceImageUrl": "https://..."
}

modelStatus is null because no 3D model is generated. To generate a 3D model after creation, call POST /api/generate/{agentId}/generate-model.

Examples

# multipart
curl -X POST https://api.estuary-ai.com/api/generate/image-to-character \
-H "X-API-Key: est_your_api_key" \
-F "image=@./photo.jpg" \
-F "persona_prompt=Generate a witty, mischievous personality."
# JSON+base64 (Spectacles-style)
B64=$(base64 -i ./photo.jpg)
curl -X POST https://api.estuary-ai.com/api/generate/image-to-character \
-H "X-API-Key: est_your_api_key" \
-H "Content-Type: application/json" \
-d "{\"image\":\"$B64\",\"mime_type\":\"image/jpeg\"}"

Generate 3D Model

POST /api/generate/{agent_id}/generate-model

Trigger 3D model generation for an existing character. The character must already have an appearance description (either from image-to-character or set manually).

This endpoint also serves as the retry path for failed generation jobs.

Response

Status: 200 OK

{
"agentId": "550e8400-e29b-41d4-a716-446655440000",
"modelStatus": "generating",
"meshyPreviewTaskId": "task_xyz123"
}
FieldTypeDescription
agentIdstringCharacter ID
modelStatusstringAlways "generating" on success
meshyPreviewTaskIdstringExternal task ID (used internally for polling)

Errors

StatusCause
400Character has no appearance description
404Character not found
409Generation already in progress (status is "generating" or "preview_ready")
5023D model service failed
5033D model service not configured

Rate Limiting

POST /api/generate/{agent_id}/generate-model shares a 50-request-per-hour bucket with POST /api/generate/image-to-character. Excess requests return 429 Too Many Requests.

Example

curl -X POST https://api.estuary-ai.com/api/generate/550e8400-e29b-41d4-a716-446655440000/generate-model \
-H "X-API-Key: est_your_api_key"

After calling this endpoint, poll GET /api/generate/{agent_id}/model-status for progress.


Get Model Status

GET /api/generate/{agent_id}/model-status

Poll the current state of 3D model generation. No player scoping is applied (operational endpoint).

Response

{
"modelStatus": "preview_ready",
"modelPreviewUrl": "https://.../preview.glb",
"modelUrl": "https://.../final.glb",
"thumbnailUrl": "https://.../thumb.png",
"progress": 60
}
FieldTypeDescription
modelStatusstring | nullOne of: "generating", "preview_ready", "refining", "ready", "failed", or null if no job has run
modelPreviewUrlstring | nullLow-poly preview GLB URL (available once status reaches "preview_ready")
modelUrlstring | nullFinal refined GLB URL (available once status reaches "ready")
thumbnailUrlstring | nullPreview thumbnail image
progressnumberProgress in percent (0-100)

Example

curl https://api.estuary-ai.com/api/generate/550e8400-e29b-41d4-a716-446655440000/model-status \
-H "X-API-Key: est_your_api_key"
async function pollModel(agentId: string, apiKey: string) {
while (true) {
const res = await fetch(
`https://api.estuary-ai.com/api/generate/${agentId}/model-status`,
{ headers: { "X-API-Key": apiKey } },
);
const data = await res.json();
if (data.modelStatus === "ready" || data.modelStatus === "failed") {
return data;
}
await new Promise((r) => setTimeout(r, 5000));
}
}