Eviworx
Docs

Assets API

The Assets API manages assets with types, categories and locations, checkout and handover with a QR/PDF record, CMDB relations, type-specific permissions, duplicate detection for manufacturer/model and inventory scanning.

📦
Features
✓ Model-Clustering (Duplicate detection)
✓ QR/PDF-Handover with confirmation
✓ CMDB relations, graph & impact analysis
✓ Usage policies per asset type
✓ Type-specific permissions
✓ Locations & categories
✓ Inventory sessions with workflow & notifications
✓ Consumables (bulk items)
✓ Purchase data & depreciation
✓ Checkout & checkin
✓ Export (CSV/PDF), bulk operations
✓ Activity log, damage reports

Authentication & Permissions

All asset endpoints accept a session (user) or an X-API-Key header (API key with an active role). In both cases the role's permissions apply. See User Management & RBAC and Authentication API.

Area Permission keys (feature.action)
Readassets.viewAll, assets.viewOwn, assets.viewDeleted (trash), assets.viewHistory
Mutateassets.create, assets.update, assets.delete, assets.restore, assets.checkout, assets.checkin
Bulk / Exportassets.bulkEdit, assets.bulkDelete, assets.export
Labels / Scanassets.generateLabel, assets.scan
Master dataassets.manageTypes, assets.manageCategories, assets.manageLocations, assets.manageClusters, assets.manageTypePermissions
Handoverassets.viewAllHandovers, assets.viewOwnHandovers, assets.initiateHandover, assets.confirmReceipt, assets.requestReturn, assets.managePolicies

Type lock: In addition to the global permissions, an asset type can be locked. For locked types the per-role or per-user grants apply (the assigned user always sees their asset); for open types only the global permissions. Lists therefore contain only the assets the caller may see.

The same visibility check precedes EVERY mutation: update, delete, check-out/check-in, install/deinstall, label and barcode actions answer 403 if the asset is not visible to the caller — a global action permission alone is not enough. The trash requires two permissions: assets.viewDeleted to see it and assets.restore to restore from it.

Critical permissions (re-checked on every request, so a revocation takes effect immediately; denied attempts are logged): assets.export, assets.managePolicies, assets.manageClusters and assets.manageTypePermissions.

Response formats: Every list in this domain returns { data } resp. { data, pagination } — assets, types, categories, locations, handovers and activities. Single objects come without a wrapper. Amounts (purchasePrice, depreciationRate) are JSON numbers. Counters such as assetCount are counted live.

Read scope follows the permission: without visibility of the individual asset the scan returns base data only (tag, name, status, type, location) — an inventory still works, but serial number and assignment stay out. The type list shows the display fields to everyone; customFieldSchema, hasTypePermissions and assetCount only with viewAll, manageTypes or manageTypePermissions. Categories and locations require viewAll or the respective manage permission. The global activity history only shows assets the reader may see.

Endpoints Overview

Core CRUD

Method Endpoint Description
GET/api/assetsList all assets (with filters); ?deleted=1 = trash (permission assets.viewDeleted)
GET/api/assets/:idGet single asset
POST/api/assetsCreate asset
PATCH/api/assets/:idUpdate asset
DELETE/api/assets/:idDelete asset (soft-delete)

Handover (QR/PDF)

Method Endpoint Description
GET/api/assets/handoversAll handovers (IT view)
GET/api/assets/handovers/pendingPending handovers for current user
POST/api/assets/handoversCreate new handover (checkout/transfer)
POST/api/assets/handovers/:id/acceptAccept handover
POST/api/assets/handovers/:id/rejectReject handover
PATCH/api/assets/handovers/:id/expires-atChange the return date of a checkout
GET/api/assets/handovers/:id/pdfGenerate PDF protocol
GET/api/handovers/:id/publicPublic QR access (no auth, HMAC token)

