Eviworx
Docs

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.

🎫
Funktionen
✓ E-Mail-to-Ticket (IMAP + Graph API)
✓ Follower und CC (FOLLOWER, CC, MENTIONED)
✓ Verknüpfung mit Problems, Changes, Assets, KB
✓ Duplikate zusammenführen (inkl. Anhänge, Links)
✓ Sub-Tickets (genau eine Ebene)
✓ Zugriffsbeschränkte Mailboxen (accessRestricted)
✓ Vertretung bei Abwesenheit
✓ Öffentliche Antworten und interne Notizen
✓ Verlauf in der Sprache des Betrachters
✓ Optimistic Locking (version)
✓ Automatisches SLA-Tracking ab Anlage

Endpoints Übersicht

CRUD & Query

Method Endpoint Beschreibung
GET/api/ticketsAlle Tickets abrufen (mit Filtering)
GET/api/tickets/statsStatistiken (Counts pro Status)
GET/api/tickets/substitute-infoVertretungs-Info (Users die ich vertrete)
GET/api/tickets/:idEinzelnes Ticket abrufen
POST/api/ticketsNeues Ticket erstellen
PATCH/api/tickets/:idTicket aktualisieren
DELETE/api/tickets/:idTicket löschen (Soft-Delete)
POST/api/tickets/:id/restoreAus 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/messagesNachricht/Kommentar hinzufügen

Cross-Entity-Linking

Method Endpoint Beschreibung
POST/api/tickets/:id/batch-update-linksBatch-Update aller Links (Problems, Changes, Assets, KB)

Advanced Operations

Method Endpoint Beschreibung
GET/api/tickets/:id/merge-previewMerge-Vorschau (was würde übertragen)
POST/api/tickets/:id/mergeTicket mergen (Duplikate zusammenführen)
POST/api/tickets/:id/transfer-attachmentsAnhänge aus einem anderen Ticket (sourceTicketId) in dieses Ticket übernehmen (tickets.editAll)
PATCH/api/tickets/:id/mailboxPostfach 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/bulkBulk-Operationen (Status/Assign/…, Permission tickets.bulk)
GET/api/tickets/:id/suggest-known-errorsBekannte Fehler (Known Errors) zum Ticket vorschlagen (problems.viewOwn)
POST/api/tickets/:id/apply-workaroundWorkaround 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/participantsAlle Teilnehmer eines Tickets → { data }. Je Zeile: id, userId, email, displayName, role, source, notificationsDisabled und user{id,name,email}.
POST/api/tickets/:id/participantsTeilnehmer hinzufügen (FOLLOWER, CC, MENTIONED)
PATCH/api/tickets/:id/participants/:participantIdTeilnehmer-Rolle ändern
DELETE/api/tickets/:id/participants/:participantIdTeilnehmer entfernen → 204 (unbekannte Teilnehmer-ID: 404 PARTICIPANT_NOT_FOUND)
POST/api/tickets/:id/followTicket 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/followTicket entfolgen (aktueller User) → 204

E-Mail-Actions

Method Endpoint Beschreibung
GET/api/tickets/:id/email-threadE-Mail-Thread eines Tickets anzeigen
POST/api/tickets/:id/email-replyE-Mail-Antwort senden (mit Signatur)
POST/api/tickets/:id/email-retry/:emailMessageIdFehlgeschlagene E-Mail erneut senden
PATCH/api/tickets/:id/email-dismiss/:emailMessageIdE-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/categoriesAlle Kategorien abrufen
GET/api/tickets/categories/:id/subcategoriesSub-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
Betrefftitle
Body (Text/HTML)description
From-AdressecustomerId (User wird auto-erstellt falls nicht vorhanden)
AttachmentsVia 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:

  1. Custom Headers: X-Ticket-ID, X-Ticket-Number
  2. In-Reply-To Header: Referenziert vorherige Message-ID
  3. References Chain: Alle vorherigen Message-IDs
  4. 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
OPENOffen (neu/unbearbeitet)
IN_PROGRESSIn Bearbeitung
WAITING_CUSTOMERWartet auf Kunde (aktiv, SLA läuft); kann nach Inaktivität automatisch auf RESOLVED (WC-Auto-Resolve, opt-in)
WAITING_SUPPORTWartet auf Support/Vendor (aktiv, SLA läuft)
ON_HOLDPausiert (inaktiv, SLA pausiert) — mit Wiedervorlage via holdReminderAt
RESOLVEDGelöst, wartet auf Kunden-Bestätigung/Auto-Close
CLOSEDGeschlossen & abgeschlossen
SPAMAls Spam markiert

Permissions

Permission Beschreibung
tickets.viewAllAlle Tickets sehen
tickets.viewOwnEigene Tickets sehen (als Kunde, Bearbeiter, Mitglied der zugewiesenen Gruppe, Vertretung oder Teilnehmer)
tickets.createTickets erstellen
tickets.createForOthersTickets für andere User erstellen
tickets.editStatusStatus ändern
tickets.editPriorityPriority ändern
tickets.editCategoryKategorie ändern, auch wenn nur die Unterkategorie (subcategoryId) geändert wird
tickets.assignTickets zuweisen/umzuweisen
tickets.editOwnEigene/zugewiesene Tickets bearbeiten (Owner-Scope)
tickets.editAllAlle Felder bearbeiten (inkl. Merge, Links) sowie als einziges Recht den Wechsel des Kunden (customerId)
tickets.reopenGeschlossene/SPAM-Tickets wiederöffnen (CLOSED/SPAM → OPEN; eigenes Recht, Grund erforderlich; editAll schließt es nicht ein)
tickets.reopenOverrideReopen-Fenster/Limit umgehen (nicht die Grund-Pflicht)
tickets.bulkBulk-Operationen (Status/Assign/…)
tickets.changeMailboxTicket einer anderen Mailbox zuordnen
tickets.linkToTicketsSub-Tickets: Anlegen mit Elternticket, Unterordnen, Lösen der Beziehung und die Kandidatenliste
tickets.viewInternalInterne Notes sehen (Agent-Only)
tickets.deleteTickets löschen (Critical, Audit-Log)
tickets.restoreGelöschte Tickets wiederherstellen
tickets.viewDeletedGelö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):

  1. Alle Messages von Source → Target kopiert
  2. Alle Attachments von Source → Target übertragen
  3. Alle Links (Problems, Changes, Assets, KB) gemerged
  4. Tags gemerged (dedupliziert)
  5. Sub-Ticket-Struktur nachgezogen (Kinder wandern, Elternbeziehung wird vererbt oder fällt)
  6. Source-Ticket auf CLOSED gesetzt
  7. 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 TicketZiel-TicketErgebnis
