Tenant system endpoints
Two B2B Customer Registry (CRS) operations are decided by your own system, not by CRS:
| A caller asks CRS | CRS forwards it to | Your system decides |
|---|---|---|
POST /api/v1/customers/{customerId}/credit-limit:validate | POST {baseUrl}/credit-limit:validate | whether the customer may buy this amount on credit |
POST /api/v1/customers:create | POST {baseUrl}/customers:create | whether 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. Anullfails 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.
What your system receives
POST {baseUrl}/credit-limit:validate
{
"customerId": "10042",
"businessUnitGroup": "b2b-north",
"requestedAmount": 150
}
| Field | What it holds |
|---|---|
customerId | your 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 |
businessUnitGroup | the customer's business unit group. Left out when the customer has none |
requestedAmount | the 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"
}
| Field | Required | What it holds |
|---|---|---|
approvalCode | yes | APPROVED, DECLINED or OVERRIDE-REQUIRED: your decision on the requested amount |
referenceNumber | no | your unique reference for this payment transaction |
reason | no | a reason to be added to the poslog |
reasonMessage | no | a 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:
- the body is invalid:
400; - the customer does not exist or is deleted:
404Customer not found; - the caller may not read the customer's business unit group:
403; - 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.
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:
| Field | What it holds |
|---|---|
externalCustomerId | your id for the customer |
creditLimit | total 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
externalCustomerIdandbusinessUnitGrouptogether. 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 sameexternalCustomerIdwith 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
versionTimestampis 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 thatexternalCustomerIdand group exists in CRS, because it is new or deleted; - a group the caller has no
crs.customer.writeon gives the caller403. 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:
- the body is invalid:
400; - the caller may not write to
businessUnitId:403; - no proxy configuration is stored:
412.
When your system cannot answer
The same rules apply to both endpoints:
| When | The caller gets |
|---|---|
| no proxy configuration is stored | 412 Precondition Failed. Your system is not called |
your system answers 2xx with a valid body | the decision (200), the stored customer (200) or your message (202) |
your system answers 4xx with a JSON body | the same status and the same body |
your system answers 4xx without a JSON body | 502 Bad Gateway |
your system answers 5xx, with any body | 502 Bad Gateway |
your system answers 2xx with an empty, non-JSON or invalid body | 502 Bad Gateway |
| your system cannot be reached (DNS, connection, TLS, timeout) | 502 Bad Gateway |
| your identity provider gives no token | a 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.
Related
- Proxy configuration: where your system is and how CRS signs in to it.
- External ingest: how your system sends customers to CRS, including those accepted with
202. - B2B Customer in the Checkout App: how the till selects a customer and shows its credit.