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:
- If
author_emailmatches an active member of the workspace, the message is attributed to that member. - Otherwise, the
author_nameandauthor_emailyou 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. - 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.