One operation per asset: Concurrent operations on the same asset are mutually exclusive — check-out, check-in, creating a handover, requesting a return, installing, deinstalling and the component take-overs all claim the asset. The second call gets 409 — so the same device is never issued to two different recipients. Also 409: a second handover on an asset that already has an open one. Database lock conflicts report 409 DEADLOCK_DETECTED.

Handovers you cannot see: Detail, PDF and return date answer 404 when the handover is not visible to the caller — the same answer as for a made-up ID, so its existence is not revealed. Whoever may see but not modify it gets a 403.

Public protocol: The QR/share link leads to a data-minimized version: it names the parties involved but no email addresses — neither in the JSON response nor in the PDF. The internal PDF stays complete. The footer of both PDFs shows the handover number (HO-00042), not the internal database ID.

Model Clustering

Method Endpoint Description
GET/api/asset-model-clustersAll clusters (duplicate candidates)
GET/api/asset-model-clusters/statsCluster statistics
PATCH/api/asset-model-clusters/:id/canonicalSet canonical values
POST/api/asset-model-clusters/:id/approveApprove cluster (ready to merge)
POST/api/asset-model-clusters/:id/mergeMerge cluster (deduplicate)
POST/api/asset-model-clusters/:id/rejectReject cluster (not duplicate)
DELETE/api/asset-model-clusters/:idDelete cluster

Relations & Inventory

Method Endpoint Description
POST/api/asset-relationsCreate CMDB relation
DELETE/api/asset-relations/:idDelete relation
GET/api/asset-relations/asset/:idAll relations of an asset
GET/api/asset-relations/asset/:id/graphCMDB graph (visualization)
GET/api/asset-relations/typesAvailable relation types

Advanced Operations

Method Endpoint Description
GET/api/assets/statsAsset statistics
GET/api/assets/exportCSV/PDF export
PATCH/api/assets/bulkBulk update (multiple assets)
DELETE/api/assets/bulkBulk delete (soft-delete)
POST/api/assets/:id/restoreRestore deleted asset (requires assets.restore AND assets.viewDeleted)
POST/api/assets/:id/checkoutCheckout asset (assign to user)
POST/api/assets/:id/checkinCheckin asset (return)
POST/api/assets/:id/installInstall LOCATION asset (→ location)
POST/api/assets/:id/deinstallDeinstall LOCATION asset
GET/api/assets/:id/labelGenerate QR label (PDF)
GET/api/assets/:id/damage-reportDamage report
GET/api/assets/my-consumablesMy consumables
GET/api/assets/suggestionsAuto-complete suggestions
GET/api/asset-activitiesActivities of all visible assets

Inventory Sessions

Stocktakes run through dedicated sessions under /api/inventory-sessions: starting one takes a snapshot of the expected assets, then items are recorded via QR/barcode and finally reconciled against that snapshot. They carry their own permissions (inventory.*), a due date with reminders and a trash. Endpoints, fields and permissions live in full on their own page: Inventory API.

The inventory routes never move assets themselves — actions on scanned, unexpected or missing devices run through the asset endpoints on this page (checkout, checkin, install, deinstall) and through the assets.* permissions.

Asset Types (CRUD + Policies)

Method Endpoint Description
GET/api/asset-typesAll asset types
POST/api/asset-typesCreate asset type (incl. policies)
PATCH/api/asset-types/:idUpdate asset type
DELETE/api/asset-types/:idDelete asset type
GET/api/asset-types/:id/permissionsGet type permissions
PUT/api/asset-types/:id/permissions/users/:userIdSet user permission
PUT/api/asset-types/:id/permissions/roles/:roleIdSet role permission

Asset Type Fields (POST/PATCH)

