Tickets API
Die Tickets API verwaltet Tickets von der Erstellung bis zum Abschluss: Anlegen, Bearbeiten und Löschen, Verknüpfung mit Problems, Changes, Assets und KB-Artikeln, Sammelbearbeitung, Zusammenführen von Duplikaten, Verlauf, Vertretungen und ein fein abgestuftes Rechtemodell.
Endpoints Übersicht
CRUD & Query
| Method | Endpoint | Beschreibung |
|---|---|---|
GET | /api/tickets | Alle Tickets abrufen (mit Filtering) |
GET | /api/tickets/stats | Statistiken (Counts pro Status) |
GET | /api/tickets/substitute-info | Vertretungs-Info (Users die ich vertrete) |
GET | /api/tickets/:id | Einzelnes Ticket abrufen |
POST | /api/tickets | Neues Ticket erstellen |
PATCH | /api/tickets/:id | Ticket aktualisieren |
DELETE | /api/tickets/:id | Ticket löschen (Soft-Delete) |
POST | /api/tickets/:id/restore | Aus dem Papierkorb zurückholen (restore + viewDeleted) → 204 |
Papierkorb: Die Liste der gelöschten Tickets läuft über ?deleted=1 und verlangt tickets.viewDeleted. Wiederherstellen verlangt tickets.restore und zusätzlich tickets.viewDeleted. Das gilt für Benutzer und API-Keys gleichermaßen. Löschen ist eine kritische Aktion und wird abgelehnt, solange aktive Verknüpfungen bestehen, auch zu Incidents (400 TICKET_HAS_ACTIVE_LINKS). Dieselbe Sperre gilt für die Sub-Ticket-Beziehung: ein Ticket mit Sub-Tickets und ein Ticket, das selbst Sub-Ticket ist, sind nicht löschbar (details.links nennt children bzw. parent).
Teilnehmer- und Folgen-Routen prüfen die Ticket-Sichtbarkeit: Sie prüfen dieselbe Sichtbarkeit wie das Ticket-Detail, inklusive Postfach-Beschränkung, Gruppen und Vertretung. Auf einem gelöschten Ticket antworten sie 404, und die Teilnehmer eines Tickets aus einem zugriffsbeschränkten Postfach bleiben ohne diesen Zugriff verborgen — auch für Rollen mit globaler Sicht.
Messages & Activity
| Method | Endpoint | Beschreibung |
|---|---|---|
POST | /api/tickets/:id/messages | Nachricht/Kommentar hinzufügen |
Cross-Entity-Linking
| Method | Endpoint | Beschreibung |
|---|---|---|
POST | /api/tickets/:id/batch-update-links | Batch-Update aller Links (Problems, Changes, Assets, KB) |
Advanced Operations
| Method | Endpoint | Beschreibung |
|---|---|---|
GET | /api/tickets/:id/merge-preview | Merge-Vorschau (was würde übertragen) |
POST | /api/tickets/:id/merge | Ticket mergen (Duplikate zusammenführen) |
POST | /api/tickets/:id/transfer-attachments | Anhänge aus einem anderen Ticket (sourceTicketId) in dieses Ticket übernehmen (tickets.editAll) |
PATCH | /api/tickets/:id/mailbox | Postfach des Tickets wechseln (Permission tickets.changeMailbox). Abgelehnt wird mit benanntem Grund: TICKET_NOT_EMAIL_SOURCED, TICKET_ALREADY_IN_MAILBOX, TARGET_MAILBOX_INACTIVE, MAILBOX_NOT_FOUND. |
PATCH | /api/tickets/bulk | Bulk-Operationen (Status/Assign/…, Permission tickets.bulk) |
GET | /api/tickets/:id/suggest-known-errors | Bekannte Fehler (Known Errors) zum Ticket vorschlagen (problems.viewOwn) |
POST | /api/tickets/:id/apply-workaround | Workaround aus einem Known Error übernehmen → 204 (problems.viewOwn + tickets.editOwn; unbekanntes Problem: 404 PROBLEM_NOT_FOUND) |
Participants & Follower
| Method | Endpoint | Beschreibung |
|---|---|---|
GET | /api/tickets/:id/participants | Alle Teilnehmer eines Tickets → { data }. Je Zeile: id, userId, email, displayName, role, source, notificationsDisabled und user{id,name,email}. |
POST | /api/tickets/:id/participants | Teilnehmer hinzufügen (FOLLOWER, CC, MENTIONED) |
PATCH | /api/tickets/:id/participants/:participantId | Teilnehmer-Rolle ändern |
DELETE | /api/tickets/:id/participants/:participantId | Teilnehmer entfernen → 204 (unbekannte Teilnehmer-ID: 404 PARTICIPANT_NOT_FOUND) |
POST | /api/tickets/:id/follow | Ticket folgen (aktueller User) → 204, idempotent. Wer bereits Kunde oder zugewiesener Agent ist, einem geschlossenen Ticket folgen will oder keine E-Mail-Adresse hat, bekommt 400 CANNOT_FOLLOW_TICKET mit dem Grund in details.reason. |
DELETE | /api/tickets/:id/follow | Ticket entfolgen (aktueller User) → 204 |
E-Mail-Actions
| Method | Endpoint | Beschreibung |
|---|---|---|
GET | /api/tickets/:id/email-thread | E-Mail-Thread eines Tickets anzeigen |
POST | /api/tickets/:id/email-reply | E-Mail-Antwort senden (mit Signatur) |
POST | /api/tickets/:id/email-retry/:emailMessageId | Fehlgeschlagene E-Mail erneut senden |
PATCH | /api/tickets/:id/email-dismiss/:emailMessageId | E-Mail-Fehlermeldung verwerfen |
E-Mail-Versand erfordert das Status-Recht: Antworten, erneut senden und verwerfen prüfen dasselbe wie PATCH /api/tickets/:id: zuerst die Sicht auf das Ticket, dann das Bearbeitungsrecht und zusätzlich tickets.editStatus oder editAll. Grund: Eine Antwort setzt das Ticket automatisch auf WAITING_CUSTOMER und erfüllt damit die Reaktions-SLA. Eine Rolle mit editOwn, aber ohne editStatus (typisch für Endanwender), erhält daher 403. Der Versand erfordert einen angemeldeten Benutzer; API-Keys werden abgelehnt.
Der Thread selbst bleibt lesbar, sobald das Ticket sichtbar ist (viewOwn genügt) — er liefert eine datenminimierte Sicht: Versand-Payload, Roh-Header, Sicherheits-Details, externe IDs und der Speicherpfad eines Anhangs bleiben serverseitig. Dateien lädt man über die Attachments-API.
Categories
| Method | Endpoint | Beschreibung |
|---|---|---|
GET | /api/tickets/categories | Alle Kategorien abrufen |
GET | /api/tickets/categories/:id/subcategories | Sub-Kategorien abrufen |
Kategorien lesen verlangt ein Recht: Es genügt EINES aus tickets.create, tickets.viewAll, tickets.viewOwn, settings.manageCategories, settings.editSLA, settings.editGeneral, inboundMailboxes.view oder agents.manageGroups. Eine Rolle ohne eines dieser Rechte erhält 403; das betrifft vor allem Integrations-Keys mit sehr schmalen Rollen. Kategorien anlegen, ändern und löschen verlangt settings.manageCategories. Diese kritische Berechtigung gilt auch für Kategorien anderer Bereiche.
E-Mail Integration
Tickets können automatisch via E-Mail erstellt und aktualisiert werden:
E-Mail-to-Ticket (Inbound)
Eingehende E-Mails an konfigurierte Mailboxen (IMAP oder Microsoft Graph API) werden automatisch zu Tickets konvertiert. Jede Mailbox kann individuell konfiguriert werden mit eigenen Zugangsdaten, Kategorien, Prioritäten und Zugriffsrechten:
| E-Mail-Field | Ticket-Field |
|---|---|
| Betreff | title |
| Body (Text/HTML) | description |
| From-Adresse | customerId (User wird auto-erstellt falls nicht vorhanden) |
| Attachments | Via Unified Attachment System (Virus-Scan) |
| - | source = EMAIL |
| - | priority (von Mailbox-Config) |
| - | categoryId (von Mailbox-Config) |
| - | assignedGroupId (von Mailbox-Config) |
Reply-to-Ticket (Thread-Matching)
E-Mail-Antworten werden via RFC 822 Threading automatisch dem korrekten Ticket zugeordnet:
- Custom Headers:
X-Ticket-ID,X-Ticket-Number - In-Reply-To Header: Referenziert vorherige Message-ID
- References Chain: Alle vorherigen Message-IDs
- Subject-Pattern:
[TK-000123],[HD-456789]
Agent-Reply via E-Mail
Antwortet ein Agent über POST /api/tickets/:id/email-reply, geht die Antwort per E-Mail an den Kunden (201):
// Agent reply by email
POST /api/tickets/:id/email-reply
{
"content": "We have identified the issue and are working on a solution...",
"ccAddresses": ["colleague@company.com"],
"includeQuotedReply": true, // ← quotes the last message of the thread (default true)
"attachmentIds": ["b6f1c1e2-9a4d-4c3e-8f7a-2d5e6c7b8a90"] // optional, max. 10
}
// The email contains:
// - Subject: "Re: [TK-000123] Laptop won't start"
// - From: support@company.com (mailbox config)
// - To: customer@example.com
// - Body: agent reply + quoted last message of the thread
// - Headers: In-Reply-To, References (for threading)
Zitat und Quell-Postfach: Zitiert wird die letzte zugestellte Nachricht des Verlaufs — die des Kunden oder die eigene vorherige Antwort. Fehlgeschlagene und verworfene E-Mails werden nie zitiert. Die Kopfzeile des Zitats steht in der Sprache des Empfängers und trägt ein formatiertes Datum; Betreff und Threading-Header richten sich nach der letzten eingehenden E-Mail. Der Versand setzt ein aktives Quell-Postfach voraus: Ist es deaktiviert, wird die Antwort mit 400 SOURCE_MAILBOX_INACTIVE abgelehnt, und im Verlauf entsteht kein Versand-Eintrag.
📧 Detaillierte E-Mail-Integration: Siehe Integrations & Notifications → für IMAP/Graph-API-Configuration, individuelle Mailboxen, E-Mail-Signaturen, Sender-Policies, Mailbox-Zugriffsrechte, Bounce-Detection und Thread-Matching-Logik.
💬 Textbausteine: Im Antwort-Composer fügen Agents vordefinierte Antwort-Vorlagen ein (mehrsprachig, Scope PERSONAL/AGENT_GROUP/ORGANIZATION). Siehe Response Templates API →.
Ticket-Status
| Status | Beschreibung |
|---|---|
OPEN | Offen (neu/unbearbeitet) |
IN_PROGRESS | In Bearbeitung |
WAITING_CUSTOMER | Wartet auf Kunde (aktiv, SLA läuft); kann nach Inaktivität automatisch auf RESOLVED (WC-Auto-Resolve, opt-in) |
WAITING_SUPPORT | Wartet auf Support/Vendor (aktiv, SLA läuft) |
ON_HOLD | Pausiert (inaktiv, SLA pausiert) — mit Wiedervorlage via holdReminderAt |
RESOLVED | Gelöst, wartet auf Kunden-Bestätigung/Auto-Close |
CLOSED | Geschlossen & abgeschlossen |
SPAM | Als Spam markiert |
Permissions
| Permission | Beschreibung |
|---|---|
tickets.viewAll | Alle Tickets sehen |
tickets.viewOwn | Eigene Tickets sehen (als Kunde, Bearbeiter, Mitglied der zugewiesenen Gruppe, Vertretung oder Teilnehmer) |
tickets.create | Tickets erstellen |
tickets.createForOthers | Tickets für andere User erstellen |
tickets.editStatus | Status ändern |
tickets.editPriority | Priority ändern |
tickets.editCategory | Kategorie ändern, auch wenn nur die Unterkategorie (subcategoryId) geändert wird |
tickets.assign | Tickets zuweisen/umzuweisen |
tickets.editOwn | Eigene/zugewiesene Tickets bearbeiten (Owner-Scope) |
tickets.editAll | Alle Felder bearbeiten (inkl. Merge, Links) sowie als einziges Recht den Wechsel des Kunden (customerId) |
tickets.reopen | Geschlossene/SPAM-Tickets wiederöffnen (CLOSED/SPAM → OPEN; eigenes Recht, Grund erforderlich; editAll schließt es nicht ein) |
tickets.reopenOverride | Reopen-Fenster/Limit umgehen (nicht die Grund-Pflicht) |
tickets.bulk | Bulk-Operationen (Status/Assign/…) |
tickets.changeMailbox | Ticket einer anderen Mailbox zuordnen |
tickets.linkToTickets | Sub-Tickets: Anlegen mit Elternticket, Unterordnen, Lösen der Beziehung und die Kandidatenliste |
tickets.viewInternal | Interne Notes sehen (Agent-Only) |
tickets.delete | Tickets löschen (Critical, Audit-Log) |
tickets.restore | Gelöschte Tickets wiederherstellen |
tickets.viewDeleted | Gelöschte Tickets anzeigen |
Sichtbarkeit & Zugriff (Mailbox + Agent-Gruppen)
Welche Tickets ein User in GET /api/tickets sieht, hängt neben tickets.viewAll/viewOwn von drei weiteren Faktoren ab:
- Mailbox-Zugriff (accessRestricted): Tickets einer zugriffsbeschränkten Mailbox sind nur für explizit berechtigte User/Rollen/Agent-Gruppen sichtbar (MailboxAccess-Liste mit separaten Flags canViewTickets / canBeAssigned). Selbst tickets.viewAll wird durch eine restringierte Mailbox eingeschränkt.
- Agent-Gruppen-Scope: Die Sichtprüfung berücksichtigt die aktiven Gruppen-Mitgliedschaften des Users (assignedGroupId ∈ eigene Gruppen) plus reporter/assignee und Vertretung. So sieht ein Agent die Queue seiner Gruppe(n), ohne globale viewAll.
- Vertretung (Substitute): Während einer Abwesenheit sieht/bearbeitet der Vertreter die Tickets des vertretenen Users; Aktionen werden mit actingAs: SUBSTITUTE geloggt.
Konzept/Konfiguration siehe Integrations (Mailbox-Zugriffsrechte) und User Management & RBAC.
Resolution-Codes (beim Lösen)
Beim Setzen auf RESOLVED/CLOSED kann ein standardisierter resolutionCode mitgegeben werden (PATCH /api/tickets/:id), zusammen mit resolutionNote (kundensichtbar), resolutionInternalComment (nur Agenten) und optional resolutionCcAddresses. Die verfügbaren Codes werden zentral gepflegt. Bei einem E-Mail-Ticket geht die Lösung als Antwort im Thread an Kunde und CC hinaus; ist das Quell-Postfach deaktiviert, wird das Ticket dennoch gelöst und die Lösungs-E-Mail unterbleibt. Resolution Codes API →
Sub-Tickets
Ein Ticket lässt sich in Sub-Tickets zerlegen, wenn ein Anliegen mehrere Teilaufgaben umfasst — etwa ein Onboarding, das Hardware, Konten und Schulung verlangt. Die Beziehung ist genau eine Ebene tief: ein Sub-Ticket nimmt keine eigenen Sub-Tickets auf, und ein Ticket mit Sub-Tickets lässt sich keinem anderen unterordnen. Damit bleibt die Liste flach und ein Anliegen wird nie doppelt gezählt.
Anlegen und Verknüpfen
# Create a sub-ticket directly under a parent (permission tickets.linkToTickets)
POST /api/tickets
{ "title": "Order laptop", "customerId": "clx...", "parentTicketId": "clx-parent-ticket-id" }
# → 201, response carries "parentTicket": { "id": "clx...", "ticketNumber": "TKT-2026-000042" }
# Subordinate an existing ticket / release it again
POST /api/linking/tickets/:parentId/link-child-ticket { "childTicketId": "clx..." } # → 204
DELETE /api/linking/tickets/:parentId/link-child-ticket/clx-child-ticket-id # → 204
# Candidates for the subordinate dialog (permission tickets.linkToTickets)
GET /api/tickets?forSubTicketOf=clx-parent-ticket-id&q=laptop
parentTicketId— optional beim Anlegen. Verknüpfung und Ticket entstehen gemeinsam: scheitert die Verknüpfung, wird auch das Ticket nicht angelegt — es bleibt kein waisenhaftes Kind zurück. Das Feld verlangt tickets.linkToTickets und einen angemeldeten Benutzer; ein API-Key kann kein Sub-Ticket anlegen.parentTicket— am Kind: id und ticketNumber des Elterntickets (ohne Titel), in Liste wie Detail.childTicketCounts— am Elternticket: { total, open }. Offen heißt: nicht RESOLVED, CLOSED oder SPAM.forSubTicketOf— liefert genau die Tickets, die sich unter das genannte Elternticket hängen lassen: nicht das Elternticket selbst, kein Ticket mit eigenen Sub-Tickets, keines mit Incident-, Problem- oder Change-Verknüpfung und keines, das schon woanders hängt (die eigenen Kinder bleiben in der Menge, der Dialog ist ein Mengen-Editor). Ohne tickets.linkToTickets antwortet die Liste 403 — die Struktur fremder Tickets ist ohne dieses Recht nicht abfragbar. Kombinierbar mit q und den übrigen Filtern.- Regeln und Ablehnungen des Verknüpfens (eine Ebene, ein Elternticket, keine Vorgangs-Verknüpfungen am Kind, offenes Elternticket) stehen bei der Entity-Linking API und gelten für das Anlegen mit parentTicketId genauso.
Sichtbarkeit der Struktur
Elternticket und Kind können verschiedenen Personen gehören — bis hin zu einer dritten Stelle. Nummer und Titel der Gegenseite gehören deshalb nicht in die Kundensicht: Sub-Ticket-Verlauf, Kinder-Liste, der SLA-Pausengrund und die Zähler sind nur für Träger von tickets.viewInternal sichtbar. Ohne dieses Recht liefert die Antwort parentTicket: null und childTicketCounts 0/0, die Kinder-Liste bleibt leer, und die Verlaufseinträge zur Eltern-Kind-Beziehung erscheinen nicht. Redigiert statt abgewiesen: das Ticket selbst bleibt für seinen Kunden vollständig lesbar.
Lösen eines Elterntickets
Ein Elternticket mit offenen Sub-Tickets geht weder nach RESOLVED noch nach SPAM: die API antwortet 409 TICKET_HAS_OPEN_CHILDREN und nennt in details.openChildren die betroffenen Tickets mit Nummer, damit der Lösen-Dialog sie auflisten kann. CLOSED braucht keine eigene Sperre — dorthin führt der Weg nur über RESOLVED. Die Sperre gilt für jeden Weg, der den Status setzt, also auch für die Sammelbearbeitung.
Automatische Wege scheitern daran nicht, sie lassen das Ticket aus: das automatische Lösen nach Kunden-Inaktivität, die Sammel-Auflösung verknüpfter Tickets über einen Incident oder ein Problem und das kaskadierende Schließen beim Abschluss eines Problems überspringen ein solches Elternticket und zählen es als übersprungen — mit benanntem Grund, damit die Oberfläche ihn anzeigen kann.
Liste und Zählung
- Die Ticket-Liste bleibt flach. Ein Sub-Ticket trägt ein Kennzeichen an seiner Nummer; die optionale Spalte „Sub-Ticket-Beziehung" zeigt am Kind die Nummer des Elterntickets und am Elternticket die Anzahl seiner Kinder samt offener. Die Spalte ist nicht vorbelegt und nicht sortierbar, weil sie zwei verschiedene Aussagen trägt.
- Standardmäßig zählen Sub-Tickets mit. Wer nur Anliegen sehen will — etwa in einer Agenten-Queue ohne Doppelzählung — blendet sie über den benannten Filter ?fn.hideSubTickets aus; er lässt sich wie jeder andere Filter in einer gespeicherten Ansicht festhalten. Die Status-Kacheln über der Liste folgen ihm.
- GET /api/tickets/stats kennt dieselbe Unterscheidung über excludeSubTickets=true; ohne den Parameter zählen Sub-Tickets mit.
In der Oberfläche stehen die Aktionen „Sub-Ticket anlegen" und „Bestehendes Ticket unterordnen" am Ticket-Detail; die Sub-Tickets selbst stehen im Verknüpfungs-Bereich des Tickets, am Kind als Elternticket.
API-Beispiele
Ticket erstellen
POST /api/tickets
Kategorie und Unterkategorie werden ausschließlich über categoryId und subcategoryId gesetzt; Namen nimmt die API nicht an. categoryId: null leert die Kategorie; die Antwort enthält zusätzlich den Namen. Der Kunde eines Tickets muss ein aktives Konto haben: Für ein archiviertes oder deaktiviertes Konto wird kein Ticket angelegt oder umgehängt (400 TICKET_CUSTOMER_NOT_ACTIVE). Bestehende Tickets bleiben bearbeitbar, auch wenn ihr Kunde später gesperrt wird; customer.accountStatus (active | inactive | archived) zeigt den Konto-Zustand in Liste und Detail, die Oberfläche zeigt nur die Abweichung. PATCH akzeptiert nur bekannte Felder; ein unbekannter Schlüssel ergibt 400 VALIDATION_ERROR. Wer eine Ticket-Antwort zurückschickt, muss sie vorher auf die änderbaren Felder reduzieren.
{
"title": "Laptop no longer starts",
"description": "After a Windows update the laptop only boots to the boot screen. Error message: 'INACCESSIBLE_BOOT_DEVICE'",
"priority": "HIGH",
"categoryId": "clx...",
"customerId": "clx...",
"assignedAgentId": "clx...",
"assignedGroupId": "clx...",
"metadata": {
"location": "Frankfurt Office, Desk 42",
"deviceSerial": "SN123456789",
"osVersion": "Windows 11 22H2"
},
"tags": ["hardware", "laptop", "windows-update"]
}
Response (201 Created)
{
"id": "clx...",
"ticketNumber": "TKT-2026-000042",
"title": "Laptop no longer starts",
"description": "After a Windows update the laptop only boots to the boot screen...",
"status": "OPEN",
"priority": "HIGH",
"category": {
"id": "clx...",
"name": "Hardware",
"color": "#ef4444"
},
"customer": {
"id": "clx...",
"name": "Max Mustermann",
"email": "max@company.com",
"accountStatus": "active"
},
"assignedAgent": {
"id": "clx...",
"userId": "clx...",
"user": {
"name": "IT Support Agent",
"email": "support@company.com"
}
},
"assignedGroup": {
"id": "clx...",
"name": "IT Support Level 1"
},
"createdAt": "2026-01-27T10:30:00Z",
"updatedAt": "2026-01-27T10:30:00Z",
"version": 1
}
Zwei Create-Modi (source): Das Create-Schema unterscheidet per source-Feld (Default WEB): WEB = klassischer Portal-Create (customerId erforderlich; optional formId/customFormData für CustomForm-getriebene Tickets). EMAIL = Agent-initiierter Outbound — der Agent wählt eine Quell-Mailbox und die description wird zum E-Mail-Body. Mobile-App und Workflow-Engine setzen kein source und bleiben auf WEB.
📝 formId/customFormData verweisen auf ein CustomForm der Kategorie TICKET. Verfügbare Formulare für den Create-Flow liefert GET /api/forms/available?category=TICKET. Die Feldregel des Formulars wird serverseitig durchgesetzt: Verstöße sind 400 FORM_SUBMISSION_INVALID (Befunde in details.issues), ein unbekanntes Formular 404 FORM_NOT_FOUND, ein deaktiviertes oder archiviertes 400 FORM_NOT_AVAILABLE. Beim PATCH gilt sie für die sichtbaren Zusatzfelder des ERGEBNISZUSTANDS — ohne customFormData im Payload wird nichts geprüft, ein Status- oder Prioritäts-PATCH bleibt also frei. Das Formular eines bestehenden Tickets lässt sich per PATCH nicht wechseln. Aufbau, Felder und Sichtbarkeit siehe Custom Forms API →.
👁 Zusatzfelder sind sichtbarkeits-gefiltert: Detail und Mutations-Antworten tragen nur die Felder, die der Betrachter sehen darf (hiddenForEndUsers, visibleToRoles) — in customFormData wie im Feld-Trail des Verlaufs. Die Liste enthält formId und customFormData nicht. Das Detail liefert zusätzlich form: das Formular des Tickets in derselben nutzer-gefilterten Form wie /forms/available, sodass ein zweiter Aufruf entfällt. Zusatzfelder bestehender Tickets bleiben sichtbar, auch wenn ihr Formular inzwischen deaktiviert ist.
Agent + Gruppe parallel
assignedAgentId und assignedGroupId existieren unabhängig nebeneinander — ein Ticket kann gleichzeitig einer Gruppe (Queue) und einem konkreten Agent zugewiesen sein. autoAssignAgent (beim Update) verlangt eine gesetzte assignedGroupId. Als assignedAgentId nimmt die API die BENUTZER-ID des Agenten an (die kanonische Eingabe, die auch die Agenten-Auswahllisten liefern). Eine Agent-Profil-ID wird ebenfalls akzeptiert.
Den Kunden eines Tickets zu wechseln verlangt tickets.editAll; tickets.editOwn genügt nicht, weil mit dem Kunden auch Sicht und Benachrichtigungen auf eine andere Person übergehen.
Ticket per E-Mail erstellen (source=EMAIL)
POST /api/tickets
{
"source": "EMAIL",
"sourceMailboxId": "clx-mailbox-id",
"title": "Re: Onboarding new laptop",
"description": "Hello, please find the setup steps attached ...",
"externalCustomer": { "email": "customer@external.com", "name": "Max External" },
"ccAddresses": ["colleague@company.com"],
"priority": "HIGH",
"assignedGroupId": "clx-group-id",
"sendInitialEmail": true,
"includeSignature": true
}
Regeln für source=EMAIL: sourceMailboxId Pflicht; XOR customerId ODER externalCustomer.email (genau eines); description darf nicht leer sein (= Mail-Body); formId/customFormData und Attachments sind hier NICHT erlaubt (Agent hängt Dateien per Reply an, nachdem das Ticket existiert). ccAddresses und includeSignature gelten nur für EMAIL.
Ticket aktualisieren
PATCH /api/tickets/:id
{
"status": "IN_PROGRESS",
"assignedAgentId": "clx...",
"priority": "MEDIUM",
"resolution": "Diagnosing boot issue. Checking Windows event logs.",
"version": 1
}
Response (200 OK)
{
"id": "clx...",
"ticketNumber": "TKT-2026-000042",
"status": "IN_PROGRESS",
"priority": "MEDIUM",
"resolution": "Diagnosing boot issue. Checking Windows event logs.",
"version": 2,
"updatedAt": "2026-01-27T11:15:00Z"
}
Optimistic Locking: Das Feld version verhindert, dass gleichzeitige Änderungen sich gegenseitig überschreiben. Bei einem Konflikt antwortet die API mit 409 TICKET_VERSION_CONFLICT; die Details nennen erwartete und aktuelle Version.
Nachricht hinzufügen (Public)
POST /api/tickets/:id/messages
{
"content": "Update: Laptop restarted in Safe Mode. Windows repair is running.",
"isInternal": false,
"type": "message"
}
Interne Note hinzufügen (Agent-Only)
{
"content": "User used admin rights for the Windows update. Backup is missing.",
"isInternal": true,
"type": "message"
}
Internal Notes: Nur sichtbar für User mit tickets.viewInternal Permission. Customer sieht diese Notes NICHT.
Cross-Entity-Linking (Batch-Update)
POST /api/tickets/:id/batch-update-links
{
"problems": {
"add": ["clx-problem-123"],
"remove": []
},
"changes": {
"add": ["clx-change-456"],
"remove": []
},
"assets": {
"add": ["clx-asset-789", "clx-asset-012"],
"remove": []
},
"kbArticles": {
"add": ["clx-kb-345"],
"remove": []
}
}
Response
{
"success": true,
"problems": { "added": 1, "removed": 0 },
"changes": { "added": 1, "removed": 0 },
"assets": { "added": 2, "removed": 0 },
"kbArticles": { "added": 1, "removed": 0 }
}
Automatisch:
- Activity-Einträge in Ticket-Timeline ("Linked to problem PRB-123")
- Activity-Einträge in Problem-Timeline ("Ticket TKT-42 linked")
- Gleich für Changes, Assets, KB-Articles
- Alle Änderungen in einer Transaktion, ganz oder gar nicht
- Audit-Eintrag (LINKS_UPDATED)
Ticket mit allen Links abrufen
GET /api/tickets/:id
Response (mit Links)
{
"id": "clx...",
"ticketNumber": "TKT-2026-000042",
"title": "Laptop no longer starts",
"status": "IN_PROGRESS",
"linkedProblems": [
{
"id": "clx...",
"problemNumber": "PRB-2026-000015",
"title": "Windows Update causes boot failures on Dell XPS series",
"status": "INVESTIGATING"
}
],
"linkedChanges": [
{
"id": "clx...",
"number": "CHG-2026-000089",
"title": "Rollback Windows Update KB5034441",
"status": "IN_PROGRESS"
}
],
"linkedAssets": [
{
"id": "clx...",
"assetTag": "00042",
"name": "Dell XPS 15",
"serialNumber": "SN123456789",
"status": "DEPLOYED"
},
{
"id": "clx...",
"assetTag": "00043",
"name": "Dell Monitor 27\"",
"status": "DEPLOYED"
}
],
"linkedKBArticles": [
{
"id": "clx...",
"title": "How to boot Dell XPS into Safe Mode",
"category": "Troubleshooting"
}
],
"messages": [
{
"id": "clx...",
"content": "Laptop restarted in Safe Mode...",
"isInternal": false,
"author": { "name": "IT Support Agent" },
"createdAt": "2026-01-27T11:30:00Z"
},
{
"id": "clx...",
"content": "User used admin rights...",
"isInternal": true,
"author": { "name": "IT Support Agent" },
"createdAt": "2026-01-27T11:35:00Z"
}
]
}
Ticket mergen (Duplikate)
POST /api/tickets/:id/merge
{
"sourceTicketId": "TKT-2026-000043",
"direction": "source-to-target"
}
Response
{
"primaryTicket": { "id": "clx...", "ticketNumber": "TKT-2026-000042", "title": "Laptop no longer starts" },
"mergedTicket": { "id": "clx...", "ticketNumber": "TKT-2026-000043", "title": "Dell XPS won't boot" },
"stats": {
"messagesCopied": 3,
"attachmentsTransferred": 2,
"tagsMerged": 2,
"problemsLinked": 0,
"changesLinked": 1,
"emailsTransferred": 4,
"sourceChanged": false,
"mailboxChanged": false
}
}
Fehlercodes beim Zusammenführen: CANNOT_MERGE_SELF, TICKET_CANNOT_RECEIVE_MERGES, TICKET_CANNOT_BE_MERGED (jeweils 400); verschwindet ein Ticket während des Vorgangs, antwortet die API mit 404. Zwei Vorab-Prüfungen antworten mit 400 und liefern Details: CONFIRMATION_REQUIRED (Bestätigung nötig, details.warnings — im Body mit confirmWarnings: true bestätigen) und SOURCE_SELECTION_REQUIRED (die beiden Tickets haben verschiedene Quellen — details.primarySource und details.mergedSource sagen, worum es geht).
Wird bei der Quellen-Wahl „Web behalten" entschieden, nimmt der Server auch den E-Mail-Modus zurück — sonst bliebe ein Ticket ohne Postfach im E-Mail-Modus zurück, dessen Antworten nirgends hinausgingen. Die Merge-Kette eines Tickets liefert GET /api/tickets/:id/merge-graph; Kandidaten sucht die Liste serverseitig über ?forMerge=true&q=….
Ablauf (atomic):
- Alle Messages von Source → Target kopiert
- Alle Attachments von Source → Target übertragen
- Alle Links (Problems, Changes, Assets, KB) gemerged
- Tags gemerged (dedupliziert)
- Sub-Ticket-Struktur nachgezogen (Kinder wandern, Elternbeziehung wird vererbt oder fällt)
- Source-Ticket auf CLOSED gesetzt
- Audit-Trail erstellt
Zusammenführen und Sub-Tickets
Beim Zusammenführen wird ein Ticket geschlossen (das gemergte), das andere bleibt offen und nimmt auf (das Ziel). Trägt eine der beiden Seiten eine Sub-Ticket-Beziehung, entscheidet eine feste Matrix — dieselbe in der Vorschau wie im Vorgang selbst, damit der Dialog nie etwas anderes ankündigt als geschieht. Die drei Ausgänge, die etwas verändern, kommen als Warnung mit Bestätigungspflicht (details.warnings) und zusätzlich als structure in der Vorschau: childrenToRehang, parentInherited und relationDropped.
| Gemergtes Ticket | Ziel-Ticket | Ergebnis |
|---|---|---|
| ohne Beziehung | beliebig | Nichts Zusätzliches. |
| Elternticket | ohne Beziehung oder selbst Elternticket | Die Sub-Tickets wandern zum Ziel (CHILDREN_REHUNG). |
| Sub-Ticket von X | X selbst oder ein Geschwister | Die Elternbeziehung fällt vor dem Schließen (CHILD_RELATION_DROPPED). |
| Sub-Ticket von X | ohne Beziehung | Das Ziel wird Sub-Ticket von X (PARENT_INHERITED). |
Vier Konstellationen würden die eine Ebene sprengen oder eine Zuordnung erraten; sie werden mit 409 und benanntem Code abgelehnt — in der Vorschau wie beim Ausführen, eine Bestätigung hilft dort nicht:
| Error Code | HTTP | Wann |
|---|---|---|
MERGE_PARENT_INTO_OWN_CHILD | 409 | Ein Elternticket soll in sein eigenes Sub-Ticket zusammengeführt werden. |
MERGE_WOULD_NEST | 409 | Es entstünde eine zweite Ebene: ein Elternticket in ein Sub-Ticket, oder ein Sub-Ticket in ein Ticket, das selbst Sub-Tickets trägt (details.reason unterscheidet beides). |
MERGE_DIFFERENT_PARENTS | 409 | Beide Seiten sind Sub-Tickets, aber unter verschiedenen Elterntickets — welches gelten soll, ist keine Server-Entscheidung. |
MERGE_TARGET_HAS_PROCESS_LINKS | 409 | Das Ziel würde die Elternbeziehung erben, trägt aber Incident-, Problem- oder Change-Verknüpfungen — die ein Sub-Ticket nicht haben darf. |
MERGE_STRUCTURE_CHANGED | 409 | Zwischen Vorschau und Ausführung hat jemand die Struktur einer der beiden Seiten geändert. Vorschau neu holen und erneut bestätigen. |
Statistiken abrufen
GET /api/tickets/stats?scope=relevant
Response
{
"total": 1234,
"byStatus": {
"OPEN": 45,
"IN_PROGRESS": 234,
"WAITING_CUSTOMER": 30,
"WAITING_SUPPORT": 18,
"ON_HOLD": 12,
"RESOLVED": 89,
"CLOSED": 731,
"SPAM": 5
},
"byPriority": {
"LOW": 234,
"MEDIUM": 567,
"HIGH": 345,
"URGENT": 120,
"CRITICAL": 88
},
"byCategory": [
{ "categoryId": "clx...", "name": "Hardware", "count": 456 },
{ "categoryId": "clx...", "name": "Software", "count": 345 },
{ "categoryId": "clx...", "name": "Network", "count": 234 }
],
"unassigned": 45,
"overdue": 12,
"avgResolutionTimeHours": 24.5
}
Substitute-Info (Vertretung)
GET /api/tickets/substitute-info
Response
{
"usersImCovering": [
{
"userId": "clx-absent-agent",
"userName": "John Doe",
"email": "john@company.com",
"absenceStart": "2026-01-20",
"absenceEnd": "2026-02-03",
"ticketCounts": {
"total": 23,
"OPEN": 3,
"IN_PROGRESS": 12,
"WAITING_CUSTOMER": 8
}
}
]
}
Das Substitute-System erlaubt einem Agent, Tickets eines abwesenden Kollegen zu bearbeiten. Alle Actions werden geloggt mit "actingAs: SUBSTITUTE".
Filtering & Query
Filter-Parameter
| Parameter | Beschreibung |
|---|---|
f.status | OPEN, IN_PROGRESS, WAITING_CUSTOMER, WAITING_SUPPORT, ON_HOLD, RESOLVED, CLOSED, SPAM |
f.priority | LOW, MEDIUM, HIGH, URGENT, CRITICAL |
f.source | WEB, EMAIL, API |
f.ticketNumber / f.title | Text-Filter |
f.categoryId / f.customerId / f.assignedUserId / f.assignedGroupId / f.sourceMailboxId | Zuordnungs-Filter |
f.createdAt / f.updatedAt / f.resolvedAt / f.closedAt | Zeit-Filter |
f.slaStatus | SLA-Zustand (siehe unten) |
q | Suche über Nummer, Titel, Beschreibung |
page / per / sort | Seitenweise Ausgabe (per Standard 50, maximal 200); sort=<feld>:asc|desc, zusätzlich sort=sla (Restzeit bis zur Lösungsfrist) |
scope | relevant, all, substitute |
deleted=1 | Papierkorb: NUR gelöschte Tickets (erfordert tickets.viewDeleted; nur der Wert 1) |
includeDeleted=true | Mischliste inkl. gelöschter (erfordert tickets.viewDeleted) |
following / involved | Tickets, denen ein Benutzer folgt bzw. an denen er beteiligt ist |
forMerge=true / excludeTicketId | Merge-Kandidaten (schließt CLOSED/SPAM aus) |
forSubTicketOf | Kandidaten zum Unterordnen unter das genannte Elternticket (erfordert tickets.linkToTickets) |
fn.hideSubTickets | Benannter Filter: blendet Sub-Tickets aus, zeigt also nur Anliegen. Ohne ihn zählen Sub-Tickets mit. |
cursor | Cursor-Pagination (für große Datenmengen) |
Strenge Parameterprüfung: Ein unbekannter scope-Wert ergibt 400. deleted und includeDeleted akzeptieren nur die genannten Werte, alles andere ergibt 400. Status- und Prioritätswerte müssen in Großschreibung übergeben werden; klein oder mit Bindestrich geschriebene Werte ergeben 400. Das gilt für Filter, Bulk-Operationen und jeden Schreib-Body.
SLA-Status filtern
Die Ticket-Liste lässt sich nach dem SLA-Zustand filtern — über die Filter-Syntax der Listen-API (f.-Präfix). Der Filter nutzt exakt dieselbe Quelle wie die SLA-Spalte der Liste (den vom Monitor gepflegten Status, ~2 Min. frisch), Spalte und Filter können sich also nie widersprechen.
# Alles, was brennt
GET /api/tickets?f.slaStatus=in:BREACH,CRITICAL
# Nur pausierte SLA-Uhren
GET /api/tickets?f.slaStatus=PAUSED
# Tickets ohne aktives SLA-Tracking
GET /api/tickets?f.slaStatus=isNull
| Wert | Bedeutung |
|---|---|
PAUSED | Die Uhr steht (ON_HOLD oder offener Incident-Link). Exklusiv — ein pausiertes Ticket erscheint NICHT zusätzlich unter seinem eingefrorenen Alt-Status. |
OK | Läuft im Rahmen — schließt Trackings ein, die der Monitor noch nicht berechnet hat (Konvention wie im Dashboard). |
WARNING · BREACH · CRITICAL | Laufende, nicht pausierte Trackings im jeweiligen Zustand |
isNull / isNotNull | Ticket OHNE bzw. MIT aktivem SLA-Tracking |
Bewusste Einschränkungen: Es gibt kein neq/notIn — „nicht BREACH" wäre mehrdeutig, weil unklar bliebe, ob Tickets ganz ohne SLA-Tracking dazugehören; dafür ist isNull/isNotNull da. Und CANCELLED fehlt: ein storniertes Tracking ist ein Dashboard-Thema, kein Arbeitszustand einer Ticket-Liste. Der Filter gilt auch für gespeicherte Ansichten samt deren Trefferzähler.
Beispiel-Queries
# Open and in-progress tickets of my group
GET /api/tickets?f.status=in:OPEN,IN_PROGRESS&f.assignedGroupId=clx...
# All HIGH-priority tickets, oldest first
GET /api/tickets?f.priority=HIGH&f.status=OPEN&sort=createdAt:asc
# Tickets relevant to me (mine plus my groups)
GET /api/tickets?scope=relevant
# Full-text search
GET /api/tickets?q=laptop+boot+problem
# Tickets of an absent colleague (substitute view)
GET /api/tickets?scope=substitute&f.assignedUserId=clx-absent-agent
# Trash (requires tickets.viewDeleted)
GET /api/tickets?deleted=1
Advanced Features
Categories & Sub-Categories
GET /api/tickets/categories
Response (Hierarchisch)
{
"data": [
{
"id": "clx...",
"name": "Hardware",
"color": "#ef4444",
"icon": "laptop",
"isActive": true,
"ticketCount": 456,
"subcategories": [
{
"id": "clx...",
"name": "Laptop",
"ticketCount": 234
},
{
"id": "clx...",
"name": "Desktop",
"ticketCount": 123
},
{
"id": "clx...",
"name": "Peripherals",
"ticketCount": 99
}
]
},
{
"id": "clx...",
"name": "Software",
"color": "#8b5cf6",
"subcategories": [...]
}
]
}
Attachments
Tickets nutzen das Unified Attachment System. Siehe: Attachments & File Settings API →
# Upload file to ticket
POST /api/attachments/TICKET/:ticketId
# Get all attachments of a ticket
GET /api/attachments/TICKET/:ticketId
# Download file
GET /api/attachments/:id/download
Transfer-Attachments
POST /api/tickets/:id/transfer-attachments
{
"sourceTicketId": "clx-source-ticket",
"attachmentIds": ["clx-att-1", "clx-att-2"]
}
Response
{
"transferred": 2
}
Error-Handling
| Error Code | HTTP Status | Beschreibung |
|---|---|---|
TICKET_NOT_FOUND | 404 | Ticket-ID existiert nicht |
CANNOT_EDIT_TICKET | 403 | Keine Edit-Permission für dieses Ticket |
CANNOT_CREATE_FOR_OTHERS | 403 | createForOthers-Permission fehlt |
CANNOT_VIEW_INTERNAL_NOTES | 403 | viewInternal-Permission fehlt |
TICKET_VERSION_CONFLICT | 409 | Zwischenzeitlich geänderter Stand (Optimistic Locking) |
TICKET_HAS_ACTIVE_LINKS | 400 | Ticket hat aktive Verknüpfungen (Problems, Changes, Incidents, Assets, KB, Sub-Tickets oder Elternticket) — zuerst lösen |
TICKET_HAS_OPEN_CHILDREN | 409 | Elternticket mit offenen Sub-Tickets kann nicht auf RESOLVED oder SPAM gesetzt werden (details.openChildren nennt sie) |
CUSTOMER_NOT_FOUND | 404 | Customer-ID existiert nicht |
SOURCE_MAILBOX_INACTIVE | 400 | Das Quell-Postfach des Tickets ist deaktiviert — es geht keine E-Mail hinaus |
TICKET_HAS_NO_MAILBOX | 400 | Das Ticket hängt an keinem Postfach, es gibt keinen Absender |
TICKET_IS_SPAM | 400 | Auf ein als Spam markiertes Ticket wird nicht geantwortet |
EMAIL_NOT_FAILED | 409 | Die E-Mail ist nicht fehlgeschlagen oder gehört zu einem anderen Ticket — beide Fälle bekommen dieselbe Antwort, damit sie nicht verrät, ob die ID anderswo existiert |
EMAIL_RETRY_DATA_MISSING | 400 | Kein gespeicherter Versand-Datensatz — stattdessen neu antworten |
EMAIL_RETRY_LIMIT_EXCEEDED | 409 | Grenze der manuellen Wiederholungen erreicht (5) |
Use-Cases
Use-Case 1: Ticket mit Problem & Change verlinken
// Scenario: laptop boot problem is a known error
// 1. Problem PRB-123 exists: "Windows Update KB5034441 breaks Dell XPS boot"
// 2. Change CHG-456 exists: "Rollback KB5034441"
// 3. Ticket TKT-42: "Laptop won't start"
// Link:
POST /api/tickets/TKT-42/batch-update-links
{
"problems": { "add": ["PRB-123"] },
"changes": { "add": ["CHG-456"] }
}
// Result:
// - Ticket timeline: "Linked to problem PRB-123"
// - Ticket timeline: "Linked to change CHG-456"
// - Problem timeline: "Ticket TKT-42 linked"
// - Change activity: "Ticket TKT-42 linked"
// Advantages:
// - Customer sees in TKT-42: "Known problem, change in progress"
// - Problem PRB-123 shows all affected tickets
// - Change CHG-456 shows all affected tickets
// - CMDB tracking complete
Use-Case 2: Duplikate mergen
// Scenario: 2 users report the same problem
// TKT-42: "Laptop won't start" (John Doe, 10:30)
// TKT-43: "Dell XPS won't boot" (Jane Smith, 10:45)
// Merge:
POST /api/tickets/TKT-42/merge
{
"sourceTicketId": "TKT-43",
"direction": "source-to-target"
}
// Result:
// TKT-42:
// - Now has messages from both users
// - Has attachments from both
// - Both users are in CC
// TKT-43:
// - Status = CLOSED
// - Resolution = "Merged into TKT-42"
// - Reference to TKT-42 in timeline
Use-Case 3: Asset zu Ticket verlinken
// Scenario: laptop problem, 2 assets affected
// - Dell XPS 15 Laptop (asset tag: 00042)
// - Dell Monitor 27" (asset tag: 00043)
POST /api/tickets/TKT-42/batch-update-links
{
"assets": {
"add": ["clx-laptop-id", "clx-monitor-id"]
}
}
// Advantages:
// - Asset history shows all tickets
// - On asset checkout: visible whether open tickets exist
// - Reporting: which assets have frequent problems?
Best Practices
💡 Tipps
1. Cross-Entity-Linking
- • Verwende batch-update-links für atomare Updates (nicht einzelne API-Calls)
- • Verlinke zu Problems bei bekannten Fehlern (Known Errors)
- • Verlinke zu Changes, wenn eine geplante Änderung die Ursache beheben soll
- • Verlinke zu Assets für Hardware-Tracking
2. Permissions
- • Customers: viewOwn, create (können eigene Tickets sehen/erstellen)
- • Agents: viewAll, editStatus, editPriority, assign, viewInternal
- • Admins: editAll, delete, restore (kritische Operationen)
- • Substitute-System aktiviert: viewOwn erlaubt Zugriff auf Tickets abwesender Kollegen
3. Performance
- • Nutze Cursor-Pagination bei > 10.000 Tickets
- • Filters verwenden um Datenmenge zu reduzieren
- • scope=relevant für Dashboard-Ansichten (schneller als die Gesamtliste)
4. Workflow-Integration
- • Workflow-Actions können Tickets erstellen (create_ticket)
- • Workflow-Actions können Tickets updaten (update_ticket)
- • Workflow-Actions können Comments hinzufügen (add_ticket_comment)
- • Workflows können via Ticket-Creation getriggert werden
Beziehungen eines Tickets
Beziehungen: Ticket (1:n) → Nachrichten & VerlaufTicket (1:n) → Sub-Tickets (genau eine Ebene)Ticket ↔ Problem (n:m) Ticket ↔ Change (n:m) Ticket ↔ Asset (n:m) Ticket ↔ KB-Artikel (n:m) Ticket (1:n) → Anhänge (über die Attachments API)Ticket (1:1) → SLA-Tracking (automatisch angelegt) Verlaufseinträge auf beiden Seiten: Ticket-Link zu Problem: ├─ Ticket-Verlauf: "Linked to problem PRB-123" └─ Problem-Timeline: "Ticket TKT-42 linked" Ticket-Link zu Change: ├─ Ticket-Verlauf: "Linked to change CHG-456" └─ Change-Aktivität: "Ticket TKT-42 linked" Ticket-Link zu Asset: └─ Ticket-Verlauf: "Linked to asset #00042"
Hinweis: Tickets sind der zentrale Entry-Point für Enduser. Sie können zu Problems (Root-Cause), Changes (Behebung) und Assets (betroffene Hardware) verlinkt werden für vollständige CMDB-Integration. Verknüpfungen laufen zentral über die Entity-Linking API. Reopen-Pfade (manuell/E-Mail/Kommentar), Merge-Redirect und Auto-Close stehen unter Reopen & Lifecycle.