Business Accounts API
/api/v1/businessAccounts is the v1 surface for the account hierarchy:
Customer → Customer Account (CA) → Billing Account (BA) → User
Account (UA). It has two shapes. The root resource
(/businessAccounts, /businessAccounts/{code}) creates or updates a full
hierarchy in one call, down to whatever hierarchyType depth you ask for.
The level endpoints (this page's focus) target one tier alone, for the
edits the flattened root cannot express: adding a single CA/BA/UA under an
existing parent, or editing one tier without echoing (and re-validating) the
others.
For the hierarchy shape and each tier's role, see The account hierarchy in the API introduction.
Prerequisites
A bearer token, per Authentication:
export TOKEN=$(curl -s -X POST "$KEYCLOAK_TOKEN_URL" \
-d "grant_type=client_credentials&client_id=$KEYCLOAK_CLIENT_ID&client_secret=$KEYCLOAK_CLIENT_SECRET" \
| python3 -c "import sys,json; print(json.load(sys.stdin)['access_token'])")
Level endpoints
Each level endpoint is a targeted PUT, keyed on that tier's own code, not
the root hierarchy code. All of them share the update semantics from API
principles: a field
that is null or absent keeps its current value; an empty value for the
type ("", []) clears it.
| Method | Path | Tier |
|---|---|---|
PUT | /api/v1/businessAccounts/customers/{code} | Customer |
PUT | /api/v1/businessAccounts/customerAccounts/{caCode} | Customer Account |
PUT | /api/v1/businessAccounts/billingAccounts/{baCode} | Billing Account |
PUT | /api/v1/businessAccounts/userAccounts/{uaCode} | User Account |
Customer node
PUT /api/v1/businessAccounts/customers/{code} edits the root Customer node
alone: identity, address, contact information, category/brand/seller/parent,
legal entity type, VAT and registration numbers, external references, and
custom fields. It closes a gap the other three level endpoints didn't have:
before this endpoint shipped, changing just the Customer tier meant going
through the full-hierarchy root resource, which also validates and can
re-touch the CA/BA/UA sections underneath it.
There is no POST for this tier (creating a Customer is the root
resource's job) and no GET (reads go through the Query
API, e.g. GET /api/v1/query/customer?code=TEST_A).
Request fields:
| Field | Type | Description |
|---|---|---|
description | string | Customer description. |
isCompany | boolean | Whether this customer is a company. |
companyName | string | Company name, when isCompany is true. See divergence 2 below; not a plain update-in-place field. |
jobTitle | string | Job title of the contact person. |
address | object | title, firstName, lastName, address1, address2, address3, city, zipCode, state, country. |
contactInformation | object | email, phone, mobile, fax. |
customerCategory | string | Category code. See divergence 1: back-filled when omitted, so it never causes a spurious 400. |
customerBrand | string | Customer brand code. |
seller | string | Seller code. |
parentCustomer | string | Parent customer code, for reparenting within the Customer tier. |
legalEntityType | string | Legal form code. |
vatNo | string | VAT registration number. |
registrationNo | string | Registration number. See divergence 3: read-back is a derived display value, not an echo of the last write. |
externalRef1 / externalRef2 | string | Free-form external references. |
customFields | object | Flat map of custom field code to value. |
updatedCode | string | New code to rename this customer to. Absent means no rename. |
The code in the request body, if sent, is ignored; the path parameter
identifies the target.
Example: partial update.
curl -X PUT -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"externalRef1": "SMOKE-REF-1", "vatNo": "FR99999999901"}' \
https://acme.billerang.com/api/v1/businessAccounts/customers/TEST_A
Response (200), live-verified, only the two sent fields change and every
other field is untouched:
{
"code": "TEST_A",
"description": "TEST A",
"isCompany": true,
"address": {
"lastName": "TestA",
"address1": "A1",
"city": "Brussels",
"zipCode": "1000",
"country": "BE"
},
"contactInformation": {
"email": "x@y.be",
"phone": "+321"
},
"customerCategory": "B2M",
"seller": "PLUXEE_BE",
"vatNo": "FR99999999901",
"externalRef1": "SMOKE-REF-1"
}
Example: clear fields. Sending an empty string clears a scalar field instead of leaving it as-is:
curl -X PUT -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"externalRef1": "", "vatNo": ""}' \
https://acme.billerang.com/api/v1/businessAccounts/customers/TEST_A
Response (200), live-verified, both fields cleared:
{
"code": "TEST_A",
"description": "TEST A",
"isCompany": true,
"customerCategory": "B2M",
"seller": "PLUXEE_BE"
}
Example: rename. Set updatedCode to change the customer's functional
code; every subsequent call (and the response) uses the new code:
curl -X PUT -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"updatedCode": "TEST_A_RENAMED"}' \
https://acme.billerang.com/api/v1/businessAccounts/customers/TEST_A
Errors:
| Status | error_code | Cause |
|---|---|---|
400 | missing-parameter | The {code} path parameter is blank. |
400 | invalid-parameter | A referenced setup entity failed validation, or the legacy layer rejected the payload. |
404 | resource-not-found | No customer with that code, or a referenced setup entity (category, brand, seller, legal entity type) does not exist. |
500 | internal-error | Unexpected server error. |
Error body shape:
{
"status": "FAIL",
"message": "No customer with code NO_SUCH_CUST",
"error_code": "resource-not-found"
}
Live-verified example, unknown code:
curl -X PUT -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"externalRef1": "x"}' \
https://acme.billerang.com/api/v1/businessAccounts/customers/NO_SUCH_CUST
{
"status": "FAIL",
"message": "No customer with code NO_SUCH_CUST",
"error_code": "resource-not-found"
}
The same 404/resource-not-found shape covers both cases (unknown
customer, unknown referenced setup entity); the message text is what
distinguishes them.
Divergences from standard update semantics
The Customer node endpoint follows the platform's null-keeps/empty-clears
rule (see API principles)
with three documented exceptions, all inherited from the legacy
CustomerApi/AccountEntityApi layer it wraps:
-
customerCategoryis back-filled when omitted. The legacy update call requirescustomerCategoryon every request and rejects a missing value as a validation error. Since that breaks partial-update semantics for this one field, the endpoint reads the customer's existing category and fills it in automatically before calling the legacy layer whenever the request omitscustomerCategory. In practice: you never need to sendcustomerCategoryjust to satisfy this requirement, and omitting it never changes it. -
companyNameis write-once through the legacy path. The legacy layer only appliescompanyNamewhen the customer has noAdditionalDetailssub-object yet, i.e. the first time that sub-object is created. SendingcompanyNameon a customer that already hasAdditionalDetailspopulated is not guaranteed to update the stored value, even though the request is accepted and returns200. -
registrationNoon read is a derived, comma-joined display value. The legacyCustomer.getRegistrationNo()accessor concatenates all registration numbers held on the customer, not just the single value last written through this endpoint. Do not treat the value returned in the response as an exact echo of your lastregistrationNowrite; treat it as a display string.
See also
- API principles for update semantics, the error model, and traceability, shared by every v1 resource.
- The account hierarchy for how Customer, Customer Account, Billing Account, and User Account relate.
- The full request/response schemas (
CustomerLevelV1,CustomerAccountLevelV1,BillingAccountLevelV1,UserAccountLevelV1,BusinessAccountV1) are in the Interactive API Explorer.