All API routes are under /api/ and return JSON responses.
GET /api/tickets
Query Parameters:
| Parameter | Type | Description |
|---|---|---|
| status | string | Filter by status (OPEN, IN_PROGRESS, etc.) |
| category | string | Filter by category |
| priority | string | Filter by priority |
| assigneeId | string | Filter by assigned team member |
| search | string | Search subject, externalId, or aiSummary |
| customerEmail | string | Filter by customer email |
| page | number | Page number (default: 1) |
| limit | number | Items per page (default: 20) |
Response:
{
"tickets": [
{
"id": "clx...",
"externalId": "0001",
"subject": "Ich möchte mein Abo kündigen",
"status": "OPEN",
"priority": "HIGH",
"category": "SUBSCRIPTION_CANCELLATION",
"language": "de",
"aiSummary": "User wants to cancel subscription",
"aiConfidence": 0.95,
"customerEmail": "[email protected]",
"assignee": { "id": "...", "name": "Linus Baumberger" },
"createdAt": "2026-03-01T10:00:00Z",
"_count": { "messages": 3 }
}
],
"total": 100,
"page": 1,
"limit": 20
}GET /api/tickets/{id}
Returns full ticket with messages, enrichments, and assignee. Messages are ordered by creation date ascending.
PATCH /api/tickets/{id}
Allowed Fields:
| Field | Type |
|---|---|
| status | string |
| priority | string |
| category | string |
| tags | string[] |
| language | string |
| translatedSubject | string |
| aiSummary | string |
| aiConfidence | number |
| aiSuggestedAssignee | string |
| assigneeId | string |
Fields not in this whitelist are silently ignored.
Response: Updated ticket object with assignee.
POST /api/tickets/{id}/messages
Request Body:
{
"body": "Thank you for contacting us...",
"source": "OUTBOUND_EMAIL",
"teamMemberId": "clx...",
"senderName": "Linus Baumberger",
"senderEmail": "[email protected]"
}Source values: OUTBOUND_EMAIL, INTERNAL_NOTE
When source is OUTBOUND_EMAIL, the ticket status is automatically updated to WAITING_ON_CUSTOMER.
GET /api/tickets/{id}/messages
Returns all messages for a ticket, ordered by creation date ascending.
POST /api/tickets/{id}/assign
Request Body:
{
"assigneeId": "clx..."
}Pass null for assigneeId to unassign. Sets ticket status to IN_PROGRESS when assigning, OPEN when unassigning.
POST /api/ai/categorize
Request Body:
{
"ticketId": "clx..."
}Categorizes a single ticket using Claude Haiku. Updates the ticket with category, priority, language, summary, confidence, and suggested assignee. Auto-assigns to default team member if configured.
Response: The categorization result.
Error: Returns 503 if ANTHROPIC_API_KEY is not configured.
POST /api/ai/categorize-batch
No request body. Finds up to 20 uncategorized tickets and categorizes them sequentially.
Response:
{
"categorized": 15,
"errors": 2
}POST /api/ai/draft
Request Body:
{
"ticketId": "clx..."
}Generates an AI response draft using Claude Sonnet based on the ticket's conversation history.
Response:
{
"draft": "Vielen Dank für Ihre Nachricht..."
}POST /api/ai/translate
Request Body:
{
"text": "Ich möchte mein Abo kündigen",
"targetLanguage": "en",
"messageId": "clx...",
"ticketId": "clx..."
}messageId and ticketId are optional. When provided, the translation is saved to the message's bodyTranslated field.
Response:
{
"translated": "I would like to cancel my subscription"
}POST /api/outlook/sync
No request body. Syncs emails from the configured Outlook shared mailbox.
- Mock mode (default): Imports 3 demo emails
- Real mode (
OUTLOOK_ENABLED=true): Fetches from Microsoft Graph API
Response:
{
"created": 3,
"updated": 0,
"total": 3,
"mock": true
}POST /api/outlook/webhook
Microsoft Graph subscription endpoint. Handles:
- Subscription validation (responds with
validationToken) - Change notifications for new emails
GET /api/enrichment/{ticketId}
Returns mock enrichment data for a ticket's customer:
Response:
{
"sentry": [
{
"title": "TypeError: Cannot read property 'flashcards' of undefined",
"count": 12,
"lastSeen": "2 hours ago",
"level": "error"
}
],
"posthog": {
"sessions": [...],
"lastActive": "2026-03-01",
"totalSessions": 47,
"featureFlags": { "new-quiz-mode": true },
"events": [...]
},
"userData": {
"plan": "monthly",
"signupDate": "2025-09-15",
"deckCount": 15,
"cardCount": 234,
"studySessions": 67
}
}GET /api/dashboard
Response:
{
"stats": {
"total": 105,
"open": 42,
"inProgress": 12,
"resolved": 38,
"autoClosed": 13
},
"categoryDistribution": [
{ "category": "FLASHCARD_ISSUES", "_count": { "category": 15 } }
],
"languageDistribution": [
{ "language": "de", "_count": { "language": 45 } }
],
"recentTickets": [...],
"assigneeWorkload": [
{ "assigneeId": "clx...", "_count": { "assigneeId": 8 }, "assignee": { "name": "Linus" } }
]
}GET /api/team-members
Returns all team members sorted by name.
Response:
[
{
"id": "clx...",
"name": "Linus Baumberger",
"email": "[email protected]",
"role": "support"
}
]All endpoints return errors in this format:
{
"error": "Description of what went wrong"
}| Status | Meaning |
|---|---|
| 400 | Invalid request (bad JSON, missing fields, no valid fields) |
| 404 | Resource not found |
| 500 | Internal server error |
| 503 | Service unavailable (AI key not configured) |