FieldTypDescription
name / displayNameStringInternal name (unique) / display name
trackingModeenum (default PERSON)PERSON | LOCATION | CONSUMABLE. Determines among other things whether the type is a consumable. Immutable once the type has assets → otherwise HTTP 409.
standaloneboolean (default true)false = built-in component (RAM/SSD): no standalone checkout/handover, follows its container via co-move. CONSUMABLE is always standalone (enforced). Freely toggleable (audited).
requiresConfirmationboolean (default false)Handover with recipient confirmation as the type default.
hasTypePermissionsBooleanType lock: enables type-specific permissions (see below).
icon / color / customFieldSchemaString / JSONBUI icon, color, custom field schema (JSON).
Mode semantics (anchor, actions, status sets) see Asset Lifecycle.

Categories & Locations (CRUD)

Method Endpoint Description
GET/api/asset-categoriesAll categories incl. hierarchy (parentId); permission: assets.viewAll or manageCategories
POST/api/asset-categoriesCreate category
PATCH/api/asset-categories/:idUpdate category
DELETE/api/asset-categories/:idDelete category
GET/api/asset-locationsAll locations incl. hierarchy (parentId); permission: assets.viewAll or manageLocations
POST/api/asset-locationsCreate location
PATCH/api/asset-locations/:idUpdate location
DELETE/api/asset-locations/:idDelete location

Asset Status

There are 11 lifecycle statuses. Full semantics, status groups, anchor rules and the transition matrix are on the Asset Lifecycle page.

Status Description
ORDEREDOrdered, not yet delivered
RECEIVEDReceived, not yet ready
AVAILABLEAvailable/ready (stock), freely assignable
RESERVEDReserved — not yet handed out
PENDING_ACCEPTANCEPerson handover running, awaiting recipient confirmation
IN_USEIn use — PERSON: with the user / LOCATION: installed at the location
MAINTENANCEIn maintenance/repair (anchor may remain)
RETURN_PENDINGReturn running, awaiting IT confirmation
RETIREDRetired (reactivatable)
LOSTLost/stolen (mandatory reason in statusNote)
DISPOSEDDisposed/sold (final)

Create Asset

Regular Asset (e.g., 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 (Bulk Item, e.g., USB Cables)

A consumable is created by an asset type with trackingMode=CONSUMABLE (a property of the type, not of the individual asset). The asset carries quantity/minQuantity; distribution runs via ConsumableAssignment, not 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: The type (trackingMode=CONSUMABLE) tracks quantity; serialNumber is optional. Handout decrements quantity via ConsumableAssignment (user OR location) instead of changing status/assignee. Allowed statuses: ORDERED, RECEIVED, AVAILABLE, RETIRED, DISPOSED. No handover, no 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

Handover is the formal handout path with recipient confirmation (→ PENDING_ACCEPTANCE), QR code, PDF protocol and email. It applies ONLY to trackingMode=PERSON (LOCATION uses install, CONSUMABLE has no handover) and is subject to the same checks as checkout/checkin. Accept → IN_USE; reject or cancel restores the previous state. Direct checkout (see below) is the fast path without confirmation.

Step 1: Create Handover (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 type & policy: The handover type (CHECKOUT_WITH_CONFIRMATION, CHECKOUT_DIRECT, RETURN, RETURN_DIRECT) and any required policy acceptance are derived server-side from the asset/type configuration — not set in the request. quantities is optional (consumables/bulk items).

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"
}

Step 2: User Scans QR Code

The QR code contains a URL with HMAC token for secure access WITHOUT authentication:

# 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: The public endpoint returns NO internal IDs, email addresses or sensitive data. Only minimal information for verification.

Step 3: User Accepts 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..."
}

