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], /:id)licenses.viewAll/viewOwn ‖ manage*
POST/PATCH/DELETE/api/software-publisherslicenses.managePublishers
GET/api/software-products (+ /:id)licenses.viewAll/viewOwn ‖ manage*
POST/PATCH/DELETE/api/software-productslicenses.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.

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 und message
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
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.