Full developer E2E path

Import a character bundle, migrate prior chat, publish a graph, chat, and read relationship evidence.

This is the full “first real integration” path. Use it when your app needs more than a smoke-test character:

  1. upload authored voice, persona, style anchors, and identity image;
  2. optionally migrate prior chat from another companion app;
  3. publish playable-character and NPC relationship graphs;
  4. start or reuse the committed session;
  5. send a message and render the reply from session events;
  6. 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:

Product needAPI proof
Voice, samples, and style anchorsPOST /v1/imports with character.voice_samples, example_dialogues, profile, direction, life template, and seeds
Primary portraitidentity_image on import commit, or Visual Asset APIs later
Prior relationship memoryTemporal extraction progress, preview staged ledgers, and commit counts
Playable character-to-character relation/v1/world/graph/validate -> import -> publish
NPC/supporting cast/v1/characters/{slug}/world/graph/validate -> import -> publish
Chat render readinessmessage.created event with role: "character"
Relationship proofrelationship-snapshot, memories, open-loops, life-feed, identity-continuity
Affection/romantic scoresPATCH /v1/apps/{app_id} opt-in, then relationship-scores

API Sequence

POST /v1/imports
GET /v1/imports/{import_id} poll until status=ready
GET /v1/imports/{import_id}/preview
POST /v1/imports/{import_id}/commit -> character + session + identity image
POST /v1/characters create another playable character
POST /v1/world/graph/validate
POST /v1/world/graph/import
POST /v1/world/graph/versions/{graph_version_id}/publish
POST /v1/characters/{slug}/world/graph/validate
POST /v1/characters/{slug}/world/graph/import
POST /v1/characters/{slug}/world/graph/versions/{graph_version_id}/publish
GET /v1/characters/{slug}/npcs
POST /v1/sessions/{session_id}/messages
GET /v1/sessions/{session_id}/events?since={cursor}
GET /v1/sessions/{session_id}/relationship-snapshot
POST /v1/sessions/{session_id}/memories
GET /v1/sessions/{session_id}/relationship-scores
GET /v1/me
PATCH /v1/apps/{app_id} { "expose_relational_numerics": true }
GET /v1/sessions/{session_id}/relationship-scores

1. Create The Import

Use mode: "hybrid" when you have both an authored character bundle and prior chat history.

1{
2 "mode": "hybrid",
3 "external_user_ref": "user_123",
4 "external_user_display_name": "Daniel",
5 "character": {
6 "slug": "lina",
7 "display_name": "Lina",
8 "locale": "ko-KR",
9 "affordance_schema": "dating",
10 "profile": {
11 "visible_bio": "A careful product diarist with dry warmth.",
12 "visible_interests": ["late-night demos", "blue notebooks"],
13 "identity_anchors": ["dry warmth", "keeps emotional pressure low"]
14 },
15 "voice_samples": [
16 {
17 "scenario_tag": "project_memory",
18 "sample_role": "anchor",
19 "text": "네가 지우려던 버전일수록 이상하게 제일 오래 남더라."
20 }
21 ],
22 "example_dialogues": [
23 {
24 "scenario_tag": "fragile_demo",
25 "turns": [
26 { "role": "user", "text": "그 데모 얘기 아직 해도 돼?" },
27 { "role": "character", "text": "응. 대신 판정 말고 기록처럼 얘기하자." }
28 ]
29 }
30 ],
31 "greeting": {
32 "bubbles": ["왔어? 파란 노트 얘기는 아직 접어두지 않았어."]
33 }
34 },
35 "identity_image": {
36 "source_url": "https://cdn.example.com/lina.png",
37 "asset_key": "identity.primary",
38 "label": "Lina identity",
39 "visual_summary": "Canonical portrait for Lina."
40 },
41 "transcript": {
42 "metadata": { "vendor": "nomi", "character_name": "Lina" },
43 "messages": [
44 { "index": 0, "speaker": "human", "text": "Remember the demo I was afraid to ship?" },
45 { "index": 1, "speaker": "character", "text": "The one you almost deleted, yes." },
46 { "index": 2, "speaker": "human", "text": "Minseo told me not to hide it." },
47 { "index": 3, "speaker": "character", "text": "Minseo was right." }
48 ]
49 }
50}

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:

