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.

Fresh-start import

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

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 "voice_samples": [
9 { "text": "I remember the exact version you almost deleted.", "sample_role": "anchor" }
10 ],
11 "profile": {
12 "visible_bio": "A companion with a precise, dry warmth."
13 }
14 },
15 "transcript": {
16 "metadata": {
17 "vendor": "nomi",
18 "character_name": "Luna"
19 },
20 "messages": [
21 { "index": 0, "speaker": "human", "text": "Remember the demo I was afraid to ship?" },
22 { "index": 1, "speaker": "character", "text": "The one you called too fragile, yes." }
23 ]
24 }
25}

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 pollable progress:

1{
2 "status": "extracting",
3 "temporal_workflow_id": "v1-character-import-extraction:...",
4 "temporal_task_queue": "background",
5 "extraction_summary": {
6 "progress": {
7 "phase": "extracting",
8 "current_step": "relational_model",
9 "completed_checkpoints": 1,
10 "total_checkpoints": 4,
11 "checkpoints": [4, 12, 24, 48]
12 }
13 },
14 "last_heartbeat_at": "2026-06-18T10:20:30+00:00"
15}

phase moves through queued, extracting, embedding, and ready. last_heartbeat_at should keep advancing while a long extraction is still alive, even if completed_checkpoints has not changed yet. 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

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: counts of committed extracted state.
  • 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.