Character bundle imports

Upload a fresh character bundle, or migrate a character plus prior relationship history.

Use POST /v1/imports when you want one reviewable object that can publish a character and, when present, a prior conversation history.

If you only need to create a character, POST /v1/characters is still the shortest path. Use /v1/imports when your app wants a single package for voice samples, authored personality, optional identity image, and optional transcript migration.

For the full production-shaped route that combines bundle import, transcript extraction, identity image registration, playable-character graph, NPC graph, chat, relationship evidence, and score opt-in, see Full developer E2E path.

Two modes

ModeUse whenTranscriptResult
authoring_onlyThe user is starting fresh and has no prior chat history.Not allowedPublishes the authored character bundle.
hybridThe user has an authored bundle and an export from another companion app.RequiredPublishes the character and commits extracted relationship memory into a new session.

transcript_migration is accepted for migration-only clients, but most apps should use hybrid because a transcript is strongest when paired with authored voice and identity anchors.

For hybrid and transcript_migration, set character.primary_reply_language explicitly. It tells LoreOS which language the migrated character should speak after import. Use voice samples and greeting text in that same language unless you are deliberately preserving a stable code-switching style.

Fresh-start import

1{
2 "mode": "authoring_only",
3 "character": {
4 "slug": "luna",
5 "display_name": "Luna",
6 "locale": "en-US",
7 "primary_reply_language": "en-US",
8 "profile": {
9 "visible_bio": "A careful archivist who notices small emotional details.",
10 "identity_anchors": ["quietly observant", "keeps promises"]
11 },
12 "voice_samples": [
13 {
14 "scenario_tag": "warm_opener",
15 "sample_role": "anchor",
16 "text": "I kept your place in the notes. Come in slowly."
17 }
18 ],
19 "example_dialogues": [
20 {
21 "scenario_tag": "boundary",
22 "turns": [
23 { "role": "user", "text": "Tell me everything right now." },
24 { "role": "character", "text": "Not all at once. I would rather keep it honest." }
25 ]
26 }
27 ]
28 },
29 "identity_image": {
30 "source_url": "https://cdn.example.com/luna.png",
31 "label": "Primary identity",
32 "visual_summary": "Luna's canonical portrait."
33 }
34}

identity_image.source_url must be a direct, publicly fetchable https image URL. If LoreOS cannot fetch it, the character can still publish, but the identity_image result in the commit response will contain a safe error and the portrait will not be registered.

Then commit:

POST /v1/imports
GET /v1/imports/{import_id}/preview
POST /v1/imports/{import_id}/commit

The commit response includes the import summary plus a commit object:

1{
2 "data": {
3 "import": { "status": "committed", "character_slug": "luna" },
4 "commit": {
5 "character": { "slug": "luna", "status": "published" },
6 "session_id": null,
7 "import_batch_id": null
8 },
9 "identity_image": { "asset_id": "...", "asset_key": "identity.primary" }
10 }
11}

For authoring_only, data.commit.session_id is null; start a session with POST /v1/sessions.

Transcript migration import

1{
2 "mode": "hybrid",
3 "external_user_ref": "user_123",
4 "external_user_display_name": "Daniel",
5 "character": {
6 "slug": "luna",
7 "display_name": "Luna",
8 "locale": "en-US",
9 "primary_reply_language": "en-US",
10 "voice_samples": [
11 { "text": "I remember the exact version you almost deleted.", "sample_role": "anchor" }
12 ],
13 "profile": {
14 "visible_bio": "A companion with a precise, dry warmth."
15 }
16 },
17 "transcript": {
18 "metadata": {
19 "vendor": "nomi",
20 "character_name": "Luna"
21 },
22 "messages": [
23 { "index": 0, "speaker": "human", "text": "Remember the demo I was afraid to ship?" },
24 { "index": 1, "speaker": "character", "text": "The one you called too fragile, yes." }
25 ]
26 }
27}

The import stages the transcript into a sandbox session and starts extraction in Temporal by default. The create response includes temporal_workflow_id and temporal_task_queue when extraction_mode is background, which is the default. Poll until status is ready:

GET /v1/imports/{import_id}
GET /v1/imports/{import_id}/preview

GET /v1/imports/{import_id} returns a renderable data.progress object. Use this object for UI state and agents; data.extraction_summary.progress remains as a backward-compatible raw worker heartbeat.

1{
2 "status": "extracting",
3 "temporal_workflow_id": "v1-character-import-extraction:...",
4 "temporal_task_queue": "background",
5 "progress": {
6 "phase": "extracting",
7 "current_step": "relational_model",
8 "current_step_label": "Extracting relationship equity and counterpart beliefs",
9 "message": "Background extraction is running. Poll this import until status becomes ready.",
10 "percent_complete": 25,
11 "completed_checkpoints": 1,
12 "total_checkpoints": 4,
13 "heartbeat": {
14 "last_heartbeat_at": "2026-06-18T10:20:30+00:00",
15 "age_seconds": 2,
16 "is_stale": false
17 },
18 "workflow": {
19 "id": "v1-character-import-extraction:...",
20 "task_queue": "background"
21 },
22 "poll_after_seconds": 2,
23 "terminal": false,
24 "can_preview": true,
25 "preview_ready": false,
26 "can_commit": false
27 },
28 "extraction_summary": {
29 "progress": {
30 "phase": "extracting",
31 "current_step": "relational_model",
32 "completed_checkpoints": 1,
33 "total_checkpoints": 4,
34 "checkpoints": [4, 12, 24, 48]
35 }
36 },
37 "last_heartbeat_at": "2026-06-18T10:20:30+00:00"
38}

