# Tickets

> Create, list, update and manage support tickets.

Source: https://tickos.dev/docs/tickets

The Ticket object represents a support conversation with a customer. Tickets belong to an inbox, carry a priority, and contain a thread of messages with optional attachments.

## Identifiers

- Ticket ids are prefixed strings such as tkv_4d5e6f... (not UUIDs). Inbox ids start with inb_, tag ids with tag_, message ids with msg_ and customer ids with cus_. User ids (assigned_to, user_id) are UUIDs.
- Every ticket also has a human-readable client_id (e.g. SUP-000123) built from its inbox prefix. Client ids are for display and are not guaranteed to be unique.
- Use the ticket id in the {id} path parameter. Client ids with the SUP- prefix are also accepted on GET/PATCH/DELETE /v1/tickets/{id} and on the /messages and /tags sub-resources. Client ids with any other prefix (inbox prefixes are configurable, default TKT) are treated as ticket ids and return 404. PATCH /v1/tickets/{id}/manage only accepts the ticket id.
- If a SUP- client id matches more than one ticket, GET and PATCH return 404 and DELETE returns 409. Use the ticket id to avoid ambiguity.

## Lifecycle

1. open -- new or reopened ticket, awaiting an agent response.
2. pending -- an agent replied; waiting on the customer.
3. review -- the customer replied to a pending ticket; needs agent attention again.
4. resolved -- the issue is resolved.
5. closed -- archived. Resolved and closed tickets reopen (status open) when the customer replies.

Automatic transitions: an outbound reply moves open or review tickets to pending; a customer (inbound) reply moves pending to review, and resolved or closed to open. Internal notes never change the status.

## Allowed values

| Field | Allowed values |
|---|---|
| status | open, pending, review, resolved, closed |
| priority | low, normal, medium, high, urgent |
| channel | direct, email |

> **Warning:** POST /v1/tickets and PATCH /v1/tickets/{id} do not validate these fields before writing them. A value outside the list is rejected by the database and the request fails with 500 instead of 400. Only PATCH /v1/tickets/{id}/manage validates status and priority (400 on invalid values).

## POST /v1/tickets -- Create a ticket

Permission: write. Creates a ticket (status open) and its first message, which is stored as an inbound message from the customer. If customer_email does not match an existing customer, the customer is created automatically.

| Field | Type | Required | Description |
|---|---|---|---|
| subject | string | Yes | The ticket subject line. |
| message | string | Yes | The initial message body (plain text). Line breaks are converted to <br> for the HTML body. |
| customer_id | string | Conditional | Existing customer id (cus_...). Provide customer_id OR customer_email. Must belong to your workspace (404 otherwise). |
| customer_email | string | Conditional | Customer email. Creates the customer if it does not exist; updates name and phone of an existing customer when provided. |
| customer_name | string | No | Customer display name. Defaults to the local part of the email when creating. |
| customer_phone | string | No | Customer phone number. |
| customer_group | string | No | Contact group name (exact, case-sensitive match on an active group). Adds the customer to the group if found; ignored otherwise. |
| priority | string | No | low, normal (default), medium, high, or urgent. |
| channel | string | No | direct (default) or email. |
| inbox_id | string | No | Target inbox id (inb_...). Uses the oldest active inbox of the workspace if no inbox field is sent. |
| inbox_email | string | No | Target inbox by email address (alternative to inbox_id). |
| inbox | string | No | Alias: accepts an inbox email or an inb_ id. Priority: inbox_id, then inbox_email, then inbox. |
| assigned_to | string | No | Email of a workspace member to assign (case-insensitive match). If no member matches, the ticket is created unassigned without an error. |
| metadata | object | No | Arbitrary JSON metadata. |
| attachments | array | No | Array of { fileName, fileData (base64), contentType }. Attached to the first message. |

```bash
curl https://api.tickos.dev/v1/tickets \
  -H 'Authorization: Bearer sk_your_api_key' \
  -H 'Content-Type: application/json' \
  -d '{
    "subject": "Cannot access my account",
    "message": "Hi, I am unable to log in since yesterday.",
    "customer_email": "jane@example.com",
    "customer_name": "Jane Doe",
    "priority": "high",
    "assigned_to": "agent@company.com",
    "metadata": { "source": "api", "plan": "pro" }
  }'
```

Returns 201 with { "ticket": {...}, "message": {...}, "attachments": [...] }. The ticket object includes customer { id, name, email }. Fires the ticket.created webhook (no separate message.created event for the first message). If the inbox has an auto-responder, Slack or forwarding configured, those run as well.

