Eviworx
Docs

Contracts & Licenses API

Die Contracts & Licenses API verwaltet Verträge und Software-Lizenzen mit AES-256-GCM-verschlüsselten License-Keys, TCO-Tracking, Seat-Management, Renewal-Monitoring, Parent-Child-Hierarchien (parentId), Software-Katalog (Publisher/Products), Asset-/User-Verknüpfungen und vollständigem Audit-Trail.

🚀
Funktionen
✓ Verschlüsselte Lizenzschlüssel (AES-256-GCM)
✓ Mehrjahres-Kostenplan (Invest-Plan, 1–10 Jahre)
✓ Seat-Management mit Überbuchungs-Erkennung
✓ Ablauf-Hinweise per CronJob (30/7/3/0 Tage)
✓ Vertrags-Hierarchie (parentId)
✓ Asset-, User- und Vertrags-Verknüpfung (auch Bulk)
✓ Software-Katalog (Publisher & Produkte)
✓ Export als CSV, XLSX, PDF (protokolliert)
✓ Optimistic Locking (409 bei Konflikt)
✓ Änderungsverlauf in der Sprache des Betrachters

Authentifizierung & Permissions

Alle Endpunkte akzeptieren eine Session oder einen X-API-Key mit Rolle; jede Aktion hat ein eigenes Recht unter contracts.* bzw. licenses.*. Bei Verträgen unterscheidet das Bearbeitungsrecht zwischen allen Verträgen (editAll) und eigenen als Verantwortlicher (editOwn). Bei Lizenzen gilt jedes Recht für alle Lizenzen. Details: User Management & RBAC.

User-Kontext vs. API-Key: Stammdaten-CRUD, Export und Statistiken akzeptieren User UND API-Keys. Zuweisungen/Verknüpfungen (License↔Asset/User, Contract↔Asset), die Activity-History sowie Bulk-Status/Delete verlangen dagegen einen eingeloggten Benutzer — ein API-Key erhält hier 403 ("requires a logged-in user account, not an API key").

Endpoints Übersicht

Contracts

MethodEndpointBeschreibungPermission
GET/api/contractsPaginierte Liste ({data, pagination})contracts.viewAll/viewOwn
GET/api/contracts/:idEinzelner Vertrag (Objekt direkt, mit ETag)contracts.viewAll/viewOwn
GET/api/contracts/statsStatistiken (Zähler je Status/Typ, Ablauf, Volumen)contracts.viewAll/viewOwn
GET/api/contracts?deleted=1Papierkorb — derselbe Listen-Endpunkt mit deleted=1 (nur dieser Wert, sonst 400). Filter, Suche, Sortierung und gespeicherte Ansichten gelten dort genauso, ebenso die Sichtbarkeitsregeln.contracts.viewDeleted
GET/api/contracts/:id/childrenUnterverträge ({data, pagination})contracts.viewAll/viewOwn
GET/api/contracts/:id/licensesVerknüpfte Licenses ({data})contracts.viewAll/viewOwn
GET/api/contracts/:id/activitiesHistorie des Vertrags ({data, pagination}, ?limit/?offset)contracts.viewHistory
POST/api/contracts/:id/activitiesKommentar hinzufügen → 201 (User-only)contracts.editAll/editOwn
GET/api/contracts/exportCSV/XLSX/PDFcontracts.export
POST/api/contractsErstellen → 201contracts.create
PATCH/api/contracts/:idAktualisieren (editAll oder editOwn; Status und Verantwortlicher mit eigenem Recht; version Pflicht)contracts.editAll/editOwn
DELETE/api/contracts/:idSoft-Delete → 204contracts.delete
POST/api/contracts/:id/restoreWiederherstellencontracts.restore + viewDeleted
PATCH/api/contracts/bulk/statusBulk-Status (User-only, max. 100)contracts.bulkUpdate
DELETE/api/contracts/bulkBulk-Delete (User-only, max. 100)contracts.delete