GET /v1/imports/{import_id}

Wait for data.status to become ready. While it runs, check:

1{
2 "status": "extracting",
3 "temporal_workflow_id": "v1-character-import-extraction:...",
4 "temporal_task_queue": "auto-dating-background",
5 "extraction_summary": {
6 "progress": {
7 "phase": "extracting",
8 "current_step": "conversation_signal"
9 }
10 },
11 "last_heartbeat_at": "2026-06-18T07:52:52Z"
12}

last_heartbeat_at should keep advancing during long extractor stages. After ready, inspect:

GET /v1/imports/{import_id}/preview

Look for staged ledger counts such as world_model_claims, character_counterpart_beliefs, and relational_state_rows.

3. Commit The Import

POST /v1/imports/{import_id}/commit

Important fields:

1{
2 "data": {
3 "commit": {
4 "character": { "slug": "lina", "status": "published" },
5 "session_id": "sess_...",
6 "import_batch_id": "batch_...",
7 "claims_written": 7,
8 "beliefs_written": 3,
9 "relational_state_set": true
10 },
11 "identity_image": {
12 "asset_id": "...",
13 "asset_key": "identity.primary",
14 "asset_status": "active",
15 "promotion_status": "approved_anchor",
16 "servable": true
17 }
18 }
19}

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

Playable characters use the app-roster graph:

1{
2 "edges": [
3 {
4 "source_ref": "lina",
5 "target_ref": "mira",
6 "source_node_type": "character",
7 "target_node_type": "character",
8 "relation_type": "launch_collaborator",
9 "label": "protective practical collaborator",
10 "visibility_tier": "planning_only",
11 "canon_tier": "soft_canon"
12 }
13 ]
14}

NPCs and supporting cast use the character graph:

1{
2 "npcs": [
3 {
4 "npc_ref": "joon-reviewer",
5 "display_name": "Joon",
6 "role": "late-night reviewer",
7 "supporting_cast_status": "developer_promoted",
8 "visibility_tier": "speakable_after_introduced",
9 "canon_tier": "soft_canon"
10 }
11 ],
12 "edges": [
13 {
14 "source_ref": "lina",
15 "target_ref": "joon-reviewer",
16 "target_node_type": "npc",
17 "relation_type": "trusted_reviewer",
18 "label": "trusted late-night reviewer",
19 "visibility_tier": "speakable_after_introduced"
20 }
21 ]
22}

Always validate, import, then publish. Published character graphs materialize NPC rows, which you can inspect with:

GET /v1/characters/{slug}/npcs
GET /v1/characters/{slug}/supporting-cast

5. Send And Render A Reply

POST /messages returns a cursor, not the reply.

POST /v1/sessions/{session_id}/messages
GET /v1/sessions/{session_id}/events?since={cursor}

Render when you see:

1{
2 "type": "message.created",
3 "role": "character",
4 "payload": {
5 "text": "...",
6 "bubbles": ["...", "..."]
7 }
8}

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.

6. Read Relationship Evidence

Start with:

GET /v1/sessions/{session_id}/relationship-snapshot

Then use narrower endpoints as needed:

GET /v1/sessions/{session_id}/memories
POST /v1/sessions/{session_id}/memories
GET /v1/sessions/{session_id}/open-loops
GET /v1/sessions/{session_id}/life-feed
GET /v1/sessions/{session_id}/identity-continuity

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.

GET /v1/sessions/{session_id}/relationship-scores

Default response:

1{
2 "redacted": true,
3 "opt_in_required": true,
4 "relationship_scores": []
5}

Opt in per app:

GET /v1/me
PATCH /v1/apps/{app_id} { "expose_relational_numerics": true }
GET /v1/sessions/{session_id}/relationship-scores

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 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.cursor from POST /messages and poll with since=cursor.
  • Treat unknown event types as additive.
  • Use Idempotency-Key on POST /messages.
  • Use the committed session_id from 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; use https://api.loreos.app/v1/openapi.json.