> For the complete documentation index, see [llms.txt](https://docs.blockbrain.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.blockbrain.ai/for-admins/cost-center-admin-api.md).

# Cost Center Admin API

### **Scope**

> Admin API for managing a tenant's valid project codes and the cost-center validation toggle: how to authenticate, the single-code and batch CRUD endpoints, bulk CSV/JSON import with a dry-run preview, value validation, and the config toggle.
>
> **Per-endpoint reference is intentionally condensed here.** The live, always-current reference lives at [analytics.theblockbrain.ai/docs](https://analytics.theblockbrain.ai/docs#tag/admin--cost-center) (Scalar, auto-rendered from the OpenAPI spec). This page covers the narrative, auth model, and request/response examples.

***

### Overview

The Cost Center Admin API manages a tenant's list of valid **project codes** and the **cost-center validation toggle**. Codes arrive through the single-code REST endpoint, the JSON batch endpoints, or a bulk CSV/JSON import, and any candidate value can be checked against the tenant's active codes.

**Base URL:** `https://analytics.theblockbrain.ai`

**Base path:** `/api/v1/admin/cb-limit/cost-center`

| Property      | Value                                                                                                 |
| ------------- | ----------------------------------------------------------------------------------------------------- |
| **Base path** | `/api/v1/admin/cb-limit/cost-center`                                                                  |
| **Host**      | `https://analytics.theblockbrain.ai`                                                                  |
| **Auth**      | Bearer **JWT** or **API key** (`sk-`), role **admin**. The acted-on tenant is derived from the token. |

> **When is validation enforced?** Only when the tenant's validation toggle is on (see `PUT /config`) and at least one active code is configured. Otherwise any value is accepted. Each JSON batch endpoint accepts up to **10,000** codes per request. Use `/import` for larger loads.

***

### API summary

Every endpoint the Cost Center Admin API exposes. Paths are relative to the base path `/api/v1/admin/cb-limit/cost-center`. Full request and response detail is in [Scalar](https://analytics.theblockbrain.ai/docs#tag/admin--cost-center).

| Group      | Method   | Endpoint                  | Description                                                        |
| ---------- | -------- | ------------------------- | ------------------------------------------------------------------ |
| Codes      | `GET`    | `/codes`                  | List the tenant's project codes (paginated, searchable, sortable). |
| Codes      | `POST`   | `/codes`                  | Add or update a single project code (idempotent upsert).           |
| Codes      | `DELETE` | `/codes`                  | Delete a project code by its value.                                |
| Codes      | `POST`   | `/codes/batch`            | Add or update many codes in one request.                           |
| Codes      | `POST`   | `/codes/batch-set-active` | Activate or deactivate many codes (pause or resume).               |
| Codes      | `POST`   | `/codes/batch-delete`     | Delete many codes in one request.                                  |
| Validation | `POST`   | `/validate`               | Check a value against the tenant's active codes.                   |
| Import     | `POST`   | `/import`                 | Bulk import codes from a CSV or JSON file.                         |
| Import     | `POST`   | `/import/preview`         | Dry-run an import and classify every row.                          |
| Config     | `GET`    | `/config`                 | Read the validation toggle.                                        |
| Config     | `PUT`    | `/config`                 | Set the validation toggle on or off.                               |

***

### Authentication

Every endpoint expects a Bearer token in the `Authorization` header:

```
Authorization: Bearer <token>
```

Two kinds of credential are accepted, and both go through the same header:

| Credential  | Looks like                         | How it is verified                                                                                                                            |
| ----------- | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| Zitadel JWT | a JWT (`header.payload.signature`) | Verified against Zitadel (JWKS, with token introspection as a fallback). This is the existing access-token path.                              |
| API key     | `sk-` prefixed key                 | Introspected through Blocky. The key authorizes by its own role, so it must carry the **admin** role, the same bar a user token has to clear. |

> Every example below uses `$TOKEN`, which can hold either credential. A missing or malformed token returns `401`, and a token whose role is below **admin** returns `403`.

***

### Shared schemas

#### ProjectCode

```json
{
  "id": "c7a1…",
  "code": "PRJ-001",
  "label": "Project One",
  "active": true,
  "source": "rest-api",
  "createdAt": "2026-07-28T09:00:00.000Z",
  "updatedAt": "2026-07-28T09:00:00.000Z"
}
```

`label` is nullable. `source` is one of `rest-api`, `csv-import`, `json-import`, `admin-manual`. Codes match case-insensitively (trimmed, lower-cased), and the display casing is whatever was last written.

#### Problem (422, application/problem+json)

```json
{
  "title": "Validation failed",
  "status": 422,
  "errors": [
    { "message": "project code does not exist" }
  ]
}
```

Returned only by `POST /validate` when the value does not match an active code. All other errors return a plain `{ "error": "…" }` JSON body.

***

### Codes

#### GET /codes

List the tenant's project codes. Paginated list with a total count. The admin view shows paused codes too. Default sort is newest first.

**Query parameters**

| Name         | Type                              | Required | Default     | Description                                                          |
| ------------ | --------------------------------- | -------- | ----------- | -------------------------------------------------------------------- |
| `search`     | string                            | No       |             | Case-insensitive substring match on code or label. Minimum length 1. |
| `activeOnly` | `"true"` \| `"false"`             | No       | `false`     | Pass `true` to hide paused codes (what the dropdown does).           |
| `from`       | date (ISO)                        | No       |             | Inclusive lower bound on `createdAt`.                                |
| `to`         | date (ISO)                        | No       |             | Exclusive upper bound on `createdAt`.                                |
| `page`       | integer ≥ 1                       | No       | `1`         | 1-based page number.                                                 |
| `pageSize`   | integer 1..100                    | No       | `25`        | Rows per page. Values above 100 return 400.                          |
| `sortBy`     | `code` \| `createdAt` \| `active` | No       | `createdAt` | Sort column.                                                         |
| `sortOrder`  | `asc` \| `desc`                   | No       | `desc`      | Sort direction.                                                      |

**Responses**

| Status | Body                                    | When                                  |
| ------ | --------------------------------------- | ------------------------------------- |
| `200`  | `{ items, totalCount, page, pageSize }` | Paginated project codes.              |
| `400`  | `{ error }`                             | Invalid query parameters.             |
| `401`  |                                         | Missing or invalid token.             |
| `403`  |                                         | Missing the cost-controls permission. |

**Request**

```bash
curl -H "Authorization: Bearer $TOKEN" \
  "https://analytics.theblockbrain.ai/api/v1/admin/cb-limit/cost-center/codes?search=PRJ&pageSize=25&sortBy=createdAt&sortOrder=desc"
```

**Response (200)**

```json
{
  "items": [ /* ProjectCode[] */ ],
  "totalCount": 1,
  "page": 1,
  "pageSize": 25
}
```

#### POST /codes

Add or update a single project code. Idempotent upsert on the case-insensitive code. This is the endpoint a client's automation calls to keep codes in sync: create on project start, pause with `active: false`. Re-adding a code refreshes its casing, label, and active flag but keeps its id and audit columns.

> Omitting `active` resets it to `true`, so a re-add reactivates a paused code.

**Body · application/json**

| Field    | Type          | Required | Description                                                |
| -------- | ------------- | -------- | ---------------------------------------------------------- |
| `code`   | string 1..256 | Yes      | The project code. Trimmed before storing.                  |
| `label`  | string ≤ 512  | No       | Human label. Defaults to null.                             |
| `active` | boolean       | No       | Defaults to `true`. Set `false` to pause without deleting. |

**Responses**

| Status      | Body          | When                              |
| ----------- | ------------- | --------------------------------- |
| `200`       | `ProjectCode` | The upserted code.                |
| `400`       | `{ error }`   | Code missing, blank, or too long. |
| `401 / 403` |               | Unauthorized or forbidden.        |

**Request**

```bash
curl -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"code":"PRJ-001","label":"Project One"}' \
  https://analytics.theblockbrain.ai/api/v1/admin/cb-limit/cost-center/codes
```

**Response (200)**

```json
{
  "id": "c7a1…",
  "code": "PRJ-001",
  "label": "Project One",
  "active": true,
  "source": "rest-api",
  "createdAt": "2026-07-28T09:00:00.000Z",
  "updatedAt": "2026-07-28T09:00:00.000Z"
}
```

#### DELETE /codes

Delete a project code. Hard-deletes the code matching `code` (case-insensitive) for the calling tenant. The client's automation calls this when a project closes.

**Query parameters**

| Name   | Type   | Required | Description                                 |
| ------ | ------ | -------- | ------------------------------------------- |
| `code` | string | Yes      | The code value to delete. Minimum length 1. |

**Responses**

| Status      | Body                  | When                                   |
| ----------- | --------------------- | -------------------------------------- |
| `200`       | `{ "deleted": true }` | A row was removed.                     |
| `400`       | `{ error }`           | The `code` query parameter is missing. |
| `404`       | `{ error }`           | No code matched.                       |
| `401 / 403` |                       | Unauthorized or forbidden.             |

**Request**

```bash
curl -X DELETE -H "Authorization: Bearer $TOKEN" \
  "https://analytics.theblockbrain.ai/api/v1/admin/cb-limit/cost-center/codes?code=PRJ-001"
```

#### POST /codes/batch

Add or update project codes (batch). The multi-code sibling of `POST /codes`. Upserts many codes in one JSON request (idempotent, case-insensitive). Each entry carries its own `label` and `active` (defaults true). Duplicate codes within the request are collapsed case-insensitively, **first wins** (same as the file import). Returns the count of unique codes written. Use `/import` for very large file loads.

**Body · application/json**

| Field            | Type           | Required | Description                      |
| ---------------- | -------------- | -------- | -------------------------------- |
| `codes`          | array 1..10000 | Yes      | Non-empty array of code entries. |
| `codes[].code`   | string 1..256  | Yes      | The project code.                |
| `codes[].label`  | string ≤ 512   | No       | Human label.                     |
| `codes[].active` | boolean        | No       | Defaults to `true`.              |

**Responses**

| Status      | Body                     | When                                                         |
| ----------- | ------------------------ | ------------------------------------------------------------ |
| `200`       | `{ "upserted": number }` | Count of unique codes written.                               |
| `400`       | `{ error }`              | Empty array, over 10,000 entries, or a blank/oversized code. |
| `401 / 403` |                          | Unauthorized or forbidden.                                   |

**Request**

```bash
curl -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"codes":[{"code":"PRJ-001","label":"One"},{"code":"PRJ-002","active":false}]}' \
  https://analytics.theblockbrain.ai/api/v1/admin/cb-limit/cost-center/codes/batch
```

**Response (200)**

```json
{ "upserted": 2 }
```

#### POST /codes/batch-set-active

Activate or deactivate project codes (batch). Flips only the `active` flag of every listed code (case-insensitive). Unlike `POST /codes`, it never rewrites the label or source, so a multi-select pause or resume keeps each code's display metadata intact. Deactivating pauses codes: they stop validating and leave the dropdown without being deleted. Returns the number of existing codes updated (codes that do not exist are ignored).

**Body · application/json**

| Field    | Type               | Required | Description                                        |
| -------- | ------------------ | -------- | -------------------------------------------------- |
| `codes`  | string\[] 1..10000 | Yes      | Non-empty array of code values, each 1..256 chars. |
| `active` | boolean            | Yes      | `true` to activate, `false` to pause.              |

**Responses**

| Status      | Body                    | When                                                                |
| ----------- | ----------------------- | ------------------------------------------------------------------- |
| `200`       | `{ "updated": number }` | Count of existing codes updated.                                    |
| `400`       | `{ error }`             | Empty array, missing or non-boolean `active`, or an oversized code. |
| `401 / 403` |                         | Unauthorized or forbidden.                                          |

**Request**

```bash
curl -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"codes":["PRJ-001","PRJ-002"],"active":false}' \
  https://analytics.theblockbrain.ai/api/v1/admin/cb-limit/cost-center/codes/batch-set-active
```

**Response (200)**

```json
{ "updated": 2 }
```

#### POST /codes/batch-delete

Delete project codes (batch). Hard-deletes every listed code (case-insensitive) in one request. Backs the admin UI's multi-select delete. Returns the number actually removed, which may be fewer than requested when some codes did not exist.

**Body · application/json**

| Field   | Type               | Required | Description                                        |
| ------- | ------------------ | -------- | -------------------------------------------------- |
| `codes` | string\[] 1..10000 | Yes      | Non-empty array of code values, each 1..256 chars. |

**Responses**

| Status      | Body                    | When                                                    |
| ----------- | ----------------------- | ------------------------------------------------------- |
| `200`       | `{ "deleted": number }` | Count of codes actually removed.                        |
| `400`       | `{ error }`             | Empty array, over 10,000 entries, or an oversized code. |
| `401 / 403` |                         | Unauthorized or forbidden.                              |

**Request**

```bash
curl -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"codes":["PRJ-001","PRJ-002"]}' \
  https://analytics.theblockbrain.ai/api/v1/admin/cb-limit/cost-center/codes/batch-delete
```

**Response (200)**

```json
{ "deleted": 2 }
```

***

### Validation

#### POST /validate

Validate a project code. Returns `{ "valid": true }` when the value matches one of the tenant's active codes. Matching is case-insensitive and trims whitespace. A paused code counts as invalid.

**Body · application/json**

| Field   | Type   | Required | Description                                                      |
| ------- | ------ | -------- | ---------------------------------------------------------------- |
| `value` | string | Yes      | The value to check. Minimum length 1 after trimming.             |
| `field` | string | No       | Present in the contract. Only `project-code` is validated today. |

**Responses**

| Status      | Body                     | When                                   |
| ----------- | ------------------------ | -------------------------------------- |
| `200`       | `{ "valid": true }`      | The value matches an active code.      |
| `400`       | `{ error }`              | `value` is missing or blank.           |
| `422`       | `Problem` (problem+json) | The value is not a valid project code. |
| `401 / 403` |                          | Unauthorized or forbidden.             |

**Request**

```bash
curl -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"field":"project-code","value":"PRJ-001"}' \
  https://analytics.theblockbrain.ai/api/v1/admin/cb-limit/cost-center/validate
```

**Response (422)**

```json
{
  "title": "Validation failed",
  "status": 422,
  "errors": [{ "message": "project code does not exist" }]
}
```

***

### Import

#### POST /import

Import project codes from a CSV or JSON file. Multipart upload. Codes are trimmed, blank rows skipped, and in-file duplicates collapsed case-insensitively (first wins). Each code is upserted so re-importing is idempotent. Imported codes are always set active. A row whose code exceeds 256 characters or whose label exceeds 512 is reported as an error row and not written (mirrors the API field caps). Max 5 MB and 200,000 rows.

**Body · multipart/form-data**

| Field    | Type                | Required | Description                                                      |
| -------- | ------------------- | -------- | ---------------------------------------------------------------- |
| `file`   | binary              | Yes      | CSV (header row with a `code` column, optional `label`) or JSON. |
| `format` | `"csv"` \| `"json"` | Yes      | How to parse the file.                                           |

**Accepted JSON shapes**

```json
[ { "code": "PRJ-001", "label": "Project One" } ]
```

```json
{ "codes": [ { "code": "PRJ-001" } ] }
```

**Responses**

| Status      | Body                                       | When                                                                                    |
| ----------- | ------------------------------------------ | --------------------------------------------------------------------------------------- |
| `200`       | `{ imported, duplicatesInFile, errors[] }` | Import summary. Each error carries `{ row, code, reason }`.                             |
| `400`       | `{ error }`                                | File missing, bad format, or unparseable (no `code` column, invalid JSON, wrong shape). |
| `413`       | `{ error }`                                | File exceeds the size or row limit.                                                     |
| `401 / 403` |                                            | Unauthorized or forbidden.                                                              |

**Request**

```bash
curl -X POST -H "Authorization: Bearer $TOKEN" \
  -F "format=csv" \
  -F "file=@codes.csv;type=text/csv" \
  https://analytics.theblockbrain.ai/api/v1/admin/cb-limit/cost-center/import
```

**Response (200)**

```json
{
  "imported": 2,
  "duplicatesInFile": 1,
  "errors": [
    { "row": 3, "code": "", "reason": "Code is required." }
  ]
}
```

#### POST /import/preview

Preview a CSV/JSON import without writing anything. Parses the file and classifies every row in file order (`new`, `update`, `duplicate`, `error`) so the admin can review before importing. A row is an `error` when its code is empty or its code/label exceeds the field caps (256 / 512). `update` versus `new` is resolved against the tenant's existing codes. Writes nothing. The `entries` array is capped at 2000 rows, but the `summary` counts every row.

**Body · multipart/form-data**

| Field    | Type                | Required | Description                    |
| -------- | ------------------- | -------- | ------------------------------ |
| `file`   | binary              | Yes      | Same file shapes as `/import`. |
| `format` | `"csv"` \| `"json"` | Yes      | How to parse the file.         |

**Responses**

| Status      | Body                                           | When                                                 |
| ----------- | ---------------------------------------------- | ---------------------------------------------------- |
| `200`       | `{ entries[], totalRows, truncated, summary }` | Per-row classification plus a summary over all rows. |
| `400`       | `{ error }`                                    | File missing, bad format, or unparseable.            |
| `413`       | `{ error }`                                    | File exceeds the size or row limit.                  |
| `401 / 403` |                                                | Unauthorized or forbidden.                           |

**Response (200)**

```json
{
  "entries": [
    { "row": 1, "code": "PRJ-001", "label": "Project One", "status": "new" },
    { "row": 2, "code": "prj-001", "label": null, "status": "duplicate", "reason": "Same code appears earlier in the file (kept once)." }
  ],
  "totalRows": 2,
  "truncated": false,
  "summary": { "new": 1, "update": 0, "duplicate": 1, "error": 0 }
}
```

***

### Config

#### GET /config

Get the tenant's cost-center validation toggle. Returns whether validation is switched on for the calling tenant. A tenant with no settings row reports `false`.

**Responses**

| Status      | Body                               | When                       |
| ----------- | ---------------------------------- | -------------------------- |
| `200`       | `{ "validationEnabled": boolean }` | Toggle state.              |
| `401 / 403` |                                    | Unauthorized or forbidden. |

**Request**

```bash
curl -H "Authorization: Bearer $TOKEN" \
  https://analytics.theblockbrain.ai/api/v1/admin/cb-limit/cost-center/config
```

**Response (200)**

```json
{ "validationEnabled": true }
```

#### PUT /config

Set the tenant's cost-center validation toggle. Switches validation on or off. Turning it on for the first time seeds a settings row from the system default. Turning it off, or back on, only touches this toggle and the audit columns and preserves all other config. Validation enforces only once the toggle is on and at least one active code is configured.

**Body · application/json**

| Field               | Type    | Required | Description           |
| ------------------- | ------- | -------- | --------------------- |
| `validationEnabled` | boolean | Yes      | The new toggle state. |

**Responses**

| Status      | Body                               | When                                             |
| ----------- | ---------------------------------- | ------------------------------------------------ |
| `200`       | `{ "validationEnabled": boolean }` | Updated toggle state.                            |
| `400`       | `{ error }`                        | `validationEnabled` is missing or not a boolean. |
| `401 / 403` |                                    | Unauthorized or forbidden.                       |

**Request**

```bash
curl -X PUT -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"validationEnabled":true}' \
  https://analytics.theblockbrain.ai/api/v1/admin/cb-limit/cost-center/config
```

***


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.blockbrain.ai/for-admins/cost-center-admin-api.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