Policies are acknowledged per version: A handover can contain devices of several asset types — the confirmation therefore acknowledges every active policy of the involved types. The responses carry policies[] for that (lists: id, version, title, acceptedAt; pending handovers and the detail additionally the full text and the recipient's acknowledgement state per version). Protocol, PDF and the public page list all acknowledged policies with version and timestamp. Versions the same recipient already acknowledged do not have to be ticked again.

When a new version of a policy is published later, the recipients of running handovers of that type are notified (in-app and email). The open versions come from GET /api/assets/handovers/policy-updates; they are acknowledged through POST /api/assets/handovers/:id/reaccept-policy — which requires assets.confirmReceipt, applies only to confirmed handovers and acknowledges all open versions in one go. Which version applies is decided solely by the server.

Automatically: Asset status transitions to IN_USE. assignedToId (recipient) stays set, deployedAt is set.

Step 4: Generate PDF Protocol

GET /api/assets/handovers/:id/pdf

Generates PDF protocol with QR code, asset details, signatures (digital), policy text. The expected return date comes from the record itself — a date changed later on the asset does not alter an older record.

Change the return date

PATCH /api/assets/handovers/:id/expires-at
{
  "expiresAt": "2027-06-30T12:00:00Z",
  "reason": "Projektlaufzeit verlaengert"
}

Applies to checkout records with status PENDING or ACCEPTED. expiresAt=null makes the checkout open-ended; a date must lie in the future (otherwise 400 VALIDATION_ERROR). reason is optional. On a confirmed checkout the date also moves to the assets involved (expectedCheckinAt), open reminders for the old date are dropped and the recipient is notified (in-app and email).

A return record has no return date — there the call answers 400 HANDOVER_EXPIRY_NOT_APPLICABLE; any status other than PENDING or ACCEPTED yields 409 HANDOVER_ALREADY_PROCESSED. The call is allowed with assets.initiateHandover or — for locked types — with the type-level grant for initiating handovers, for every asset type involved; not only the original issuer may change it.

In the asset history the change appears as a single value pair "expected return date: old → new" in the user's format, plus the handover number and — if given — the reason. The audit entry carries the same value pair.

Model Clustering (Duplicate Detection)

Eviworx automatically detects duplicates in manufacturer/model spellings and suggests cleanup.

How Does Clustering Work?

  1. Detection: CronJob runs periodically (e.g., daily)
  2. Analysis: Similar manufacturer/model combinations found (string similarity)
  3. Cluster: Variants grouped (e.g., "DELL", "Dell", "dell")
  4. Review: Admin reviews cluster and sets canonical values
  5. Merge: All assets in cluster get canonical values

Get Clusters

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 }
}

Counters per status come from GET /api/asset-model-clusters/stats as an object status → { count, assetCount }.

Set Canonical Values

PATCH /api/asset-model-clusters/:id/canonical
{
  "canonicalManufacturer": "Dell",
  "canonicalModel": "XPS 15"
}

Merge Cluster

POST /api/asset-model-clusters/:id/merge

Automatically: All 43 assets get manufacturer="Dell" and model="XPS 15". Cluster status → MERGED.

Reject Cluster (not duplicate)

POST /api/asset-model-clusters/:id/reject

Cluster status → REJECTED. The cluster then does not appear among the open duplicate candidates.

CMDB Relations, Graph & Impact

Assets are linked as configuration items via typed relations. Each relation is stored once and is visible from both assets; a relation that already exists in the reverse direction is rejected as a duplicate.

Endpoints

MethodEndpointDescription
POST/api/asset-relationsCreate relation
DELETE/api/asset-relations/:relationIdDelete relation
GET/api/asset-relations/asset/:assetIdRelations of an asset
GET/api/asset-relations/asset/:assetId/graph?depth=1..3Transitive relation graph
GET/api/asset-relations/typesAvailable relation types
GET/api/assets/:id/impact?targetStatus=XImpact/dependency analysis (read-only)
POST/api/assets/:id/impact/applyGuided co-update of neighbors
GET/api/assets/:id/impact/recoveryRecovery when maintenance ends
POST/api/assets/impact/batchAggregated impact over a bulk selection
GET/api/assets/:id/containmentBuilt-in components (transitive)

