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:
- 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.
- Generate Model (optional) -- Trigger 3D model generation for an existing character, then poll for status.
Authentication & Player Scoping
Same authentication as the Characters API.
| Header | Required | Description |
|---|---|---|
X-API-Key | Yes | Developer API key (est_...) |
X-Player-Id | No | If 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.
| Field | Type | Required | Description |
|---|---|---|---|
image | file | Yes | Image to analyze (jpeg/png/webp) |
appearance_prompt | string | No | Override appearance instruction (max 500 chars). Output is still capped at 750 chars. |
voice_prompt | string | No | Override voice instruction (max 300 chars). Output is still capped at 200 chars. |
persona_prompt | string | No | Override 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)"
}
| Field | Type | Required | Description |
|---|---|---|---|
image | string | Yes | Base64-encoded image data |
mime_type | string | Yes | MIME type, e.g. "image/jpeg" |
appearance_prompt | string | No | Same as multipart variant |
voice_prompt | string | No | Same as multipart variant |
persona_prompt | string | No | Same 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"
}
| Field | Type | Description |
|---|---|---|
agentId | string | Character ID |
modelStatus | string | Always "generating" on success |
meshyPreviewTaskId | string | External task ID (used internally for polling) |
Errors
| Status | Cause |
|---|---|
400 | Character has no appearance description |
404 | Character not found |
409 | Generation already in progress (status is "generating" or "preview_ready") |
502 | 3D model service failed |
503 | 3D 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
}
| Field | Type | Description |
|---|---|---|
modelStatus | string | null | One of: "generating", "preview_ready", "refining", "ready", "failed", or null if no job has run |
modelPreviewUrl | string | null | Low-poly preview GLB URL (available once status reaches "preview_ready") |
modelUrl | string | null | Final refined GLB URL (available once status reaches "ready") |
thumbnailUrl | string | null | Preview thumbnail image |
progress | number | Progress 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));
}
}