> ## Documentation Index
> Fetch the complete documentation index at: https://www.plain.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Importing threads

<Snippet file="graphql/sdk-note.mdx" />

Plain has [built-in importers](https://help.plain.com/article/migration) for common providers. If your source system is supported, use those first — they handle the mapping for you. These mutations are for when you need to build a custom import, for example from a less common provider or an internal tool.

The `importThread` and `importThreadMessages` mutations let you bring across historic threads while preserving original timestamps, authors, and attachments so your team has full context in Plain.

Unlike [`createThread`](/graphql/threads/create), imported threads do not trigger SLAs or autoresponders and are marked with import provenance tracking.

## Overview

Importing a thread is a two-step process:

1. **Create the thread** with `importThread` — this sets up the thread with its metadata (title, status, priority, labels, etc.) and the original creation timestamp.
2. **Add messages** with `importThreadMessages` — this adds the conversation history (inbound messages, outbound replies, and internal notes) to the thread.

You must create the thread before importing its messages. Each mutation is idempotent: if you call it again with the same `externalId`, the duplicate is skipped (the result will be `NOOP`).

## Permissions

To import threads you need an API key with the following permissions:

* `thread:import`
* `attachment:create` (if importing messages with attachments)

## Import a thread

The `importThread` mutation creates a thread tied to an existing customer. You can identify the customer by their Plain customer ID, email address, or external ID.

The `statusDetail.type` must match the thread `status`:

* `TODO` allows `NEW_REPLY` or `IN_PROGRESS`
* `SNOOZED` allows `WAITING_FOR_CUSTOMER`
* `DONE` allows `DONE_MANUALLY_SET` or `IGNORED`

If you provide a `tenantId`, the customer must already be a [member of the tenant](/graphql/tenants/add-customers) or the import will fail.

<Snippet file="graphql/import-thread.mdx" />

The mutation returns a `result` field which is one of:

* `CREATED` — the thread was imported successfully.
* `NOOP` — a thread with this `externalId` already exists, so the import was skipped.

## Import thread messages

Once you have a thread, use `importThreadMessages` to add conversation history. You can import up to **25 messages per call**.

Each message has a `type` that determines which `author` field to set:

* `INBOUND` — set `author.customerId` (the customer who sent the message)
* `OUTBOUND` — set `author.userId` (the support agent who replied)
* `NOTE` — set `author.userId` (the agent who wrote the internal note)

Exactly one of `customerId` or `userId` must be provided.

<Snippet file="graphql/import-thread-messages.mdx" />

The mutation returns a `results` array with one entry per message in the same order as the input. Each result contains:

* `result` — `CREATED` or `NOOP` (if a message with that `externalId` already exists).
* `threadMessage` — the created `TimelineEntry` (for `INBOUND`/`OUTBOUND`) or `Note` (for `NOTE`). Null if the message failed.
* `error` — per-message error details, null on success.

If some messages fail while others succeed, the top-level `error` will have the code `bulk_partial_failure`.

## Importing messages with attachments

To import messages that have attachments, you need to upload the attachments first and then reference them by ID.

<Steps>
  <Step title="Upload the attachment">
    Use `createAttachmentUploadUrl` to get an upload URL, then upload the file. See the [attachments guide](/graphql/attachments) for the full upload flow.

    When creating the upload URL, use the attachment type `CUSTOM_TIMELINE_ENTRY` for `INBOUND` and `OUTBOUND` messages, or `NOTE` for `NOTE` messages.
  </Step>

  <Step title="Reference the attachment in the message">
    Pass the attachment ID (returned by `createAttachmentUploadUrl`) in the `attachmentIds` array of the message:

    ```json theme={null}
    {
      "author": {
        "customerId": "c_01H14DFQ4PDYBH398J1E99TWSS"
      },
      "text": "Here is a screenshot of the error.",
      "createdAt": "2024-06-15T10:32:00Z",
      "type": "INBOUND",
      "externalId": "msg_004",
      "attachmentIds": ["att_01HB924PME9C0YWKW1N4AK3BZA"]
    }
    ```
  </Step>
</Steps>

<Warning>
  Attachments that are uploaded but not referenced by any message are deleted after 24 hours. Make sure to call `importThreadMessages` promptly after uploading.
</Warning>

## Putting it all together

A typical migration script follows this order:

1. [Upsert customers](/graphql/customers/upsert) so they exist in Plain.
2. Call `importThread` for each ticket in the source system.
3. Upload any attachments using [`createAttachmentUploadUrl`](/graphql/attachments).
4. Call `importThreadMessages` with the thread ID and messages (in batches of up to 25).
5. Check the `result` and `error` fields to confirm each import succeeded.

Since both mutations are idempotent on `externalId`, you can safely re-run a migration script without creating duplicates.
