Skip to content

Profiles & config

A profile holds an avatar’s system_prompt, backstory, tools_json and config_json. It is what the dashboard writes when you edit an avatar’s behaviour, and what every session resolves against unless you override it.

Profiles are separate from versions on purpose. A version is the sealed identity — face, voice, bundle — and changing it means re-sealing and re-syncing the GPU fleet. A profile is text: appending one is cheap, does not re-seal the bundle, does not change its content_hash, and does not make any box re-download anything. That is why prompt editing is a different operation from publishing a new version.

Profiles are append-only and hash-deduped: saving identical content returns the existing row instead of creating a duplicate. An avatar’s default_profile_id is what every mint without a profile_id resolves to.

All routes exist in both shapes — token-relative (/api/v1/avatars/…) and explicit (/api/v1/workspaces/{wsId}/avatars/…).

Create a profile

This is what “Save” in the dashboard does.

POST /api/v1/avatars/{avatarId}/profiles

Required scope: avatars:write (or user:write), plus the custom_avatars feature.

{
"system_prompt": "You are Lyra, Acme's product guide. Be concise and concrete.",
"backstory": "A former archivist who now helps people find things.",
"tools_json": { "inline": [ { "name": "lookup_order", "description": "Look up an order by id", "input_schema": { "type": "object", "properties": { "order_id": { "type": "string" } }, "required": ["order_id"] } } ] },
"config_json": { "greeting_enabled": true, "greeting_hold_ms": 2000 },
"set_default": true
}

Response 201 (or 200 — see below):

{
"id": "019fe4c1-2b7a-7000-8044-9e1d0f7a5c33",
"avatar_id": "019fe463-fbd3-7000-80e8-85dad81d3fee",
"hash": "a2271f20509d8c…",
"hash_short": "a2271f20509d",
"created_at": 1786249001,
"created_by": "usr_3f21…",
"is_default": true,
"created": true
}
DetailBehaviour
set_default: trueMakes it the avatar’s default — the dashboard sends this by default
created: true + 201A genuinely new profile
created: false + 200Identical content already existed; you got that row back
Omitted fieldStored as null, meaning “fall through to the version” — not “empty string”

The 200-vs-201 distinction lets an editor tell the user “no changes — this is already version a2271f20509d instead of appearing to save nothing.

List profiles

GET /api/v1/avatars/{avatarId}/profiles

Required scope: avatars:read (or user:read)

{
"data": [
{
"id": "019fe4c1-2b7a-7000-8044-9e1d0f7a5c33",
"hash": "a2271f20509d8c…",
"hash_short": "a2271f20509d",
"created_at": 1786249001,
"created_by": "usr_3f21…",
"is_default": true,
"sizes": { "system_prompt": 812, "backstory": 240, "tools_json": 318, "config_json": 46 }
}
],
"default_profile_id": "019fe4c1-2b7a-7000-8044-9e1d0f7a5c33"
}

The list returns sizes, not bodies — a list of forty 40 KB prompts is not a list.

Get one profile

GET /api/v1/avatars/{avatarId}/profiles/{profileId}

Same fields plus the text itself — this is what prefills an editor:

{
"id": "019fe4c1-…",
"avatar_id": "019fe463-…",
"hash_short": "a2271f20509d",
"is_default": true,
"sizes": { "system_prompt": 812, "backstory": 240, "tools_json": 318, "config_json": 46 },
"system_prompt": "You are Lyra, Acme's product guide. Be concise and concrete.",
"backstory": "A former archivist who now helps people find things.",
"tools_json": { "inline": [ ] },
"config_json": { "greeting_enabled": true }
}

Switch the default

POST /api/v1/avatars/{avatarId}/profiles/{profileId}/default

This is also how you roll back a prompt change: profiles are immutable, so the previous one is still there.

{ "ok": true, "avatar_id": "019…", "default_profile_id": "019…", "hash_short": "a2271f20509d" }

Delete a profile

DELETE /api/v1/avatars/{avatarId}/profiles/{profileId}

Deleting the current default is refused with 409 profile_is_default — switch first, then delete. Otherwise the avatar would silently fall back to its sealed version’s text, which looks like data loss to whoever is talking to it.

Stock avatars are read-only

Profile mutations on a ws_stock avatar are refused with 403 stock_read_only. Reads stay open — seeing what a stock avatar says is how you decide whether to clone it. To customize one, clone it into your own workspace; per-session overrides also work against stock avatars, since they change nothing stored.

Avatars created before profiles existed

