Assets API
Die Assets API verwaltet Assets mit Typen, Kategorien und Standorten, Ausgabe und Übergabe mit QR/PDF-Protokoll, CMDB-Beziehungen, typbezogenen Berechtigungen, Duplikat-Erkennung für Hersteller/Modell und Inventur-Scan.
Authentifizierung & Permissions
Alle Asset-Endpunkte akzeptieren eine Session (Benutzer) oder einen X-API-Key-Header (API-Key mit aktiver Rolle). In beiden Fällen gelten die Rechte der Rolle. Details siehe User Management & RBAC und Authentication API.
| Bereich | Permission-Keys (feature.action) |
|---|---|
| Lesen | assets.viewAll, assets.viewOwn, assets.viewDeleted (Papierkorb), assets.viewHistory |
| Bearbeiten | assets.create, assets.update, assets.delete, assets.restore, assets.checkout, assets.checkin |
| Bulk / Export | assets.bulkEdit, assets.bulkDelete, assets.export |
| Labels / Scan | assets.generateLabel, assets.scan |
| Stammdaten | assets.manageTypes, assets.manageCategories, assets.manageLocations, assets.manageClusters, assets.manageTypePermissions |
| Handover | assets.viewAllHandovers, assets.viewOwnHandovers, assets.initiateHandover, assets.confirmReceipt, assets.requestReturn, assets.managePolicies |
Typ-Sperre: Zusätzlich zu den globalen Rechten kann ein Asset-Typ gesperrt werden. Für gesperrte Typen gelten dann die Freigaben pro Rolle oder Benutzer (der zugewiesene Benutzer sieht sein Asset immer); für offene Typen nur die globalen Rechte. Listen enthalten deshalb nur die Assets, die der Aufrufer sehen darf.
Dieselbe Sicht-Prüfung steht vor JEDER Mutation: Aktualisieren, Löschen, Aus-/Einchecken, Installieren/Deinstallieren, Etikett und Barcode-Aktionen antworten 403, wenn das Asset für den Aufrufer nicht sichtbar ist — ein globales Handlungsrecht allein genügt nicht. Für den Papierkorb sind zwei Rechte nötig: assets.viewDeleted, um ihn zu sehen, und assets.restore zum Wiederherstellen.
Kritische Rechte (werden bei jeder Anfrage neu geprüft, sodass ein Entzug sofort wirkt; abgelehnte Versuche werden protokolliert): assets.export, assets.managePolicies, assets.manageClusters und assets.manageTypePermissions.
Antwort-Formate: Alle Listen der Domäne liefern { data } bzw. { data, pagination } — Assets, Typen, Kategorien, Standorte, Übergaben und Aktivitäten. Einzelobjekte kommen ohne Hülle. Beträge (purchasePrice, depreciationRate) sind JSON-Zahlen. Zähler wie assetCount werden aktuell gezählt.
Lese-Umfang je nach Recht: Ohne Sicht auf das einzelne Asset liefert der Scan nur die Basis-Angaben (Tag, Name, Status, Typ, Standort) — eine Inventur funktioniert damit weiter, Seriennummer und Zuweisung bleiben aber außen vor. Die Typen-Liste zeigt allen die Anzeige-Felder; customFieldSchema, hasTypePermissions und assetCount nur mit viewAll, manageTypes oder manageTypePermissions. Kategorien und Standorte verlangen viewAll oder das jeweilige manage-Recht. Die globale Aktivitäts-Historie zeigt ausschließlich Assets, die der Leser sehen darf.
Endpoints Übersicht
Kern-CRUD
| Method | Endpoint | Beschreibung |
|---|---|---|
GET | /api/assets | Alle Assets (mit Filtern); ?deleted=1 = Papierkorb (Recht assets.viewDeleted) |
GET | /api/assets/:id | Einzelnes Asset |
POST | /api/assets | Asset erstellen |
PATCH | /api/assets/:id | Asset aktualisieren |
DELETE | /api/assets/:id | Asset löschen (Soft-Delete) |
Handover (QR/PDF)
| Method | Endpoint | Beschreibung |
|---|---|---|
GET | /api/assets/handovers | Alle Handovers (IT-View) |
GET | /api/assets/handovers/pending | Pending Handovers für aktuellen User |
POST | /api/assets/handovers | Neues Handover erstellen (Checkout/Transfer) |
POST | /api/assets/handovers/:id/accept | Handover akzeptieren |
POST | /api/assets/handovers/:id/reject | Handover ablehnen |
PATCH | /api/assets/handovers/:id/expires-at | Rückgabedatum einer Ausgabe ändern |
GET | /api/assets/handovers/:id/pdf | PDF-Protokoll generieren |
GET | /api/handovers/:id/public | Public QR-Zugriff (ohne Auth, HMAC-Token) |
Ein Vorgang je Asset: Gleichzeitige Vorgänge auf demselben Asset schließen sich aus — Ausgabe, Rücknahme, Übergabe anlegen, Rückgabe anfordern, Installieren, Deinstallieren und die Komponenten-Mitnahmen beanspruchen das Asset. Der zweite Aufruf bekommt 409 — so entstehen nie zwei Ausgaben desselben Geräts an verschiedene Empfänger. Ebenfalls 409: eine zweite Übergabe an einem Asset, das bereits eine offene hat. Sperrkonflikte der Datenbank melden 409 DEADLOCK_DETECTED.
Nicht sichtbare Übergaben: Detail, PDF und Rückgabedatum antworten mit 404, wenn die Übergabe für den Aufrufer nicht sichtbar ist — dieselbe Antwort wie auf eine erfundene ID, damit nicht erkennbar ist, ob die Übergabe existiert. Wer sie sehen, aber nicht ändern darf, bekommt 403.
Öffentliches Protokoll: Der QR-/Share-Link führt auf eine datenminimierte Fassung: Sie nennt die Namen der Beteiligten, aber keine E-Mail-Adressen — weder in der JSON-Antwort noch im PDF. Das interne PDF bleibt vollständig. Die Fußzeile beider PDFs nennt die Übergabe-Nummer (HO-00042), nicht die interne Datenbank-ID.
Model-Clustering
| Method | Endpoint | Beschreibung |
|---|---|---|
GET | /api/asset-model-clusters | Alle Cluster (Duplikat-Kandidaten) |
GET | /api/asset-model-clusters/stats | Cluster-Statistiken |
PATCH | /api/asset-model-clusters/:id/canonical | Canonical-Werte setzen |
POST | /api/asset-model-clusters/:id/approve | Cluster bestätigen (bereit zum Merge) |
POST | /api/asset-model-clusters/:id/merge | Cluster mergen (Duplikate bereinigen) |
POST | /api/asset-model-clusters/:id/reject | Cluster ablehnen (kein Duplikat) |
DELETE | /api/asset-model-clusters/:id | Cluster löschen |
Relations & Inventory
| Method | Endpoint | Beschreibung |
|---|---|---|
POST | /api/asset-relations | CMDB-Relation erstellen |
DELETE | /api/asset-relations/:id | Relation löschen |
GET | /api/asset-relations/asset/:id | Alle Relationen eines Assets |
GET | /api/asset-relations/asset/:id/graph | CMDB-Graph (Visualisierung) |
GET | /api/asset-relations/types | Verfügbare Relation-Typen |
Erweiterte Operationen
| Method | Endpoint | Beschreibung |
|---|---|---|
GET | /api/assets/stats | Asset-Statistiken |
GET | /api/assets/export | CSV/PDF-Export |
PATCH | /api/assets/bulk | Bulk-Update (mehrere Assets) |
DELETE | /api/assets/bulk | Bulk-Delete (Soft-Delete) |
POST | /api/assets/:id/restore | Gelöschtes Asset wiederherstellen (verlangt assets.restore UND assets.viewDeleted) |
POST | /api/assets/:id/checkout | Asset auschecken (User zuweisen) |
POST | /api/assets/:id/checkin | Asset einchecken (zurückgeben) |
POST | /api/assets/:id/install | LOCATION-Asset installieren (→ Standort) |
POST | /api/assets/:id/deinstall | LOCATION-Asset deinstallieren |
GET | /api/assets/:id/label | QR-Label generieren (PDF) |
GET | /api/assets/:id/damage-report | Schadensbericht |
GET | /api/assets/my-consumables | Meine Verbrauchsartikel |
GET | /api/assets/suggestions | Auto-Complete-Vorschläge |
GET | /api/asset-activities | Aktivitäten aller sichtbaren Assets |
Inventur-Sessions
Inventuren laufen über eigene Sessions unter /api/inventory-sessions: Beim Start entsteht ein Snapshot der erwarteten Assets, danach wird per QR/Barcode erfasst und am Ende gegen das Soll ausgewertet. Sie tragen eigene Rechte (inventory.*), eine Frist samt Erinnerungen und einen Papierkorb. Endpunkte, Felder und Rechte stehen vollständig auf der eigenen Seite: Inventory API.
Die Inventur-Routen bewegen selbst keine Assets — Aktionen an gescannten, unerwarteten oder fehlenden Geräten laufen über die Asset-Endpunkte dieser Seite (checkout, checkin, install, deinstall) und über die assets.*-Rechte.
Asset-Typen (CRUD + Policies)
| Method | Endpoint | Beschreibung |
|---|---|---|
GET | /api/asset-types | Alle Asset-Typen |
POST | /api/asset-types | Asset-Typ erstellen (inkl. Policies) |
PATCH | /api/asset-types/:id | Asset-Typ aktualisieren |
DELETE | /api/asset-types/:id | Asset-Typ löschen |
GET | /api/asset-types/:id/permissions | Typ-Berechtigungen abrufen |
PUT | /api/asset-types/:id/permissions/users/:userId | User-Berechtigung setzen |
PUT | /api/asset-types/:id/permissions/roles/:roleId | Rollen-Berechtigung setzen |
Asset-Typ-Felder (POST/PATCH)
| Feld | Typ | Beschreibung |
|---|---|---|
name / displayName | String | Interner Name (unique) / Anzeigename |
trackingMode | Enum (Default PERSON) | PERSON | LOCATION | CONSUMABLE. Legt u. a. fest, ob der Typ Verbrauchsmaterial ist. Unveränderlich, sobald der Typ Assets hat → sonst HTTP 409. |
standalone | Boolean (Default true) | false = Einbau-Komponente (RAM/SSD): keine eigenständige Ausgabe/Handover, folgt dem Container via Co-Move. CONSUMABLE ist immer standalone (erzwungen). Frei umschaltbar (audit-pflichtig). |
requiresConfirmation | Boolean (Default false) | Handover mit Empfänger-Bestätigung als Typ-Default. |
hasTypePermissions | Boolean | Typ-Sperre: aktiviert typbezogene Berechtigungen (siehe unten). |
icon / color / customFieldSchema | String / JSONB | UI-Icon, Farbe, Custom-Field-Schema (JSON). |
Semantik der Modi (Anker, Aktionen, Statusmengen) siehe Asset-Lebenszyklus.
Kategorien & Standorte (CRUD)
| Method | Endpoint | Beschreibung |
|---|---|---|
GET | /api/asset-categories | Alle Kategorien inkl. Hierarchie (parentId); Recht: assets.viewAll oder manageCategories |
POST | /api/asset-categories | Kategorie erstellen |
PATCH | /api/asset-categories/:id | Kategorie aktualisieren |
DELETE | /api/asset-categories/:id | Kategorie löschen |
GET | /api/asset-locations | Alle Standorte inkl. Hierarchie (parentId); Recht: assets.viewAll oder manageLocations |
POST | /api/asset-locations | Standort erstellen |
PATCH | /api/asset-locations/:id | Standort aktualisieren |
DELETE | /api/asset-locations/:id | Standort löschen |
Asset-Status
Es gibt 11 Lebenszyklus-Status. Vollständige Semantik, Status-Gruppen, Anker-Regeln und die Übergangsmatrix stehen auf der Seite Asset-Lebenszyklus-Seite.
| Status | Beschreibung |
|---|---|
ORDERED | Bestellt, noch nicht geliefert |
RECEIVED | Geliefert, noch nicht einsatzbereit |
AVAILABLE | Verfügbar/einsatzbereit (Lager), frei zuweisbar |
RESERVED | Vorgemerkt — noch keine Ausgabe |
PENDING_ACCEPTANCE | Personen-Handover läuft, wartet auf Empfänger-Bestätigung |
IN_USE | In Betrieb — PERSON: beim User / LOCATION: am Standort installiert |
MAINTENANCE | In Wartung/Instandsetzung (Anker kann bleiben) |
RETURN_PENDING | Rückgabe läuft, wartet auf IT-Bestätigung |
RETIRED | Außer Betrieb (reaktivierbar) |
LOST | Verloren/gestohlen (Pflicht-Grund in statusNote) |
DISPOSED | Entsorgt/Verkauft (final) |
Asset erstellen
Reguläres Asset (z.B. Laptop)
POST /api/assets
{
"name": "Dell XPS 15",
"description": "Developer laptop with 32GB RAM",
"serialNumber": "SN123456789",
"manufacturer": "Dell",
"model": "XPS 15 9520",
"typeId": "clx-laptop-type",
"categoryId": "clx-hardware-category",
"locationId": "clx-office-munich",
"status": "RECEIVED",
"criticality": "HIGH",
"purchaseDate": "2026-01-15",
"purchasePrice": 2499.00,
"purchaseOrder": "PO-2026-001",
"vendor": "Dell Direct",
"costCenterId": "clx-cost-center-id",
"warrantyEnd": "2029-01-15",
"usefulLifeMonths": 36,
"depreciationMethod": "LINEAR",
"customFields": {
"ramGB": 32,
"storageGB": 1024,
"cpu": "Intel i7-12700H",
"display": "15.6\" 4K OLED"
},
"tags": ["developer", "high-performance", "mobile"]
}
Consumable (Mengenartikel, z.B. USB-Kabel)
Ein Consumable entsteht durch einen Asset-Typ mit trackingMode=CONSUMABLE (eine Eigenschaft des Typs, nicht des einzelnen Assets). Das Asset trägt quantity/minQuantity; ausgegeben wird über ConsumableAssignment, nicht über Status/Assignee.
{
"name": "USB-C to USB-A Cable (1m)",
"typeId": "clx-consumable-type",
"categoryId": "clx-cables-category",
"status": "RECEIVED",
"quantity": 50,
"minQuantity": 10,
"purchasePrice": 5.99,
"purchaseOrder": "PO-2026-002",
"tags": ["cable", "usb-c", "consumable"]
}
Consumables: Der Typ (trackingMode=CONSUMABLE) trackt quantity; SerialNumber ist optional. Ausgabe dekrementiert die Menge via ConsumableAssignment (User ODER Standort) statt Status/Assignee zu ändern. Erlaubte Status: ORDERED, RECEIVED, AVAILABLE, RETIRED, DISPOSED. Kein Handover, keine CMDB-Relations.
Response (201 Created)
{
"id": "clx...",
"assetTag": "00042",
"name": "Dell XPS 15",
"serialNumber": "SN123456789",
"manufacturer": "Dell",
"model": "XPS 15 9520",
"status": "RECEIVED",
"type": {
"id": "clx...",
"name": "Laptop",
"displayName": "Laptop"
},
"category": {
"id": "clx...",
"name": "Hardware",
"color": "#3b82f6"
},
"location": {
"id": "clx...",
"name": "Munich Office - 2nd Floor"
},
"createdAt": "2026-01-27T17:00:00.000Z"
}
QR/PDF-Handover-Workflow
Der Handover ist der formelle Ausgabe-Weg mit Empfänger-Bestätigung (→ PENDING_ACCEPTANCE), QR-Code, PDF-Protokoll und Mail. Er gilt NUR für trackingMode=PERSON (LOCATION nutzt Install, CONSUMABLE hat keinen Handover) und unterliegt denselben Prüfungen wie Checkout/Checkin. Annehmen → IN_USE; Ablehnen oder Abbrechen stellt den vorherigen Zustand wieder her. Der Direkt-Checkout (siehe unten) ist der schnelle Weg ohne Bestätigung.
Schritt 1: Handover erstellen (IT)
POST /api/assets/handovers
{
"recipientId": "clx-user-id",
"assetIds": ["clx-asset1", "clx-asset2"],
"quantities": { "clx-asset1": 1 },
"note": "New laptop for developer onboarding",
"expiresAt": "2026-12-31T23:59:59Z"
}
Handover-Typ & Policy: Der Handover-Typ (CHECKOUT_WITH_CONFIRMATION, CHECKOUT_DIRECT, RETURN, RETURN_DIRECT) und eine ggf. erforderliche Policy-Bestätigung werden serverseitig aus der Asset-/Typ-Konfiguration abgeleitet — nicht im Request gesetzt. quantities ist optional (Mengenartikel/Consumables).
Response (201 Created)
{
"id": "clx...",
"handoverNumber": "HO-00042",
"type": "CHECKOUT_WITH_CONFIRMATION",
"status": "PENDING",
"qrUrl": "https://your-domain.com/handover/clx.../verify?token=abc123...",
"pdfUrl": "/api/assets/handovers/clx.../pdf",
"initiatedBy": { "name": "IT Admin" },
"recipient": { "name": "John Doe", "email": "john@example.com" },
"items": [
{
"asset": {
"assetTag": "00042",
"name": "Dell XPS 15",
"serialNumber": "SN123456789"
}
}
],
"initiatedAt": "2026-01-27T17:00:00.000Z"
}
Schritt 2: User scannt QR-Code
Der QR-Code enthält eine URL mit HMAC-Token für sicheren Zugriff OHNE Authentifizierung:
# PUBLIC endpoint (no JWT needed!)
GET /api/handovers/:id/public?token=HMAC_TOKEN
{
"handoverNumber": "HO-00042",
"type": "CHECKOUT_WITH_CONFIRMATION",
"status": "PENDING",
"initiatedBy": { "name": "IT Admin" },
"recipient": { "name": "John Doe" },
"items": [
{
"asset": {
"assetTag": "00042",
"name": "Dell XPS 15",
"serialNumber": "SN123456789",
"type": { "name": "Laptop" }
}
}
],
"policies": [
{ "id": "clx...", "version": 3, "title": "Laptop Usage Policy", "acceptedAt": "2026-01-27T17:15:00.000Z" }
],
"companyName": "Your Company"
}
Security: Der Public-Endpoint gibt KEINE internen IDs, E-Mail-Adressen oder sensiblen Daten zurück. Nur minimale Informationen für Verifizierung.
Schritt 3: User akzeptiert Handover
POST /api/assets/handovers/:id/accept
{
"note": "Asset received in good condition. All components present.",
"policyAccepted": true,
"policyLinkOpened": true
}
Response
{
"id": "clx...",
"handoverNumber": "HO-00042",
"status": "CONFIRMED",
"confirmedAt": "2026-01-27T17:15:00.000Z",
"confirmedBy": { "name": "John Doe" },
"recipientNote": "Asset received in good condition..."
}
Richtlinien werden je Version quittiert: Eine Übergabe kann Geräte mehrerer Asset-Typen enthalten — die Bestätigung quittiert daher jede aktive Richtlinie der beteiligten Typen. Die Antworten tragen dazu policies[] (Listen: id, version, title, acceptedAt; ausstehende Übergaben und das Detail zusätzlich den Volltext und den Quittungs-Stand des Empfängers je Version). Protokoll, PDF und die öffentliche Seite listen alle quittierten Richtlinien mit Version und Zeitpunkt. Bereits quittierte Versionen muss derselbe Empfänger nicht erneut abhaken.
Erscheint später eine neue Version einer Richtlinie, werden die Empfänger laufender Übergaben dieses Typs benachrichtigt (In-App und E-Mail). Die offenen Fassungen liefert GET /api/assets/handovers/policy-updates; quittiert werden sie über POST /api/assets/handovers/:id/reaccept-policy — das verlangt assets.confirmReceipt, betrifft nur bestätigte Übergaben und quittiert alle offenen Versionen in einem Vorgang. Welche Fassung gilt, bestimmt ausschließlich der Server.
Automatisch: Asset-Status wechselt zu IN_USE. assignedToId (Empfänger) bleibt gesetzt, deployedAt wird gesetzt.
Schritt 4: PDF-Protokoll generieren
GET /api/assets/handovers/:id/pdf
Generiert PDF-Protokoll mit QR-Code, Asset-Details, Unterschriften (digital), Policy-Text. Das erwartete Rückgabedatum stammt aus dem Protokoll selbst — ein später geändertes Datum am Asset verändert ein altes Protokoll nicht.
Rückgabedatum ändern
PATCH /api/assets/handovers/:id/expires-at
{
"expiresAt": "2027-06-30T12:00:00Z",
"reason": "Projektlaufzeit verlaengert"
}
Gilt für Ausgabe-Protokolle im Status PENDING oder ACCEPTED. expiresAt=null macht die Ausgabe unbefristet; ein Datum muss in der Zukunft liegen (sonst 400 VALIDATION_ERROR). reason ist optional. Bei einer bestätigten Ausgabe wandert das Datum zugleich an die enthaltenen Assets (expectedCheckinAt), offene Erinnerungen zum alten Termin verfallen und der Empfänger wird benachrichtigt (In-App und E-Mail).
Ein Rückgabe-Protokoll hat kein Rückgabedatum — der Aufruf antwortet dort 400 HANDOVER_EXPIRY_NOT_APPLICABLE; jeder andere Status als PENDING oder ACCEPTED ergibt 409 HANDOVER_ALREADY_PROCESSED. Erlaubt ist der Aufruf mit assets.initiateHandover oder — bei gesperrten Typen — mit der Typ-Freigabe für das Initiieren von Übergaben, und zwar für jeden beteiligten Asset-Typ; nicht nur der ursprüngliche Aussteller darf ändern.
Im Verlauf des Assets steht die Änderung als ein Wertepaar „erwartetes Rückgabedatum: alt → neu" im Format des Benutzers, dazu die Übergabe-Nummer und — falls angegeben — der Grund. Der Audit-Eintrag trägt dasselbe Wertepaar.
Model-Clustering (Duplikat-Erkennung)
Eviworx erkennt automatisch Duplikate in Manufacturer/Model-Schreibweisen und schlägt Bereinigung vor.
Wie funktioniert Clustering?
- Erkennung: CronJob läuft periodisch (z.B. täglich)
- Analyse: Ähnliche Manufacturer/Model-Kombinationen werden gefunden (String-Similarity)
- Cluster: Varianten werden gruppiert (z.B. "DELL", "Dell", "dell")
- Review: Admin prüft Cluster und setzt canonical-Werte
- Merge: Alle Assets im Cluster bekommen canonical-Werte
Cluster abrufen
GET /api/asset-model-clusters?status=PENDING
Response
{
"data": [
{
"id": "clx...",
"canonicalManufacturer": null,
"canonicalModel": null,
"variants": [
{ "manufacturer": "DELL", "model": "XPS 15", "count": 15 },
{ "manufacturer": "Dell", "model": "XPS 15", "count": 23 },
{ "manufacturer": "dell", "model": "xps 15", "count": 5 }
],
"assetCount": 43,
"variantCount": 3,
"status": "PENDING",
"detectedAt": "2026-01-27T08:00:00.000Z"
}
],
"pagination": { "page": 1, "limit": 25, "total": 12, "totalPages": 1, "hasMore": false }
}
Die Zähler je Status liefert GET /api/asset-model-clusters/stats als Objekt status → { count, assetCount }.
Canonical-Werte setzen
PATCH /api/asset-model-clusters/:id/canonical
{
"canonicalManufacturer": "Dell",
"canonicalModel": "XPS 15"
}
Cluster mergen
POST /api/asset-model-clusters/:id/merge
Automatisch: Alle 43 Assets bekommen manufacturer="Dell" und model="XPS 15". Cluster-Status → MERGED.
Cluster ablehnen (kein Duplikat)
POST /api/asset-model-clusters/:id/reject
Cluster-Status → REJECTED. Der Cluster erscheint danach nicht unter den offenen Duplikat-Kandidaten.
CMDB-Relations, Graph & Impact
Assets werden als Configuration Items über typisierte Beziehungen verknüpft. Jede Beziehung wird einmal gespeichert und ist von beiden Assets aus sichtbar; eine Beziehung, die es in Gegenrichtung schon gibt, wird als Duplikat abgelehnt.
Endpunkte
| Method | Endpoint | Beschreibung |
|---|---|---|
POST | /api/asset-relations | Relation erstellen |
DELETE | /api/asset-relations/:relationId | Relation löschen |
GET | /api/asset-relations/asset/:assetId | Relationen eines Assets |
GET | /api/asset-relations/asset/:assetId/graph?depth=1..3 | Transitiver Beziehungs-Graph |
GET | /api/asset-relations/types | Verfügbare Relation-Typen |
GET | /api/assets/:id/impact?targetStatus=X | Impact-/Dependency-Analyse (read-only) |
POST | /api/assets/:id/impact/apply | Geführte Mit-Aktualisierung der Nachbarn |
GET | /api/assets/:id/impact/recovery | Recovery beim Wartungsende |
POST | /api/assets/impact/batch | Aggregierter Impact über eine Bulk-Auswahl |
GET | /api/assets/:id/containment | Verbaute Komponenten (transitiv) |
Relation-Typen
CONNECTED_TO– Bidirektional: physisch verbunden (Laptop ↔ Monitor)INSTALLED_ON– Software auf HardwarePART_OF– Komponente ist Teil von (RAM → Server)DEPENDS_ON– Funktionale Abhängigkeit (VM → Host)DOCKING_STATION– Bidirektional: Laptop-DockingBACKUP_OF– Backup-/Redundanz-BeziehungREPLACES– Ersetzt (Hardware-Tausch)OTHER– Sonstiges mit Freitext
Relation erstellen
POST /api/asset-relations
{
"parentAssetId": "clx-laptop-id",
"childAssetId": "clx-monitor-id",
"relationType": "CONNECTED_TO",
"description": "DisplayPort cable"
}
Zuerst wird geprüft, ob der Aufrufer beide Assets sehen und bearbeiten darf, erst danach die fachlichen Regeln — so verraten Fehlermeldungen nichts über fremde Assets. Für jedes der beiden Assets entsteht ein Audit-Eintrag. Abgelehnt werden: Selbst-Referenz, Duplikat (auch reverse), gelöschte Enden, CONSUMABLE-Enden (Verbrauchsmaterial ist kein CI) sowie Containment-Zyklen (PART_OF/INSTALLED_ON in die Gegenrichtung → ASSET_RELATION_CYCLE, HTTP 409).
Beziehungs-Graph
GET /api/asset-relations/asset/:assetId/graph?depth=1..3
Beziehungs-Graph über 1–3 Ebenen, nach Ebenen angeordnet. Assets, die der Aufrufer nicht sehen darf, erscheinen ohne Details. Beziehungen anlegen und löschen ist in der Oberfläche nur in der Desktop-Ansicht möglich.
Impact-/Dependency-Awareness
Beim Statuswechsel eines Assets zeigt die Impact-Analyse die transitiv betroffenen Nachbarn — unter Beachtung von Richtung und Beziehungstyp (PART_OF wirkt nur in eine Richtung), bis zu 10 Ebenen weit; nicht sichtbare Assets erscheinen ohne Details. Betroffene Nachbarn werden nur geändert, wenn sie im Apply-Schritt bestätigt werden.
GET /api/assets/:id/impact?targetStatus=MAINTENANCE
{
"impacted": [
{ "assetId": "clx...", "assetTag": "00099", "name": "App-Server", "distance": 1, "propagates": true }
],
"counts": { "total": 1, "propagating": 1, "hints": 0, "redacted": 0 }
}
Geführte Mit-Aktualisierung (je Asset gilt das Bearbeitungsrecht; einzelne Assets können scheitern, ohne die übrigen aufzuhalten):
POST /api/assets/:id/impact/apply
{
"items": [ { "assetId": "clx...", "version": 3, "status": "MAINTENANCE" } ],
"reason": "Host in Wartung — abhängige VMs mit"
}
Weiter: GET /api/assets/:id/impact/recovery liefert Nachbarn in MAINTENANCE, die beim Wartungsende dieses Assets reaktiviert werden können; POST /api/assets/impact/batch { assetIds, targetStatus } aggregiert den Impact über eine Bulk-Auswahl (read-only, mit counts.inSelection). Die Mit-Aktualisierung selbst erfolgt immer vom einzelnen Asset aus.
Verbaute Komponenten (Containment & Co-Move)
Ein Asset-Typ mit standalone=false ist eine Einbau-Komponente (RAM/SSD/PCIe): keine eigenständige Ausgabe/Handover — sie folgt ihrem Container. GET /api/assets/:id/containment liefert die transitiv verbauten Komponenten (über PART_OF/INSTALLED_ON; nicht sichtbare Komponenten werden nur mitgezählt).
- Co-Move / Co-Locate / Co-Return: checkout, checkin, install, deinstall, confirmReturn und PATCH /api/assets/:id (Standortwechsel) nehmen die Komponenten in einem Schritt mit (coMoveAssetIds / coLocateAssetIds / coReturnAssetIds — Items im selben Protokoll bzw. am selben Standort). Fehler: CO_ITEM_NOT_CONTAINED / CO_ITEM_INVALID_STATE.
- Co-LOST: beim „Container verloren melden" werden die verbauten Komponenten optional mit als verloren gemeldet (Default angehakt, derselbe Pflicht-Grund) — per API ist das für jede Komponente ein eigener Aufruf nach dem Container.
- Inventory-Auto-Account: eine nicht gescannte Komponente gilt als erfasst, wenn ihr Container in der Session gescannt wurde. Details auf der Seite Inventory API.
Verknüpfungs-Regeln: Assets außer Betrieb (RETIRED/LOST/DISPOSED) können nicht neu mit Lizenzen/Verträgen verknüpft werden (Entfernen bleibt immer erlaubt); Ausmustern und Entsorgen sind blockiert, solange aktive Vertrags-/Lizenz-Verknüpfungen bestehen. Asset↔Asset-Beziehungen zu Assets außer Betrieb bleiben bewusst erlaubt (CMDB-Historie). Verbrauchsmaterial (CONSUMABLE) ist von CMDB-Beziehungen ausgeschlossen.
Typbezogene Berechtigungen
Berechtigungen lassen sich pro Asset-Typ festlegen (z. B. dürfen nur bestimmte Rollen Laptops ausgeben).
Permission erstellen
Typ-ID und Rollen-/User-ID stehen im Pfad; der Body enthält nur die Capability-Flags:
PUT /api/asset-types/:id/permissions/roles/:roleId
{
"canView": true,
"canCreate": true,
"canEdit": true,
"canCheckout": true,
"canCheckin": true,
"canDelete": true,
"canInitiateHandover": true
}
Oder für spezifischen User:
PUT /api/asset-types/:id/permissions/users/:userId
{
"canView": true,
"canEdit": true,
"canDelete": false
}
Permission-Check
Beim Asset-Zugriff prüft das System automatisch:
- Globale Asset-Permissions (RBAC)
- Typbezogene Berechtigungen (falls vorhanden)
- User-spezifische Overrides (höchste Priorität)
Inventur-Scan (Bulk-Upload)
Für Hardware-Inventuren können Assets via Barcode/QR-Scanner erfasst werden.
Zwei Mechanismen: Einzel-Lookup eines gescannten Codes via GET /api/assets/scan und die vollständige, mehrstufige Inventur via Inventur-Sessions (siehe Abschnitt „Inventur-Sessions" oben).
Einzel-Lookup (Code → Asset)
GET /api/assets/scan?code=00042
Löst einen gescannten assetTag oder eine Seriennummer zum Asset auf (z.B. um es während der Inventur in eine Session aufzunehmen). Erfordert die Permission assets.scan.
Scan in eine Inventur-Session
POST /api/inventory-sessions/:id/scan
{
"code": "00042",
"quantity": 1
}
Entweder code (der gescannte QR-/Barcode-Wert) oder assetId — eines von beiden ist Pflicht. quantity ist optional (Default 1) und zählt bei Verbrauchsmaterial.
lastSeenAt: Wird bei jedem Scan aktualisiert. Assets ohne lastSeenAt in den letzten X Monaten können als "vermisst" markiert werden.
Locations & Categories
Locations (Standorte)
GET /api/asset-locations
{
"data": [
{
"id": "clx...",
"name": "Munich Office - 2nd Floor",
"address": "Sample Street 1, 80333 Munich",
"building": "Main Building",
"floor": "2",
"room": "201",
"isActive": true,
"assetCount": 42
}
]
}
Categories (Kategorien)
GET /api/asset-categories
{
"data": [
{
"id": "clx...",
"name": "Hardware",
"description": "Physical hardware devices",
"color": "#3b82f6",
"isActive": true,
"assetCount": 156
},
{
"id": "clx...",
"name": "Software",
"description": "Software licenses and subscriptions",
"color": "#8b5cf6",
"isActive": true,
"assetCount": 89
}
]
}
Checkout / Checkin & Install / Deinstall (Flow-Endpunkte)
Zuweisungen laufen ausschließlich über diese Endpunkte — ein PATCH, der assignedToId zusammen mit dem Wechsel nach IN_USE setzt, wird mit ASSET_ASSIGN_VIA_FLOW_ONLY abgelehnt. PERSON-Assets nutzen Checkout/Checkin, LOCATION-Assets Install/Deinstall. Jeder Endpunkt schreibt einen Aktivitätseintrag und prüft die Anker-Regeln (siehe Asset-Lebenszyklus).
Checkout (PERSON → IN_USE)
POST /api/assets/:id/checkout
{
"userId": "clx-user-id",
"expectedCheckin": "2026-12-31",
"note": "Issued for home office setup",
"coMoveAssetIds": ["clx-ram-id", "clx-ssd-id"]
}
Setzt status=IN_USE + assignedToId (CHECKOUT_DIRECT). Quelle: AVAILABLE, RESERVED oder MAINTENANCE (user-los). Externe Empfänger statt userId: externalFirstName / externalLastName / externalEmail (legt einen END_USER an + Mail). coMoveAssetIds nimmt verbaute Komponenten im selben Protokoll mit. expectedCheckin ist das erwartete Rückgabedatum; es steht am Asset (expectedCheckinAt) und am erzeugten Ausgabe-Protokoll und lässt sich dort später ändern (siehe Handover-Workflow).
Checkin (Rückgabe → AVAILABLE | MAINTENANCE | RETIRED)
POST /api/assets/:id/checkin
{
"status": "AVAILABLE",
"note": "Returned in good condition",
"damageReport": "Minor scratch on lid",
"coReturnAssetIds": ["clx-ram-id"]
}
Leert assignedToId, checkedOutAt, expectedCheckinAt, checkoutNote und deployedAt. status Default AVAILABLE (erlaubt: AVAILABLE, MAINTENANCE, RETIRED); LOST/DISPOSED laufen nicht über Checkin. Beim Ziel RETIRED gelten die Regeln fürs Ausmustern (blockiert bei aktiven Vertrags-/Lizenz-Verknüpfungen). damageReport wird am Handover-Record gespeichert (Overview-Karte).
Install (LOCATION → IN_USE)
POST /api/assets/:id/install
{
"locationId": "clx-serverroom-b12",
"note": "Rack 4, Slot 12",
"coLocateAssetIds": []
}
Nur für trackingMode=LOCATION. Setzt status=IN_USE + locationId (kein User), deployedAt=now. Kein Handover, keine Mail — reine IT-Aktion mit Activity-Log.
Deinstall (LOCATION → AVAILABLE | MAINTENANCE)
POST /api/assets/:id/deinstall
{
"status": "AVAILABLE",
"keepLocation": false,
"note": "Decommissioned"
}
Leert deployedAt und (Default) locationId; keepLocation=true lässt den Standort stehen (Gerät vor Ort, außer Betrieb). status AVAILABLE (default) oder MAINTENANCE.
Formelle Übergabe: Für Übergaben mit Empfänger-Bestätigung, PDF/QR und Mail dient der Handover-Workflow (nur PERSON, siehe oben). Feldbelegung & Anker-Regeln: Asset-Lebenszyklus.
Kaufdaten & Abschreibung
Felder
| Feld | Typ | Beschreibung |
|---|---|---|
purchaseDate | DateTime | Kaufdatum |
purchasePrice | Decimal | Kaufpreis |
purchaseOrder | String | Bestellnummer |
vendor | String | Lieferant |
warrantyEnd | DateTime | Garantieende |
maintenanceEnd | DateTime | Wartungsende |
bookValue | Decimal (berechnet) | Aktueller Buchwert — nur lesbar, bei jedem Abruf aus purchasePrice − Abschreibung berechnet |
depreciationMethod | String | LINEAR, DEGRESSIVE, NONE |
usefulLifeMonths | Int | Nutzungsdauer in Monaten |
depreciationStartDate | DateTime | Abschreibungs-Start |
Währung & Brutto/Netto: Assets speichern nur einen einzelnen purchasePrice (Decimal) — es gibt kein währungs- oder Brutto/Netto-Feld pro Asset. Die Systemwährung (general settings: systemCurrency, Default EUR, keine Umrechnung) und der Preismodus (priceTaxMode: net | gross, Default net — reine Kennzeichnung, keine Steuerberechnung) werden global in den Allgemeinen Einstellungen konfiguriert und auf alle Beträge angewendet. Siehe Settings & Global Search API.
Beispiel: Abschreibung
{
"purchaseDate": "2026-01-15",
"purchasePrice": 2499.00,
"depreciationMethod": "LINEAR",
"usefulLifeMonths": 36,
"depreciationStartDate": "2026-01-15"
}
Berechnung: Bei linearer Abschreibung über 36 Monate: Monatliche Abschreibung = €2.499 / 36 = €69,42. Nach 12 Monaten: bookValue = €1.665,96.
Custom Fields
Felder, die je Asset-Typ im customFieldSchema definiert sind, werden im Objekt customFields gespeichert:
Beispiel: Laptop
{
"customFields": {
"ramGB": 32,
"storageGB": 1024,
"storagetype": "NVMe SSD",
"cpu": "Intel i7-12700H",
"display": "15.6\" 4K OLED",
"gpu": "NVIDIA RTX 3050 Ti",
"battery": "86 Wh",
"weight": "2.0 kg"
}
}
Beispiel: Server
{
"customFields": {
"rackUnit": "42U",
"position": "Rack A, U15-U18",
"cpuCores": 32,
"ramGB": 128,
"storageType": "SAS RAID 10",
"networkPorts": 4,
"ipAddress": "192.168.1.100",
"powerSupply": "Redundant 800W"
}
}
Filtern: Nach jedem Custom Field lässt sich in der Asset-Liste filtern: f.customField.<key>=<operator>:<wert> mit den Operatoren eq, neq, contains, isNull und isNotNull (z. B. f.customField.cpu=contains:i7). Die Volltextsuche q durchsucht Asset-Tag, Name, Seriennummer und Beschreibung, nicht die Custom Fields.
Sensible Custom-Fields
Ein Feld kann im customFieldSchema als sensitive markiert werden (z.B. Zugangsdaten, Schlüssel). Sensible Werte werden in ALLEN Lese-Pfaden (Detail, Liste, Create-Prefill) sowie in Activity-/Audit-Einträgen mit einer festen Maske überschrieben — der Klartext verlässt die normale Antwort nie.
| Method | Endpoint | Beschreibung |
|---|---|---|
GET | /api/assets/:id/custom-fields/:field/reveal | Klartext EINES sensiblen Feldes; Recht: Sicht auf das Asset (viewAll/viewOwn oder canView des Asset-Typs); jeder Abruf wird protokolliert (wie /licenses/:id/key) |
Speichern sensibler Felder: Bei einem sensiblen Feld gilt: Key weglassen = Bestand bleibt, null = leeren, den Masken-Wert zurücksenden = 400. So kann eine maskierte Anzeige nie versehentlich als echter Wert gespeichert werden.
Assets abrufen (Liste)
Request
GET /api/assets?f.status=IN_USE&f.typeId=clx-laptop&per=50
Query Parameters
| Parameter | Beschreibung |
|---|---|
q | Volltextsuche (Asset-Tag, Name, Seriennummer, Beschreibung) |
f.status | Nach Status filtern, z. B. f.status=in:AVAILABLE,IN_USE |
f.typeId / f.categoryId / f.locationId | Nach Asset-Typ, Kategorie oder Standort filtern |
f.assignedToId | Nach zugewiesenem Benutzer filtern |
f.criticality | Nach Kritikalität filtern (LOW, MEDIUM, HIGH, CRITICAL) |
f.manufacturer / f.model | Nach Hersteller oder Modell filtern |
f.type.trackingMode | Nach Tracking-Modus filtern (PERSON, LOCATION, CONSUMABLE) |
f.customField.<key> | Nach einem Custom Field filtern (siehe Custom Fields) |
page / per | Seite und Seitengröße (per Default 25, max. 100) |
sort | Sortierung, z. B. sort=createdAt:desc |
mine / myTeam / myDepartment | =1: nur Assets des Aufrufers, seines Teams bzw. seiner Abteilung |
lowStock | =1: nur Verbrauchsmaterial auf oder unter dem Mindestbestand |
notLinkedToLicenseId | Lizenz-ID: nur Assets, die dieser Lizenz nicht zugewiesen sind — die Kandidatenliste für eine Lizenz-Zuweisung |
deleted | =1: Papierkorb (Recht assets.viewDeleted) |
Lifecycle-Management
Assets tracken ihren kompletten Lifecycle:
| Feld | Beschreibung |
|---|---|
createdAt | Erstellt in System |
deployedAt | Erstmals in Betrieb genommen |
retiredAt | Außer Betrieb genommen |
disposedAt | Entsorgt/Verkauft |
deletedAt | Soft-Delete (Papierkorb) |
Code-Beispiel: Kompletter Handover-Flow
// ===================================================
// ASSET HANDOVER - FROM CREATION TO CONFIRMATION
// ===================================================
const API_URL = 'https://your-instance.com/api';
// Auth via HttpOnly Cookies (credentials: 'include')
// 1. IT creates handover (issue laptop)
const handover = await fetch(`${API_URL}/assets/handovers`, {
method: 'POST',
credentials: 'include', // HttpOnly cookie auth
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
recipientId: 'clx-john-doe',
assetIds: ['clx-laptop-id'],
note: 'Laptop for new developer',
expiresAt: '2026-12-31T23:59:59Z'
})
}).then(r => r.json());
console.log('Handover created:', handover.handoverNumber); // HO-00042
console.log('QR URL:', handover.qrUrl);
// 2. Generate PDF protocol
const pdfBlob = await fetch(
`${API_URL}/assets/handovers/${handover.id}/pdf`,
{
credentials: 'include' // HttpOnly cookie auth
}
).then(r => r.blob());
// Save or print PDF
// PDF contains QR code + asset details + policy text
// 3. User scans QR code (opens handover.qrUrl in browser)
// NO authentication required! HMAC token in URL
const publicData = await fetch(handover.qrUrl)
.then(r => r.json());
console.log('Public handover data:', publicData);
// Shows: Asset details, initiator name, policy text
// NO internal IDs, NO emails
// 4. User confirms handover (in UI after QR scan)
const confirmed = await fetch(
`${API_URL}/assets/handovers/${handover.id}/accept`,
{
method: 'POST',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
note: 'Laptop received. All components present.',
policyAccepted: true
})
}
).then(r => r.json());
console.log('Handover confirmed:', confirmed.handoverNumber);
console.log('Status:', confirmed.status); // CONFIRMED
// Asset status is now IN_USE
// assignedToId = John Doe
// checkedOutAt = now
Code-Beispiel: Model-Clustering
// ===================================================
// MODEL CLUSTERING - DEDUPLICATE MANUFACTURER/MODEL
// ===================================================
// 1. Get pending clusters (detected by CronJob)
const clusters = await fetch(`${API_URL}/asset-model-clusters?status=PENDING`, {
credentials: 'include'
}).then(r => r.json());
console.log('Pending clusters:', clusters.data.length);
// Example cluster:
// {
// "variants": [
// { "manufacturer": "DELL", "model": "XPS 15", "count": 15 },
// { "manufacturer": "Dell", "model": "XPS 15", "count": 23 },
// { "manufacturer": "dell", "model": "xps 15", "count": 5 }
// ],
// "assetCount": 43,
// "variantCount": 3,
// "status": "PENDING"
// }
const cluster = clusters.data[0];
// 2. Set canonical values
await fetch(`${API_URL}/asset-model-clusters/${cluster.id}/canonical`, {
method: 'PATCH',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
canonicalManufacturer: 'Dell',
canonicalModel: 'XPS 15'
})
});
// 3. Merge cluster (apply canonical values to all 43 assets)
await fetch(`${API_URL}/asset-model-clusters/${cluster.id}/merge`, {
method: 'POST',
credentials: 'include'
});
console.log('Merged! All 43 assets now have manufacturer="Dell", model="XPS 15"');
// Cluster status is now MERGED
// All assets updated in single transaction
// Activity log created for each asset
Attachments
Assets nutzen das zentrale Anhang-System für Bestellungen, Garantiebelege, Rechnungen usw.:
# Upload file to asset
POST /api/attachments/ASSET/:assetId
# All attachments of an asset
GET /api/attachments/ASSET/:assetId
# Download
GET /api/attachments/:id/download
Details: Siehe Attachments & File Settings API für Virenscan, Datei-Einstellungen und Aufbewahrungsfristen.
Workflows API →
Erfahre mehr über die Workflows API
Entity Linking API →
Assets mit Tickets/Problems/Incidents/Changes/Verträgen verknüpfen