Docs Tickets

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_cursor from the meta object to fetch the next page. When has_more is false, 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 from new to in_progress — you must first transition to open.

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).