Characters API
Create and manage AI characters. Characters define the persona, voice, language model, and actions for a conversational AI agent.
Base path: /api/v1/characters
Authentication & Player Scoping
All Characters API requests authenticate with the X-API-Key header. Optionally supply X-Player-Id to scope operations to a single end user.
| Header | Required | Description |
|---|---|---|
X-API-Key | Yes | Developer API key (est_...) |
X-Player-Id | No | Opaque player identifier. When present, scopes operations to characters owned by this player. Estuary trusts the API key holder to provide verified player IDs. |
X-Org-Id | No | Organization id. When present, creates and lists resources owned by that organization (you must be an active member). Absent means your personal space. Applies to single create, batch create and knowledge documents. |
Player Scoping Rules
- No
X-Player-Id-- full developer access (all characters owned by the API key account). - With
X-Player-Id-- operations are restricted to characters whoseplayer_idmatches. GET /api/v1/characters/{id}returns 404 on player mismatch (anti-enumeration -- the same response as a non-existent character).PUTandDELETEreturn 403 on player mismatch.- Players cannot modify or delete developer-created characters (those with
player_id = null).
Create a Character
POST /api/v1/characters
All configuration sections are optional -- sensible defaults are applied.
Request Body
{
"persona": {
"name": "Axiom",
"tagline": "Curious marine researcher",
"personality": "You are Axiom, an enthusiastic axolotl and marine researcher.",
"background": "Axiom has spent three years studying bioluminescent organisms."
},
"llm": {
"model": "gpt-5-mini",
"temperature": 0.8
},
"tts": {
"voice": "MF3mGyEYCl7XYWbV9V6O"
},
"actions": [
{ "name": "follow_user", "description": "Start following the user" },
{ "name": "stop_following_user", "description": "Stop following the user" }
]
}
persona object
| Field | Type | Default | Description |
|---|---|---|---|
name | string | "New Character" | Display name (1-255 chars) |
tagline | string | null | Short description (max 500 chars) |
personality | string | null | How the character speaks and behaves (max 5000 chars) |
background | string | null | Backstory and lore (max 32000 chars) |
system_prompt | string | null | Full system prompt override -- replaces the auto-generated prompt |
llm object
| Field | Type | Default | Description |
|---|---|---|---|
provider | string | "openai" | LLM provider |
model | string | "gpt-5-mini" | Model name |
temperature | number | 0.7 | Sampling temperature (0-2) |
max_tokens | integer | 2048 | Max response tokens (1-4096) |
verbosity | string | "medium" | Response length: "short", "medium", "long" |
tts object
| Field | Type | Default | Description |
|---|---|---|---|
provider | string | "inworld" | TTS provider |
model | string | "inworld-tts-2-flash" | TTS model |
voice | string | null | Provider-specific voice ID |
speed | number | 1.0 | Playback speed (0.5-2.0) |
delivery_mode | string | null | Inworld TTS-2 family only (inworld-tts-2, inworld-tts-2-flash): "STABLE", "BALANCED", or "CREATIVE". The quality knob those models use instead of temperature; ignored by other models. null = provider default. |
stt object
| Field | Type | Default | Description |
|---|---|---|---|
provider | string | "deepgram" | STT provider |
language | string | "en" | Language code (ISO 639-1) |
vad object
| Field | Type | Default | Description |
|---|---|---|---|
sensitivity | number | 0.5 | VAD sensitivity (0-1) |
min_speech_duration_ms | integer | 250 | Min speech duration in ms (50-2000) |
vlm object
| Field | Type | Default | Description |
|---|---|---|---|
model | string | "gpt-4.1-mini" | Vision-language model for image understanding |
Top-level fields
| Field | Type | Default | Description |
|---|---|---|---|
text_only | boolean | auto | Skip TTS. Auto-detected from voice config if omitted |
verbose | boolean | false | Enable verbose logging |
is_public | boolean | false | Whether the character is publicly listed |
avatar | string | null | Avatar identifier or URL |
actions | array | null | Actions the character can invoke |
document_ids | array | null | IDs of already-ingested documents to attach as a knowledge bank. See Knowledge Documents for adding documents by URL. |
actions array
Each action object:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Action identifier (snake_case) |
description | string | No | What the action does |
parameters | array | No | Parameters the action accepts |
Each parameter object:
| Field | Type | Default | Description |
|---|---|---|---|
name | string | required | Parameter name |
type | string | "string" | "string", "number", or "boolean" |
required | boolean | false | Whether the parameter is required |
description | string | null | Parameter description |
Response
Status: 201 Created
Returns a Character object.
Examples
curl -X POST https://api.estuary-ai.com/api/v1/characters \
-H "X-API-Key: est_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"persona": {
"name": "Sage",
"personality": "A wise and patient mentor who speaks in calm, measured tones."
},
"tts": { "voice": "MF3mGyEYCl7XYWbV9V6O" },
"llm": { "temperature": 0.6 }
}'
const res = await fetch("https://api.estuary-ai.com/api/v1/characters", {
method: "POST",
headers: {
"X-API-Key": "est_your_api_key",
"Content-Type": "application/json",
},
body: JSON.stringify({
persona: {
name: "Sage",
personality: "A wise and patient mentor who speaks in calm, measured tones.",
},
tts: { voice: "MF3mGyEYCl7XYWbV9V6O" },
llm: { temperature: 0.6 },
}),
});
const character = await res.json();
console.log(character.id); // use this as character_id
List Characters
GET /api/v1/characters
Returns a paginated list of your characters, ordered by creation date (newest first).
| Parameter | Type | Default | Description |
|---|---|---|---|
limit | integer | 20 | Results per page (1-100) |
offset | integer | 0 | Number of results to skip (minimum 0) |
search | string | none | Case-insensitive substring match on the character's name, tagline or ID (max 100 characters). It narrows total as well as the returned page. |
Response
{
"characters": [ /* Character objects */ ],
"total": 42,
"limit": 20,
"offset": 0
}
Example
curl "https://api.estuary-ai.com/api/v1/characters?limit=10" \
-H "X-API-Key: est_your_api_key"
# narrow the list to characters matching "sage"
curl "https://api.estuary-ai.com/api/v1/characters?search=sage&limit=10" \
-H "X-API-Key: est_your_api_key"
Get a Character
GET /api/v1/characters/{character_id}
Example
curl https://api.estuary-ai.com/api/v1/characters/550e8400-e29b-41d4-a716-446655440000 \
-H "X-API-Key: est_your_api_key"
Response
Returns a Character object.
Update a Character
PUT /api/v1/characters/{character_id}
Only provided fields are updated -- omitted fields keep their current values. The request body accepts the same fields as Create.
Example
curl -X PUT https://api.estuary-ai.com/api/v1/characters/550e8400-e29b-41d4-a716-446655440000 \
-H "X-API-Key: est_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"persona": { "name": "Sage v2" },
"llm": { "temperature": 0.9 }
}'
Response
Status: 200 OK
Returns the updated Character object.
Delete a Character
DELETE /api/v1/characters/{character_id}
Permanently deletes a character and all associated data (conversations, messages, documents).
Example
curl -X DELETE https://api.estuary-ai.com/api/v1/characters/550e8400-e29b-41d4-a716-446655440000 \
-H "X-API-Key: est_your_api_key"
Response
{
"deleted": true,
"id": "550e8400-e29b-41d4-a716-446655440000"
}
Replace the Avatar
PUT /api/v1/characters/{character_id}/avatar
Upload a new avatar image. The request is multipart/form-data with a single image file part: JPEG, PNG or WebP, 10 MB maximum.
Requires edit access to the character. When X-Player-Id is present, only the owning player may update the avatar; anyone else gets 403.
:::info Pointing at an existing URL
To set the avatar to an image you already host, send the avatar field on PUT /api/v1/characters/{character_id} instead of uploading bytes here.
:::
Example
curl -X PUT https://api.estuary-ai.com/api/v1/characters/550e8400-e29b-41d4-a716-446655440000/avatar \
-H "X-API-Key: est_your_api_key" \
-F "image=@avatar.png"
Response
Status: 200 OK
Returns the updated Character object, with avatar resolved to a URL. Validation failures (wrong format, oversized file) return 400.
Replace the 3D Model
PUT /api/v1/characters/{character_id}/model-file
Upload a 3D model for the character. The request is multipart/form-data with a single model file part: a binary glTF .glb file, 150 MB maximum. The file is validated (extension, glTF magic bytes, size) before anything is stored, so an invalid file returns 400 and changes nothing.
Requires edit access to the character. When X-Player-Id is present, only the owning player may update the model; anyone else gets 403.
Uploading a model consumes no model-generation quota. That quota applies only to POST /api/v1/characters/{character_id}/model.
Example
curl -X PUT https://api.estuary-ai.com/api/v1/characters/550e8400-e29b-41d4-a716-446655440000/model-file \
-H "X-API-Key: est_your_api_key" \
-F "model=@character.glb"
Response
Status: 200 OK
Returns the updated Character object, with model_status set to "completed" and model_url resolved to a URL.
Create Characters in Bulk
POST /api/v1/characters/batch
Create up to 100 characters in one call, each with an optional knowledge bank fetched from URLs you supply. The characters exist as soon as the request returns. Document download and ingestion continue in the background, so the endpoint returns a batch that you poll until it is finished.
Request Body
{
"requests": [
{
"customId": "npc-blacksmith",
"persona": { "name": "Torvald", "tagline": "Gruff village smith", "personality": "..." },
"tts": { "voice": "yj30vwTGJxSHezdAGsv9" },
"documents": [
{ "url": "https://cdn.example.com/lore/blacksmith.pdf", "name": "Blacksmith lore" },
{ "url": "https://files.example.com/private/guild-rules.pdf",
"headers": { "Authorization": "Bearer eyJ..." } }
]
},
{
"customId": "npc-innkeeper",
"persona": { "name": "Mara" },
"documents": [{ "url": "https://cdn.example.com/lore/blacksmith.pdf" }]
}
]
}
Each entry in requests accepts every field of Create a Character plus:
| Field | Type | Description |
|---|---|---|
customId | string | Required. Your handle for this character: 1-64 characters, letters, digits, _ and -, unique within the batch. Results are matched by customId, never by position. |
documents | array | Up to 10 documents to fetch and ingest. See document object. |
document_ids | array | Ids of documents you already ingested, for example ids returned by Knowledge Documents. Each must be ready and owned by you (or by the organization named in X-Org-Id). A document that is already attached to another character is copied. |
Unknown fields are rejected with 422.
Document object
| Field | Type | Description |
|---|---|---|
url | string | Required. https:// or http:// URL reachable from the public internet, up to 2,048 characters, without embedded credentials. Presigned S3 or GCS links work. Addresses on private networks, loopback, link-local and similar ranges are rejected, including after redirects, as are localhost and .local / .internal names. |
name | string | Display name, up to 200 characters. Defaults to the file name in the URL, or the file name the origin sends once fetched. |
type | string | "pdf" (default and the only supported type today). json, text and markdown are reserved and rejected. |
headers | object | Request headers to send when fetching a private URL, for example {"Authorization": "Bearer ..."}. At most 8 headers, 4 KB in total, 2 KB per value; hop-by-hop names such as Host, Content-Length, Proxy-* and X-Forwarded-* are refused. Encrypted at rest, dropped on cross-origin redirects, deleted once the document finishes, and never returned. |
Two items that reference the same URL share one download: the first item fetches it and the other receives a copy, flagged with a document_copied warning. If the first item fails to be created, the copy fetches on its own.
X-Player-Id, when sent, is stamped on every character in the batch.
Send Idempotency-Key (any string up to 255 characters) to make a network retry safe: the same key returns the same batch for 24 hours.
Response
Status: 202 Accepted, with Location: /api/v1/characters/batch/{id} and Retry-After: 5.
{
"id": "cb_01j9x4k2m7qz8f3e2d1c0b9a8765",
"status": "processing",
"orgId": null,
"counts": { "total": 2, "queued": 0, "processing": 2, "succeeded": 0, "failed": 0, "cancelled": 0 },
"documentCounts": { "total": 2, "queued": 2, "fetching": 0, "processing": 0, "ready": 0, "failed": 0, "cancelled": 0 },
"createdAt": "2026-09-05T18:02:11.418223Z",
"completedAt": null,
"expiresAt": "2026-10-05T18:02:11.418223Z",
"retryOf": null
}
status | Meaning |
|---|---|
processing | Documents are being fetched and ingested. |
cancelling | A cancel was requested; fetches already in flight are finishing. |
completed | Every item finished. Each item may have succeeded or failed; check counts. |
cancelled | Stopped by a cancel request. |
failed | Processing stalled. Remaining documents were marked failed with code executor_error; use retry. |
A batch with no documents comes back already completed.
Poll a batch
GET /api/v1/characters/batch/{batch_id}
Returns the batch with every item. The response carries Retry-After: 5 while status is processing or cancelling. Stop polling when status is completed, cancelled or failed. completed means every item finished, not that every item succeeded.
{
"id": "cb_01j9x4k2m7qz8f3e2d1c0b9a8765",
"status": "completed",
"orgId": null,
"counts": { "total": 2, "queued": 0, "processing": 0, "succeeded": 2, "failed": 0, "cancelled": 0 },
"documentCounts": { "total": 2, "queued": 0, "fetching": 0, "processing": 0, "ready": 1, "failed": 1, "cancelled": 0 },
"createdAt": "2026-09-05T18:02:11.418223Z",
"completedAt": "2026-09-05T18:05:40.102911Z",
"expiresAt": "2026-10-05T18:02:11.418223Z",
"retryOf": null,
"items": [
{
"customId": "npc-blacksmith",
"index": 0,
"status": "succeeded",
"characterId": "b275203f-91d2-4c8e-a6f1-3e2d1c0b9a87",
"documents": [
{ "name": "Blacksmith lore", "url": "https://cdn.example.com/lore/blacksmith.pdf", "documentId": "d1c0...", "status": "ready", "chunkCount": 128, "totalTokens": 33120 },
{ "name": "guild-rules.pdf", "url": "https://files.example.com/private/guild-rules.pdf", "documentId": "e2d1...", "status": "failed",
"error": { "code": "fetch_http_error", "message": "Origin returned 403", "httpStatus": 403 } }
],
"error": null,
"warnings": [
{ "code": "document_failed", "message": "1 of 2 documents failed; character was created without them", "path": "documents[1]" }
]
}
]
}
Item fields
| Field | Description |
|---|---|
customId | The handle you supplied. |
index | Position in requests, counting from 0. |
status | queued, processing, succeeded, failed or cancelled. A document problem never fails the item: the character is created and the problem is reported under documents and warnings. |
characterId | The created character. Set as soon as the batch is accepted; null only when the item itself failed. |
error | null unless the item failed. { "code": "executor_error", "message": "..." } means the character could not be created; retry re-creates it. |
documents[] | One entry per document you supplied, in order: name, url, documentId and status, plus chunkCount and totalTokens once ready, and error on failure. |
documents[].status | queued, fetching, processing, ready, failed or cancelled. |
documents[].url | Always shortened to scheme, host and path. Query strings (such as presigned signatures) are never echoed. |
documents[].error | { "code", "message" }, plus httpStatus for fetch_http_error. Codes below. |
warnings[] | { "code", "message", "path" } entries for things that did not go to plan without failing the item. Codes below. |
Document error codes
| Code | Meaning |
|---|---|
fetch_blocked | The URL, one of its DNS answers, or a redirect target pointed at a private, loopback, link-local or otherwise blocked address. Not retryable. |
fetch_dns_failed | The host name did not resolve. |
fetch_connection_error | The connection failed or was reset. |
fetch_timeout | Connecting took longer than 10 s or the whole download longer than 120 s. |
fetch_http_error | The origin answered with a non-2xx status, carried in httpStatus. |
fetch_too_large | The file is over 50 MB. |
fetch_too_many_redirects | More than 3 redirects. |
unsupported_type | The bytes are not a PDF (the content type header alone is not trusted), or type named a reserved format. |
pdf_unreadable | The PDF could not be opened, for example because it is corrupt or encrypted. |
too_many_pages | More than 500 pages. |
too_much_text | More than 2,000,000 characters of text. Split the file. |
no_extractable_text | No text layer, for example a scanned image. Estuary does not run OCR. |
ingest_failed | Indexing or storage failed after the download. Usually transient; retry. |
cancelled | The batch was cancelled before this document ran. |
executor_error | Processing stalled or hit an unexpected error. Retry. |
Warning codes
| Code | Meaning |
|---|---|
document_failed | One or more documents failed to ingest; path lists them (documents[1], for example). The character was created without them. Also used when a document_ids entry turned out to be in failed state. |
document_cancelled | One or more documents were cancelled. |
document_copied | The document shares a URL with another item, or the document_ids entry was already attached to another character, so it was copied rather than fetched or indexed again. |
text_only_inferred | No voice was configured, so text_only was set to true. |
document_not_found, document_not_ready, document_link_failed | A document_ids entry could not be attached while the character was being created. Submit-time validation normally catches these with 422; they appear only if the document changed in between or the copy failed. |
List, retry and cancel
GET /api/v1/characters/batch?limit=20&offset=0
POST /api/v1/characters/batch/{batch_id}/retry
POST /api/v1/characters/batch/{batch_id}/cancel
List returns { "batches": [...], "total": n, "limit": 20, "offset": 0 }, newest first, without items. Batches are scoped to your personal space or, with X-Org-Id, to that organization. Batches past their expiresAt are omitted.
Retry starts a new batch whose retryOf points at the original. It contains every failed item (and every cancelled item that never received a character), re-created from the stored request, plus, for every item that has a character, a document-only entry that re-fetches that item's failed or cancelled documents onto the same character (characterId unchanged). The optional body { "customIds": ["npc-blacksmith"], "headers": { "https://files.example.com/private/guild-rules.pdf": { "Authorization": "Bearer ..." } } } narrows the retry to some items and re-supplies headers for private URLs. Headers are deleted when a document finishes, so a retry of a private URL needs them again, keyed by the shortened URL shown in results; without them the URL is fetched anonymously. A retry counts against the same daily quota, hourly submit limit and running-batch limit as a new submit. Returns 409 while the original batch is still running and 400 when there is nothing to retry.
Cancel marks documents that have not started as cancelled (error code cancelled). Fetches already in flight finish, and every character that was created stays. The batch moves to cancelling and then cancelled. Returns 409 once the batch is already finished.
Limits and errors
| Limit | Value |
|---|---|
| Items per batch | 100 |
| Request body | 10 MB |
| Documents per item | 10 |
| Distinct document URLs per batch | 100 |
| Documents per character | 10 |
| Document size | 50 MB, 500 pages, 2,000,000 extracted characters |
| Fetch | 10 s to connect, 120 s in total, 3 redirects |
| Characters created per day | 500 per account, counting batch submits and retries |
| Batch submits per hour | 20, counting retries and single document additions |
| Running batches | 1 per account (or per organization) |
| Result retention | 30 days, then the batch returns 404 and disappears from the list |
Error bodies use the usual { "detail": ... } envelope.
| Status | When |
|---|---|
422 | The body failed validation: duplicate customId, more than 100 items, more than 100 distinct URLs, more than 10 documents on an item, a rejected URL, header or type, or an unknown field. detail is the standard validation array; each entry's loc points at the field (for example ["body", "requests", 3, "documents", 0, "url"]) and msg names the reason: duplicate_custom_id, too_many_items, too_many_documents, too_many_documents_per_item, unsupported_type, url_scheme_not_allowed, url_blocked_host, url_userinfo, url_too_long, headers_forbidden, headers_too_many, headers_too_large or headers_invalid. A document_ids entry that is missing, not yours or not ready returns detail: { "message", "errors": [{ "customId", "path", "code", "message" }] } with code document_not_found, document_not_ready or document_failed. Nothing is created. |
413 | Body over 10 MB. |
429 | The daily character quota would be exceeded: detail: { "message", "remaining", "limit" }, with Retry-After set to the seconds until midnight UTC. Or more than 20 submits in the last hour (Retry-After in seconds). Or a batch is already running for this account or organization: detail: { "message", "runningBatchId" }, Retry-After: 30. |
409 | The same Idempotency-Key is being submitted right now. |
404 | Unknown batch id, a batch older than 30 days, or a batch that belongs to another account or organization. |
403 | X-Org-Id names an organization you are not an active member of, or the request used a share session token (sst_...), which cannot manage characters. |
503 | Batch creation is temporarily disabled. |
PDFs must contain extractable text; scanned images fail with no_extractable_text (no OCR). At conversation time, a knowledge bank of up to about 30,000 tokens in total is given to the character in full on every turn, while a larger one is searched each turn and only the relevant passages are supplied. Many small documents therefore add cost to every turn; a few large ones are answered from search.
Example
curl -X POST https://api.estuary-ai.com/api/v1/characters/batch \
-H "X-API-Key: est_your_api_key" \
-H "Idempotency-Key: 6b1f0d2e-3c1a-4f7e-9b2d-0a5c8d1e2f34" \
-H "Content-Type: application/json" \
-d '{
"requests": [
{ "customId": "npc-blacksmith", "persona": { "name": "Torvald" },
"documents": [{ "url": "https://cdn.example.com/lore/blacksmith.pdf" }] },
{ "customId": "npc-innkeeper", "persona": { "name": "Mara" } }
]
}'
# then poll
curl https://api.estuary-ai.com/api/v1/characters/batch/cb_01j9x4k2m7qz8f3e2d1c0b9a8765 \
-H "X-API-Key: est_your_api_key"
Knowledge Documents
Add PDFs to a character's knowledge bank by URL, watch them ingest, download the stored copy, and remove them.
POST /api/v1/characters/{character_id}/documents
GET /api/v1/characters/{character_id}/documents
DELETE /api/v1/characters/{character_id}/documents/{document_id}
GET /api/v1/characters/documents/{document_id}
GET /api/v1/characters/documents/{document_id}/view-url
Adding or removing a document needs edit access to the character; listing, polling and downloading need read access. The player scoping rules apply, so a player cannot change a developer-created character's documents.
Add a document
The body is one document object. Ingestion runs in the background, so the response is 202 Accepted with Location: /api/v1/characters/documents/{documentId}, Retry-After: 5, and the document id to poll:
{
"documentId": "d1c0a8e2-7f3b-4c5d-9e1f-2a3b4c5d6e7f",
"batchId": "cb_01j9x4k2m7qz8f3e2d1c0b9a8765",
"status": "queued",
"name": "Blacksmith lore",
"url": "https://cdn.example.com/lore/blacksmith.pdf"
}
batchId is a one-document batch, so retry and cancel work on it; it does not count toward the running-batch limit. A character may hold at most 10 documents that are queued, in progress or ready (400 above that). Each call counts toward the 20 submits per hour shared with batch creation (429), and returns 503 while batch creation is disabled.
Poll or list documents
{
"id": "d1c0a8e2-7f3b-4c5d-9e1f-2a3b4c5d6e7f",
"agentId": "b275203f-91d2-4c8e-a6f1-3e2d1c0b9a87",
"filename": "blacksmith.pdf",
"status": "ready",
"contentType": "application/pdf",
"fileSize": 482113,
"chunkCount": 128,
"totalTokens": 33120,
"pageCount": 48,
"sourceUrl": "https://cdn.example.com/lore/blacksmith.pdf",
"error": null,
"createdAt": "2026-09-05T18:02:11.418223Z",
"updatedAt": "2026-09-05T18:03:40.220481Z"
}
status moves through queued, fetching and processing and ends at ready, failed or cancelled. On failure error holds { "code", "message" } (plus httpStatus for fetch_http_error) using the document error codes. sourceUrl is the shortened URL; fileSize, chunkCount, totalTokens and pageCount are filled in as ingestion progresses. Polling one document carries Retry-After: 5 until it finishes. The list endpoint returns { "documents": [...], "total": n } in every status, oldest first.
A ready document's id can be reused in document_ids when creating another character; that character receives a copy.
Download a document
GET /api/v1/characters/documents/{document_id}/view-url
Returns a short-lived link to the stored file, available once the document is ready:
{
"url": "https://...",
"filename": "blacksmith.pdf",
"contentType": "application/pdf",
"fileSizeBytes": 482113,
"expiresInSeconds": 900
}
The link expires after 15 minutes and the response is sent with Cache-Control: no-store, so request a fresh one each time instead of storing it.
Remove a document
DELETE returns 204 and removes the document, its search index, and the stored file when no other character references it. Returns 409 while the document is still being ingested; cancel its batch first.
Example
curl -X POST https://api.estuary-ai.com/api/v1/characters/b275203f-91d2-4c8e-a6f1-3e2d1c0b9a87/documents \
-H "X-API-Key: est_your_api_key" \
-H "Content-Type: application/json" \
-d '{ "url": "https://cdn.example.com/lore/blacksmith.pdf", "name": "Blacksmith lore" }'
# poll until status is ready or failed
curl https://api.estuary-ai.com/api/v1/characters/documents/d1c0a8e2-7f3b-4c5d-9e1f-2a3b4c5d6e7f \
-H "X-API-Key: est_your_api_key"
Character Object
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"name": "Sage",
"tagline": "A wise mentor",
"personality": "A wise and patient mentor...",
"background": null,
"appearance": "A silver-haired figure in a deep blue robe",
"avatar": null,
"source_image_url": null,
"system_prompt": null,
"tts_provider": "inworld",
"tts_model": "inworld-tts-2-flash",
"tts_voice": "MF3mGyEYCl7XYWbV9V6O",
"generated_voice_id": null,
"player_id": null,
"tts_speed": 1.0,
"tts_delivery_mode": "BALANCED",
"vlm_model": "gpt-4.1-mini",
"llm_provider": "openai",
"llm_model": "gpt-5-mini",
"llm_temperature": 0.6,
"llm_max_tokens": 2048,
"llm_verbosity": "medium",
"stt_provider": "deepgram",
"stt_language": "en",
"vad_sensitivity": 0.5,
"vad_min_speech_duration_ms": 250,
"text_only": false,
"verbose": false,
"actions": [
{
"name": "wave",
"description": "Wave at the user"
}
],
"is_public": false,
"is_active": false,
"launch_status": "stopped",
"created_at": "2026-01-15T10:30:00",
"updated_at": "2026-01-15T10:30:00"
}
| Field | Type | Description |
|---|---|---|
id | string | Character UUID -- use as character_id in SDK connections and as agent_id in Conversations/Memories APIs |
name | string | Display name |
player_id | string | null | Owning player ID. null means the character was created by a developer (not by a player). |
tagline | string | null | Short description |
personality | string | null | Personality description |
background | string | null | Backstory |
appearance | string | null | Visual description of the character, used when generating a 3D model |
avatar | string | null | Avatar URL |
source_image_url | string | null | The image this character was generated from, when it was created with character generation |
system_prompt | string | null | System prompt override |
tts_provider | string | TTS provider |
tts_model | string | TTS model |
tts_delivery_mode | string | null | Inworld TTS-2 delivery mode (STABLE / BALANCED / CREATIVE), null when unset |
tts_voice | string | null | TTS voice ID |
generated_voice_id | string | null | Voice created for this character by voice generation, null when the character uses a stock voice |
tts_speed | number | Playback speed |
vlm_model | string | Vision model |
llm_provider | string | LLM provider |
llm_model | string | LLM model |
llm_temperature | number | Sampling temperature |
llm_max_tokens | integer | Max tokens |
llm_verbosity | string | Response length preference |
stt_provider | string | STT provider |
stt_language | string | Language code |
vad_sensitivity | number | VAD sensitivity |
vad_min_speech_duration_ms | integer | Min speech duration |
text_only | boolean | Whether TTS is disabled |
verbose | boolean | Verbose logging |
actions | array | Configured actions |
is_public | boolean | Publicly listed |
is_active | boolean | Currently running |
launch_status | string | "stopped", "starting", or "running" |
created_at | string | ISO 8601 timestamp |
updated_at | string | ISO 8601 timestamp |