Uploading files
Overview
Attachments and inline images are uploaded in two steps. First you upload the file on its own, which streams it to storage and returns a short lived token. Then you reference that token when you create a ticket or post a message. This keeps the message endpoints as plain JSON while letting you send large files without encoding them into the request body.
The same token mechanism powers both regular attachments and inline images.
Step 1: Upload the file
Send the file as multipart/form-data to the upload endpoint.
POST /api/attachments
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
file |
file | Yes | The file to upload, up to 25 MB |
Example Request
curl -X POST https://there-there.app/api/attachments \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json" \
-F "file=@screenshot.png"
Example Response (201 Created)
{
"data": {
"token": "upl_screenshot_8f3c1a...",
"filename": "screenshot.png",
"mime_type": "image/png",
"size": 84213,
"expires_at": "2026-06-24T14:30:00+00:00"
}
}
The token is single use. It includes a slug of the file name (for example upl_screenshot_...) to make it easier to recognise while debugging, but you should treat the whole value as opaque. The expires_at field tells you when the upload will be deleted if you do not attach it to a message (see Lifetime and cleanup).
Step 2: Reference the token
Every message endpoint accepts an attachment_ids array of upload tokens. List each token you want to attach to the message you are creating.
| Endpoint | Where to put attachment_ids |
|---|---|
POST /api/tickets |
inside the message object |
POST /api/tickets/{ticket}/reply |
top level |
POST /api/tickets/{ticket}/forward |
top level |
POST /api/tickets/{ticket}/note |
top level |
A message can reference up to 10 tokens.
Example: reply with an attachment
curl -X POST https://there-there.app/api/tickets/01HX9F3K2M.../reply \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"body": "<p>Here is the file you asked for.</p>",
"attachment_ids": ["upl_screenshot_8f3c1a..."]
}'
Example: create a ticket with an attachment
curl -X POST https://there-there.app/api/tickets \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"channel": "01HX...",
"subject": "Imported ticket",
"from_contact": { "email": "john@customer.com" },
"message": {
"body": "<p>Original message.</p>",
"attachment_ids": ["upl_screenshot_8f3c1a..."]
}
}'
Inline images
To place an uploaded image inside the message body (rather than as a separate attachment), embed it with a cid: reference to its token, then also list the token in attachment_ids.
<p>See the highlighted area:</p>
<p><img src="cid:upl_screenshot_8f3c1a..."></p>
When the message is created, There There replaces the cid: reference with the image and renders it inline, both in the conversation and in any outbound email. A token whose cid: reference appears in the body becomes an inline image. A token that is only listed in attachment_ids becomes a regular attachment.
curl -X POST https://there-there.app/api/tickets/01HX9F3K2M.../reply \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"body": "<p>See below:</p><p><img src=\"cid:upl_screenshot_8f3c1a...\"></p>",
"attachment_ids": ["upl_screenshot_8f3c1a..."]
}'
Lifetime and cleanup
An upload is parked on its own until you attach it to a message.
- A token is single use. Once a message references it, the file moves onto that message and the token can no longer be used.
- An upload that is never attached is deleted roughly 30 minutes after it was created. The
expires_atfield in the upload response is the exact deadline. - A token can only be used by the same API token that created it. A token from a different API token, or from a different workspace, is rejected.
These rules keep parked uploads from accumulating, so the upload endpoint cannot be used as general purpose storage.
Limits
| Limit | Value |
|---|---|
| Maximum file size | 25 MB |
| Attachments per message | 10 |
| Unattached upload lifetime | 30 minutes |
Executable file types and other disallowed content types are rejected at upload time. Read only API tokens cannot upload.
Errors
| Status | Meaning |
|---|---|
422 |
The file is too large, has a disallowed type, or a referenced token is unknown, already used, expired, or owned by another token |
429 |
Too many uploads in a short period, or too much is currently parked for the workspace |