Skip to main content

Tenant system endpoints

Two B2B Customer Registry (CRS) operations are decided by your own system, not by CRS:

A caller asks CRSCRS forwards it toYour system decides
POST /api/v1/customers/{customerId}/credit-limit:validatePOST {baseUrl}/credit-limit:validatewhether the customer may buy this amount on credit
POST /api/v1/customers:createPOST {baseUrl}/customers:createwhether and how a customer is created

Your system implements both endpoints. {baseUrl} and the credentials CRS signs in with are stored in the proxy configuration; both endpoints sit under the same baseUrl and use the same credentials. Note the colon in each path: credit-limit:validate and customers:create are literal.

CRS sets three headers on every call: Authorization, Content-Type: application/json and Tenant-Id. No correlation id, idempotency key or store or workstation information is sent; the standard headers of the HTTP client, such as Host, Content-Length, User-Agent, Accept and Accept-Encoding, are also present. CRS makes each call once and never repeats it. See What every call to your system carries.

Rules for every answer​

🛑 Answer with Content-Type: application/json and a non-empty JSON body, on success too. The content type must contain the text application/json, so application/json; charset=utf-8 works but application/problem+json does not. A 204 No Content, an empty 200, or an answer of another content type reaches the caller as 502 Bad Gateway.

  • A success body is checked against the endpoint's contract. A body that fails the check reaches the caller as 502.
  • Leave optional fields out instead of sending null. A null fails the check.
  • Types and values are strict. A number where a string is expected fails, and enum values are case-sensitive.
  • Extra fields are ignored and never reach the caller.

Credit-limit validation​

A caller, typically the Checkout App at the till, asks CRS whether a customer may pay an amount on credit:

POST https://crs-api.retailsvc.com/api/v1/customers/{customerId}/credit-limit:validate
{ "requestedAmount": 150 }

{customerId} is the CRS customer id. requestedAmount is a number, 0 or more; decimals are allowed. The caller needs the permission crs.credit-limit.validate and also crs.customer.read on the customer's business unit group (for a customer without a group, on all groups).

CRS looks the customer up and calls your system with your own id for it.

Credit-limit validation is decided by your system. The Checkout App asks CRS to validate an amount for a customer. CRS checks the request, looks the customer up (404 if it is missing or deleted), checks the caller's access to the customer's group (403) and sends your system the customer's external id, its business unit group and the requested amount, signed in with your credentials. Your system answers with an approval code, and CRS returns that decision unchanged with status 200, also for DECLINED and OVERRIDE-REQUIRED. Without a proxy configuration the caller gets 412 and your system is not called; a JSON 4xx from your system is passed on as it is, and a 5xx, an invalid answer or no reply gives 502.

What your system receives​

POST {baseUrl}/credit-limit:validate
{
"customerId": "10042",
"businessUnitGroup": "b2b-north",
"requestedAmount": 150
}
FieldWhat it holds
customerIdyour id for the customer, the externalCustomerId stored in CRS, not the CRS id. Always present. A customer stored without an external id carries its CRS id here, because CRS uses that as its external id
businessUnitGroupthe customer's business unit group. Left out when the customer has none
requestedAmountthe amount the caller asked for, unchanged. No currency is sent

Nothing else is sent: no CRS customer id, store, workstation or transaction id.

CRS does not judge the credit itself. It does not compare requestedAmount with the credit limit it stores for the customer, and it does not look at the customer's status, such as IsCreditBlocked or Inactive. Your system decides.

What your system answers​

Any 2xx with a decision:

{
"approvalCode": "APPROVED",
"referenceNumber": "123456789"
}
{
"approvalCode": "DECLINED",
"reason": "Customer blocked",
"reasonMessage": "The customer has been blocked from credit payments"
}
FieldRequiredWhat it holds
approvalCodeyesAPPROVED, DECLINED or OVERRIDE-REQUIRED: your decision on the requested amount
referenceNumbernoyour unique reference for this payment transaction
reasonnoa reason to be added to the poslog
reasonMessagenoa translated reason that can be shown to an end user

Every decision reaches the caller as 200. CRS returns approvalCode and whichever of the optional fields you sent, unchanged. A DECLINED or OVERRIDE-REQUIRED is still a successful call: only the body differs. Use an error status only when your system cannot decide, as described in When your system cannot answer.

An answer without a valid approvalCode, such as { "referenceNumber": "123456789" } or "approved" in lower case, reaches the caller as 502.

Order of checks​

CRS answers the caller without calling your system when:

  1. the body is invalid: 400;
  2. the customer does not exist or is deleted: 404 Customer not found;
  3. the caller may not read the customer's business unit group: 403;
  4. no proxy configuration is stored: 412.

Customer creation​

A caller, typically the Checkout App at the till, asks CRS to create a customer from the few details known at the counter:

POST https://crs-api.retailsvc.com/api/v1/customers:create
{
"phone": "+46 70 123 45 67",
"businessUnitId": "store-12",
"name": "Acme Builders",
"email": "buyer@acme.example.com",
"address": "Main Street 1"
}

phone and businessUnitId are required; name, email and address are optional. phone must be an international number with its country code. The caller needs crs.customer.write on businessUnitId.

