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"
}
| Field | Meaning |
|---|---|
projectId | The CRS id of the project, generated by CRS. You cannot choose it. |
customerId | Required. 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. |
externalCustomerId | In responses only: the customer's own id. |
externalProjectId | Your own id for the project. If you leave it out when the project is created, it is set to the projectId. Fixed once set. |
businessUnitGroup | The project's own group; see below. |
addressLine1 | Required. name, addressLine2, zipCode and city are optional. |
referenceId | Optional 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, toDate | Optional validity period; see Dates and status. |
restrictions | What may be bought for the project; see Restrictions. Always present in responses, empty when there are none. |
status | Upcoming, 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.
| Dates | status |
|---|---|
fromDate is after today | Upcoming |
otherwise, toDate is before today | Expired |
| otherwise, including a project with no dates | Active |
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.
type | Checks | Fields |
|---|---|---|
WHITELIST | an item property is one of the allowed values | propertyName, allowedValues, optional allowIfMissing |
BLACKLIST | an item property is not one of the denied values | propertyName, deniedValues, optional allowIfMissing |
WHITELISTEDITEMS | the item id is one of the listed ids | itemIds |
BLACKLISTEDITEMS | the item id is not one of the listed ids | itemIds |
What CRS accepts:
- Each restriction must match exactly one of the four forms, with no extra properties. An unknown
type,deniedValueson aWHITELISTorallowIfMissingon an item type returns400with the messageeach value in restrictions must be a valid instance of one of: .... - Value lists and
itemIdsneed at least one entry. A value is a non-empty string, a number or a boolean. Strings,propertyNameand item ids are stored without surrounding whitespace. allowIfMissingis stored and returned only when you send it. When you leave it out, checkout treats it asfalseforWHITELISTand astrueforBLACKLIST. The API reference givesfalseas the default forBLACKLISTtoo; checkout usestrue.
⚠ 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:
projectIdin the body: updates that project (200).404 Project not foundif it does not exist or is deleted.- No
projectId, butexternalProjectId: looks up the project byexternalProjectId, the customer'sexternalCustomerIdand thebusinessUnitGroupin the body. A match is updated (200), a deleted match is restored with its originalprojectId(201), and no match creates a project (201). - Neither: creates a project with a generated
projectId(201);externalProjectIdis 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
externalProjectIdreturns409 externalProjectId cannot be changed on an existing project. Leaving it out keeps the stored one. businessUnitGroupmust equal the stored value exactly: a different group, a group when none is stored, or no group when one is stored returns409 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:
| Parameter | Matches |
|---|---|
customerId | The customer's CRS id, exactly. |
externalCustomerId | The customer's own id, exactly. |
externalProjectId | Exactly. |
name, city, referenceId | Fuzzy and case-insensitive: close spellings match too, ranked below exact, starts-with and contains matches. |
addressLine | Fuzzy, in addressLine1 or addressLine2. |
fromDate, toDate | Projects whose validity period overlaps the dates, inclusive. A project without dates always overlaps. |
status | Upcoming, Active or Expired. |
businessUnitId | The 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.
| Status | Message | When |
|---|---|---|
400 | one message per failed rule | The body breaks a rule above, or carries an unknown property. |
403 | Forbidden | You have no access to the project's business unit group. |
404 | Customer not found | The customerId does not exist or the customer is deleted (not checked when you address the project by projectId). |
404 | Project not found | The projectId does not exist or the project is deleted. |
409 | externalProjectId cannot be changed on an existing project | A different externalProjectId for an existing project. |
409 | businessUnitGroup cannot be changed on an existing project | The project's group would change. |
409 | Business Unit Group does not exist, or is not part of Customer Registry type | Creating or restoring a project in a group CRS does not know. |
409 | Project external key conflict | Another request stored the same key at the same time. Retry. |