Proxy configuration
Two B2B Customer Registry (CRS) operations are answered by your own system, not by CRS: validating a customer's credit limit and creating a customer. CRS forwards them to your system, and you tell CRS where that system is, and how to sign in to it, with the proxy configuration described here.
This is CRS's own configuration, stored and read by CRS alone. It is separate from the logistics proxy configuration service: a configuration stored there is never read by CRS, and this one is never read by the logistics services. If your system serves both, configure it in both places.
The endpoints your system must implement are described in Tenant system endpoints.
Setting it
PUT https://crs-api.retailsvc.com/api/v1/proxy-config
GET https://crs-api.retailsvc.com/api/v1/proxy-config
Test tenants use https://crs-api.retailsvc-test.com/api/v1.
- One configuration per tenant. The tenant is taken from your token, never from the body or the path. The same configuration serves both forwarded operations: there is no per-operation or per-service key.
- Permissions:
crs.proxy-config.writeto write andcrs.proxy-config.readto read. The rolecrs.apihas both; the rolecrs.readonlyhas onlycrs.proxy-config.read. Without the permission the call is refused with403. PUTanswers204 No Contentand replaces what was stored, whether or not something was stored before.GETanswers404when nothing is stored.
The B2B Customer Registry API reference has the full contract.
The body
With HTTP Basic:
{
"baseUrl": "https://erp.example.com/crs",
"type": "basic",
"config": { "username": "hii-retail", "password": "s3cret" }
}
With OAuth 2.0 client credentials:
{
"baseUrl": "https://erp.example.com/crs",
"type": "oauth",
"config": {
"clientId": "hii-retail",
"clientSecret": "s3cret",
"authUrl": "https://login.example.com"
}
}
| Field | What it holds |
|---|---|
baseUrl | the base address of your system. CRS appends /credit-limit:validate or /customers:create to it, so include any prefix and version your system needs. Surrounding whitespace and trailing slashes are removed when it is stored |
type | basic or oauth, in lower case |
config.username, config.password | for basic: the credentials CRS sends to your system |
config.clientId, config.clientSecret | for oauth: the client CRS signs in to your identity provider as |
config.authUrl | for oauth: your identity provider's address. See OAuth: what authUrl may be |
All three top-level fields are required, and so is every field of config for the chosen type. type decides which config shape is accepted:
- a
configwith a field the type does not have (for examplescope), a missing or empty field, or the shape of the other type is refused with400and the messageconfig must be a valid instance of BasicProxyConfigDto(orOAuthProxyConfigDto); - any
typeother thanbasicoroauth, includingBASIC, is refused with400andtype must be one of the following values: basic, oauth; - any other top-level property is refused with
400andproperty <name> should not exist.
A 400 stores nothing. Its body lists every problem found:
{
"statusCode": 400,
"message": ["type must be one of the following values: basic, oauth"],
"error": "Bad Request"
}
⚠ baseUrl and authUrl are not checked as addresses when you store them. Any non-empty text is accepted with 204, and a wrong address only shows up when a forwarded call fails. Use an absolute https:// URL, without a query string: CRS builds the address by appending the path to baseUrl as text.
⚠ Credentials are used exactly as you send them. Values inside config are not trimmed, so a username sent as " hii-retail " reaches your system with the spaces. Send them without surrounding whitespace.
Credentials are stored encrypted and are never returned by the API.
Reading it back
GET returns the address and the type, and nothing else:
{
"baseUrl": "https://erp.example.com/crs",
"type": "oauth"
}
No field of config comes back, not even masked. baseUrl is returned as stored, so without a trailing slash.
To change one field, send the whole configuration again, credentials included. There is no partial update, and a PUT without the credentials is refused with 400. A PUT may also switch type, for example from basic to oauth.
When nothing is stored, GET answers:
{
"statusCode": 404,
"message": "Proxy is not configured for tenant",
"error": "Not Found"
}
Switching it off
A stored configuration cannot be removed or switched off through the API. There is no DELETE, and baseUrl is required on every PUT. To send calls somewhere else, store a configuration with the other address.
Until a configuration is stored, the two forwarded operations answer 412 Precondition Failed and nothing is sent anywhere. See When your system cannot answer.
When a change takes effect
On the next forwarded call. CRS does not keep the configuration in memory: every forwarded call reads the stored one, so a PUT applies straight away, including a first one.
Only OAuth access tokens are kept, as described below. They are kept per authUrl, clientId and clientSecret, so changing any of the three makes the next call use a token for the changed client.
OAuth: what authUrl may be
CRS finds your token endpoint with OpenID Connect discovery, so authUrl takes one of two forms:
- your identity provider's issuer address, such as
https://login.example.com. CRS reads the discovery document athttps://login.example.com/.well-known/openid-configuration(the path is appended to any path inauthUrl), and theissuerin that document must equalauthUrl, otherwise the call fails. A trailing slash on a bare host does not matter, buthttps://login.example.com/realm/does not match an issuer ofhttps://login.example.com/realm. Microsoft Entra ID (login.microsoftonline.com) and Azure AD B2C (*.b2clogin.com) issuers are compared by those providers' own discovery rules. - the address of the discovery document itself, any URL containing
/.well-known/, such ashttps://login.example.com/.well-known/openid-configuration. CRS reads it as it is and does not compare the issuer.
🛑 A token endpoint is not accepted as authUrl. CRS always reads a discovery document first and requests tokens from the token_endpoint it names. An identity provider without OpenID Connect discovery cannot be used. (The logistics proxy configuration does accept a token endpoint; CRS does not.)
More rules for the identity provider:
authUrland the discovered token endpoint must both behttps.- The discovery document is read without following redirects, so
authUrlmust point at it directly. - Reading the discovery document and requesting a token each have a time limit of 30 seconds.
Requesting the token
- Grant: client credentials (
grant_type=client_credentials). Noscopeor other parameter is sent. - Client authentication: HTTP Basic on the token request, with
clientIdandclientSecretform-URL-encoded before they are joined, as OAuth 2.0 specifies. - Response: a JSON object with a non-empty
access_token, atoken_typeofBearer(in any case) and anexpires_inin seconds. A response without them fails the call.
If the discovery document or a token cannot be fetched, the call fails with a 5xx error and your system is not called.
Reusing the token
A token is reused until 1 minute before it expires. Then CRS fetches another one. A token with an expires_in of 60 seconds or less is never reused, so every call fetches one.
- ⚠ A token you revoke at your identity provider keeps being sent until it nears expiry. A
401from your system does not make CRS fetch another token, and the call is not repeated: if the401has a JSON body, the caller gets that401. - Your identity provider may see several token requests for the same client close together, and several valid tokens may be in use at the same time.
What every call to your system carries
Every forwarded call is a POST to baseUrl plus the operation's path, with a JSON body. CRS sets these headers:
| Header | Value |
|---|---|
Authorization | Basic with base64 of username:password (the raw values, not URL-encoded), or Bearer with a token from your identity provider |
Content-Type | application/json |
Tenant-Id | the tenant the call is made for, taken from the caller's token. Match header names case-insensitively |
CRS sends no correlation id, no idempotency key, and nothing about the store or workstation. The standard headers of the HTTP client, such as Host, Content-Length, User-Agent, Accept and Accept-Encoding, are also present.
Each call is made once. CRS does not repeat a call that fails.
Do not redirect. Redirects are followed, but a 301, 302 or 303 turns the POST into a GET without a body, and a redirect to another host drops the Authorization header. Point baseUrl at the final address.
Time limits. CRS sets no time limit of its own on the call to your system, so a slow answer keeps the caller, often a cashier at the till, waiting for it. A call that fails at the network level, including a timeout in the HTTP client, reaches the caller as 502 Bad Gateway, and a very slow one can also end as 504 Gateway Timeout. The OAuth limits above apply to your identity provider.
What the caller gets for each answer your system can give is on Tenant system endpoints.
If you know the logistics proxy configuration
The two configurations look alike but behave differently:
| CRS proxy configuration | Logistics proxy configuration | |
|---|---|---|
| Address | https://crs-api.retailsvc.com/api/v1/proxy-config | https://logistics-proxy-config.retailsvc.com/api/v1/proxy-config?service=... |
| Scope | one per tenant, no service key | one per tenant and service key |
| Address of your system | top-level baseUrl | apiConfig.url |
| Permissions | crs.proxy-config.write, crs.proxy-config.read | lpc.proxy-config.set, lpc.proxy-config.get |
| Malformed address | accepted, fails at call time | refused with 400 |
authUrl | issuer or discovery document only | issuer or token endpoint |
GET returns | baseUrl and type only | the configuration, secrets masked |
| Partial update | no: resend the credentials every time | yes, by sending back the masked secret |
| Switching off | not possible | leave out apiConfig |
| A change applies | on the next call | within 10 minutes |
401 from your system | passed to the caller, not repeated | new token fetched, call repeated once |