API reference
Personas API
Create, read, update and delete your AI personas.
A persona is a reusable bundle of instruction and sampling settings that shapes how the
Krallo AI responds — a system_prompt that steers its behaviour plus the tuning knobs
(temperature, max_tokens, top_p, and the two penalties). The
/api/v1/personas endpoints let you manage your personas programmatically, so you can build,
version and delete them from your own tooling. Attach a persona to an inference call to make the AI answer
in that voice; see Chat completions.
Personas are owner-scoped. Every operation acts only on personas owned by the account
behind your API key (linked Discord and Google identities are treated as the same owner). A persona that
is not yours is indistinguishable from one that does not exist — both return 404 not_found.
The persona object
Every endpoint that returns a persona serializes it with exactly these fields:
| Field | Type | Description |
|---|---|---|
id | integer | Stable identifier for the persona. Use it in the item-level paths. |
name | string | Display name. Always present. |
description | string · null | Optional free-text note. null when unset. |
system_prompt | string | The instruction text that steers the AI’s behaviour. |
icon_emoji | string · null | Optional emoji shown alongside the persona. null when unset. |
temperature | number · null | Sampling temperature, 0–2. null means the server default applies. |
max_tokens | integer · null | Response length cap, 1–32000. null means the server default applies. |
top_p | number · null | Nucleus-sampling cutoff, 0–1. null means the server default applies. |
frequency_penalty | number · null | Repetition penalty, -2–2. null means the server default applies. |
presence_penalty | number · null | Topic-novelty penalty, -2–2. null means the server default applies. |
locked | boolean | Whether the persona is locked. A locked persona cannot be updated or deleted through this API. Always present; defaults to false. |
message_count | integer | How many messages have been exchanged under this persona. Read-only. |
The underlying model is deliberately not part of the persona object and cannot be set
through this API — model selection is governed by Krallo’s server-side routing. Responses use the
native Krallo envelope: successful bodies are wrapped as
{ "data": …, "meta": { "request_id": … } }, and errors as
{ "error": { "code", "message", … }, "meta": … }. See
Errors for the full contract.
Scopes
Each persona operation requires the matching scope on your API key:
| Operation | Method | Scope |
|---|---|---|
| List personas | GET | personas:read |
| Retrieve a persona | GET | personas:read |
| Create a persona | POST | personas:write |
| Update a persona | PATCH | personas:write |
| Delete a persona | DELETE | personas:write |
A request missing the required scope returns 403 insufficient_scope with the needed scope
echoed in error.required_scope. Grant scopes when you mint a key in the
Developer console; see Scopes and
Authentication.
List personas
/api/v1/personas
Returns your personas as a JSON array. Only personas you own are included; personas belonging to other
owners (or bound to a guild rather than a user) are never returned. Requires personas:read.
There are no query parameters — the full owned set is returned.
Request
cURLcurl https://exe.krallo.com/api/v1/personas \
-H "Authorization: Bearer krallo_live_..."
Response
{
"data": [
{
"id": 412,
"name": "Release Notes Writer",
"description": "Terse, factual changelog voice.",
"system_prompt": "You write concise, factual release notes. No marketing language.",
"icon_emoji": "📝",
"temperature": 0.4,
"max_tokens": 800,
"top_p": 1,
"frequency_penalty": 0,
"presence_penalty": 0,
"locked": false,
"message_count": 37
}
],
"meta": { "request_id": "5f3a91c0-b2d4-4e16-9a58-c07d2f4b8e31" }
}
An owner with no personas receives "data": [] with a 200 status.
Create a persona
/api/v1/personas
Creates a persona owned by your account and returns it. Requires personas:write. Responds
201 Created on success. Body is JSON. Unknown fields are ignored.
Body parameters
| Name | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Display name. Trimmed; must be non-blank and at most 120 characters. |
system_prompt |
string | Yes | Instruction text that steers the AI. Must be non-blank and at most 12,000 characters. |
description |
string | No | Free-text note. At most 600 characters. |
icon_emoji |
string | No | Emoji to display with the persona. At most 16 characters. |
temperature |
number | No | Sampling temperature. Must be between 0 and 2. |
max_tokens |
integer | No | Response length cap. Must be between 1 and 32000. |
top_p |
number | No | Nucleus-sampling cutoff. Must be between 0 and 1. |
frequency_penalty |
number | No | Repetition penalty. Must be between -2 and 2. |
presence_penalty |
number | No | Topic-novelty penalty. Must be between -2 and 2. |
Omit any optional field to leave it unset (it will serialize as null, and the server default
applies at inference time). The model is not accepted here.
Validation
Validation runs before the call is billed, so a rejected create costs nothing:
nameandsystem_promptare required. A missing or blank value returns400 invalid_request("'name' is required."/"'system_prompt' is required.").- Wrong types are rejected, not coerced. A present-but-non-numeric value for any sampling field returns
400— e.g."temperature": "hot"gives"'temperature' must be a number.", and a non-integermax_tokensgives"'max_tokens' must be an integer." - Out-of-range numbers are rejected with a message naming the allowed bounds (see the ranges above).
- Over-length strings are rejected for
name,system_prompt,descriptionandicon_emoji.
Request
cURLcurl https://exe.krallo.com/api/v1/personas \
-H "Authorization: Bearer krallo_live_..." \
-H "Content-Type: application/json" \
-d '{
"name": "Release Notes Writer",
"system_prompt": "You write concise, factual release notes. No marketing language.",
"description": "Terse, factual changelog voice.",
"icon_emoji": "📝",
"temperature": 0.4,
"max_tokens": 800
}'
Response
{
"data": {
"id": 412,
"name": "Release Notes Writer",
"description": "Terse, factual changelog voice.",
"system_prompt": "You write concise, factual release notes. No marketing language.",
"icon_emoji": "📝",
"temperature": 0.4,
"max_tokens": 800,
"top_p": null,
"frequency_penalty": null,
"presence_penalty": null,
"locked": false,
"message_count": 0
},
"meta": { "request_id": "9c1e77aa-04f3-4b8d-a25c-6e19d0b74f82" }
}
Retrieve a persona
/api/v1/personas/{id}
Returns one persona by id. Requires personas:read. An id you do not own, or one that does not
exist, returns 404 not_found — the two cases are intentionally indistinguishable. A non-numeric
path segment also returns 404.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id |
integer | Yes | The id of the persona, from a list or create response. |
Request
cURLcurl https://exe.krallo.com/api/v1/personas/412 \
-H "Authorization: Bearer krallo_live_..."
Response
{
"data": {
"id": 412,
"name": "Release Notes Writer",
"description": "Terse, factual changelog voice.",
"system_prompt": "You write concise, factual release notes. No marketing language.",
"icon_emoji": "📝",
"temperature": 0.4,
"max_tokens": 800,
"top_p": null,
"frequency_penalty": null,
"presence_penalty": null,
"locked": false,
"message_count": 37
},
"meta": { "request_id": "7a1d0e4b-83c2-4f61-95a8-d34e6b0c9f17" }
}
Update a persona
/api/v1/personas/{id}
Applies a partial update and returns the full, updated persona. Requires
personas:write. Send only the fields you want to change — any field you omit is left as-is.
Responds 200 on success. PUT is accepted and behaves identically to
PATCH (it is still a partial update, not a full replace).
Locked personas cannot be edited. If the target persona has locked: true,
the update is refused with 409 persona_locked and nothing changes. Ownership
is checked first, so a persona you do not own returns 404 not_found even if it is locked.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id |
integer | Yes | The id of the persona to update. |
Body parameters
All of the create fields are accepted, and all are optional here. Each is validated with the same rules and
bounds as on create; a wrong-typed or out-of-range value returns 400 invalid_request and the
persona is unchanged. The model is not accepted. An empty body is valid and is a no-op that
returns the persona unchanged.
| Name | Type | Required | Description |
|---|---|---|---|
name | string | No | New display name. Non-blank, at most 120 characters. |
system_prompt | string | No | New instruction text. Non-blank, at most 12,000 characters. |
description | string | No | New note. At most 600 characters. |
icon_emoji | string | No | New emoji. At most 16 characters. |
temperature | number | No | 0–2. |
max_tokens | integer | No | 1–32000. |
top_p | number | No | 0–1. |
frequency_penalty | number | No | -2–2. |
presence_penalty | number | No | -2–2. |
Request
cURLcurl -X PATCH https://exe.krallo.com/api/v1/personas/412 \
-H "Authorization: Bearer krallo_live_..." \
-H "Content-Type: application/json" \
-d '{ "temperature": 0.2, "max_tokens": 1200 }'
Response
{
"data": {
"id": 412,
"name": "Release Notes Writer",
"description": "Terse, factual changelog voice.",
"system_prompt": "You write concise, factual release notes. No marketing language.",
"icon_emoji": "📝",
"temperature": 0.2,
"max_tokens": 1200,
"top_p": null,
"frequency_penalty": null,
"presence_penalty": null,
"locked": false,
"message_count": 37
},
"meta": { "request_id": "c22f61a9-d04e-4b28-8c75-3a91e5f0d6b2" }
}
Delete a persona
/api/v1/personas/{id}
Permanently deletes one persona by id. Requires personas:write. Deletion is owner-scoped: an id
you do not own, or one that does not exist, returns 404 not_found. A locked persona cannot be
deleted and returns 409 persona_locked.
Path parameters
| Name | Type | Required | Description |
|---|---|---|---|
id |
integer | Yes | The id of the persona to delete. A non-numeric path segment returns 404 not_found. |
Request
cURLcurl -X DELETE https://exe.krallo.com/api/v1/personas/412 \
-H "Authorization: Bearer krallo_live_..."
Response
{
"data": { "id": 412, "deleted": true },
"meta": { "request_id": "e70b2c88-15d9-4a37-b642-90c1f5e8a3d4" }
}
Billing & rate limits
Every persona call — list, retrieve, create, update, and delete — is a flat metered request. Calls first
draw down your app’s free daily allowance; once that is exhausted, and if wallet billing is enabled,
each call debits a small, fixed number of credit units from the owner’s balance. If the wallet cannot
cover a billable call, the request returns 402 insufficient_credits with
required_units and balance in the error body, and nothing is changed. Validation
failures on create/update are checked before billing, and refused or errored calls are refunded and do not
burn quota. See Billing for the full model.
Requests are rate-limited per key by your app’s tier. Every authenticated response carries
X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset
headers (pre-auth 401/403 responses do not); exceeding the limit returns
429 with a Retry-After header. See
Rate limits.
Errors
Persona endpoints can return the following errors, in addition to the standard bearer-auth failures:
| Status | Code | When |
|---|---|---|
| 400 | invalid_request | Missing/blank name or system_prompt on create; a wrong-typed sampling field; an out-of-range number; or an over-length string. |
| 401 | unauthorized | No bearer token was supplied. |
| 401 | invalid_token · token_revoked · token_expired | The key is unknown, revoked, or past its expiry. |
| 402 | insufficient_credits | Over the free quota and the wallet cannot cover the billable call. |
| 403 | insufficient_scope | The key lacks personas:read (reads) or personas:write (create/update/delete). The needed scope is echoed in required_scope. |
| 403 | app_disabled · owner_suspended | The owning app is disabled, or the account is suspended. |
| 404 | not_found | The persona does not exist or is not owned by your account, a non-numeric id, or the developer platform is disabled. |
| 405 | method_not_allowed | An unsupported method for the path. |
| 409 | persona_locked | An update or delete targeted a locked persona. |
| 429 | rate_limited | The per-key rate limit was exceeded. This response uses a bare { "error": "rate_limited", "hint", "retryAfterSeconds" } shape (not the standard envelope) and carries a Retry-After header. |
| 500 | internal_error | An unexpected server-side failure; the call is refunded. |
Apart from the 429 noted above, all errors share the native envelope shape. For status
semantics and retry guidance, see
Errors. Related reading:
Memories,
Chat completions, and
Authentication.