Skip to main content

Project

A project is a piece of work a B2B customer buys for, typically a building site, whose purchases are tracked and can be restricted. Every project belongs to exactly one customer.

At the till, the cashier picks a project after the customer: project search only shows the selected customer's projects and can filter by name, address, city and a date range; the results show each project's status. When the customer has requireProject: true, the sale cannot go on without a project. See B2B Customer in the Checkout App.

A project record​

{
"projectId": "c4a1e7d2-6b39-4f0e-8d25-7a9b3e1f6c08",
"customerId": "0b7f6a52-3c1e-4d8f-9a63-2f5d1c7e8b40",
"externalCustomerId": "10042",
"externalProjectId": "P-2026-031",
"businessUnitGroup": "b2b-north",
"name": "Solna school extension",
"addressLine1": "Skolgatan 4",
"zipCode": "171 48",
"city": "Solna",
"referenceId": "ERP-ADDR-7781",
"fromDate": "2026-03-01",
"toDate": "2026-12-31",
"restrictions": [
{ "type": "BLACKLIST", "propertyName": "department", "deniedValues": ["tobacco", "alcohol"] }
],
"status": "Active"
}
FieldMeaning
projectIdThe CRS id of the project, generated by CRS. You cannot choose it.
customerIdRequired. The CRS id of the customer. It must be a customer that exists and is not deleted, or the request returns 404 Customer not found. When you address an existing project by projectId, the customerId in the body is not used: the project keeps its customer.
externalCustomerIdIn responses only: the customer's own id.
externalProjectIdYour own id for the project. If you leave it out when the project is created, it is set to the projectId. Fixed once set.
businessUnitGroupThe project's own group; see below.
addressLine1Required. name, addressLine2, zipCode and city are optional.
referenceIdOptional reference such as the project's address id in your ERP. It does not have to be unique, and you can search by it.
fromDate, toDateOptional validity period; see Dates and status.
restrictionsWhat may be bought for the project; see Restrictions. Always present in responses, empty when there are none.
statusUpcoming, Active or Expired, computed from the dates.

A field without a value is left out of the response. The API reference has the full schema.

The project's business unit group​

A project carries its own businessUnitGroup. It is not copied from the customer: PUT /api/v1/projects stores the group in the body, or none. Through external ingest, the group you send is also used to find the customer, so it is the customer's group.

The group is part of the project's key and is fixed once set (see Writing a project). See Business unit group for which groups CRS knows.

externalProjectId is unique per the customer's externalCustomerId and the project's businessUnitGroup. A PUT with a key that already exists updates or restores that project; see Writing a project.

Dates and status​

fromDate and toDate are calendar dates in YYYY-MM-DD form, without a time (2026-01-01T00:00:00Z returns 400). Both dates are inclusive: the project is valid on its fromDate and on its toDate. CRS does not check that fromDate is before toDate; make sure it is.

The returned status is computed every time the project is read, from today's date in UTC. It is never stored and cannot be written.

Datesstatus
fromDate is after todayUpcoming
otherwise, toDate is before todayExpired
otherwise, including a project with no datesActive

A project with only a fromDate in the past stays Active.

Restrictions​

Restrictions limit what may be bought for the project. They are enforced at checkout by the Entity Conditions service: see Project restrictions for exactly how each type decides. CRS stores and validates them.

typeChecksFields
WHITELISTan item property is one of the allowed valuespropertyName, allowedValues, optional allowIfMissing
BLACKLISTan item property is not one of the denied valuespropertyName, deniedValues, optional allowIfMissing
WHITELISTEDITEMSthe item id is one of the listed idsitemIds
BLACKLISTEDITEMSthe item id is not one of the listed idsitemIds

What CRS accepts:

  • Each restriction must match exactly one of the four forms, with no extra properties. An unknown type, deniedValues on a WHITELIST or allowIfMissing on an item type returns 400 with the message each value in restrictions must be a valid instance of one of: ....
  • Value lists and itemIds need at least one entry. A value is a non-empty string, a number or a boolean. Strings, propertyName and item ids are stored without surrounding whitespace.
  • allowIfMissing is stored and returned only when you send it. When you leave it out, checkout treats it as false for WHITELIST and as true for BLACKLIST. The API reference gives false as the default for BLACKLIST too; checkout uses true.

⚠ Values are compared with their JSON type. "1" does not match an item property 1, and "true" does not match true. Send values with the same type the item's properties carry.