Relation Types

  • CONNECTED_TO – Bidirectional: physically connected (laptop ↔ monitor)
  • INSTALLED_ON – Software on hardware
  • PART_OF – Component is part of (RAM → server)
  • DEPENDS_ON – Functional dependency (VM → host)
  • DOCKING_STATION – Bidirectional: laptop docking
  • BACKUP_OF – Backup/redundancy relationship
  • REPLACES – Replaces (hardware swap)
  • OTHER – Other, free text

Create Relation

POST /api/asset-relations
{
  "parentAssetId": "clx-laptop-id",
  "childAssetId": "clx-monitor-id",
  "relationType": "CONNECTED_TO",
  "description": "DisplayPort cable"
}

First the server checks that the caller may see and edit both assets, only then the business rules — so error messages reveal nothing about assets the caller cannot see. An audit entry is written for each of the two assets. Rejected are: self-reference, duplicate (also reverse), deleted ends, CONSUMABLE ends (a consumable is not a CI) and containment cycles (PART_OF/INSTALLED_ON in the reverse direction → ASSET_RELATION_CYCLE, HTTP 409).

Relation Graph

GET /api/asset-relations/asset/:assetId/graph?depth=1..3

Relation graph across 1–3 levels, arranged by level. Assets the caller may not see appear without details. Creating and deleting relations in the UI is available in the desktop view only.

Impact/Dependency Awareness

On an asset status change the impact analysis shows the transitively affected neighbors — respecting direction and relation type (PART_OF works in one direction only), up to 10 levels deep; assets the caller cannot see appear without details. Affected neighbors are only changed when confirmed in the apply step.

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 }
}

Guided co-update (the edit permission applies per asset; individual assets may fail without blocking the rest):

POST /api/assets/:id/impact/apply
{
  "items": [ { "assetId": "clx...", "version": 3, "status": "MAINTENANCE" } ],
  "reason": "Host in Wartung — abhängige VMs mit"
}

Also: GET /api/assets/:id/impact/recovery returns neighbors in MAINTENANCE that can be reactivated when this asset's maintenance ends; POST /api/assets/impact/batch { assetIds, targetStatus } aggregates impact over a bulk selection (read-only, with counts.inSelection). The co-update itself is always done from a single asset.

Built-in Components (Containment & Co-Move)

An asset type with standalone=false is a built-in component (RAM/SSD/PCIe): no standalone checkout/handover — it follows its container. GET /api/assets/:id/containment returns the transitively built-in components (via PART_OF/INSTALLED_ON; components the caller cannot see are only counted).

  • Co-Move / Co-Locate / Co-Return: checkout, checkin, install, deinstall, confirmReturn and PATCH /api/assets/:id (location change) move the components in one step (coMoveAssetIds / coLocateAssetIds / coReturnAssetIds — items in the same record or at the same location). Errors: CO_ITEM_NOT_CONTAINED / CO_ITEM_INVALID_STATE.
  • Co-LOST: when reporting a container as lost, the built-in components can optionally be reported lost too (checked by default, same mandatory reason) — via the API this is a separate call per component after the container.
  • Inventory auto-account: an unscanned component counts as accounted for once its container was scanned in the session. Details on the Inventory API page.

Linking rules: out-of-service assets (RETIRED/LOST/DISPOSED) cannot be newly linked to licenses/contracts (removal is always allowed); retire/dispose is blocked while active contract/license links exist. Asset↔asset relations to out-of-service assets remain intentionally allowed (CMDB history). Consumables (CONSUMABLE) are excluded from CMDB relations.

Type-specific Permissions

Permissions can be set per asset type (e.g., only certain roles may check out laptops).

Create Permission

Type ID and role/user ID go in the path; the body contains only the capability flags:

PUT /api/asset-types/:id/permissions/roles/:roleId
{
  "canView": true,
  "canCreate": true,
  "canEdit": true,
  "canCheckout": true,
  "canCheckin": true,
  "canDelete": true,
  "canInitiateHandover": true
}

Or for specific user:

PUT /api/asset-types/:id/permissions/users/:userId
{
  "canView": true,
  "canEdit": true,
  "canDelete": false
}

