> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.loreos.app/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.loreos.app/_mcp/server.

# Character bundle imports

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](https://docs.loreos.app/getting-started/full-developer-e-2-e-path).

## Two modes

| Mode             | Use when                                                                  | Transcript  | Result                                                                                |
| ---------------- | ------------------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------- |
| `authoring_only` | The user is starting fresh and has no prior chat history.                 | Not allowed | Publishes the authored character bundle.                                              |
| `hybrid`         | The user has an authored bundle and an export from another companion app. | Required    | Publishes 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

```json
{
  "mode": "authoring_only",
  "character": {
    "slug": "luna",
    "display_name": "Luna",
    "locale": "en-US",
    "primary_reply_language": "en-US",
    "profile": {
      "visible_bio": "A careful archivist who notices small emotional details.",
      "identity_anchors": ["quietly observant", "keeps promises"]
    },
    "voice_samples": [
      {
        "scenario_tag": "warm_opener",
        "sample_role": "anchor",
        "text": "I kept your place in the notes. Come in slowly."
      }
    ],
    "example_dialogues": [
      {
        "scenario_tag": "boundary",
        "turns": [
          { "role": "user", "text": "Tell me everything right now." },
          { "role": "character", "text": "Not all at once. I would rather keep it honest." }
        ]
      }
    ]
  },
  "identity_image": {
    "source_url": "https://cdn.example.com/luna.png",
    "label": "Primary identity",
    "visual_summary": "Luna's canonical portrait."
  }
}
```

`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:

```text
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:

```json
{
  "data": {
    "import": { "status": "committed", "character_slug": "luna" },
    "commit": {
      "character": { "slug": "luna", "status": "published" },
      "session_id": null,
      "import_batch_id": null
    },
    "identity_image": { "asset_id": "...", "asset_key": "identity.primary" }
  }
}
```

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

## Transcript migration import

```json
{
  "mode": "hybrid",
  "external_user_ref": "user_123",
  "external_user_display_name": "Daniel",
  "character": {
    "slug": "luna",
    "display_name": "Luna",
    "locale": "en-US",
    "primary_reply_language": "en-US",
    "voice_samples": [
      { "text": "I remember the exact version you almost deleted.", "sample_role": "anchor" }
    ],
    "profile": {
      "visible_bio": "A companion with a precise, dry warmth."
    }
  },
  "transcript": {
    "metadata": {
      "vendor": "nomi",
      "character_name": "Luna"
    },
    "messages": [
      { "index": 0, "speaker": "human", "text": "Remember the demo I was afraid to ship?" },
      { "index": 1, "speaker": "character", "text": "The one you called too fragile, yes." }
    ]
  }
}
```

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`:

```text
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.

```json
{
  "status": "extracting",
  "temporal_workflow_id": "v1-character-import-extraction:...",
  "temporal_task_queue": "background",
  "progress": {
    "phase": "extracting",
    "current_step": "relational_model",
    "current_step_label": "Extracting relationship equity and counterpart beliefs",
    "message": "Background extraction is running. Poll this import until status becomes ready.",
    "percent_complete": 25,
    "completed_checkpoints": 1,
    "total_checkpoints": 4,
    "heartbeat": {
      "last_heartbeat_at": "2026-06-18T10:20:30+00:00",
      "age_seconds": 2,
      "is_stale": false
    },
    "workflow": {
      "id": "v1-character-import-extraction:...",
      "task_queue": "background"
    },
    "poll_after_seconds": 2,
    "terminal": false,
    "can_preview": true,
    "preview_ready": false,
    "can_commit": false
  },
  "extraction_summary": {
    "progress": {
      "phase": "extracting",
      "current_step": "relational_model",
      "completed_checkpoints": 1,
      "total_checkpoints": 4,
      "checkpoints": [4, 12, 24, 48]
    }
  },
  "last_heartbeat_at": "2026-06-18T10:20:30+00:00"
}
```

`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:

```text
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:

```text
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:

```text
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.