Restrictions of the same type for the same property combine, as do all WHITELISTEDITEMS or all BLACKLISTEDITEMS rules. An empty list means no restrictions.

GET /api/v1/projects/{projectId}/restrictions returns only { "restrictions": [...] }. Checkout reads the restrictions from there and caches them for up to 60 seconds, so a change can take about a minute to reach checkout.

🛑 If checkout cannot read a project's restrictions, it holds back every item. When the lookup fails, for example because the project was deleted or does not exist, each item is soft-denied with the reason project_restriction.project_unreachable.

Writing a project​

PUT /api/v1/projects finds its target in this order:

  1. projectId in the body: updates that project (200). 404 Project not found if it does not exist or is deleted.
  2. No projectId, but externalProjectId: looks up the project by externalProjectId, the customer's externalCustomerId and the businessUnitGroup in the body. A match is updated (200), a deleted match is restored with its original projectId (201), and no match creates a project (201).
  3. Neither: creates a project with a generated projectId (201); externalProjectId is set to the same value.

The response is the stored project.

The group is part of the key. A PUT without projectId that sends the same externalProjectId with a different group, or without a group, does not find the existing project: it creates a second one. Send the same businessUnitGroup every time, or address the project by projectId.

On an existing project addressed by projectId:

  • A different externalProjectId returns 409 externalProjectId cannot be changed on an existing project. Leaving it out keeps the stored one.
  • businessUnitGroup must equal the stored value exactly: a different group, a group when none is stored, or no group when one is stored returns 409 businessUnitGroup cannot be changed on an existing project.

⚠ PUT replaces the whole project. name, addressLine2, zipCode, city, referenceId, fromDate and toDate are cleared when you leave them out, and leaving out restrictions removes all restrictions.

PATCH /api/v1/projects/{projectId} changes only the fields in the body: name, the address fields, referenceId, the dates and restrictions. restrictions replaces the whole list; [] removes all restrictions, null returns 400. projectId, customerId, externalProjectId and businessUnitGroup cannot be patched (400).

When a project is created or restored with a businessUnitGroup, the group must be one CRS knows, or the request returns 409 Business Unit Group does not exist, or is not part of Customer Registry type.

Deleting a project​

DELETE /api/v1/projects/{projectId} answers 204. Delete is soft: the project disappears from reads and search, and a second delete returns 404. Its external id stays reserved, so a PUT without projectId that sends the same key restores it with its original projectId (201). A PUT with the deleted projectId returns 404.

A customer cannot be deleted while it has projects that are not deleted, whatever their status. Delete the projects first. Projects of a deleted customer cannot be written (404 Customer not found) until the customer is restored.

Searching​

GET /api/v1/projects returns projects matching all the filters you send:

ParameterMatches
customerIdThe customer's CRS id, exactly.
externalCustomerIdThe customer's own id, exactly.
externalProjectIdExactly.
name, city, referenceIdFuzzy and case-insensitive: close spellings match too, ranked below exact, starts-with and contains matches.
addressLineFuzzy, in addressLine1 or addressLine2.
fromDate, toDateProjects whose validity period overlaps the dates, inclusive. A project without dates always overlaps.
statusUpcoming, Active or Expired.
businessUnitIdThe projects a business unit may see: see Search by business unit.

Page through the results with skip and take (at most 200 per page).

Results come in order of how well they match: exact, then starts-with, then contains, then by fuzzy similarity, where similarity in name weighs twice as much as in the other text fields. The response is { "items": [...], "page": { "skip": 0, "take": 50, "hasMore": false } }; each item carries its computed status, and there is no total count. Deleted projects are never returned.

Permissions and errors​

Reading, including the restrictions, needs crs.project.read; writing and deleting needs crs.project.write, checked against the project's business unit group. See Who may read and write which records.

StatusMessageWhen
400one message per failed ruleThe body breaks a rule above, or carries an unknown property.
403ForbiddenYou have no access to the project's business unit group.
404Customer not foundThe customerId does not exist or the customer is deleted (not checked when you address the project by projectId).
404Project not foundThe projectId does not exist or the project is deleted.
409externalProjectId cannot be changed on an existing projectA different externalProjectId for an existing project.
409businessUnitGroup cannot be changed on an existing projectThe project's group would change.
409Business Unit Group does not exist, or is not part of Customer Registry typeCreating or restoring a project in a group CRS does not know.
409Project external key conflictAnother request stored the same key at the same time. Retry.