Sichtbarkeit vor Aktion: contracts.delete und contracts.viewHistory gelten für alle Verträge, unabhängig vom Verantwortlichen. Löschen und Historie prüfen deshalb zusätzlich die Sicht auf genau diesen Vertrag — wer ihn nicht sehen darf, bekommt 404, sodass nicht erkennbar ist, ob er existiert. Im Papierkorb gelten zwei Rechte: contracts.viewDeleted öffnet den Papierkorb, und darin gelten weiter die normalen Sichtbarkeitsregeln (Verantwortlicher + Vorgesetzten-Kette). Wiederherstellen verlangt beide Rechte und die Sicht auf den Vertrag — so zeigen Papierkorb und Wiederherstellung dieselbe Menge. Auch das Massen-Löschen hält sich daran: Ein nicht sichtbarer Vertrag kommt als Fehlzeile mit CONTRACT_NOT_FOUND zurück — bewusst derselbe Code wie für unbekannte IDs, damit die Antwort nicht verrät, welche anderen Verträge existieren.

Kritische Aktionen: contracts.delete, contracts.restore und contracts.export werden bei jeder Nutzung neu geprüft, sodass ein entzogenes Recht sofort wirkt; verweigerte Versuche werden protokolliert.

Contract-Asset-Verknüpfung (User-only)

MethodEndpointPermission
GET/api/contracts/:id/assetscontracts.viewAll/viewOwn (je Zeile zusätzlich das Sichtrecht auf das Asset — sonst ein Platzhalter)
POST/api/contracts/:id/assetscontracts.linkAssets
PATCH/api/contracts/:id/assets/:assetIdcontracts.linkAssets
DELETE/api/contracts/:id/assets/:assetIdcontracts.unlinkAssets
POST/api/contracts/:id/assets/bulkcontracts.bulkLinkAssets

Der Assets-Tab zeigt nur, was der Aufrufer auch sehen darf: Wer den Vertrag sehen darf, sieht damit nicht automatisch seine Assets. Jede Zeile trägt entweder das volle Asset oder einen Platzhalter aus id, assetTag, status und restricted: true — etwa bei einem gesperrten Asset-Typ ohne Freigabe. Die Zeile bleibt, damit der Zähler stimmt; Name, Typ, Standort und Zuweisung fehlen, und die Zeile führt nicht ins Asset-Detail.

Eine Verknüpfung hat keinen eigenen Typ; die Vertragsart ergibt sich aus dem contractType des Vertrags. isPrimary gilt PRO ASSET, nicht pro Vertrag: ein Asset hat höchstens einen primären Vertrag — setzt man einen neuen, stuft der Server den bisherigen automatisch herab. Verträge in den Zuständen CANCELLED und EXPIRED nehmen keine neuen Verknüpfungen an.

Für die Liste der verknüpften Assets gelten dieselben Sichtbarkeitsregeln wie für das Detail (inklusive Vorgesetzten-Kette): Wer den Vertrag nicht sehen darf, bekommt 404, sodass nicht erkennbar ist, ob er existiert.

Licenses

MethodEndpointBeschreibungPermission
GET/api/licensesListe (Scope)licenses.viewAll/viewOwn
GET/api/licenses/:idEinzelne Licenselicenses.viewAll/viewOwn
GET/api/licenses/:id/keyLizenzschlüssel im Klartext (jeder Abruf wird protokolliert)licenses.viewKeys
GET/api/licenses/statsStatistiken (Seats, Kosten)licenses.viewAll/viewOwn
GET/api/licenses?deleted=1Papierkorb: gelöschte Licenses (?deleted=1 auf der Liste; Einträge im Papierkorb tragen deletedAt/deletedBy)licenses.viewDeleted
GET/api/licenses/exportCSV/XLSX/PDFlicenses.export
POST/api/licensesErstellenlicenses.create
PATCH/api/licenses/:idAktualisierenlicenses.update
DELETE/api/licenses/:idSoft-Deletelicenses.delete
POST/api/licenses/:id/restoreWiederherstellenlicenses.restore + viewDeleted
POST/api/licenses/:id/link-contractMit Contract verknüpfenlicenses.linkToContract
POST/api/licenses/:id/unlink-contractVon Contract trennenlicenses.unlinkFromContract
POST/api/licenses/bulk/link-contractBulk-Linklicenses.bulkLinkToContract
POST/api/licenses/bulk/unlink-contractBulk-Unlinklicenses.bulkUnlinkFromContract
PATCH/api/licenses/bulk/statusBulk-Statuslicenses.bulkUpdate
DELETE/api/licenses/bulkBulk-Deletelicenses.delete

