# Cost centres

A cost centre is a code a company charges an order line to: a department, a project, a site or a budget. Each company keeps its own list. Cost centres can be turned on by the store setting, or for one company by `costCentreMode` in its [`b2bSettings`](/guides/b2b-companies#company-overrides-b2bsettings). With `optional`, the buyer may pick one for each line at checkout; with `required`, they must pick one for every line. The line keeps the code and name on the order and in the order CSV export, and the company's statement lists each order's codes. Emails, the invoice PDF, the Stripe invoice and webhook payloads do not carry them.

## The cost centre object

| Field | Type | Meaning |
|---|---|---|
| `id` | uuid | Orbit's id. |
| `companyId` | uuid | The company it belongs to. |
| `code` | string, 1 to 32 characters on one line, without a semicolon | The code buyers pick. Unique within the company, ignoring case. It cannot be `all`, `none` or `default`, in any casing: the CSV files give those words a meaning. |
| `name` | string, 1 to 120 characters on one line | What the code stands for, shown beside it. |
| `status` | `active` or `archived` | Only active cost centres are offered at checkout. |
| `externalRef` | string, 1 to 64 characters on one line, without a semicolon, or `null` | Your own reference for it, unique within the company, ignoring case. It cannot be `all`, `none` or `default`, in any casing: the CSV files give those words a meaning. |
| `userIds` | array of uuid | The company users given this cost centre: the same assignments as each user's `allowedCostCentreIds`, seen from the cost centre. |
| `createdAt`, `updatedAt` | date-time | |

`null` clears `externalRef`. `code`, `name`, `status` and `userIds` never take `null`: a write that sends it answers `400` naming the field (send `[]` to take a cost centre away from every user).

Cost centres are never deleted. Archiving one takes it out of checkout and leaves every order that used it intact, because each order line stores the code and name it was placed with.

## Routes

| Method and path | Scope | Purpose |
|---|---|---|
| `GET /v1/companies/{companyId}/cost-centres` | `company:read` | The company's cost centres: active, or archived or all with `?status=`. |
| `GET /v1/companies/{companyId}/cost-centres/{costCentreId}` | `company:read` | One cost centre. |
| `POST /v1/companies/{companyId}/cost-centres` | `company:update` | Add one: `code`, `name`, optional `externalRef`, `status` and `userIds`. |
| `PATCH /v1/companies/{companyId}/cost-centres/{costCentreId}` | `company:update` | Change its `code`, `name`, `status` or `externalRef`; `userIds` replaces the assignments. |
| `DELETE /v1/companies/{companyId}/cost-centres/{costCentreId}` | `company:update` | Archive it. |
| `PUT /v1/companies/{companyId}/cost-centres` | `company:update` | Replace the company's whole list in one call, matched by code. |
| `PUT /v1/companies/{companyId}/cost-centres/by-external-ref/{externalRef}` | `company:update` | Create or update one by your own reference. |

`GET` answers `{ items, meta, managedExternally }`: active cost centres unless `status` asks for `archived` or `all`, up to 1000 per company, and `managedExternally` is `true` when the list is kept by the company's accounts system. `GET .../cost-centres/{costCentreId}` answers the cost centre whatever its status, and `POST`, `PATCH` and `DELETE` answer the cost centre itself.

Like every `/v1/companies` write, these need the store's B2B feature (`403` `FEATURE_FROZEN` without it). A value outside the limits above is refused with a plain validation `400`, and these refusals carry an `errorCode`:

| Status and `errorCode` | When |
|---|---|
| `409` `B2B_COST_CENTRE_CODE_TAKEN` | Another cost centre of the company has the code, ignoring case. |
| `409` `B2B_COST_CENTRE_EXTERNAL_REF_TAKEN` | Another cost centre of the company has the reference, ignoring case. |
| `400` `B2B_COST_CENTRE_LIMIT_REACHED` | The company already holds 1000 cost centres, archived ones included: cost centres are never deleted, so an archived one keeps its place in the 1000. |
| `400` `B2B_COST_CENTRE_DUPLICATE_CODE` | The list sent to `PUT .../cost-centres` names a code twice, ignoring case. |
| `400` `B2B_COST_CENTRE_DUPLICATE_EXTERNAL_REF` | The list sent to `PUT .../cost-centres` names a reference twice, ignoring case. |
| `400` `B2B_CONTACT_NOT_IN_COMPANY` | An id in `userIds` is not a user of the company. |

## Replacing the list by code

Usually an accounts system owns the list: send it whole, as often as you like. The same list again writes nothing and fires no webhook. If you send an `Idempotency-Key`, mint a new one for each run and reuse it only to retry that run's request: see [Retrying writes safely](/guides/b2b-erp-integration#retrying-writes-safely).

```http
PUT /v1/companies/6f2a1c3e-9d4b-4a7f-8e2c-1b3f5a7c9d0e/cost-centres HTTP/1.1
Host: api.myorbitcommerce.net
Authorization: Bearer oc_sk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Idempotency-Key: costcentres-CUST-004821-run-0042
Content-Type: application/json

{
  "items": [
    { "code": "MAINT", "name": "Maintenance", "externalRef": "DEP-0101" },
    { "code": "FLEET", "name": "Fleet and vehicles", "externalRef": "DEP-0102" }
  ]
}
```

- A code already in the company's list is updated with what you send: its `name`, its `status` (`active` when you leave it out, so an archived code back in the list becomes active again), its `externalRef` (kept when you leave it out, cleared by `null`) and, when you send them, its `userIds`. A new code is created.
- An active cost centre whose code is missing from the list is archived.
- Codes match ignoring case, and a matched code keeps its stored casing.
- Sending the same list again changes nothing and fires no webhook.

The answer is every cost centre of the company after the change, active and archived, with what changed: `{ "items": [ ... ], "created": 1, "updated": 1, "archived": 1, "unchanged": 0 }`. `created`, `updated` and `unchanged` count the items you sent, so they add up to the length of your list; `archived` counts the active cost centres your list left out.

## Upserting one cost centre by your reference

`PUT /v1/companies/{companyId}/cost-centres/by-external-ref/{externalRef}` creates the cost centre with that reference, or updates it, in one call. The body takes `code` and `name`, both needed even when the cost centre exists, and optionally `status` and `userIds`. Leaving `status` out keeps the current one, so an archived cost centre stays archived (a new one is `active`); leaving `userIds` out keeps the assignments, and `[]` clears them. An `externalRef` in the body is accepted and ignored without being checked: the reference in the path always wins. The rest of the body is checked as usual.

- The reference matches ignoring case and surrounding spaces, and the cost centre takes your spelling of it from the path.
- A cost centre with that reference is updated, and a different `code` renames it, unless another cost centre has that code.
- Otherwise a cost centre with the same code and no reference yet takes the reference.
- A code that belongs to another cost centre answers `409` with `B2B_COST_CENTRE_CODE_TAKEN`.
- Otherwise a new cost centre is created.

The answer is `{ "costCentre": { ... }, "created": true }`, with `created: false` when it updated an existing one. A reference in the path that is not 1 to 64 characters on one line without a semicolon, or that is `all`, `none` or `default` in any casing, answers `400` with `B2B_EXTERNAL_REF_INVALID`.

## Who may charge which cost centre

- A company user's `allowedCostCentreIds` lists the cost centres they may pick. An empty list means every active cost centre of the company. Set it with `PATCH /v1/companies/{companyId}/users/{contactId}`; see [Company users](/guides/b2b-companies#company-users).
- A cost centre's own `userIds` lists the company users allowed to charge it, set with `POST` or `PATCH` on the cost centre. A user given any cost centre can charge only the active ones they are given: when all of those are archived, they can charge none.
- `costCentreMode` decides whether the field is `off`, `optional` or `required` at checkout.
- With `costCentresManagedExternally` set `true` in the company's `b2bSettings`, the list itself (codes, names, status, references, and users assigned from a cost centre) is read-only in the dashboard, in the storefront's company account area and in the cost centres file import, so only your API calls change it. Who may charge which stays editable everywhere: a user's `allowedCostCentreIds` can still be changed in the dashboard, in the account area, through the company file's `User Cost Centres` column and through the API. Set the flag once for every company your system owns.

## On orders

Each order line carries `costCentreId`, plus `costCentreCode` and `costCentreName` as they were when the order was placed. To find the orders charged to one cost centre, use `GET /v1/orders?costCentreId=&includeArchived=true`; without `includeArchived=true` the list leaves archived orders out.

## Webhooks

| Topic | Fires on | `data` |
|---|---|---|
| `cost_centre.created` | A cost centre is added, by the dashboard, the storefront company account, an import or the API. | `costCentreId`, `companyId`, `storeId` |
| `cost_centre.updated` | Its code, name, reference, status or assigned users change, including a return from archived to active. A change made through a user's `allowedCostCentreIds` fires `company_user.updated` instead. | `costCentreId`, `companyId`, `storeId` |
| `cost_centre.archived` | It is archived. | `costCentreId`, `companyId`, `storeId` |

All three need `company:read` at subscribe time.

## Next steps

- The user fields that decide who may charge which cost centre: [Company users](/guides/b2b-companies#company-users).
- The order lines that carry a cost centre: [Orders, approvals and pay on account](/guides/b2b-orders).
- The cost centres file, for bulk loads: [CSV imports and exports](/guides/csv-imports).