Their profile #1 is synthesized lazily from the published version’s text on first read or mint. You never see an avatar with no profile; you may see one whose profile is newer than the avatar.

The config group

Structured options that shape behaviour without being prompt text. Stored as config_json on the profile, overridable per session with the mint’s config parameter.

The namespace is curated: unknown keys are rejected, both when authoring and at mint.

KeyTypeDefaultRangeWhat it does
greeting_enabledbooleantrueWhether the avatar speaks a proactive opener when the session starts
greeting_hold_msinteger200005000How long the box waits for your session context to arrive before the first generated turn. 0 disables the hold
renderbooleantrueSession-only. false is brain-only mode: the box runs the conversation brain but skips speech and video, and turns come back as text plus a brain payload

The defaults are descriptive — they document what the GPU fleet does when a key is unset. Setting a key is what overrides the fleet-wide behaviour.

greeting_hold_ms in practice. When a session carries overrides, that text reaches the box a moment after the connection opens. The hold makes the box wait — up to this many milliseconds — before generating its first spoken turn, so the opener reflects your context instead of the baseline persona. Raise it when the opener must always reflect per-session context; lower it (or 0) for the fastest possible first word. The scripted intro clip plays immediately either way.

Session-only keys. A key marked session-only may be sent in a mint’s config, but a profile may not store it. render is one: a stored render: false would turn off video for every session on that avatar, including the ones that expected a talking head, so authoring rejects it with 400 invalid_config_json and detail: "config_key_session_only:render". Ask for brain-only mode one session at a time.

Overriding config for one session

{ "avatar_id": "019…", "config": { "greeting_enabled": false } }

Config merges per key, not wholesale:

session config[key] ?? profile config_json[key] ?? GPU fleet default

A profile with {greeting_enabled: false, greeting_hold_ms: 3000} plus a session {greeting_hold_ms: 0} runs with greeting_enabled: false and greeting_hold_ms: 0. Sending one key never clears the others.

null for a key means “unset” and is dropped, so {}, {"greeting_enabled": null} and an absent config are identical and hash the same.

Config errors

ResponseCause
400 invalid_config + detail: "unknown_config_key:foo"Key not in the catalog
400 invalid_config + detail: "config_not_boolean:greeting_enabled"Wrong type
400 invalid_config + detail: "config_out_of_range:greeting_hold_ms"Outside the documented range
400 invalid_config + detail: "config_too_large"Over 1,024 serialized bytes
400 invalid_config_json + detail: "config_key_session_only:render"A session-only key sent to profile authoring
400 invalid_config_json + same detail vocabularyThe profile-authoring equivalent

Tools

Tools let the avatar call back into your systems mid-conversation. They resolve on the same ladder as the prompt text:

1. mint `tools` — replaces every lower layer wholesale
2. profile `tools_json` — the avatar's stored default
3. version `tools_json` — the sealed baseline
+ mint `extra_tools` — merged in on top of whatever won

Config shape

{
"inline": [
{
"name": "lookup_order",
"description": "Look up an order by its id and return status and ETA.",
"input_schema": {
"type": "object",
"properties": { "order_id": { "type": "string" } },
"required": ["order_id"]
}
}
],
"servers": ["mcp_billing"],
"allow": ["lookup_order"],
"max_calls": 20,
"history": true
}
FieldMeaning
inlineTool definitions declared here. Max 16 tools; description ≤ 300 chars; names unique
serversReferences to MCP servers registered in your workspace. Max 4
allowAllowlist filter over everything resolved — useful to narrow a stored set for one session
max_callsTool calls per session. Default 20, max 100
historyWhether conversation history is sent to your tool endpoint

Any other key is 400 unknown_tools_field:<key>.

Narrowing or replacing for one session

// Replace: only this tool, whatever the profile says
{ "avatar_id": "019…", "tools": { "allow": ["lookup_order"], "inline": [ ] } }
// Add: the avatar's stored tools PLUS one more for this session
{ "avatar_id": "019…", "extra_tools": { "inline": [ { "name": "escalate_ticket", "description": "", "input_schema": { } } ] } }

The mint response echoes what resolved as tools_resolved: ["lookup_order", "escalate_ticket"]. If that field is absent, the session has no tools.

Stored vs. sent

A malformed stored tools_json is never fatal: the mint runs with no tools rather than failing a live call. A malformed sent tools is a 400 — it is your input, this request. Same asymmetry as the prompt caps, for the same reason: authoring time is where you should learn about bad configuration, not connect time.

  • Prompt & overrides — how these stored values compose with per-session parameters
  • Avatars — the avatar and version resources these profiles hang off