Skip to main content

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.

The CRS data model. Inside your tenant, a customer optionally belongs to one business unit group. It has a CRS id (customerId) and your own id (externalCustomerId), unique per business unit group, and requireAgent and requireProject make the till ask for an agent or a project. A customer has any number of trusted agents and projects. A trusted agent always has its customer's group, and its externalAgentId is unique within its customer. A project carries its own businessUnitGroup, a required addressLine1, validity dates and a status computed from them, and restrictions checked at checkout; its externalProjectId is unique within its customer. External ids are fixed once set, and delete is soft: sending the same external ids again restores the record with its CRS id.

Identity​

FieldWhat it is
customerIdThe 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.
externalCustomerIdYour 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.
businessUnitGroupOptional. 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:

statusAt the till
ActiveThe customer can be added to a sale.
IsCreditBlockedThe receipt shows Blocked instead of the available credit, and the credit limit alert is not shown.
InactiveThe 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:

FieldRule
totalA number, 0 or more. Decimals are allowed; a numeric string such as "10" is rejected.
availableA number, 0 or more, and not more than total.
versionTimestampThe 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.

FieldMeaning
idRequired. Your id for the input.
displayTextRequired. The text shown to the cashier.
receiptLabelOptional label for the receipt.
regularExpressionOptional validation pattern.
inputRequiredOptional. 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​

FieldMeaning
phoneAn 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.
customerTypeCASH or CREDIT. It is independent of the credit limit; absent means unknown.
licensesA list of { "type": ..., "level": ... }, both required free text.
promotionsA 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:

loyaltyMeaning
absentUnknown: 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:

  1. customerId in the body: updates that customer. 404 if it does not exist or is deleted.
  2. No customerId, but externalCustomerId: looks up externalCustomerId together with businessUnitGroup. A match is updated (200), a deleted match is restored (201), and no match creates a customer (201).
  3. 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​

FieldWhat happens if you try to change it
customerIdAssigned by CRS; cannot be set.
externalCustomerIdPUT 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.
businessUnitGroupPUT 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 PUT with the same externalCustomerId and the same businessUnitGroup restores the customer under its original customerId, answers 201 and applies the body as a full replace, credit limit included whatever its timestamp.
  • A PUT with the deleted customer's customerId returns 404.

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:

ParameterMatches
externalCustomerIdExact and case-sensitive (acme-1 does not find ACME-1), in every business unit group, so it can return one customer per group.
statusActive, IsCreditBlocked or Inactive.
queryName or external id, at least 2 characters.
name, phone, addressOne field each, at least 2 characters.
businessUnitIdThe 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.

StatusMessageWhen
400one message per failed ruleThe body or query breaks a rule above, or carries an unknown property.
403ForbiddenYou have no access to the customer's business unit group.
404Customer not foundThe customerId does not exist or the customer is deleted.
409externalCustomerId cannot be changed on an existing customerSee Fields that are fixed once set.
409businessUnitGroup cannot be changed on an existing customerSee Fields that are fixed once set.
409Business Unit Group does not exist, or is not part of Customer Registry typeCreating or restoring a customer in a group CRS does not know. See Business unit group.
409Customer external key conflictAnother request stored the same external id at the same time. Retry.
409Customer has active agents or projectsDeleting a customer that still has agents or projects.