ohne BeziehungbeliebigNichts Zusätzliches.
Elternticketohne Beziehung oder selbst ElternticketDie Sub-Tickets wandern zum Ziel (CHILDREN_REHUNG).
Sub-Ticket von XX selbst oder ein GeschwisterDie Elternbeziehung fällt vor dem Schließen (CHILD_RELATION_DROPPED).
Sub-Ticket von Xohne BeziehungDas 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 CodeHTTPWann
MERGE_PARENT_INTO_OWN_CHILD409Ein Elternticket soll in sein eigenes Sub-Ticket zusammengeführt werden.
MERGE_WOULD_NEST409Es 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_PARENTS409Beide Seiten sind Sub-Tickets, aber unter verschiedenen Elterntickets — welches gelten soll, ist keine Server-Entscheidung.
MERGE_TARGET_HAS_PROCESS_LINKS409Das Ziel würde die Elternbeziehung erben, trägt aber Incident-, Problem- oder Change-Verknüpfungen — die ein Sub-Ticket nicht haben darf.
MERGE_STRUCTURE_CHANGED409Zwischen 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.statusOPEN, IN_PROGRESS, WAITING_CUSTOMER, WAITING_SUPPORT, ON_HOLD, RESOLVED, CLOSED, SPAM
f.priorityLOW, MEDIUM, HIGH, URGENT, CRITICAL
f.sourceWEB, EMAIL, API
f.ticketNumber / f.titleText-Filter
f.categoryId / f.customerId / f.assignedUserId / f.assignedGroupId / f.sourceMailboxIdZuordnungs-Filter
f.createdAt / f.updatedAt / f.resolvedAt / f.closedAtZeit-Filter
f.slaStatusSLA-Zustand (siehe unten)
qSuche über Nummer, Titel, Beschreibung
page / per / sortSeitenweise Ausgabe (per Standard 50, maximal 200); sort=<feld>:asc|desc, zusätzlich sort=sla (Restzeit bis zur Lösungsfrist)
scoperelevant, all, substitute
deleted=1Papierkorb: NUR gelöschte Tickets (erfordert tickets.viewDeleted; nur der Wert 1)
includeDeleted=trueMischliste inkl. gelöschter (erfordert tickets.viewDeleted)
following / involvedTickets, denen ein Benutzer folgt bzw. an denen er beteiligt ist
forMerge=true / excludeTicketIdMerge-Kandidaten (schließt CLOSED/SPAM aus)
forSubTicketOfKandidaten zum Unterordnen unter das genannte Elternticket (erfordert tickets.linkToTickets)
fn.hideSubTicketsBenannter Filter: blendet Sub-Tickets aus, zeigt also nur Anliegen. Ohne ihn zählen Sub-Tickets mit.
cursorCursor-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
WertBedeutung
PAUSEDDie Uhr steht (ON_HOLD oder offener Incident-Link). Exklusiv — ein pausiertes Ticket erscheint NICHT zusätzlich unter seinem eingefrorenen Alt-Status.
OKLäuft im Rahmen — schließt Trackings ein, die der Monitor noch nicht berechnet hat (Konvention wie im Dashboard).
WARNING · BREACH · CRITICALLaufende, nicht pausierte Trackings im jeweiligen Zustand
isNull / isNotNullTicket 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_FOUND404Ticket-ID existiert nicht
CANNOT_EDIT_TICKET403Keine Edit-Permission für dieses Ticket
CANNOT_CREATE_FOR_OTHERS403createForOthers-Permission fehlt
CANNOT_VIEW_INTERNAL_NOTES403viewInternal-Permission fehlt
TICKET_VERSION_CONFLICT409Zwischenzeitlich geänderter Stand (Optimistic Locking)
TICKET_HAS_ACTIVE_LINKS400Ticket hat aktive Verknüpfungen (Problems, Changes, Incidents, Assets, KB, Sub-Tickets oder Elternticket) — zuerst lösen
TICKET_HAS_OPEN_CHILDREN409Elternticket mit offenen Sub-Tickets kann nicht auf RESOLVED oder SPAM gesetzt werden (details.openChildren nennt sie)
CUSTOMER_NOT_FOUND404Customer-ID existiert nicht
SOURCE_MAILBOX_INACTIVE400Das Quell-Postfach des Tickets ist deaktiviert — es geht keine E-Mail hinaus
TICKET_HAS_NO_MAILBOX400Das Ticket hängt an keinem Postfach, es gibt keinen Absender
TICKET_IS_SPAM400Auf ein als Spam markiertes Ticket wird nicht geantwortet
EMAIL_NOT_FAILED409Die 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_MISSING400Kein gespeicherter Versand-Datensatz — stattdessen neu antworten
EMAIL_RETRY_LIMIT_EXCEEDED409Grenze 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.