Full developer E2E path
This is the full “first real integration” path. Use it when your app needs more than a smoke-test character:
- upload authored voice, persona, style anchors, and identity image;
- optionally migrate prior chat from another companion app;
- publish playable-character and NPC relationship graphs;
- start or reuse the committed session;
- send a message and render the reply from session events;
- correlate that turn’s run, trace, settled usage, and message-level cost;
- read public-safe relationship evidence and opt-in scores.
For the shortest possible chat loop, start with
Quickstart. For this full path, keep every call server-side and
use only /v1/*.
What This Proves
When this path works, your app has proven that a user can bring a planned character plus existing relationship history into LoreOS and get back:
Fresh App/Key Acceptance
The full internal L3 has two layers:
scripts/v1_developer_happy_path_l3.pyproves the full public runtime path with a temporary provisioned app key.scripts/v1_fresh_app_key_authoring_l3.pyproves the self-serve handoff:/v1/account/*creates a fresh app andck_runtime key, then that same key validates, creates, bulk-creates, lists, starts a session, receives a real reply event, lists end-users, and exports opt-in relationship scores.
This matters because account tokens (lat_...) and Clerk Account API keys
(ak_...) are not LoreOS runtime keys. Runtime calls to /v1/characters,
/v1/sessions, /v1/external-users, and relationship evidence must use a
LoreOS ck_... key issued for the selected app.
API Sequence
1. Create The Import
Use mode: "hybrid" when you have both an authored character bundle and prior
chat history.
identity_image.source_url must be a direct, publicly fetchable https image
URL. If the image fetch fails, the character can still publish, but
data.identity_image will contain a safe error and no portrait is registered.
2. Poll The Import
Background extraction runs through Temporal by default. The create response
contains data.temporal_workflow_id and data.temporal_task_queue.
Poll:
Wait for data.status to become ready. While it runs, check:
Use data.progress for UI state. progress.percent_complete is a coarse
checkpoint percentage, progress.heartbeat.is_stale tells you whether the
Temporal worker has stopped updating recently, and progress.poll_after_seconds
is the recommended polling delay. data.extraction_summary.progress is kept for
backward compatibility with older integrations.
After progress.preview_ready becomes true, inspect:
Look for staged ledger counts such as world_model_claims,
character_counterpart_beliefs, and relational_state_rows.
3. Commit The Import
For transcript imports, this synchronous step materializes actor relationship
evidence and can take longer than 60 seconds. Configure a client read timeout of
at least 180 seconds. If the client times out, poll
GET /v1/imports/{import_id}. Do not resubmit while data.status is
committing; after it becomes committed, repeat the commit request to recover
the original data.commit envelope without applying the import twice.
Important fields:
data.commit.native_actor_core is the current runtime authority. Require
native_ready: true, confirm imported_turn_count, and inspect the native lens,
memory, belief, and indexed-document counts. The legacy top-level counters such
as claims_written and relational_state_set can remain zero/false after a
successful native import, so coding agents must not use those fields alone as
the success gate.
For authoring_only, data.commit.session_id is null; create a session
yourself with POST /v1/sessions. For hybrid, use the committed session_id
because it carries the imported relationship state.
4. Publish Relationship Graphs
The two graph families are deliberately separate. Tooling can branch on the OpenAPI
x-loreos-scope field instead of guessing from the title:
Playable characters use the app-roster graph:
NPCs and supporting cast use the character graph:
Always validate, import, then publish. Published character graphs materialize NPC rows, which you can inspect with:
Include every spelling that users may type in aliases, especially localized
names such as 준 for Joon. The native runtime resolves a referenced third
party deterministically from display_name and aliases; it does not guess
transliterations. Without the matching alias, the graph is still valid and
inspectable, but that mention will not select the NPC relationship card for the
reply context.
5. Send And Render A Reply
POST /messages returns a cursor, not the reply.
Render when you see:
Do not wait for a separate delivered status before showing text that is
already committed to the event log. Delivery status is for webhook or managed
channel push attempts.
After rendering, use the run_ref returned by POST /messages to correlate the
same turn across the public observability and billing views:
Require a /runs row whose trace_id equals run_ref, /trace rows whose
run_ref equals it, and a message-cost row whose trace_id equals it. The
settled turn credits in /runs, /trace, and the message drilldown should
agree. The session usage total can be larger because it may also include other
settled work attributed to that session. Cost settlement happens after reply
generation, so poll these reads with a bounded timeout instead of assuming the
ledger is ready when the reply event first appears.
Keep include_preview=false unless a trusted operator explicitly needs message
text in a cost view. /trace contains the conversation event text by design;
/runs stays aggregate and does not expose raw model ids, prompts, provider
payloads, or private engine context.
6. Read Relationship Evidence
Start with:
Then use narrower endpoints as needed:
These are public-safe projections. They do not expose raw prompts, raw world-model rows, private Story Room plans, raw extractor traces, or provider payloads.
7. Opt In To Scores
Numeric relationship scores are redacted by default.
Default response:
Opt in per app:
After opt-in, dating schemas include final 0..1 values such as
affection_toward_user, romantic_interest, trust_level, comfort_level,
and boundary_level.
The response includes score_contract, updated_at, and version. Scores can
move down or decay, so use them for app-specific pacing/unlocks within the same
schema, not as a universal cross-app leaderboard.
For dashboard sync after opt-in:
These return final per-session score exports with session_id, character,
external_user_ref, updated_at, version, and relationship_scores[]. They
do not return raw conversation text or model traces.
To inject app-owned relationship context without overwriting canon/persona:
Use this for labels such as “founding friend” or “inner circle”. The write is a TTL-scoped, planning-guarded session context event; it does not overwrite learned relational scores and should not be contradicted by generic first-meeting assumptions.
The score endpoint never exposes raw extractor deltas, patch history, signal
events, critic reasoning, or prompt text. It may list those names under
omitted_fields to make the privacy boundary explicit.
Implementation Notes For Agents
- Use
response.data, not top-level response fields. - Persist
data.cursorfromPOST /messagesand poll withsince=cursor. - Treat unknown event types as additive.
- Use
Idempotency-KeyonPOST /messages. - Use the committed
session_idfrom a hybrid import; it is the session that contains imported relationship state. - Use direct, fetchable image URLs for import identity images.
- Do not generate against
https://api.loreos.app/openapi.json; usehttps://api.loreos.app/v1/openapi.json.