Customer creation is decided by your system. The Checkout App asks CRS to create a customer, and CRS checks the request and the caller's access, then forwards the details to your system. If your system creates the customer at once, it answers 200 with the full customer and CRS stores it and returns the stored customer, but only if the returned group is a CRS group the caller may write to; otherwise the caller gets 409 or 403. If creation needs more time, your system answers 202 with a message, CRS stores nothing and passes the message on, and the customer appears once your back office pushes it through external ingest. An invalid answer gives 502 and nothing is stored. A 4xx with a JSON body from your system reaches the caller with the same status and body, no proxy configuration gives 412, and a failing system gives 502.

What your system receives​

POST {baseUrl}/customers:create
{
"phone": "+46701234567",
"businessUnitId": "store-12",
"name": "Acme Builders",
"email": "buyer@acme.example.com",
"address": "Main Street 1"
}

The caller's body after it is checked and cleaned up: phone in E.164 form, every other value with surrounding whitespace removed. Optional fields the caller did not send are left out. businessUnitId is the business unit the customer is created from; resolve the customer's scope from it.

What your system answers​

You choose whether creation completes now or later.

Created now: 200​

Answer 200 (any 2xx other than 202) with the customer as your system holds it:

{
"externalCustomerId": "10077",
"name": "Acme Builders",
"phone": "+46701234567",
"address": "Main Street 1",
"businessUnitGroup": "b2b-north",
"creditLimit": {
"total": 1000,
"available": 1000,
"versionTimestamp": "2026-09-29T10:00:00.000Z"
}
}

The body has the same shape as a customer sent through external ingest, without customerId. Only two fields are required:

FieldWhat it holds
externalCustomerIdyour id for the customer
creditLimittotal and available, numbers of 0 or more with available not above total, and versionTimestamp, an ISO 8601 date-time

Every other customer field is optional. The Customer page explains them, and the API reference has the full schema. A loyalty object must match one of its forms exactly. An email or a customerId in your answer is ignored: CRS stores no email for a customer and keeps its own ids.

CRS then stores the customer as if it had arrived through external ingest:

  • It is matched on externalCustomerId and businessUnitGroup together. A customer with the same pair is updated and keeps its CRS id, and a deleted one is restored; otherwise a customer is created with a CRS id of its own. The same externalCustomerId with another group is another customer.
  • Your answer replaces the stored fields. Optional fields you leave out are cleared or set to their defaults. For a customer CRS already holds and has not deleted, the stored credit limit is replaced only when your versionTimestamp is newer than the stored one.

The caller gets 200 with the stored customer, including its CRS customerId, whether it was created or matched.

⚠ Return a businessUnitGroup that exists in CRS and that the caller may write to. CRS checks both before it stores the customer, after your system has already created it:

  • a group that is not a Customer Registry business unit group gives the caller 409 (Business Unit Group does not exist, or is not part of Customer Registry type) when no active customer with that externalCustomerId and group exists in CRS, because it is new or deleted;
  • a group the caller has no crs.customer.write on gives the caller 403. With no group in your answer, the caller needs that permission on all groups.

In both cases the customer exists in your system but not in CRS.

Created later: 202​

Answer 202 when creation needs more time, for example until the customer approves a membership request on their phone:

{
"message": "Ask the customer to approve the membership request sent to their phone."
}

message is required and must not be empty. It is meant to be shown at the till, and CRS passes it to the caller unchanged, with status 202 and the same body.

CRS stores nothing on a 202. The customer appears in CRS once your system sends it with PUT /api/v1/external-ingest/customers, as described in External ingest. Until then, searching CRS does not find it.

Rejected: 4xx with JSON​

To refuse the creation, answer a 4xx with a JSON body. The caller gets the same status and body:

{ "message": "Phone already registered", "code": "DUPLICATE" }

CRS sends no idempotency key, so a caller that sends the same details twice reaches your system twice.

Order of checks​

CRS answers the caller without calling your system when:

  1. the body is invalid: 400;
  2. the caller may not write to businessUnitId: 403;
  3. no proxy configuration is stored: 412.

When your system cannot answer​

The same rules apply to both endpoints:

WhenThe caller gets
no proxy configuration is stored412 Precondition Failed. Your system is not called
your system answers 2xx with a valid bodythe decision (200), the stored customer (200) or your message (202)
your system answers 4xx with a JSON bodythe same status and the same body
your system answers 4xx without a JSON body502 Bad Gateway
your system answers 5xx, with any body502 Bad Gateway
your system answers 2xx with an empty, non-JSON or invalid body502 Bad Gateway
your system cannot be reached (DNS, connection, TLS, timeout)502 Bad Gateway
your identity provider gives no tokena 5xx error. Your system is not called

A 412 reads:

{
"statusCode": 412,
"message": "This endpoint requires a proxy to be configured for the tenant. Configure one via PUT /api/v1/proxy-config before calling it.",
"error": "Precondition Failed"
}

A 502 from CRS has the body { "statusCode": 502, "message": "...", "error": "Bad Gateway" }. Do not rely on the text of message.

⚠ A JSON 401, 403 or 404 from your system reaches the caller as it is, and the caller cannot tell it from the same status sent by CRS itself. After a 401, CRS does not fetch another token or repeat the call. Keep these statuses for answers the caller should act on.

Nothing is stored when the call fails. A customer is stored only after a valid 200 from customers:create.