| Status | Cause |
|---|---|
| 400 | Missing subject, message, or both customer_id and customer_email; inbox identifier that is neither an email nor an inb_ id; workspace without any inbox. |
| 403 | The customer (by customer_id or customer_email) is blocked. |
| 404 | customer_id or inbox not found in your workspace. |
| 429 | Plan ticket or storage limit reached. Body: { error: "Limit exceeded", message, current, limit, percentage, recommended_plan }. |

> **Note:** Blocked customers (is_blocked: true) cannot open new tickets -- the API returns 403.

## GET /v1/tickets -- List tickets

Permission: read. Returns { "data": [...], "pagination": { "limit", "offset", "total" } }. Each ticket includes customer { id, name, email }, assigned_to_user { id, email, full_name }, tags [{ id, name, color }], message_count and attachment_count.

| Parameter | Type | Default | Description |
|---|---|---|---|
| status | string | -- | Comma-separated: open, pending, review, resolved, closed. |
| priority | string | -- | Comma-separated: low, normal, medium, high, urgent. |
| archived | string | -- | true or false. Omit to include both. |
| is_read | string | -- | true or false. Omit to include both. |
| snoozed | string | all | only_snoozed, exclude_snoozed, or all. |
| assigned_to | string | -- | User UUID or "unassigned". |
| inbox_id | string | -- | Filter by inbox id (inb_...). |
| tag_id | string | -- | Filter by tag id (tag_...). |
| has_scheduled | string | -- | true: only tickets with at least one scheduled (not yet sent) email. |
| has_draft | string | -- | true: only tickets with at least one draft message. |
| search | string | -- | Free-text search on subject, client ID, customer email and name. |
| updated_after | string | -- | ISO 8601 date for delta sync (400 if not a valid date). |
| sort_order | string | desc | asc or desc. Sorts by last_message_at. |
| limit | number | 50 | Max 100. |
| offset | number | 0 | Pagination offset. |

## GET /v1/tickets/{id} -- Get a ticket

Permission: read. Returns the bare ticket object (no wrapper) with customer { id, name, email, phone }, assigned_to_user { id, email, full_name }, tags [{ id, name, color }] and an embedded messages array. {id} is the ticket id or a SUP- client id (see Identifiers).

> **Note:** The embedded messages array is a lightweight summary (id, body_text, body_html, direction, is_customer, from_email, from_name, created_at). It is not ordered, does not include attachments, and may include draft messages. Use GET /v1/tickets/{id}/messages for the ordered thread with attachments.

## PATCH /v1/tickets/{id} -- Update a ticket

Permission: write. Updates direct fields; other fields in the body are ignored (400 if none of these is present). Returns the updated ticket object with customer and inbox { id, name, email_address }. Fires the ticket.updated webhook.

| Field | Type | Description |
|---|---|---|
| subject | string | New subject line. |
| description | string | Ticket description. |
| status | string | open, pending, review, resolved, or closed. Unlike the manage action, does not set closed_at. |
| priority | string | low, normal, medium, high, or urgent. |
| assigned_to | string \| null | User UUID of a workspace member (not an email, unlike POST). 404 if the user is not a member. null unassigns. |
| metadata | object | Arbitrary JSON. Replaces the existing metadata. |

## DELETE /v1/tickets/{id} -- Delete a ticket

Permission: admin. Permanently deletes the ticket and its attachments from storage. Returns { "deleted": true, "id": "<id as sent in the path>" }. Fires the ticket.deleted webhook. Returns 409 if a SUP- client id matches more than one ticket (nothing is deleted).

## PATCH /v1/tickets/{id}/manage -- Management actions

Permission: write. Performs a single operation such as snooze, archive or assign. Send { "action": "...", "data": { ... } }. {id} must be the ticket id (client ids are not accepted). Returns { "ticket": {...}, "action": "...", "message": "..." }, where ticket includes customer and assigned_to_user. Management actions do not fire webhooks.

| Action | Data | Description |
|---|---|---|
| mark_read | -- | Mark the ticket as read. |
| mark_unread | -- | Mark the ticket as unread. |
| archive | -- | Archive the ticket. |
| unarchive | -- | Unarchive the ticket. |
| snooze | { "until": "ISO 8601" } | Snooze until the given date (must be in the future, otherwise 400). |
| unsnooze | -- | Cancel the snooze. |
| update_status | { "status": "open" } | open, pending, review, resolved or closed (400 otherwise). Closing sets closed_at; any other status clears it. |
| update_priority | { "priority": "high" } | low, normal, medium, high or urgent (400 otherwise). |
| assign | { "user_id": "uuid" } | Assign to a workspace member. 404 if the user is not a member of the workspace. |
| unassign | -- | Remove assignment. |