Permission Check

On asset access, the system automatically checks:

  1. Global asset permissions (RBAC)
  2. Type-specific permissions (if present)
  3. User-specific overrides (highest priority)

Inventory Scan (Bulk Upload)

For hardware inventories, assets can be captured via barcode/QR scanner.

Two mechanisms: Single-code lookup via GET /api/assets/scan and the full multi-step inventory via inventory sessions (see the "Inventory Sessions" section above).

Single Lookup (code → asset)

GET /api/assets/scan?code=00042

Resolves a scanned assetTag or serial number to the asset (e.g. to add it to a session during inventory). Requires the assets.scan permission.

Scan into an inventory session

POST /api/inventory-sessions/:id/scan
{
  "code": "00042",
  "quantity": 1
}

Either code (the scanned QR/barcode value) or assetId — one of the two is required. quantity is optional (default 1) and counts for consumables.

lastSeenAt: Updated on every scan. Assets without lastSeenAt in last X months can be marked as "missing".

Locations & Categories

Locations

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

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 endpoints)

Assignment runs exclusively through these endpoints — a PATCH that sets assignedToId together with the transition to IN_USE is rejected with ASSET_ASSIGN_VIA_FLOW_ONLY. PERSON assets use checkout/checkin, LOCATION assets install/deinstall. Each endpoint writes an activity entry and checks the anchor rules (see Asset Lifecycle).

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"]
}

Sets status=IN_USE + assignedToId (CHECKOUT_DIRECT). Source: AVAILABLE, RESERVED or MAINTENANCE (user-less). External recipient instead of userId: externalFirstName / externalLastName / externalEmail (creates an END_USER + email). coMoveAssetIds moves built-in components in the same record. expectedCheckin is the expected return date; it is held on the asset (expectedCheckinAt) and on the checkout record created, where it can be changed later (see the handover workflow).

Checkin (return → AVAILABLE | MAINTENANCE | RETIRED)

POST /api/assets/:id/checkin
{
  "status": "AVAILABLE",
  "note": "Returned in good condition",
  "damageReport": "Minor scratch on lid",
  "coReturnAssetIds": ["clx-ram-id"]
}

Clears assignedToId, checkedOutAt, expectedCheckinAt, checkoutNote and deployedAt. status defaults to AVAILABLE (allowed: AVAILABLE, MAINTENANCE, RETIRED); LOST/DISPOSED do not run via checkin. For target RETIRED the retirement rules apply (blocked while active contract/license links exist). damageReport is stored on the handover record (overview card).

Install (LOCATION → IN_USE)

POST /api/assets/:id/install
{
  "locationId": "clx-serverroom-b12",
  "note": "Rack 4, Slot 12",
  "coLocateAssetIds": []
}

For trackingMode=LOCATION only. Sets status=IN_USE + locationId (no user), deployedAt=now. No handover, no email — a pure IT action with an activity log.

Deinstall (LOCATION → AVAILABLE | MAINTENANCE)

POST /api/assets/:id/deinstall
{
  "status": "AVAILABLE",
  "keepLocation": false,
  "note": "Decommissioned"
}

Clears deployedAt and (by default) locationId; keepLocation=true keeps the location (device on site, out of service). status AVAILABLE (default) or MAINTENANCE.

Formal handover: For handovers with recipient confirmation, PDF/QR and email use the handover workflow (PERSON only, see above). Field assignment & anchor rules: Asset Lifecycle.

Financial Tracking & Depreciation

Fields

Field Type Description
purchaseDateDateTimePurchase date
purchasePriceDecimalPurchase price
purchaseOrderStringPurchase order number
vendorStringVendor
warrantyEndDateTimeWarranty end
maintenanceEndDateTimeMaintenance end
bookValueDecimal (computed)Current book value — read-only, computed on every read from purchasePrice − depreciation
depreciationMethodStringLINEAR, DEGRESSIVE, NONE
usefulLifeMonthsIntUseful life in months
depreciationStartDateDateTimeDepreciation start

