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

# Importing conversations

Move a transcript from another system into Runtype so an Agent's next turn continues
where the old system left off. Runtype stores the transcript on the conversation and
replays it on the first turn after the import.

An imported conversation is an API conversation, so the conversation that the API
returns has `source: "app"`. It isn't added to a Persona widget visitor's history, and
you can't continue a widget conversation with a stored-history turn. For widget history, see
[Conversation history in an embedded widget](/developer-guides/guides/client-conversation-history).

## Before you begin

You need the following:

* A Runtype API key with the **Records (Write)** permission to create and update
  conversations, **Records (Read)** to read them back, and **Agents (Execute)** to run
  the Agent with `POST /v1/agents/{id}/execute`. To run it with `POST /v1/dispatch`
  instead, the key needs **Flows (Execute)**. For more information, see
  [Managing API keys](/user-guide/settings/managing-api-keys).
* A saved Agent that you own. Claude Managed Agents and external Agents keep their own
  history, so they can't continue an imported conversation.

## Import a conversation

To import a conversation and continue it, follow these steps:

1. Create the conversation with `POST /v1/conversations`. Send `source`, `agentId`, and
   the first chunk of `messages`. The response contains the conversation `id`.
2. Optional: Send each later chunk with `PUT /v1/conversations/{id}` and
   `messagesMode: "append"`, in order. For more information, see
   [Import a long transcript in chunks](#import-a-long-transcript-in-chunks).
3. Read the conversation back to check the import. For more information, see
   [Check the import](#check-the-import).
4. Run the next turn with `history: "stored"`. For more information, see
   [Continue the conversation](#continue-the-conversation).

The following request imports a conversation in which the assistant called a tool,
with the tool result in place, so that the next turn can replay the whole exchange:

#### TypeScript

```typescript
import { RuntypeClient } from '@runtypelabs/sdk'

const client = new RuntypeClient({ apiKey: process.env.RUNTYPE_API_KEY })

const conversation = await client.conversations.create({
  title: 'Refund for order 1182',
  agentId: 'YOUR_AGENT_ID',
  ownerId: 'cust_4471',
  source: { system: 'intercom', externalId: 'conv_88213' },
  messages: [
    { role: 'user', content: 'Where is my refund for order 1182?' },
    {
      role: 'assistant',
      content: '',
      toolCalls: [
        { toolCallId: 'call_1', toolName: 'lookup_order', args: { orderId: '1182' } },
      ],
    },
    {
      role: 'tool',
      content: '',
      toolResults: [
        { toolCallId: 'call_1', toolName: 'lookup_order', result: { status: 'refunded' } },
      ],
    },
    { role: 'assistant', content: 'Your refund was issued on August 30.' },
  ],
})
```

#### cURL

```bash
curl -X POST https://api.runtype.com/v1/conversations \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "title": "Refund for order 1182",
    "agentId": "YOUR_AGENT_ID",
    "ownerId": "cust_4471",
    "source": { "system": "intercom", "externalId": "conv_88213" },
    "messages": [
      { "role": "user", "content": "Where is my refund for order 1182?" },
      {
        "role": "assistant",
        "content": "",
        "toolCalls": [
          { "toolCallId": "call_1", "toolName": "lookup_order", "args": { "orderId": "1182" } }
        ]
      },
      {
        "role": "tool",
        "content": "",
        "toolResults": [
          { "toolCallId": "call_1", "toolName": "lookup_order", "result": { "status": "refunded" } }
        ]
      },
      { "role": "assistant", "content": "Your refund was issued on August 30." }
    ]
  }'
```

Replace the following:

* `YOUR_API_KEY`: your Runtype API key.
* `YOUR_AGENT_ID`: the ID of the Agent that continues the conversation.

The Runtype MCP server's `create_conversation` and `update_conversation` tools accept
the same top-level fields in snake case (`agent_id`, `owner_id`, `messages_mode`).

## Conversation fields

`POST /v1/conversations` and `PUT /v1/conversations/{id}` accept the following fields:

| Field          | Type   | Notes                                                                                                                                                                                                                                                                                                       |
| -------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `messages`     | array  | The transcript. For the message shape, see [Message fields](#message-fields).                                                                                                                                                                                                                               |
| `agentId`      | string | The Agent that the conversation belongs to. For more information, see [Bind an Agent and record the source](#bind-an-agent-and-record-the-source).                                                                                                                                                          |
| `source`       | object | Where the transcript came from. With `externalId`, also the idempotency key for re-running an import.                                                                                                                                                                                                       |
| `ownerId`      | string | 1 to 255 characters. The end user that the conversation belongs to. Filter by it with `GET /v1/conversations?ownerId=`.                                                                                                                                                                                     |
| `title`        | string | Up to 500 characters. On create, defaults to `New Chat`. Give each imported conversation a distinct title, such as one that includes the source conversation ID. When a title is already in use, Runtype adds a short suffix, and a create can fail with `500` after several conversations share one title. |
| `metadata`     | object | Optional key/value pairs stored on the conversation.                                                                                                                                                                                                                                                        |
| `modelId`      | string | Optional. Stored on the conversation's metadata.                                                                                                                                                                                                                                                            |
| `systemPrompt` | string | Optional. Stored on the conversation's metadata.                                                                                                                                                                                                                                                            |
| `messagesMode` | string | `PUT` only. `replace` (the default) or `append`.                                                                                                                                                                                                                                                            |

We recommend that you set `ownerId` to the end user's ID in the source system. You can
then list one user's imported conversations.

## Message fields

Each entry in `messages` has the following fields:

| Field         | Notes                                                                                                                             |
| ------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `role`        | `system`, `user`, `assistant`, or `tool`.                                                                                         |
| `content`     | A plain string, or an array of `text`, `image`, `file`, `reasoning`, and `asset_ref` parts.                                       |
| `toolCalls`   | Assistant messages only. Each entry is `{ toolCallId, toolName, args }`.                                                          |
| `toolResults` | Tool messages only, and required on them. Each entry is `{ toolCallId, toolName, result }`.                                       |
| `id`          | Optional. Runtype creates one when you omit it. Use the source system's message ID to make the import resumable.                  |
| `createdAt`   | Optional timestamp string, stored exactly as sent. Runtype creates an ISO 8601 value when you omit it.                            |
| `author`      | Optional `{ type: "end_user" \| "operator" \| "agent" \| "system", id?, externalId?, name? }`.                                    |
| `metadata`    | Optional key/value pairs stored with the message.                                                                                 |
| `model`       | Optional. The model that wrote an assistant message. Runtype stores it as sent. On an imported message, it is informational only. |

`author` records who wrote the message in the system that you're migrating from, so a
`role: "user"` turn stays attributable to a specific person. Runtype stores and returns
it as you sent it. Runtype doesn't send it to a model.

Runtype stores an imported `reasoning` part but leaves it out of the replay. A model's
thinking signature is bound to the provider that produced it, so a foreign one would
fail the next turn. This is true even when the imported message names a `model`.

A reply that a stored-history turn appends is different. Runtype stores the Agent's own
transcript: one assistant message and one tool message for each model round, in order,
with `reasoning` parts and `model` on each assistant message. When the next turn runs on
that same model, Runtype replays those reasoning parts. When the next turn runs on
another model, Runtype removes them. Runtype marks these messages itself, and drops that
mark from any message you write, so only a reply the Agent wrote can replay its reasoning.

## Transcript validation rules

Model providers reject a tool call without a matching result. Runtype checks the
transcript when you write it, so a bad transcript fails at import and not at the next
turn. A transcript must follow these rules:

* User and system messages must have non-empty content.
* Each message `id` must be unique in the transcript.
* A `tool` message must carry at least one `toolResults` entry.
* Every assistant `toolCalls` entry must be answered by a `tool` message before the
  next non-tool message.
* Each `toolCallId` can be issued only once, and answered only once.
* A result can't answer a `toolCallId` that no earlier assistant message issued.
* `toolCalls` is valid only on an assistant message, and `toolResults` only on a tool
  message.

If the source system doesn't record tool calls, import the text turns only.

A transcript that breaks a rule returns `400` with a body similar to the following:

```json
{
  "error": "Validation Error",
  "code": "CONVERSATION_TRANSCRIPT_INVALID",
  "message": "messages[1]: assistant toolCalls entry has no matching tool result before the next non-tool message (toolCallId call_1)",
  "details": {
    "issues": [{ "index": 1, "toolCallId": "call_1", "reason": "tool_call_without_result" }]
  }
}
```

Each entry in `details.issues` names the message `index` and one of the following
`reason` values:

| Reason                         | Fix                                                                          |
| ------------------------------ | ---------------------------------------------------------------------------- |
| `duplicate_message_id`         | Give each message a unique `id`.                                             |
| `duplicate_tool_call_id`       | Issue each `toolCallId` only once.                                           |
| `duplicate_tool_result_id`     | Answer each `toolCallId` only once.                                          |
| `empty_content`                | Give the user or system message non-empty `content`.                         |
| `tool_call_without_result`     | Add a `tool` message that answers the call before the next non-tool message. |
| `tool_calls_on_non_assistant`  | Move `toolCalls` to an assistant message.                                    |
| `tool_message_without_results` | Add `toolResults` to the `tool` message, or remove the message.              |
| `tool_result_without_call`     | Remove the result, or add the assistant `toolCalls` entry that it answers.   |
| `tool_results_on_non_tool`     | Move `toolResults` to a `tool` message.                                      |

## Bind an Agent and record the source

`agentId` binds the conversation to one Agent. It must name an Agent that you own.
Otherwise, the API returns `404`. `source` records where the transcript came from, with
the following fields:

| Field        | Type   | Notes                                                                                            |
| ------------ | ------ | ------------------------------------------------------------------------------------------------ |
| `system`     | string | Required. 1 to 100 characters. For example, `intercom`.                                          |
| `externalId` | string | Optional. 1 to 500 characters. The conversation's ID in the source system.                       |
| `importedAt` | string | Optional. An ISO 8601 date and time with a time zone offset, for example `2026-09-01T12:00:00Z`. |

Runtype stores both on the conversation's metadata, as `metadata.agentId` and
`metadata.importSource`.

A stored-history turn must run the bound Agent. A turn that names another Agent
returns `409` with code `CONVERSATION_AGENT_MISMATCH`. If the conversation has no
`agentId`, the first stored-history turn binds it to the Agent that runs it. To remove
the binding, send `agentId: null` in a `PUT` request.

## Re-run an import safely

`source.externalId` is an idempotency key. A fresh create returns `201` with
`"imported": true`. If a conversation with the same `source.system` and
`source.externalId` already exists, Runtype doesn't create a second one. It returns
`200` with the existing conversation and `"imported": false`:

```json
{
  "id": "YOUR_CONVERSATION_ID",
  "imported": false,
  "metadata": { "importSource": { "system": "intercom", "externalId": "conv_88213" } }
}
```

Runtype returns the existing conversation unchanged and doesn't write the `messages` in
the repeated request. Runtype still validates those `messages` first, so a repeated
request with an invalid transcript returns `400`. To add turns, use `PUT` with
`messagesMode: "append"`.

If two requests with the same `source.system` and `source.externalId` arrive at the
same time, Runtype creates only one conversation. Use the source system's conversation
ID as `externalId`, and you can restart a migration script from the beginning without
duplicates. Runtype doesn't deduplicate a create without `source.externalId`.

## Import a long transcript in chunks

`PUT /v1/conversations/{id}` takes `messagesMode`:

* `replace` (the default) replaces the whole stored transcript with the messages that
  you send.
* `append` adds your messages to the end of the stored transcript.

In `append` mode, Runtype skips a message whose `id` the conversation already stores. A
chunk that you resend after a timeout adds nothing. Give every message the source
system's message ID so that you can retry each chunk on its own.

Runtype validates the stored transcript and the incoming chunk together. A `tool`
message in the chunk can answer a `toolCallId` that the conversation already stores. A
result that answers nothing still returns `400` `CONVERSATION_TRANSCRIPT_INVALID`.
Keep an assistant `toolCalls` message and the `tool` message that answers it in the
**same** chunk. A chunk that ends on an unanswered tool call is rejected.

The following request appends one message to the conversation:

#### TypeScript

```typescript
await client.conversations.update('YOUR_CONVERSATION_ID', {
  messagesMode: 'append',
  messages: [{ id: 'src_412', role: 'user', content: 'Can you resend the receipt?' }],
})
```

#### cURL

```bash
curl -X PUT https://api.runtype.com/v1/conversations/YOUR_CONVERSATION_ID \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "messagesMode": "append",
    "messages": [{ "id": "src_412", "role": "user", "content": "Can you resend the receipt?" }]
  }'
```

Replace `YOUR_CONVERSATION_ID` with the `id` that the create request returned.

If another write changes the conversation during an append, the API returns `409` with
code `CONVERSATION_MODIFIED` and writes nothing. Read the conversation again and resend
the chunk.

## Check the import

To check the import, read the conversation back with `GET /v1/conversations/{id}`:

```bash
curl https://api.runtype.com/v1/conversations/YOUR_CONVERSATION_ID \
  -H 'Authorization: Bearer YOUR_API_KEY'
```

In the CLI, run `runtype conversations get <conversation-id>`.

Check the number of `messages`, the `id` of the last message, and
`metadata.importSource`. If a chunked import stopped partway, the last message `id`
tells you which chunk to send next.

## Size limits

Limits apply to the whole conversation after the write. An append counts the stored
transcript plus the incoming messages. The following table lists the limits:

| Limit                                   | Value  |
| --------------------------------------- | ------ |
| Messages on one conversation            | 10,000 |
| Content of a single message             | 1 MB   |
| All message content on one conversation | 32 MB  |
| The conversation's `metadata`           | 256 KB |

A write that exceeds any of these returns `400` and changes nothing. Split a transcript
that doesn't fit across several conversations, or drop the oldest turns.

Every stored-history turn adds messages to the conversation. Leave room under the
limits. A turn that would exceed them doesn't run, and a reply that would exceed them
isn't stored.

## Attachments

To import an attachment, send it inline as an `image` or `file` content part with
base64 data. Runtype moves inline media larger than 256 KB to asset storage when the
write lands. Runtype doesn't fetch attachment URLs for you.

When you create a conversation, Runtype applies the 1 MB per-message limit to the
inline content before it moves the media. The base64 data of a message's attachments
must fit in 1 MB, which is about 750 KB of source file. `PUT` moves the media before it
checks the limit, so it doesn't have this restriction.

For a larger file, upload it first with `POST /v1/assets`, which needs the
**Assets (Write)** permission. Then reference it with an `asset_ref` part that has the
following fields:

* `type`: `asset_ref`.
* `assetId`: the `assetId` that the upload returned.
* `refKind`: `image` or `file`.
* `mimeType`: the file's content type.
* `sizeBytes`: the file's size in bytes.
* `orgKey`: required, but Runtype replaces it with your organization's key on write.
* `filename`: optional.

## What import doesn't do

An import is a write, not a run. It doesn't fire surface webhooks, start an Agent turn,
or count toward execution usage.

The transcript doesn't carry an Agent's long-term memory. Long-term facts that an Agent
needs from the start are seeded through the memory API
(`POST /v1/runtime/memory/save`, read back with `POST /v1/runtime/memory/recall`), not
through `messages`. Those routes are an Enterprise-plan feature.

## Continue the conversation

The turn after an import calls `POST /v1/agents/{id}/execute` with `history: "stored"`,
the `conversationId` from the import, and only the new user message. Runtype replays
the stored transcript ahead of the new message, so the imported tool pairs reach the
provider:

```bash
curl -X POST https://api.runtype.com/v1/agents/YOUR_AGENT_ID/execute \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "history": "stored",
    "conversationId": "YOUR_CONVERSATION_ID",
    "messages": [{ "role": "user", "content": "Can you resend the receipt?" }]
  }'
```

`POST /v1/dispatch` takes the same `history` field, with `agent.agentId` and
`conversationId`:

```json
{
  "agent": { "agentId": "YOUR_AGENT_ID" },
  "conversationId": "YOUR_CONVERSATION_ID",
  "history": "stored",
  "messages": [{ "role": "user", "content": "Can you resend the receipt?" }]
}
```

With the default `history: "inline"`, your application keeps the transcript and sends
the whole `messages` array on every turn.

A stored-history turn has the following requirements on both routes:

* The request names a saved Agent that isn't a Claude Managed or external Agent.
  Inline Agents and flow dispatches are refused.
* The request includes `conversationId`. Without it, the API returns `400` with code
  `STORED_HISTORY_REQUIRES_CONVERSATION`.
* `messages` contains only the new messages and ends with a `user` message. The stored
  transcript plus the new messages must also follow the
  [transcript validation rules](#transcript-validation-rules). Otherwise, the API
  returns `400` with code `STORED_HISTORY_DELTA_INVALID`.
* The request doesn't send `Prefer: respond-async` or a steer request
  (`x-runtype-concurrency: steer`). On `POST /v1/dispatch`, it also isn't a resume or a
  client-tool continuation.

Other refusals return `400` with code `STORED_HISTORY_UNSUPPORTED`.

When Runtype accepts the request, it appends your new messages to the stored
transcript before the model runs. A failed turn still keeps the question. When the turn
completes, Runtype appends the Agent's reply, including any tool calls and results. An
appended reply message can carry `metadata.executionId`, the ID of the execution that
wrote it.

Runtype doesn't append a reply for a turn that fails, or for a turn that stops to wait
for a client-side tool result or an approval. To keep that reply in the transcript,
read it from the execution's events and append it with `messagesMode: "append"`.

A stored-history turn can also return the following errors. Nothing runs in these
cases:

| Status | Code                     | Cause and fix                                                                                   |
| ------ | ------------------------ | ----------------------------------------------------------------------------------------------- |
| `404`  | `CONVERSATION_NOT_FOUND` | The conversation doesn't exist, isn't yours, or is a Persona widget conversation.               |
| `409`  | `CONVERSATION_MODIFIED`  | Another write changed the conversation. Read it again and retry.                                |
| `413`  | `MESSAGES_TOO_LARGE`     | The conversation is at a size limit. The code is in `details[].code`. Start a new conversation. |

Dispatch `messages` also accept the `tool` role with `toolCalls` and `toolResults`, in
the same shape that the transcript uses. An application that keeps `history: "inline"`
can still replay full tool turns on every request. The `messages` override on
`POST /v1/dispatch/resume` and `POST /v1/dispatch/continue` accepts the same shape.
Answer every `toolCalls` entry with the `tool` message that follows it.

## Next steps

Continue with these guides:

* [Creating and configuring agents](/user-guide/agents/creating-and-configuring-agents): set up the Agent that an imported conversation is bound to.
* [Agent tools](/user-guide/agents/agent-tools): give the Agent the tools that an imported transcript's tool pairs refer to.
* [Create a conversation](/api-reference/runtype-api/conversations/create-a-conversation): every field and response of the conversations API.
* [Steer an active agent execution](/developer-guides/guides/live-message-steering): add a message to a turn that is already running.
* [What are Agents?](/user-guide/agents/what-are-agents): how Agents use tools, system prompts, and conversation history.