Customer
A customer is a business account your stores sell to: a company whose purchases are tracked and invoiced against its account. The customer record tells the till who the customer is, whether it may buy right now, how much credit it has, and what the cashier must ask for before the sale can go on.
A customer lives in your tenant, which is always taken from your access token, and optionally in one business unit group. Trusted agents and projects belong to a customer.
Identity
| Field | What it is |
|---|---|
customerId | The CRS id. CRS assigns it when the customer is created and it never changes. Treat it as an opaque string. You cannot choose it: a PUT with a customerId that does not exist returns 404. |
externalCustomerId | Your own customer number. If you leave it out when the customer is created, it is set to the customerId. It is always present in responses. |
businessUnitGroup | Optional. The id of the business unit group the customer belongs to. A customer without a group is shown in every business unit. See Business unit group. |
externalCustomerId is unique per tenant and business unit group, not per tenant. The same customer number can exist once in each group and once without a group, and each of those is a separate customer.
The paths GET, PATCH and DELETE /api/v1/customers/{customerId} take the CRS id, not your customer number. There is no lookup by external id; search with ?externalCustomerId= instead (see Searching).
A customer record
{
"customerId": "0b7f6a52-3c1e-4d8f-9a63-2f5d1c7e8b40",
"externalCustomerId": "10042",
"businessUnitGroup": "b2b-north",
"name": "Nordbygg AB",
"phone": "+46701230199",
"address": "Industrivagen 12, 171 48 Solna",
"status": "Active",
"customerType": "CREDIT",
"creditLimit": {
"total": 50000,
"available": 32500.5,
"versionTimestamp": "2026-04-02T10:15:30.000Z"
},
"requireAgent": true,
"requireProject": false,
"additionalInputs": [
{
"id": "po-number",
"displayText": "PO Number",
"receiptLabel": "PO",
"regularExpression": "^PO-[0-9]{6}$",
"inputRequired": true
}
],
"licenses": [{ "type": "explosives", "level": "2" }],
"promotions": [],
"loyalty": { "eligible": true, "identifier": "+46701230199" }
}
A field without a value is left out of the response, not returned as null. customerId, externalCustomerId, creditLimit, additionalInputs, licenses and promotions are always present; every other field appears only when it has a value. Responses carry no created, updated or deleted timestamps.
The API reference has the full schema. The sections below explain what the fields mean.
Status: what blocks a customer
status is one of Active, IsCreditBlocked or Inactive (case-sensitive). It defaults to Active.
CRS stores the status and returns it; it does not act on it. The till does:
status | At the till |
|---|---|
Active | The customer can be added to a sale. |
IsCreditBlocked | The receipt shows Blocked instead of the available credit, and the credit limit alert is not shown. |
Inactive | The customer cannot be added to a sale. |
See B2B Customer in the Checkout App for the cashier's side.
Credit limit
creditLimit is a snapshot of the customer's credit from your system, and the only field every PUT must carry:
| Field | Rule |
|---|---|
total | A number, 0 or more. Decimals are allowed; a numeric string such as "10" is rejected. |
available | A number, 0 or more, and not more than total. |
versionTimestamp | The time your system changed the credit limit. Send a full ISO 8601 date-time with Z or an offset, for example 2026-04-02T10:15:30Z. Responses return it in UTC with milliseconds. |
At the till, the receipt shows the Available credit, and the cashier is alerted when the amount due exceeds it, unless the customer is IsCreditBlocked.
A credit limit is only written when its versionTimestamp is strictly newer than the stored one. If it is equal or older, CRS ignores the credit limit without an error, still writes every other field, and answers 200 with the credit limit it already had. Always set versionTimestamp to the time your system changed the credit limit, not the time you send it. A customer that is created, or restored after a delete, always takes the credit limit you send. The same rule applies to PATCH.
CRS does not decide whether a sale fits the credit. When the till asks for a credit decision, CRS forwards the question to your system; see Tenant system endpoints.
Agent and project at the till
requireAgent and requireProject are booleans with no default. When true, the till marks the agent or the project as Required, and the cashier cannot submit the selection until one is picked. Agents and projects are records of their own: see Trusted agent and Project.
Additional inputs
additionalInputs are questions the till asks the cashier before the customer is added to the sale, for example a purchase order number. Each customer has its own list; the till takes each field's label, whether it is required and its validation pattern from the record.
| Field | Meaning |
|---|---|
id | Required. Your id for the input. |
displayText | Required. The text shown to the cashier. |
receiptLabel | Optional label for the receipt. |
regularExpression | Optional validation pattern. |
inputRequired | Optional. Whether the cashier must fill it in. |
⚠ Send regularExpression as a bare pattern, for example ^PO-[0-9]{6}$. It must compile as a JavaScript regular expression, or the request is rejected with 400. There is no field for flags: /^po$/i is accepted, but as a pattern that contains the slashes literally.
There is no orderNumber field; sending one returns 400. Use an additional input for a purchase order number the purchaser gives at the till.
Other fields
| Field | Meaning |
|---|---|
phone | An international number with country code. Spaces, dashes and parentheses are accepted and the number is stored in E.164 form, so +46 70 123 01 99 becomes +46701230199. A national number such as 0701234567 returns 400. |
customerType | CASH or CREDIT. It is independent of the credit limit; absent means unknown. |
licenses | A list of { "type": ..., "level": ... }, both required free text. |
promotions | A list of JSON objects. CRS stores them as sent and does not check their contents; each element must be an object. |
name and address are free text; address is one field. The API reference has every field's format. Contact details of the people who buy for the customer belong on the trusted agent, not on the customer.
Loyalty
loyalty tells the till whether to offer the customer a loyalty membership. It has three forms, plus absent:
loyalty | Meaning |
|---|---|
| absent | Unknown: your system has not said. |
{ "eligible": true, "identifier": "+46701230199" } | Already a member. The identifier is stored without surrounding whitespace and otherwise not interpreted: a phone number, a member number or anything else. |
{ "eligible": true } | May be offered loyalty, not enrolled yet. "identifier": null means the same. |
{ "eligible": false } | Must never be offered loyalty. This form cannot carry an identifier (400). |
Absent is not the same as not eligible. A stored loyalty value CRS cannot interpret is also returned as absent, never as { "eligible": false }. eligible must be a JSON boolean. identifier must not be empty; leave it out, or send null, for a customer who is not enrolled.
Writing a customer
You write customers in one of three ways:
PUT /api/v1/customers, described here.- The external ingest endpoints, which address every record by your own ids.
POST /api/v1/customers:create, which asks your own system to create the customer; see Tenant system endpoints.
PUT /api/v1/customers finds its target in this order:
customerIdin the body: updates that customer.404if it does not exist or is deleted.- No
customerId, butexternalCustomerId: looks upexternalCustomerIdtogether withbusinessUnitGroup. A match is updated (200), a deleted match is restored (201), and no match creates a customer (201). - Neither: creates a customer with a generated id (
201).
The response is the stored customer.
🛑 PUT replaces the whole record. Every optional field you leave out is reset: text fields and booleans are cleared, loyalty is cleared, the lists become empty and status goes back to Active. Send the complete customer every time. null is rejected for every field except loyalty; to clear a field, leave it out.
PATCH /api/v1/customers/{customerId} changes name, phone, address, requireAgent, requireProject, customerType, loyalty and creditLimit only when they are in the body, and null clears any of them except creditLimit. A list or loyalty in the body replaces the stored value whole.
Fields that are fixed once set
| Field | What happens if you try to change it |
|---|---|
customerId | Assigned by CRS; cannot be set. |
externalCustomerId | PUT by customerId with a different value: 409 externalCustomerId cannot be changed on an existing customer. Leaving it out keeps the stored value. In a PATCH body: 400. |
businessUnitGroup | PUT by customerId with a different value, with a value when none is stored, or without one when one is stored: 409 businessUnitGroup cannot be changed on an existing customer. In a PATCH body: 400. |
To move a customer to another group, delete it and create a customer in the other group.
⚠ Addressed by your own id, a different group is a different customer. A PUT without customerId that sends the same externalCustomerId with another group, or without the group, does not update the existing customer: it creates a second one. Always send the same businessUnitGroup.
Deleting a customer
DELETE /api/v1/customers/{customerId} answers 204. Delete is soft. The customer disappears from reads, updates and search (404), but its external id stays reserved:
- A later
PUTwith the sameexternalCustomerIdand the samebusinessUnitGrouprestores the customer under its originalcustomerId, answers201and applies the body as a full replace, credit limit included whatever its timestamp. - A
PUTwith the deleted customer'scustomerIdreturns404.
A customer with active agents or projects cannot be deleted. The request returns 409 Customer has active agents or projects. Active means not deleted: a project's status (Upcoming, Active or Expired) does not matter. Delete the customer's agents and projects first. The other way round, agents and projects cannot be written for a deleted customer (404 Customer not found): restore the customer first.
Searching
GET /api/v1/customers returns customers matching all the filters you send:
| Parameter | Matches |
|---|---|
externalCustomerId | Exact and case-sensitive (acme-1 does not find ACME-1), in every business unit group, so it can return one customer per group. |
status | Active, IsCreditBlocked or Inactive. |
query | Name or external id, at least 2 characters. |
name, phone, address | One field each, at least 2 characters. |
businessUnitId | The customers a business unit may see: see Search by business unit. |
Page through the results with skip and take (at most 200 per page).
Text filters are fuzzy, not exact substrings. A customer matches when the term shares short fragments with the field, so typing errors still match (amnda finds Amanda) and loosely related names can appear too. query, name and address match regardless of case, including the external id that query searches; only the externalCustomerId filter is exact and case-sensitive. Phone matches against the stored E.164 number, and the search term is not normalised: search with digits as stored, such as +4670, not with spaces or dashes.
Results come in order of how well they match: exact, then starts-with, then contains, then fuzzy, then by customerId. Without a text filter they are ordered by customerId. Deleted customers are never returned.
{
"items": [
{
"customerId": "0b7f6a52-3c1e-4d8f-9a63-2f5d1c7e8b40",
"externalCustomerId": "10042",
"name": "Nordbygg AB",
"status": "Active",
"creditLimit": { "total": 50000, "available": 32500.5, "versionTimestamp": "2026-04-02T10:15:30.000Z" },
"promotions": [],
"licenses": [],
"additionalInputs": []
}
],
"page": { "skip": 0, "take": 50, "hasMore": false }
}
Each item is a full customer. hasMore is true when there are more results after this page; there is no total count. Empty parameters count as not sent, an unknown parameter (including customerId) returns 400.
Permissions and errors
Reading needs crs.customer.read, writing and deleting crs.customer.write, checked against the customer's business unit group. See Who may read and write which records.
| Status | Message | When |
|---|---|---|
400 | one message per failed rule | The body or query breaks a rule above, or carries an unknown property. |
403 | Forbidden | You have no access to the customer's business unit group. |
404 | Customer not found | The customerId does not exist or the customer is deleted. |
409 | externalCustomerId cannot be changed on an existing customer | See Fields that are fixed once set. |
409 | businessUnitGroup cannot be changed on an existing customer | See Fields that are fixed once set. |
409 | Business Unit Group does not exist, or is not part of Customer Registry type | Creating or restoring a customer in a group CRS does not know. See Business unit group. |
409 | Customer external key conflict | Another request stored the same external id at the same time. Retry. |
409 | Customer has active agents or projects | Deleting a customer that still has agents or projects. |