Currency & gross/net: Assets store only a single purchasePrice (Decimal) — there is no per-asset currency or gross/net field. The system currency (general settings: systemCurrency, default EUR, no conversion) and the price mode (priceTaxMode: net | gross, default net — labeling only, no tax calculation) are configured globally in the general settings and applied to all amounts. See Settings & Global Search API.

Example: Depreciation

{
  "purchaseDate": "2026-01-15",
  "purchasePrice": 2499.00,
  "depreciationMethod": "LINEAR",
  "usefulLifeMonths": 36,
  "depreciationStartDate": "2026-01-15"
}

Calculation: With linear depreciation over 36 months: Monthly depreciation = €2,499 / 36 = €69.42. After 12 months: bookValue = €1,665.96.

Custom Fields

Fields defined per asset type in the customFieldSchema are stored in the customFields object:

Example: 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"
  }
}

Example: 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"
  }
}

Filtering: The asset list can be filtered by any custom field: f.customField.<key>=<operator>:<value> with the operators eq, neq, contains, isNull and isNotNull (e.g. f.customField.cpu=contains:i7). The full-text search q covers asset tag, name, serial number and description, not the custom fields.

Sensitive Custom Fields

A field can be marked as sensitive in the customFieldSchema (e.g. credentials, keys). Sensitive values are overwritten with a constant mask in ALL read paths (detail, list, create prefill) and in activity/audit entries — the cleartext never leaves the normal response.

MethodEndpointDescription
GET/api/assets/:id/custom-fields/:field/revealCleartext of ONE sensitive field; permission: view access to the asset (viewAll/viewOwn or the asset type's canView); every reveal is logged (like /licenses/:id/key)

Saving sensitive fields: For a sensitive field: omit the key = keep the stored value, null = clear it, sending the mask value back = 400. This prevents a masked display from ever being saved as the real value.

List Assets

Request

GET /api/assets?f.status=IN_USE&f.typeId=clx-laptop&per=50

Query Parameters

Parameter Description
qFull-text search (asset tag, name, serial number, description)
f.statusFilter by status, e.g. f.status=in:AVAILABLE,IN_USE
f.typeId / f.categoryId / f.locationIdFilter by asset type, category or location
f.assignedToIdFilter by assignee
f.criticalityFilter by criticality (LOW, MEDIUM, HIGH, CRITICAL)
f.manufacturer / f.modelFilter by manufacturer or model
f.type.trackingModeFilter by tracking mode (PERSON, LOCATION, CONSUMABLE)
f.customField.<key>Filter by a custom field (see Custom Fields)
page / perPage and page size (per defaults to 25, max 100)
sortSorting, e.g. sort=createdAt:desc
mine / myTeam / myDepartment=1: only assets of the caller, their team or their department
lowStock=1: only consumables at or below their minimum quantity
notLinkedToLicenseIdLicense ID: only assets not assigned to this license — the candidate list for a license assignment
deleted=1: trash (permission assets.viewDeleted)

Lifecycle Management

Assets track their complete lifecycle:

Field Description
createdAtCreated in system
deployedAtFirst deployment
retiredAtRetired from service
disposedAtDisposed/sold
deletedAtSoft-delete (trash)
🔍
Model Clustering
Detects "DELL"/"Dell"/"dell" as duplicates.
📄
QR/PDF-Handover
Public QR link with a signed token, PDF record, policy acknowledgement.
🔗
CMDB Relations
8 relation types, visible from both sides.

Code Example: Complete 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 Example: 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 use the central attachment system for purchase orders, warranty certificates, invoices, etc.:

# 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: See Attachments & File Settings API for virus scanning, file settings and retention periods.
Next Step

Workflows API → Learn more about the Workflows API
Entity Linking API → Link assets with tickets/problems/incidents/changes/contracts