phase moves through queued, extracting, embedding, and ready. Poll again after progress.poll_after_seconds. Show progress.message and progress.current_step_label if you need user-facing progress copy. If progress.heartbeat.is_stale becomes true, keep polling briefly, then inspect progress.workflow.id and progress.workflow.task_queue in your operator logs. When progress.preview_ready is true, fetch /preview; when progress.can_commit is true, the import is ready to publish.

If the workflow fails, status becomes failed and last_error contains the safe error summary. A ready import may still carry non-fatal extraction_summary.errors; show those as review warnings, then inspect the preview before commit. Use extraction_mode: "none" only for tests or custom review flows where you want to stage the transcript without running extractors yet.

The preview returns authored-character readiness plus staged ledger counts such as world-model claims, canon facts, counterpart beliefs, NPCs, and relational state rows. Transcript imports may also include supporting_cast_candidates: reviewable NPC candidates projected from repeated extracted person signals, with candidate, review_recommended, or auto_promoted status. These candidates remain planning-only until commit and later developer/user review; they are not raw graph imports or user-visible facts. The preview does not expose prompts, raw extractor traces, or private engine internals.

After commit, review the materialized supporting cast through the Character World APIs:

GET /v1/characters/{slug}/supporting-cast
GET /v1/characters/{slug}/supporting-cast/candidates
POST /v1/characters/{slug}/supporting-cast/{npc_ref}/promote
POST /v1/characters/{slug}/supporting-cast/{npc_ref}/dismiss
POST /v1/characters/{slug}/supporting-cast/{npc_ref}/request-review

Use promote when a candidate should become explicit developer-promoted cast. Use dismiss when LoreOS should retain the row for audit and duplicate avoidance but stop using it in planner context. Use request-review to keep a candidate visible in the review queue without making a final decision. You can also PATCH /v1/characters/{slug}/npcs/{npc_ref} or PUT the NPC relationship after promotion to refine role, voice, stance, visibility tier, and relationship graph details.

If the import or a later Story Room run produces life-event candidates, agenda state, or offscreen-life state, inspect and review those through the same Character World surface:

GET /v1/characters/{slug}/life
GET /v1/characters/{slug}/agenda/items
GET /v1/characters/{slug}/offscreen/heartbeat
GET /v1/characters/{slug}/offscreen/scenes
GET /v1/characters/{slug}/life-event-candidates
POST /v1/characters/{slug}/life-event-candidates/{candidate_id}/approve
POST /v1/characters/{slug}/life-event-candidates/{candidate_id}/approve-with-edits
POST /v1/characters/{slug}/life-event-candidates/{candidate_id}/reject
POST /v1/characters/{slug}/life-event-candidates/{candidate_id}/request-alternatives

These endpoints are redacted projections. They exclude raw prompts, provider payloads, raw extractor inputs, private agenda payloads, and raw offscreen scene transcripts.

Commit when the preview is acceptable:

POST /v1/imports/{import_id}/commit

Transcript commits synchronously materialize actor relationship evidence and can take longer than 60 seconds. Use a client read timeout of at least 180 seconds. If the client times out, poll GET /v1/imports/{import_id}. While data.status is committing, do not resubmit. Once it is committed, repeat the commit request to recover the original data.commit response without applying the import again.

If the accepted-memory write path is disabled in the current environment, commit returns 503 import_commit_disabled. Keep polling or inspecting the preview is not enough to fix that state; an operator must enable import commits for that environment. This protects staging extraction from accidentally publishing external memory before the write policy is enabled.

The commit response includes:

  • data.import: the pollable import row after commit.
  • data.commit.character: the published character slug and status.
  • data.commit.session_id: the new LoreOS session carrying the imported relationship state.
  • data.commit.import_batch_id: the deletion handle for imported memory rows.
  • data.commit.claims_written, data.commit.canon_written, data.commit.beliefs_written, data.commit.npcs_written, and relational_state_set: legacy writer counters retained for compatibility.
  • data.commit.native_actor_core: the current runtime authority. Require native_ready: true, confirm imported_turn_count, and inspect directed_lens_count, claim_memory_count, belief_count, and indexed_document_count. A successful native import may leave the legacy counters at zero/false; do not treat that alone as failure.
  • data.identity_image: the registered identity asset, or a safe per-image error if the provided URL could not be fetched.

Review and deletion

PATCH /v1/imports/{import_id}/review-items/{item_id} records a review decision for UI and audit. In the first release, commit applies the staged accepted ledgers as a batch; item-level pruning is not yet a hard filter for every extractor output.

DELETE /v1/imports/{import_id} soft-deletes the import row. If the import was committed, LoreOS also deletes rows stamped with the returned import_batch_id.

Safety model

  • The raw transcript is not stored in the v1_character_imports row.
  • Transcript turns are replayed into a sandbox staging session.
  • Imported memory is stamped as external provenance and starts with conservative trust. It can inform planning, but it is not treated as verified user consent or a speakable fact by default.
  • Identity images are registered from public image URLs after the character is published. For uploads or ongoing image management, use the visual asset APIs.