Skip to content

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

FieldTypeMeaning
iduuidOrbit's id.
companyIduuidThe company it belongs to.
codestring, 1 to 32 characters on one line, without a semicolonThe 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.
namestring, 1 to 120 characters on one lineWhat the code stands for, shown beside it.
statusactive or archivedOnly active cost centres are offered at checkout.
externalRefstring, 1 to 64 characters on one line, without a semicolon, or nullYour 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.
userIdsarray of uuidThe company users given this cost centre: the same assignments as each user's allowedCostCentreIds, seen from the cost centre.
createdAt, updatedAtdate-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 pathScopePurpose
GET /v1/companies/{companyId}/cost-centrescompany:readThe company's cost centres: active, or archived or all with ?status=.
GET /v1/companies/{companyId}/cost-centres/{costCentreId}company:readOne cost centre.
POST /v1/companies/{companyId}/cost-centrescompany:updateAdd one: code, name, optional externalRef, status and userIds.
PATCH /v1/companies/{companyId}/cost-centres/{costCentreId}company:updateChange its code, name, status or externalRef; userIds replaces the assignments.
DELETE /v1/companies/{companyId}/cost-centres/{costCentreId}company:updateArchive it.
PUT /v1/companies/{companyId}/cost-centrescompany:updateReplace the company's whole list in one call, matched by code.
PUT /v1/companies/{companyId}/cost-centres/by-external-ref/{externalRef}company:updateCreate 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 errorCodeWhen
409 B2B_COST_CENTRE_CODE_TAKENAnother cost centre of the company has the code, ignoring case.
409 B2B_COST_CENTRE_EXTERNAL_REF_TAKENAnother cost centre of the company has the reference, ignoring case.
400 B2B_COST_CENTRE_LIMIT_REACHEDThe 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_CODEThe list sent to PUT .../cost-centres names a code twice, ignoring case.
400 B2B_COST_CENTRE_DUPLICATE_EXTERNAL_REFThe list sent to PUT .../cost-centres names a reference twice, ignoring case.
400 B2B_CONTACT_NOT_IN_COMPANYAn 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.

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.
  • 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

TopicFires ondata
cost_centre.createdA cost centre is added, by the dashboard, the storefront company account, an import or the API.costCentreId, companyId, storeId
cost_centre.updatedIts 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.archivedIt is archived.costCentreId, companyId, storeId

All three need company:read at subscribe time.

Next steps