Home Pricing Docs Blog Changelog About Log in Go to your tickets
Browse docs

From any helpdesk using our API

Overview

Use this endpoint to backfill tickets that already happened somewhere else (a previous helpdesk, a spreadsheet, your own database). Unlike Create Ticket, import lets you set the original timestamps and status, and it records everything silently.

Importing never reaches your customers or your team. No email is sent to the contact, no workflows run, no notifications fire, and no automations are triggered.

POST /api/tickets/import

Importing requires an admin token (the same access level as managing workspace settings).

Request Body

Field Type Required Description
channel string yes The ULID of the channel
subject string yes Ticket subject (max 255 characters)
status string yes One of open, waiting, closed, spam
created_at string no ISO 8601 timestamp the ticket was originally created. Defaults to the earliest message
closed_at string no ISO 8601 timestamp the ticket was resolved. Used only when status is closed
external_id string no Your own identifier for the ticket. Makes re-running an import idempotent (see below)
assignee_email string no Email of a workspace member to assign the ticket to. Ignored unless it is an active member with access to the channel
tags array no Tag names to attach. A tag is created if it does not exist yet (max 50)
from_contact.email string yes Contact email address
from_contact.name string no Contact name
messages array yes The conversation, in chronological order. Between 1 and 200 messages
messages[].type string yes One of inbound (from the contact), outbound (your reply), note (internal team note)
messages[].body string yes HTML body of the message
messages[].created_at string no ISO 8601 timestamp of the message. Defaults to the ticket created_at
messages[].author_email string no Email of the agent who wrote an outbound message or note (see "Message authors" below)
messages[].author_name string no Display name of that agent
messages[].external_id string no Your own identifier for the message, stored so you can correlate it with your source system
messages[].attachment_ids array no Upload tokens to attach to this message (max 10). See Uploading files

All timestamps must be in the past.

You can find a channel's ULID on its settings page in There There. Open the channel, and the "Channel ID" is shown with a copy button.

Example Request

curl -X POST "https://there-there.app/api/tickets/import" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "channel": "01HX9F3K2M...",
    "subject": "Refund request",
    "status": "closed",
    "created_at": "2024-01-10T09:00:00+00:00",
    "closed_at": "2024-01-12T16:30:00+00:00",
    "external_id": "legacy-ticket-9182",
    "assignee_email": "sara@yourcompany.com",
    "tags": ["billing", "refund"],
    "from_contact": {
      "email": "john@customer.com",
      "name": "John Doe"
    },
    "messages": [
      {
        "type": "inbound",
        "body": "<p>I would like a refund for order 1234.</p>",
        "created_at": "2024-01-10T09:00:00+00:00"
      },
      {
        "type": "outbound",
        "body": "<p>Your refund has been processed.</p>",
        "created_at": "2024-01-11T11:00:00+00:00",
        "author_email": "sara@oldhelpdesk.com",
        "author_name": "Sara (Support)"
      }
    ]
  }'

The response has the same shape as Get Ticket.

Contacts

Contacts are matched by email within the workspace. Importing several tickets with the same from_contact.email reuses the existing contact instead of creating a duplicate. A new contact is created only when the email has not been seen before, and an existing contact's name is left unchanged.

Message authors

For inbound messages, the author is always the contact, so no author fields are needed.

For outbound messages and notes, the author is resolved in this order:

  1. If author_email matches an active member of the workspace, the message is attributed to that member.
  2. Otherwise, the author_name and author_email you provide are stored on the message itself. This preserves the original agent's identity without creating a new (billable) user, which is useful for agents who have since left. In the ticket view, the message appears as a normal agent reply labelled with that name and email, just without a linked team-member profile or avatar.
  3. If no author is given at all, the message is attributed to the token owner performing the import.

Attachments

To carry a message's files and inline images, first upload each file to get an upload token (see Uploading files), then list those tokens in the message's attachment_ids. The files are moved onto the imported message, and inline images referenced as cid:TOKEN in the body are rewritten automatically, exactly as on the live reply and create endpoints. Tokens must belong to the same workspace and API token performing the import.

Idempotency

If you pass an external_id, a second import with the same external_id returns the existing ticket instead of creating a duplicate. This lets you safely retry or resume a large migration.

Rate limits

Because importing is one ticket per request, it has its own higher limit of 300 requests per minute, separate from the standard API limit (see Authentication) so a migration does not crawl or starve your normal API usage. If you exceed it, retry on a 429 response using the Retry-After header.

Importing open tickets

Importing resolved conversations as closed is the common case. You can import open tickets too, but they then behave like any other open ticket going forward (for example, a "no reply" workflow you have configured may act on them). Import the conversation as closed if you only want a historical record.

Dedicated importers

This endpoint is the general purpose way to import any conversation. For step-by-step guides tailored to a specific helpdesk, see From FreshDesk and From HelpSpot.