Tickets
Create Ticket
Create a new inbound ticket on a specific channel. This is useful for integrating external forms or support widgets with There There.
POST /api/tickets
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
channel |
string | yes | The ULID of the channel |
subject |
string | yes | Ticket subject (max 255 characters) |
from_contact.email |
string | yes | Contact email address |
from_contact.name |
string | no | Contact name |
message.body |
string | yes | HTML body of the initial message |
Example Request
curl -X POST "https://there-there.app/api/tickets" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-d '{
"channel": "01HX9F3K2M...",
"subject": "Cannot connect to the API",
"from_contact": {
"email": "john@customer.com",
"name": "John Doe"
},
"message": {
"body": "<p>I keep getting a 403 error when trying to connect.</p>"
}
}'
Example Response
{
"data": {
"id": 42,
"ulid": "01HX9F3K2M...",
"subject": "Cannot connect to the API",
"status": "open",
"channel": { "id": 1, "name": "Support", "type": "email", "color": "#3b82f6" },
"assignee": null,
"contact": {
"id": 12,
"ulid": "01HX9F3K2N...",
"name": "John Doe",
"email": "john@customer.com",
"avatar_url": null
},
"tags": [],
"created_at": "2025-06-01T10:30:00+00:00",
"updated_at": "2025-06-01T10:30:00+00:00"
}
}
The ticket will be processed through the standard inbound pipeline, which means workflows, AI title generation, notifications, and other automations will run as if the ticket arrived via email.
If the contact email does not exist yet, a new contact will be created automatically.
List Tickets
Retrieve a paginated list of tickets in the current workspace.
GET /api/tickets
Query Parameters
| Parameter | Type | Description |
|---|---|---|
per_page |
integer | Results per page (default: 25, max: 100) |
sort |
string | Sort field: created_at, updated_at, last_activity_at. Prefix with - for descending (default: -updated_at) |
filter[status] |
string | Comma separated statuses: open, waiting, closed, spam |
filter[channel_ids] |
string | Comma separated channel IDs |
filter[tag_ids] |
string | Comma separated tag IDs |
filter[search] |
string | Search term for subject and message content |
filter[assigned_to_me] |
boolean | Only tickets assigned to the authenticated user |
filter[unassigned] |
boolean | Only unassigned tickets |
Example Request
curl "https://there-there.app/api/tickets?filter[status]=open&sort=-updated_at&per_page=10" \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"
Example Response
{
"data": [
{
"id": 42,
"ulid": "01HX9F3K2M...",
"subject": "Cannot reset my password",
"status": "open",
"channel": { "id": 1, "name": "Support", "type": "email", "color": "#3b82f6" },
"assignee": {
"id": 5,
"name": "Jane Smith",
"email": "jane@example.com",
"avatar_url": null,
"timezone": "UTC"
},
"contact": {
"id": 12,
"ulid": "01HX9F3K2N...",
"name": "John Doe",
"email": "john@customer.com",
"avatar_url": null
},
"tags": [{ "id": 3, "ulid": "01HX9F3K2P...", "name": "urgent", "color": "red" }],
"latest_message_preview": "I tried clicking the link but...",
"summary": null,
"created_at": "2025-06-01T10:30:00+00:00",
"updated_at": "2025-06-02T14:15:00+00:00"
}
],
"links": { "first": "...", "last": "...", "prev": null, "next": "..." },
"meta": { "current_page": 1, "last_page": 5, "per_page": 10, "total": 48 }
}
Get Ticket
Retrieve a single ticket with its messages and activity log.
GET /api/tickets/{id}
Example Request
curl https://there-there.app/api/tickets/42 \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json"
Example Response
{
"data": {
"id": 42,
"ulid": "01HX9F3K2M...",
"subject": "Cannot reset my password",
"status": "open",
"channel": { "id": 1, "name": "Support", "type": "email", "color": "#3b82f6" },
"assignee": null,
"contact": {
"id": 12,
"ulid": "01HX9F3K2N...",
"name": "John Doe",
"email": "john@customer.com",
"avatar_url": null
},
"tags": [],
"latest_message_preview": "I tried clicking the link but...",
"summary": null,
"created_at": "2025-06-01T10:30:00+00:00",
"updated_at": "2025-06-02T14:15:00+00:00",
"messages": [
{
"id": 101,
"ulid": "01HX9F3K2Q...",
"type": "inbound",
"body_html": "<p>I tried clicking the link but it expired.</p>",
"sender": {
"id": 12,
"type": "contact",
"name": "John Doe",
"email": "john@customer.com",
"avatar_url": null
},
"attachments": [],
"is_forward": false,
"created_at": "2025-06-01T10:30:00+00:00"
}
],
"activities": [
{
"type": "status_changed",
"user": { "id": 5, "name": "Jane Smith" },
"created_at": "2025-06-02T14:15:00+00:00"
}
]
}
}
Update Ticket Status
PUT /api/tickets/{id}/status
{ "status": "closed" }
Valid status values: open, waiting, closed, spam.
Update Ticket Assignee
PUT /api/tickets/{id}/assignee
{ "assignee_id": 5 }
Pass null for assignee_id to unassign.
Update Ticket Team
PUT /api/tickets/{id}/team
{ "team_id": 3 }
Pass null for team_id to unassign.
Delete Ticket
Tickets are not deleted via the API. Use the Spam status instead.