Tickets
Tickets are the core resource in Tiqora. Each ticket represents a customer support request with a subject, description, status, priority, and full conversation history through replies.
List Tickets
GET /api/v1/external/tickets
Returns a paginated list of tickets for your tenant.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
per_page |
integer | 25 | Results per page (max: 100) |
cursor |
string | — | Cursor for next page |
status |
string | — | Filter by status |
priority |
string | — | Filter by priority |
customer_id |
uuid | — | Filter by customer |
department_id |
uuid | — | Filter by department |
category_id |
uuid | — | Filter by category |
assigned_to |
uuid | — | Filter by assigned agent |
created_after |
datetime | — | ISO 8601 datetime |
created_before |
datetime | — | ISO 8601 datetime |
search |
string | — | Full-text search in subject and description |
sort_by |
string | created_at |
Sort field |
sort |
string | desc |
Sort direction: asc or desc |
Example Request
GET /api/v1/external/tickets?status=open&priority=high&per_page=10
Example Response
{
"data": [
{
"id": "01912345-6789-7abc-def0-123456789abc",
"ticket_number": "1042",
"subject": "Cannot access billing portal",
"description": "When I click on 'Billing' I get a 403 error...",
"status": "open",
"priority": "high",
"channel": "api",
"customer": {
"id": "01912345-0000-7abc-def0-123456789abc",
"email": "jane@example.com",
"name": "Jane Smith"
},
"department": {
"id": "01912345-1111-7abc-def0-123456789abc",
"name": "Technical Support"
},
"category": {
"id": "01912345-2222-7abc-def0-123456789abc",
"name": "Account Access"
},
"assigned_to": null,
"tags": ["billing", "access"],
"metadata": {},
"first_response_at": null,
"resolved_at": null,
"closed_at": null,
"created_at": "2025-01-15T09:30:00Z",
"updated_at": "2025-01-15T09:30:00Z"
}
],
"meta": {
"per_page": 10,
"has_more": true,
"next_cursor": "eyJpZCI6IjAxOTEyMzQ1In0="
}
}
Hint: Use
next_cursorfrom themetaobject to fetch the next page. Whenhas_moreisfalse, you've reached the end.
Create a Ticket
POST /api/v1/external/tickets
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
subject |
string | Yes | Ticket subject (max 500 chars) |
description |
string | Yes | Ticket description (Markdown supported) |
priority |
string | No | low, medium, high, or urgent. Default: medium |
customer |
object | Yes | Customer info (see below) |
customer.email |
string | Yes | Customer's email address |
customer.name |
string | No | Customer's display name |
category_id |
uuid | No | Assign to a specific category |
department_id |
uuid | No | Assign to a specific department |
tags |
string[] | No | Array of tag strings |
metadata |
object | No | Custom key-value pairs |
Example Request
{
"subject": "Payment not processing",
"description": "I'm trying to upgrade my plan but the payment keeps failing.\n\n**Error message:** Transaction declined.\n\nCard ending in 4242.",
"priority": "high",
"customer": {
"email": "jane@example.com",
"name": "Jane Smith"
},
"tags": ["billing", "payment"],
"metadata": {
"plan": "professional",
"card_last4": "4242"
}
}
Example Response (201 Created)
{
"data": {
"id": "01912345-6789-7abc-def0-123456789abc",
"ticket_number": "1043",
"subject": "Payment not processing",
"status": "new",
"priority": "high",
"channel": "api",
"customer": {
"id": "01912345-0000-7abc-def0-123456789abc",
"email": "jane@example.com",
"name": "Jane Smith"
},
"created_at": "2025-01-15T10:00:00Z"
}
}
Hint: If a customer with the provided email doesn't exist, Tiqora creates one automatically. You don't need to create the customer first.
Hint: After creation, the AI pipeline processes the ticket asynchronously — it may be auto-categorized, routed to a department, and even get a suggested reply depending on your AI settings.
Get a Ticket
GET /api/v1/external/tickets/{ticket_id}
Returns a single ticket with all details.
Example Response
{
"data": {
"id": "01912345-6789-7abc-def0-123456789abc",
"ticket_number": "1043",
"subject": "Payment not processing",
"description": "I'm trying to upgrade my plan...",
"status": "in_progress",
"priority": "high",
"channel": "api",
"customer": {
"id": "01912345-0000-7abc-def0-123456789abc",
"email": "jane@example.com",
"name": "Jane Smith"
},
"department": {
"id": "01912345-1111-7abc-def0-123456789abc",
"name": "Billing"
},
"assigned_to": {
"id": "01912345-3333-7abc-def0-123456789abc",
"name": "Alex Johnson",
"email": "alex@company.com"
},
"tags": ["billing", "payment"],
"metadata": {
"plan": "professional",
"card_last4": "4242"
},
"first_response_at": "2025-01-15T10:15:00Z",
"resolved_at": null,
"closed_at": null,
"created_at": "2025-01-15T10:00:00Z",
"updated_at": "2025-01-15T10:30:00Z"
}
}
Update a Ticket
PATCH /api/v1/external/tickets/{ticket_id}
Update one or more ticket fields.
Request Body
| Field | Type | Description |
|---|---|---|
status |
string | New status (must follow transition rules) |
priority |
string | low, medium, high, or urgent |
category_id |
uuid | Reassign category |
department_id |
uuid | Reassign department |
assigned_to |
uuid or null | Assign or unassign an agent |
tags |
string[] | Replace tags |
metadata |
object | Merge into existing metadata |
Example: Update Status
{
"status": "open"
}
Example: Assign and Prioritize
{
"priority": "urgent",
"assigned_to": "01912345-3333-7abc-def0-123456789abc"
}
Status Transitions
Ticket status follows a state machine. Not all transitions are valid:
┌──────────────────────────────────────────┐
│ │
▼ │
[new] ──────► [open] ──────► [in_progress] │
│ │ │ │ │
│ │ │ │ │
│ ▼ ▼ │ │
│ [waiting_on_customer] │ [escalated]
│ │ │ │
│ │ │ │
│ ▼ ▼ │
│ [resolved] ◄──────────────┘
│ │
│ ▼
└──────► [auto_resolved] [closed]
Allowed Transitions
| From | Allowed Next States |
|---|---|
new |
open, auto_resolved |
open |
in_progress, waiting_on_customer, resolved |
in_progress |
waiting_on_customer, resolved, escalated |
waiting_on_customer |
in_progress, resolved, closed |
escalated |
in_progress, resolved |
resolved |
closed, open (reopen) |
closed |
open (reopen) |
auto_resolved |
open (reopen) |
Important: Attempting an invalid transition returns a
422 VALIDATION_ERROR. For example, you cannot go directly fromnewtoin_progress— you must first transition toopen.
Pagination
All list endpoints use cursor-based pagination:
// Fetch all tickets page by page
let cursor = null;
let allTickets = [];
do {
const params = new URLSearchParams({ per_page: '100' });
if (cursor) params.set('cursor', cursor);
const response = await tiqoraRequest(
'GET',
`/api/v1/external/tickets?${params}`
);
allTickets.push(...response.data);
cursor = response.meta.has_more ? response.meta.next_cursor : null;
} while (cursor);
Hint: Use
per_page=100(the maximum) when fetching large datasets to minimize the number of requests.
Public Support Endpoint
Tiqora also provides an unauthenticated endpoint for public-facing support forms. This does not require API key authentication — it's designed for embedding a contact form on your website.
POST /api/v1/support/tickets
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Submitter's full name (max 255 chars) |
email |
string | Yes | Submitter's email address (max 255 chars) |
mobile_number |
string | Yes | Phone number (7-20 digits, optional + prefix) |
nationality |
string | Yes | Submitter's nationality (max 100 chars) |
topic |
string | Yes | One of: general_inquiry, bug_report, billing_issue, feature_request |
subject |
string | Yes | Ticket subject (max 500 chars) |
description |
string | Yes | Ticket description (min 20 chars) |
website |
string | No | Honeypot field — must be empty. See below. |
Example Request
{
"name": "Jane Smith",
"email": "jane@example.com",
"mobile_number": "+1 555-123-4567",
"nationality": "American",
"topic": "bug_report",
"subject": "Login page not loading",
"description": "When I try to access the login page, I get a blank white screen. This started happening today."
}
Example Response (201 Created)
{
"data": {
"ticket_number": "TIQ-001042",
"message": "Your support request has been submitted successfully. We will get back to you shortly."
}
}
Honeypot Field
The website field is a spam prevention mechanism. It should be included in your HTML form but hidden from users via CSS:
<!-- Hidden honeypot field — bots auto-fill it, humans never see it -->
<div style="position: absolute; left: -9999px;" aria-hidden="true">
<input type="text" name="website" tabindex="-1" autocomplete="off" />
</div>
If the website field contains any value, the request is rejected with a 422 error. Legitimate users will never fill it because they can't see it, but automated bots typically fill all form fields.
Rate Limits
The public support endpoint has multiple layers of rate limiting to prevent abuse:
| Protection | Limit | Error Code |
|---|---|---|
| Per-IP throttle | 10 requests per minute | RATE_LIMIT_EXCEEDED (429) |
| Per-tenant rate limit | 30 tickets per hour | RATE_LIMIT_EXCEEDED (429) |
| Per-email cooldown | 1 submission per 5 minutes per email address | RATE_LIMIT_EXCEEDED (429) |
| Monthly ticket quota | Based on your plan's ticket limit | TICKET_LIMIT_EXCEEDED (429) |
Successful responses include rate limit headers:
X-RateLimit-Limit: 30
X-RateLimit-Remaining: 27
Important: If you're building a custom form that submits to this endpoint, implement client-side rate limit handling. Show users a friendly message when they hit the per-email cooldown (5 minutes between submissions).