License-Zuweisungen (User-only)

MethodEndpointPermission
GET/api/licenses/:id/assetslicenses.viewAll/viewOwn
POST/api/licenses/:id/assets (+ /bulk)licenses.assignToAsset
DELETE/api/licenses/:id/assets/:assetId (+ /bulk)licenses.unassignFromAsset
GET/api/licenses/:id/userslicenses.viewAll/viewOwn
POST/api/licenses/:id/users (+ /bulk)licenses.assignToUser
DELETE/api/licenses/:id/users/:userId (+ /bulk)licenses.unassignFromUser

Software-Katalog

MethodEndpointPermission
GET/api/software-publishers (+ /search [q/per, höchstens 50])licenses.viewAll/viewOwn ‖ manage*
POST · PATCH · DELETE/api/software-publishers (201) · /:id (200 · 204)licenses.managePublishers
GET/api/software-productslicenses.viewAll/viewOwn ‖ manage*
POST · PATCH · DELETE/api/software-products (201) · /:id (200 · 204)licenses.manageProducts

Activities

MethodEndpointBeschreibung
GET / POST/api/contracts/:id/activitiesVerlauf/Kommentar je Vertrag — nur für Benutzer, die den Vertrag sehen dürfen. Übergreifend im Audit-Log.
GET / POST/api/licenses/:id/activitiesVerlauf/Kommentar je Lizenz — nur für Benutzer, die die Lizenz sehen dürfen. Übergreifend im Audit-Log.

Verlaufseinträge tragen neben den Feld-Änderungen einen Textschlüssel mit Parametern (bodyKey, bodyParams). Die Oberfläche setzt daraus den Text in der Sprache des Betrachters zusammen, statt einen fest gespeicherten Satz anzuzeigen.

Sammel-Aktionen

Verträge kennen zwei Sammel-Aktionen (Status setzen, Löschen), Lizenzen vier (Status setzen, Löschen, mit einem Vertrag verknüpfen, vom Vertrag lösen). Jeder Aufruf nimmt höchstens 100 Einträge und verlangt einen eingeloggten Benutzer. Die Antwort ist immer { processed, failed, errors[] }: Ein Teilerfolg ist der Normalfall, und jede einzelne Zeile gilt ganz oder gar nicht — eine Zeile, die scheitert, hinterlässt keine halbe Änderung.

Jede Fehlzeile trägt die ID des Eintrags, einen errorCode, eine englische message für Protokolle und — wo die Aussage Werte hat — details. Maßgeblich ist der errorCode: Die Oberfläche übersetzt ihn und zeigt die englische Meldung nie an. Es sind dieselben Codes und dieselben details, die auch der jeweilige Einzel-Aufruf liefert. Beim Verknüpfen muss der Ziel-Vertrag existieren, sonst antwortet der ganze Aufruf mit 404, ohne eine Zeile zu verarbeiten.

