Quickstart

Your first character reply in four calls.

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. (Add a persona, voice, and a face for selfies later — see Characters and Visual assets in the API Reference.)

$curl -X POST https://api.loreos.app/v1/characters \
> -H "Authorization: Bearer $LOREOS_KEY" \
> -H "Content-Type: application/json" \
> -d '{
> "slug": "luna",
> "display_name": "Luna"
>}'

Representative response:

1{
2 "schema_version": "v0",
3 "data": {
4 "character_id": "char_...",
5 "slug": "luna",
6 "display_name": "Luna",
7 "status": "published",
8 "version_number": 1
9 },
10 "next_actions": [
11 {
12 "command": "POST /v1/sessions {\"character\": \"luna\", \"external_user_ref\": \"<your-user-id>\"}",
13 "description": "Start a chat session for one of your end-users"
14 }
15 ]
16}

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.

$curl -X POST https://api.loreos.app/v1/sessions \
> -H "Authorization: Bearer $LOREOS_KEY" \
> -H "Content-Type: application/json" \
> -d '{
> "character": "luna",
> "external_user_ref": "user_123"
> }'

Save the session_id from the response’s data.

Representative response:

1{
2 "schema_version": "v0",
3 "data": {
4 "session_id": "sess_...",
5 "character": "luna",
6 "external_user_ref": "user_123"
7 },
8 "next_actions": [
9 {
10 "command": "POST /v1/sessions/sess_.../messages {\"text\": \"hello!\"}",
11 "description": "Send the end-user's first message"
12 },
13 {
14 "command": "GET /v1/sessions/sess_.../events?since=0",
15 "description": "Poll the event log for the character's replies"
16 }
17 ]
18}

3. Send a message

$curl -X POST https://api.loreos.app/v1/sessions/$SESSION_ID/messages \
> -H "Authorization: Bearer $LOREOS_KEY" \
> -H "Content-Type: application/json" \
> -d '{ "text": "hey, what are you up to?" }'

Representative response:

1{
2 "schema_version": "v0",
3 "data": {
4 "accepted": true,
5 "cursor": 12,
6 "sent_turn_index": 1
7 },
8 "next_actions": [
9 {
10 "command": "GET /v1/sessions/sess_.../events?since=12",
11 "description": "Poll for the character's reply"
12 }
13 ]
14}

4. Read the reply

The character replies asynchronously. Poll the session’s events until the reply lands:

$curl https://api.loreos.app/v1/sessions/$SESSION_ID/events \
> -H "Authorization: Bearer $LOREOS_KEY"

Look for a character event after the user-message cursor:

1{
2 "schema_version": "v0",
3 "data": {
4 "session_id": "sess_...",
5 "events": [
6 {
7 "cursor": 13,
8 "event_type": "message.created",
9 "role": "character",
10 "payload": {
11 "text": "hey, I was just wrapping up a small thing. what are you up to?",
12 "bubbles": ["hey, I was just wrapping up a small thing.", "what are you up to?"]
13 }
14 }
15 ],
16 "next_cursor": 13
17 },
18 "next_actions": [
19 {
20 "command": "GET /v1/sessions/sess_.../events?since=13",
21 "description": "Poll again for newer events"
22 }
23 ]
24}

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.

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:

1{
2 "schema_version": "...",
3 "data": { "...": "the result" },
4 "next_actions": [ { "...": "what to call next" } ]
5}

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.