Skip to main content
Using TypeScript? Check out our GraphQL SDK for a fully typed client.
Plain has built-in importers 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, 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 or the import will fail.
Mutation
Variables
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.
Mutation
Variables
The mutation returns a results array with one entry per message in the same order as the input. Each result contains:
  • resultCREATED 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.
1

Upload the attachment

Use createAttachmentUploadUrl to get an upload URL, then upload the file. See the attachments guide 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.
2

Reference the attachment in the message

Pass the attachment ID (returned by createAttachmentUploadUrl) in the attachmentIds array of the message:
Attachments that are uploaded but not referenced by any message are deleted after 24 hours. Make sure to call importThreadMessages promptly after uploading.

Putting it all together

A typical migration script follows this order:
  1. Upsert customers so they exist in Plain.
  2. Call importThread for each ticket in the source system.
  3. Upload any attachments using createAttachmentUploadUrl.
  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.