Error CodeSammel-AktionBedeutung
CONTRACT_NOT_FOUNDVerträge: Status, LöschenUnbekannte ID; beim Löschen zusätzlich jeder Vertrag, den der Aufrufer nicht sehen darf (siehe „Sichtbarkeit vor Aktion"). Ohne details.
FORBIDDENVerträge: StatusFür genau diesen Vertrag fehlt das Bearbeiten- oder das Status-Recht.
CONTRACT_STATUS_UNCHANGEDVerträge: StatusDer Vertrag steht bereits auf dem Zielstatus.
INVALID_CONTRACT_STATUS_TRANSITIONVerträge: StatusÜbergang laut Matrix nicht erlaubt; details: currentStatus, newStatus.
CONTRACT_HAS_LINKED_ASSETS · CONTRACT_HAS_LINKED_LICENSESVerträge: LöschenVerknüpfungen zuerst lösen; details: contractId und assetCount bzw. licenseCount.
LICENSE_NOT_FOUNDLizenzen: alle vierUnbekannte ID, nicht sichtbare Lizenz oder nicht sichtbarer Ziel-Vertrag — immer dieselbe, konstante Meldung ohne details.
LICENSE_ALREADY_LINKED · LICENSE_NOT_LINKEDLizenzen: Verknüpfen, LösenDie Lizenz hängt bereits an diesem Vertrag bzw. an gar keinem.
LICENSE_STATUS_UNCHANGEDLizenzen: StatusDie Lizenz steht bereits auf dem Zielstatus.
INVALID_LICENSE_STATUS_TRANSITIONLizenzen: StatusÜbergang laut Matrix nicht erlaubt; details: fromStatus, toStatus.
LICENSE_HAS_ASSIGNMENTSLizenzen: LöschenZuweisungen zuerst entfernen; details: licenseId, assetCount, userCount.
BULK_ROW_FAILEDalleUnerwarteter Fehler an dieser Zeile. Die Meldung ist konstant — interne Fehlertexte verlassen den Server nicht.

Warum die Lizenz-Zeile unspezifisch bleibt: Unbekannte ID, fehlende Sicht und verweigerte Aktion tragen bei Lizenzen bewusst dieselbe Zeile: gleicher Code, gleiche Meldung, keine Zusatzangaben. Eine Meldung, die die ID nennt oder zwischen „gibt es nicht" und „darfst du nicht" unterscheidet, wäre eine Auskunft darüber, welche Lizenzen im System existieren.

Protokoll: Jede verarbeitete Zeile schreibt denselben Protokolleintrag wie der Einzel-Aufruf, gekennzeichnet als Teil einer Sammel-Aktion — auch beim Sammel-Löschen von Verträgen und Lizenzen. Dazu kommt ein Eintrag für den Vorgang als Ganzes mit allen betroffenen IDs. Beim Verknüpfen und Lösen erhält der beteiligte Vertrag zusätzlich einen Eintrag in seiner eigenen Historie.

Antwort-Formate

AntwortForm
Listen (Verträge, Papierkorb, Unterverträge, Historie){ data, pagination }
Sub-Listen (verknüpfte Lizenzen, verknüpfte Assets){ data }
Einzelobjekt und MutationenObjekt direkt, ohne Hülle
Löschen204 ohne Body
Massen-Operationen{ processed, failed, errors[] } — je Fehlzeile id, errorCode, message und bei Bedarf details
Fehler{ error, errorCode, details? }

Die Pagination der Vertragsliste trägt page, limit, total, totalPages und hasMore; die Historie blättert dagegen per total, limit und offset. Der Verantwortliche (owner) und der Ersteller kommen in der Liste nur mit ID und Name — die E-Mail-Adresse trägt allein das Detail, wo die Seitenleiste sie anzeigt.

ETag: Das Vertrags-Detail liefert einen ETag und beantwortet ein passendes If-None-Match mit 304. Der Wert deckt neben dem Vertrag selbst auch seine abgeleiteten Zähler (Lizenzen, Assets, Unterverträge) ab — sonst bliebe eine frisch verknüpfte Lizenz im Browser unsichtbar, weil sich der Vertrag selbst nicht geändert hat.

Contract-Typen (contractType)

TypeBeschreibung
LICENSE_SUBSCRIPTIONSoftware-Abo (Microsoft 365, Adobe CC)
LICENSE_VOLUMEVolumenvertrag (z.B. Microsoft EA)
MAINTENANCEHardware-Wartungsvertrag
SUPPORTSupport-Vertrag
SLAService Level Agreement
LEASELeasing-Vertrag
OTHERSonstige

Contract-Status

StatusBeschreibung
DRAFTEntwurf, noch nicht aktiv
ACTIVEAktiv laufend
EXPIREDAbgelaufen
CANCELLEDGekündigt
SUSPENDEDPausiert
RENEWEDErneuert

Erlaubte Status-Übergänge

VonNach
DRAFTACTIVE · CANCELLED
ACTIVESUSPENDED · EXPIRED · CANCELLED · RENEWED
SUSPENDEDACTIVE · CANCELLED · EXPIRED
EXPIREDRENEWED
RENEWEDACTIVE · EXPIRED · CANCELLED
CANCELLED— (Endzustand)

Ein unzulässiger Wechsel endet mit 400 INVALID_CONTRACT_STATUS_TRANSITION. Für den Status-Wechsel braucht es ein eigenes Recht: zusätzlich zur Bearbeitungs-Berechtigung contracts.changeStatus — das gilt auch beim Anlegen, sobald ein anderer Status als DRAFT (der Vorgabewert) gesetzt wird. Ebenso ist das Setzen oder Wechseln des Verantwortlichen (ownerId) an contracts.assign gebunden.

Als „läuft bald ab" gilt ein ACTIVE-Vertrag, dessen Enddatum höchstens 30 Tage entfernt ist — der Ablauftag selbst zählt dazu. Die Liste liefert dafür das abgeleitete Feld isExpiringSoon; als Filter dient das Flag ?expiringSoon=true.

License-Typen (licenseType)

TypeBeschreibung
PERPETUALEinmalkauf, unbefristet
SUBSCRIPTIONAbo-Modell (mit Contract verknüpft)
VOLUMEVolumenlizenzen
OEMAn Hardware gebunden
SITEStandortlizenz (unbegrenzte User)
USERNamed-User-Lizenz
DEVICEDevice-Lizenz
CONCURRENTConcurrent/Floating
TRIALTestversion
FREEWAREKostenlos
OPEN_SOURCEOpen Source
OTHERSonstige

License-Keys werden nur für PERPETUAL und OEM erzwungen — Subscription/Volume sind oft account-basiert ohne klassischen Key.

License-Status

StatusBeschreibung
PENDINGNoch nicht aktiviert (Default)
ACTIVEAktiv und nutzbar
EXPIREDAbgelaufen
SUSPENDEDTemporär deaktiviert
CANCELLEDGekündigt
RETIREDAusgemustert

Kosten, Abrechnung & Währung

Contracts: oneTimeCost, recurringCost, totalValue + billingCycle — alle Beträge sind JSON-Zahlen. Licenses: purchasePrice, recurringCost + billingInterval (Int) und billingUnit. costPerSeat ist ein Boolean-Flag (gibt an, ob der Betrag pro Seat oder gesamt gilt) — NICHT der Seat-Preis selbst.

FeldWerte
billingCycle (Contract)MONTHLY, QUARTERLY, SEMI_ANNUALLY, ANNUALLY, BIENNIAL, TRIENNIAL, ONE_TIME, ON_DEMAND
billingUnit (License)WEEK, MONTH, YEAR
billingInterval (License)Int (z.B. 1 = pro billingUnit)

Gesamtwert (totalValue) ist kein Eingabefeld: Der Server berechnet ihn als Einmalkosten plus alle wiederkehrenden Zahlungen über die Vertragslaufzeit (ohne Enddatum: hochgerechnet auf ein Jahr). Ein im Request mitgeschickter Wert wird verworfen. Genau dieser Wert steht in Liste, Detail, Kennzahlen und Export. Er wird beim Anlegen und bei jedem Speichern berechnet; ein Vertrag mit totalValue = null erhält ihn beim nächsten Speichern.

Währung: Weder Contract noch License haben ein eigenes Währungsfeld. Alle Beträge werden in der globalen Systemwährung (general settings: systemCurrency, keine Umrechnung) interpretiert; das gilt auch für Exporte. Siehe Settings & Global Search API. Kostenzuordnung erfolgt über costCenterId (Contract & License) — siehe Cost Centers API.

AES-256-GCM Encryption (License-Keys)

License-Keys werden verschlüsselt in der Datenbank gespeichert:

• AES-256-GCM = Authenticated Encryption (AEAD)  - Key: 32 Bytes (256 Bit), hex-encoded (LICENSE_ENCRYPTION_KEY)
  - IV: 16 Bytes (random pro Encryption)
  - AuthTag: 16 Bytes (verhindert Tampering)

Speicherung in DB: "iv:authTag:encrypted" (hex)

Sicherheit:• Key nie in API-Response — nur licenseKeyMasked• Voller Key nur via GET /:id/key (licenses.viewKeys) + Audit-Log (IP, UserAgent)
# Generate key (exactly 32 bytes!)
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

# In docker-compose.yaml:
LICENSE_ENCRYPTION_KEY=46a0bb175f00dadf828a90042bfbb3cada81b385f73162c66059a32035826f0a
KRITISCH: LICENSE_ENCRYPTION_KEY NIEMALS ändern nach ersten Lizenzen — sonst sind alle bestehenden Keys nicht mehr entschlüsselbar. Für Key-Rotation müssten alle Keys re-encrypted werden.

API-Beispiele

Contract erstellen

POST /api/contracts
{
  "name": "Microsoft 365 Enterprise Agreement 2026",
  "contractNumber": "MS-EA-2026-001",
  "contractType": "LICENSE_VOLUME",
  "status": "ACTIVE",
  "vendor": "Microsoft Corporation",
  "publisher": "Microsoft",
  "startDate": "2026-01-01",
  "endDate": "2027-12-31",
  "renewalDate": "2027-10-01",
  "autoRenew": true,
  "noticePeriodDays": 90,
  "billingCycle": "ANNUALLY",
  "oneTimeCost": 0,
  "recurringCost": 450000,
  "purchaseOrder": "PO-2026-0123",
  "costCenterId": "clx-cost-center-id",
  "budgetCode": "CC-IT-001",
  "ownerId": "clx-user-id",
  "department": "IT",
  "tags": ["microsoft", "office365", "enterprise"]
}

License erstellen (mit Encryption)

POST /api/licenses
{
  "name": "Microsoft 365 E5 - Pool License",
  "licenseType": "USER",
  "status": "ACTIVE",
  "publisherId": "clx-publisher-id",
  "productId": "clx-product-id",
  "productName": "Microsoft 365 E5",
  "productVersion": "2026",
  "licenseKey": "XXXXX-YYYYY-ZZZZZ-AAAAA-BBBBB",
  "quantityPurchased": 500,
  "purchasePrice": 22950.00,
  "recurringCost": 22950.00,
  "billingInterval": 1,
  "billingUnit": "MONTH",
  "costPerSeat": false,
  "contractId": "clx-contract-id",
  "costCenterId": "clx-cost-center-id",
  "purchaseDate": "2026-01-15",
  "expirationDate": "2027-01-14",
  "tags": ["microsoft", "office365", "e5"]
}
Hinweis: Der licenseKey wird AES-256-GCM verschlüsselt gespeichert. Responses enthalten nur licenseKeyMasked; den vollen Key liefert GET /api/licenses/:id/key (mit Audit-Logging). publisherId/productId referenzieren den Software-Katalog; productName/productVersion sind freie Felder.

License zu User zuweisen

POST /api/licenses/:id/users
{
  "userId": "clx-user-id",
  "activationDate": "2026-01-28",
  "userEmail": "john.doe@company.com",
  "userAccount": "john.doe",
  "notes": "Assigned for sales team onboarding"
}

Export

GET /api/contracts/export?format=xlsx&f.status=ACTIVE&expiringSoon=true

Der Export übernimmt Filter und Suche der Liste (Formate csv, xlsx, pdf; Vorgabe csv) und ist auf 10.000 Datensätze begrenzt. Spaltenköpfe, Blattname sowie PDF-Titel und -Fußzeile folgen der Sprache des ausführenden Kontos (Benutzersprache, sonst die Standardsprache der Installation); API-Key- und System-Exporte laufen auf Englisch. Datumswerte in CSV und XLSX sind maschinenlesbar im Format JJJJ-MM-TT, das PDF zeigt sie lokalisiert. Jeder Export wird protokolliert.

Bevorstehende Verlängerungen von Verträgen UND Lizenzen zeigt der Renewal-Kalender der Reports-Fläche. Der Invest-Plan dort rechnet die Kosten aktueller Verträge und Lizenzen einschließlich automatischer Verlängerungen über 1–10 Jahre voraus (Vorgabe 5). Reports API

Hinweise vor dem Ablauf verschickt der mitgelieferte CronJob „Expiry Monitor" (täglich um 07:00, bei Auslieferung deaktiviert): 30, 7, 3 und 0 Tage vor dem Vertragsende (endDate) bzw. dem Ablaufdatum der Lizenz (expirationDate), je Stufe nur einmal. Bei Verträgen geht der Hinweis an den Vertragsverantwortlichen (ownerId). CronJobs API →

Parent-Child Hierarchie

Verträge können über parentId hierarchisch verschachtelt werden (z.B. übergeordneter Volumenvertrag mit Unterverträgen je Produkt). Die Unterverträge eines Vertrags liefert GET /api/contracts/:id/children; gefiltert wird per parentId / hasParent.

// 1. Parent contract
POST /api/contracts
{ "name": "Microsoft EA 2026-2028", "contractType": "LICENSE_VOLUME", "status": "ACTIVE", "startDate": "2026-01-01", "endDate": "2028-12-31", "billingCycle": "ANNUALLY", "recurringCost": 500000 }

// 2. Sub-contract (child)
POST /api/contracts
{ "name": "Microsoft 365 E5 Subscription", "contractType": "LICENSE_SUBSCRIPTION", "parentId": "clx-parent-id", "startDate": "2026-01-01", "endDate": "2027-12-31", "billingCycle": "ANNUALLY", "recurringCost": 225000 }

Filtering

Contract-Filters

ParameterBeschreibung
f.contractType / f.status / f.billingCycleEnum-Filter (eq/neq/in/notIn)
f.contractNumber / f.name / f.vendorText-Filter (eq/contains/startsWith)
f.ownerId / f.costCenterIdVerantwortlicher / Kostenstelle (eq/in/isNull/isNotNull)
f.totalValue / f.recurringCost / f.oneTimeCostKosten-Filter (gt/gte/lt/lte/between/isNull/isNotNull)
f.startDate / f.endDate / f.createdAt / f.updatedAtZeit-Filter (gt/gte/lt/lte/between/relative)
f.autoRenewAutomatische Verlängerung (true/false)
qSuche über Nummer, Name, Vendor, Publisher, Beschreibung
page / per / sortPaginierung und Sortierung (per Default 25, maximal 200)
expiringSoontrue = ACTIVE und Enddatum höchstens 30 Tage entfernt
myTeam / myDepartmentTab-Flags: eigenes Team bzw. eigene Abteilung
parentId / hasParentHierarchie-Filter
GET /api/contracts?f.status=in:ACTIVE,SUSPENDED&f.totalValue=gte:10000&f.endDate=relative:next_30_days&sort=endDate:asc&page=1&per=50

Ein Filter hat die Form f.<feld>=<operator>:<wert>; ohne Operator-Präfix gilt Gleichheit (f.status=ACTIVE). Mehrwertige Operatoren nehmen eine Komma-Liste (in:A,B — Kommas im Wert werden kodiert), isNull und isNotNull stehen ohne Wert. Sortiert wird mit sort=<feld>:asc|desc, mehrstufig per Komma.

Liste und Export akzeptieren für Paginierung, Suche und Sortierung nur page, per, q und sort; limit, offset, search, sortBy und sortDirection werden mit 400 LEGACY_QUERY_PARAM_REMOVED abgelehnt. Der Papierkorb ist dieselbe Liste mit deleted=1; dort gelten Filter, Suche, Sortierung und gespeicherte Ansichten ebenso. Die Untervertrags-Liste ist schlanker: sie nimmt limit/offset und sortBy aus einer festen Feldliste (name, contractNumber, contractType, status, startDate, endDate, updatedAt; Default startDate absteigend), aber keine gespeicherten Ansichten. Unbekannte Werte werden mit 400 abgelehnt.

License-Filters

ParameterBeschreibung
f.licenseTypePERPETUAL, SUBSCRIPTION, VOLUME, OEM, SITE, USER, DEVICE, CONCURRENT, TRIAL, FREEWARE, OPEN_SOURCE, OTHER (eq/neq/in/notIn)
f.statusPENDING, ACTIVE, EXPIRED, SUSPENDED, CANCELLED, RETIRED (eq/neq/in/notIn)
f.publisherId / f.productId / f.contractIdPublisher / Produkt / Vertrag (eq/in/isNull/isNotNull)
f.nameText-Filter (eq/contains/startsWith)
f.quantityPurchased / f.quantityUsed / f.purchasePrice / f.recurringCostMengen- und Kosten-Filter (gt/gte/lt/lte/between)
f.expirationDate / f.createdAt / f.updatedAtZeit-Filter (gt/gte/lt/lte/between/relative)
qSuche über Name, Beschreibung, Seriennummer, Produkt- und Publisher-Name
page / per / sortPaginierung und Sortierung
overAssigned / expiringSoon / myTeam / myDepartmentFlags (true): überbuchte Lizenzen, bald ablaufend, eigenes Team, eigene Abteilung

Permissions

contracts.*licenses.*
viewAll, viewOwn, viewDeleted, viewHistoryviewAll, viewOwn, viewDeleted, viewHistory, viewKeys
create, editAll, editOwn, delete, restorecreate, update, delete, restore
changeStatus, assign, bulkUpdate, export, reportingbulkUpdate, export, reporting
linkAssets, unlinkAssets, bulkLinkAssetsassignToAsset, unassignFromAsset, assignToUser, unassignFromUser
—linkToContract, unlinkFromContract, bulkLinkToContract, bulkUnlinkFromContract, managePublishers, manageProducts

Error-Handling

Error CodeHTTPBeschreibung
CONTRACT_NOT_FOUND404Contract existiert nicht — oder ist für den Aufrufer nicht sichtbar (Löschen, Historie, Assets-Liste)
CONTRACT_NUMBER_CONFLICT409Contract-Number bereits vergeben
CONTRACT_VERSION_CONFLICT409Gleichzeitige Änderung durch jemand anderen (version ist beim Update Pflicht)
INVALID_CONTRACT_STATUS_TRANSITION400Status-Wechsel laut Matrix nicht erlaubt
CONTRACT_HAS_LINKED_ASSETS · CONTRACT_HAS_LINKED_LICENSES409Vertrag trägt noch Verknüpfungen
CONTRACT_CIRCULAR_REFERENCE · CONTRACT_MAX_DEPTH400Hierarchie: Zyklus bzw. maximale Verschachtelung überschritten
CONTRACT_INACTIVE_STATUS400Verknüpfen an einem gekündigten oder abgelaufenen Vertrag
ASSET_CONTRACT_LINK_EXISTS409Asset ist bereits mit diesem Vertrag verknüpft
ASSET_CONTRACT_LINK_NOT_FOUND404Verknüpfung existiert nicht
LEGACY_QUERY_PARAM_REMOVED400limit/offset an der Vertragsliste (dort gelten page/per)
LICENSE_NOT_FOUND404License existiert nicht
LICENSE_VERSION_CONFLICT409Gleichzeitige Änderung durch jemand anderen
LICENSE_HAS_ASSIGNMENTS409License hat Assignments (zuerst entfernen)
SESSION_ONLY403Aktion erfordert User-Kontext (kein API-Key)

Attachments

Contracts & Licenses nutzen das zentrale Anhang-System (signierte Verträge, Nachträge, Rechnungen):

POST /api/attachments/CONTRACT/:contractId
POST /api/attachments/LICENSE/:licenseId
GET  /api/attachments/CONTRACT/:contractId
GET  /api/attachments/LICENSE/:licenseId

Anhänge folgen ihrem Vertrag bzw. ihrer Lizenz: Beim Löschen wandern sie mit in den Papierkorb, beim Wiederherstellen kommen sie zurück — einzeln vorher gelöschte Anhänge bleiben gelöscht. Beides gilt gemeinsam oder gar nicht; es entsteht kein Zustand, in dem der Vertrag noch da ist, seine Anhänge aber weg sind. Das Sammel-Löschen verhält sich dabei genauso wie das Löschen einer einzelnen Zeile.

Details: Siehe Attachments & File Settings API für Virenscan, Datei-Einstellungen und Aufbewahrungsfristen.
Nächster Schritt

Cost Centers API → Kostenstellen für die Kostenzuordnung von Verträgen & Lizenzen.