```bash
curl -X PATCH https://api.tickos.dev/v1/tickets/{id}/manage \
  -H 'Authorization: Bearer sk_your_api_key' \
  -H 'Content-Type: application/json' \
  -d '{ "action": "snooze", "data": { "until": "2025-03-15T09:00:00Z" } }'
```

An unknown action or missing data returns 400.

## GET /v1/tickets/stats -- Ticket statistics

Permission: read. Returns counts by status, priority and recent activity (tickets created today, in the last 7 days and this calendar month). Optionally filter by inbox_id. by_status covers open, pending, resolved and closed only (no review count).

```json
{
  "total": 284,
  "unread": 12,
  "archived": 45,
  "snoozed": 3,
  "active": 156,
  "by_status": { "open": 80, "pending": 40, "resolved": 20, "closed": 16 },
  "by_priority": { "low": 30, "normal": 100, "medium": 40, "high": 10, "urgent": 2 },
  "recent_activity": { "today": 15, "this_week": 67, "this_month": 210 }
}
```

## GET /v1/tickets/agent-stats -- Agent performance statistics

Permission: read. Returns per-agent ticket counts by status (agent_id null = unassigned), plus average first response time (first outbound message) and average resolution time in seconds. Query params: start_date, end_date (ISO 8601, filter by ticket creation) and include_timeline=true to add a daily series of resolved tickets per agent.

```json
{
  "agents": [
    {
      "agent_id": "uuid", "name": "Jane Doe", "email": "jane@acme.com",
      "total": 120, "open": 10, "pending": 5, "review": 2, "resolved": 80, "closed": 23,
      "urgent": 3, "unread": 4,
      "avg_first_response_seconds": 5400, "avg_resolution_seconds": 86400
    }
  ],
  "totals": { "total": 250, "open": 30, "pending": 12, "review": 5, "resolved": 150, "closed": 53, "urgent": 6, "unread": 9 },
  "timeline": { "days": [ { "date": "2026-07-01", "agents": { "uuid": 12, "unassigned": 3 } } ] }
}
```

## GET /v1/tickets/tag-stats -- Tag statistics

Permission: read. Counts tickets created in the period, grouped by tag, with a breakdown by status. A ticket with several tags counts once in each tag; tags with no tickets in the period are omitted. Tags are sorted by total, descending.

| Parameter | Type | Default | Description |
|---|---|---|---|
| start_date | string | -- | ISO 8601. Tickets created on or after this date. |
| end_date | string | -- | ISO 8601. Tickets created before this date (exclusive). |
| include_timeline | string | false | true adds a daily series (by ticket creation date) of tickets per tag. |
| inbox_id | string | -- | One or more inbox ids, comma-separated. |

```json
{
  "tags": [
    {
      "tag_id": "tag_7g8h...", "name": "Billing", "color": "#22c55e", "inbox_id": "inb_2b3c...",
      "total": 42, "open": 5, "pending": 3, "review": 1, "resolved": 30, "closed": 3
    }
  ],
  "totals": { "tickets": 120, "tagged": 80, "untagged": 40 },
  "timeline": { "days": [ { "date": "2026-07-01", "tags": { "tag_7g8h...": 4 } } ] }
}
```

timeline is only present with include_timeline=true. truncated: true is added when the period contains more tickets than the endpoint processes in one request; narrow the date range in that case.

## POST /v1/tickets/compose -- Compose an email ticket

Permission: write. Creates a ticket and sends the first message as an outbound email via your verified domain. Only works with email-type inboxes.

| Field | Type | Required | Description |
|---|---|---|---|
| inbox_id | string | Conditional | Inbox id (inb_...). Provide inbox_id, inbox_email, or inbox. |
| inbox_email | string | Conditional | Inbox email address. |
| inbox | string | Conditional | Alias for inbox_id or inbox_email. |
| to_email | string | Yes | Recipient email address. |
| subject | string | Yes | Email subject. |
| body_text | string | Yes | Plain text email body. |
| body_html | string | No | HTML email body. |
| cc | string | No | CC recipients, comma-separated. |
| bcc | string | No | BCC recipients, comma-separated. |
| from_name | string | No | Sender display name. |
| files | array | No | Array of { fileName, fileData (base64), contentType }. |

Returns 201 with { "success": true, "ticket": { id, client_id, subject, status }, "message_id", "email_status", "attachments_uploaded" }.

> **Tip:** Use the ticket id (tkv_...) as the {id} path parameter. Client ids with the SUP- prefix are also accepted on most ticket endpoints -- see Identifiers.
