Quickstart
Everything below uses the base URL https://api.loreos.app and your key as
Authorization: Bearer $LOREOS_KEY. Replies are asynchronous — you send a message,
then read the reply from the session’s events.
Using Cursor, Claude Code, Codex, Devin, or another coding agent? Read Build with a coding agent first, then come back here for the raw API flow.
1. Create a character
slug and display_name are the only required fields, but set
primary_reply_language explicitly for any real character. It controls the language of
visible character replies. locale is regional/provider metadata; it does not control
speech. If you omit primary_reply_language, it defaults to en-US.
Use voice samples in the same language as primary_reply_language unless the character has
a deliberately authored code-switching style.
Representative response:
status="published" means the character is runtime-available. It does not
mean the character is production-quality. Use publication.launch_ready,
publication.authoring_quality_ready, and authoring_readiness.status for the
launch decision. A minimal character can be created and chatted with while still
returning publication.state="needs_attention".
For roster seeding, call GET /v1/characters/authoring-limits first. It tells an
agent the bulk-create limit, demo sandbox cap if relevant, readiness targets, and
publication-state meanings. Its field_enum_reference lists the allowed values + numeric
ranges for the fields that most often 422 (usage, allowed_event_sources, visibility,
behavioral_thresholds, …), so you don’t have to reverse-engineer them.
To list your characters (e.g. a roster page), call GET /v1/characters — it returns a
summary per character with character_id, app_id, slug, and display_name. Add
?expand=profile to also get each character’s visible_bio + visible_interests in the
list, so a roster UI doesn’t need an N+1 GET /v1/characters/{slug} per card. (A single
character’s full authored fields come back nested under data.content.) The default list
is active inventory only. Use GET /v1/characters?status=archived to show archived
characters, or ?status=all for active + archived. Deleted characters are not returned.
Supported primary_reply_language values:
The character may understand user input in other languages, but it will not switch its reply
language just because the latest user message did. Re-publishing the same slug with a
different primary_reply_language returns 409 primary_reply_language_fixed.
For multilingual characters, do not create one character per language. Create one character
and add locale packs with localizations, or later with
PATCH /v1/characters/{slug}/localizations/{locale}:
Start a locale-specific session with "locale":"ko-KR". LoreOS selects exactly one locale
pack for that session; other locale voice samples, examples, greetings, and localized profile
fields are not injected into the runtime context. If the developer-facing slug needs to
change, use POST /v1/characters/{slug}/rename {"slug":"new-slug"}. character_id stays
stable and the old slug remains an alias.
To remove a character from inventory without losing it, call
POST /v1/characters/{slug}/archive; restore it with
POST /v1/characters/{slug}/restore. Archived characters cannot start sessions or receive
new messages on existing sessions. To permanently remove an archived character, call
DELETE /v1/characters/{slug}?confirm_slug={canonical_slug} and poll
GET /v1/characters/{slug}/deletion until complete. Do not use
POST /v1/characters/{slug}/block for cleanup; block is a per-end-user safety control.
2. Start a session
A session is one character talking to one of your end-users. You choose the
external_user_ref — any opaque id you control (your user’s id in your system). LoreOS
never logs your end-user in; it’s just a reference.
Save the session_id from the response’s data.
Representative response:
3. Send a message
Representative response:
4. Read the reply
The character replies asynchronously. Poll the session’s events until the reply lands:
Look for a character event after the user-message cursor:
Read type as the canonical event name; event_type is a compatibility alias. Replies
arrive as message.created (role character). An authored greeting — or a
proactive message the character sends first — arrives as character.initiated. Both
carry payload.bubbles (an ordered list of message bubbles); payload.text is the
joined convenience form. Render bubbles when present.
For chat UI rendering, the committed message.created or character.initiated event is the
ready signal. Do not wait for a separate delivered status before showing the reply;
delivery fields are push-channel bookkeeping for webhook/Telegram delivery attempts, not the
source of truth for whether text is visible in the event log.
Prefer a live push? Stream instead: GET /v1/sessions/{session_id}/events/stream (SSE).
What the responses look like
Every response is wrapped in an envelope:
Errors are nested under detail: { "detail": { "code", "message", "fix" } } — read
body.detail.code, not a top-level code. The full model — async runs, end-users, usage
and limits — is in Core concepts.