Cost Centers API
The Cost Centers API manages cost centers as central master data — with code/name, lifecycle (DRAFT/ACTIVE/CLOSED), validity period, budget, hierarchy (cost-center groups/rollups), owner and ERP sync. Cost centers are assigned to assets, contracts and licenses via costCenterId and form the basis of the cost reports.
Authentication & Permissions
Each action has its own permission under costCenters.*, which applies equally to all cost centers. There are no restrictions to individual cost centers or their owners. See User Management & RBAC.
| Action | Permission |
|---|---|
| View / list / search | costCenters.view |
| See and set budget amounts | costCenters.viewBudget |
| Create | costCenters.create |
| Update | costCenters.update |
| Delete (soft delete) | costCenters.delete |
| Merge | costCenters.merge (default: admin) |
| Import / ERP sync | costCenters.import |
Users and API keys: All reads (list, search, detail, activity trail) are open to logged-in users and API keys holding costCenters.view alike — so the ERP key that may import can also read the list for reconciliation. Create, update, delete and merge require a logged-in user; an X-API-Key is rejected there with 403 ("This operation requires a logged-in user account, not an API key"). POST /api/cost-centers/import accepts both. This keeps manual maintenance attributable to a person (audit), while reads and ERP sync run via key.
Budget amounts are protected separately: Without costCenters.viewBudget every response contains budget: null — list, detail and also the create/update response. The field is always present, only the value is withheld. Rejected with 403 COST_CENTER_BUDGET_FORBIDDEN: sorting by budget (?sort=budget:…, the order would reveal the amounts) and budget in the create/update body. In the import the affected row is reported as an error and the import continues. Whoever may not see the budget may not set it either. In the cost report, reports.viewBudget controls visibility of the same amounts.
Endpoints Overview
| Method | Endpoint | Description | Permission |
|---|---|---|---|
GET | /api/cost-centers | Paginated, filter-spec-driven list | costCenters.view |
GET | /api/cost-centers/search?q=&per= | Search for select fields: only assignable (ACTIVE and valid) cost centers. Only q and per are accepted; other parameters (e.g. limit) → 400 | costCenters.view |
GET | /api/cost-centers/:id | Single cost center | costCenters.view |
GET | /api/cost-centers/:id/activities | Activity trail of the cost center | costCenters.view |
POST | /api/cost-centers | Create cost center (source=MANUAL) | costCenters.create |
PATCH | /api/cost-centers/:id | Update cost center | costCenters.update |
DELETE | /api/cost-centers/:id | Soft delete (status → CLOSED) → 204; with active child cost centers: 400 COST_CENTER_HAS_CHILDREN (+ details.children) | costCenters.delete |
POST | /api/cost-centers/:id/merge | Reassign references (assets/contracts/licenses) AND children to targetId, close the source — all in one step. The target must be assignable and must not be a descendant of the source. With two concurrent, opposing merges one loses with 409 COST_CENTER_MERGE_CONFLICT | costCenters.merge |
POST | /api/cost-centers/import | Idempotent upsert (ERP sync, source=SYNCED) — user OR API key | costCenters.import |
Fields
| Field | Type | Description |
|---|---|---|
code | String (1–50) | Cost-center number/code — unique, required |
name | String (1–200) | Display name — required |
description | String? (≤2000) | Description |
color | String (#rrggbb) | Badge color (default #3b82f6) |
sortOrder | Int | Sort order (settable via import only, not a create/update input) |
status | enum | DRAFT, ACTIVE, CLOSED |
validFrom / validUntil | DateTime? | Validity period (controls assignability); validFrom ≤ validUntil enforced (400 COST_CENTER_VALIDITY_INVALID) |
ownerId | String? | Responsible user — must exist and not be archived (400 COST_CENTER_OWNER_NOT_FOUND) |
budget | Decimal? (14,2) | Budget (in system currency — see below) |
parentId | String? | Parent cost center (hierarchy/rollups). Deleting does NOT tidy up the hierarchy — only the merge reattaches children, so restructuring stays a deliberate decision |
externalId | String? (≤100) | Stable ERP key (SAP/DATEV) — unique |
source | enum | MANUAL, SYNCED (read-only, set by the system) |
Currency: Cost centers have no own currency field — budget is interpreted in the global system currency (general settings: systemCurrency). No conversion takes place. See Settings & Global Search API.
Create Cost Center
POST /api/cost-centers
{
"code": "CC-1000",
"name": "IT Operations",
"description": "Betrieb & Infrastruktur",
"color": "#3b82f6",
"status": "ACTIVE",
"validFrom": "2026-01-01T00:00:00Z",
"ownerId": "clx-user-id",
"budget": 250000.00,
"parentId": "clx-parent-cost-center-id"
}
Response (201 Created)
{
"id": "clx...",
"code": "CC-1000",
"name": "IT Operations",
"status": "ACTIVE",
"color": "#3b82f6",
"source": "MANUAL",
"createdAt": "2026-06-18T08:00:00.000Z"
}
Via this route source is always MANUAL. SYNCED records are created exclusively through the import/ERP-sync endpoint.
ERP Import (Sync)
Idempotent upsert keyed by externalId (if present), else code. Imported records are flagged source=SYNCED. A previously soft-deleted code is restored on re-import. Up to 1000 items per request. This endpoint accepts both user and API-key actors.
POST /api/cost-centers/import
X-API-Key: <your-api-key>
{
"items": [
{
"code": "CC-1000",
"name": "IT Operations",
"externalId": "SAP-1000",
"status": "ACTIVE",
"budget": 250000.00
},
{
"code": "CC-2000",
"name": "Facility Management",
"externalId": "SAP-2000",
"status": "ACTIVE"
}
]
}
Note: parentId (cuid) is optional; linking the hierarchy by parent code is intentionally NOT supported here (use update/UI for that).
Lifecycle & Assignability
| Status | Description |
|---|---|
DRAFT | Draft — not yet assignable |
ACTIVE | Active — assignable within the validity period |
CLOSED | Closed (soft delete) — no longer assignable |
Cost centers are assigned to assets, contracts and licenses via the costCenterId field. Assignability is validated server-side: only ACTIVE cost centers within their validity period are assignable — a CLOSED or expired cost center returns 400 Bad Request. The assignment is a plain field on the respective record, not a link via the linking API.