Skip to main content

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.

MethodPathTier
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:

FieldTypeDescription
descriptionstringCustomer description.
isCompanybooleanWhether this customer is a company.
companyNamestringCompany name, when isCompany is true. See divergence 2 below; not a plain update-in-place field.
jobTitlestringJob title of the contact person.
addressobjecttitle, firstName, lastName, address1, address2, address3, city, zipCode, state, country.
contactInformationobjectemail, phone, mobile, fax.
customerCategorystringCategory code. See divergence 1: back-filled when omitted, so it never causes a spurious 400.
customerBrandstringCustomer brand code.
sellerstringSeller code.
parentCustomerstringParent customer code, for reparenting within the Customer tier.
legalEntityTypestringLegal form code.
vatNostringVAT registration number.
registrationNostringRegistration number. See divergence 3: read-back is a derived display value, not an echo of the last write.
externalRef1 / externalRef2stringFree-form external references.
customFieldsobjectFlat map of custom field code to value.
updatedCodestringNew 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:

Statuserror_codeCause
400missing-parameterThe {code} path parameter is blank.
400invalid-parameterA referenced setup entity failed validation, or the legacy layer rejected the payload.
404resource-not-foundNo customer with that code, or a referenced setup entity (category, brand, seller, legal entity type) does not exist.
500internal-errorUnexpected 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:

  1. customerCategory is back-filled when omitted. The legacy update call requires customerCategory on 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 omits customerCategory. In practice: you never need to send customerCategory just to satisfy this requirement, and omitting it never changes it.

  2. companyName is write-once through the legacy path. The legacy layer only applies companyName when the customer has no AdditionalDetails sub-object yet, i.e. the first time that sub-object is created. Sending companyName on a customer that already has AdditionalDetails populated is not guaranteed to update the stored value, even though the request is accepted and returns 200.

  3. registrationNo on read is a derived, comma-joined display value. The legacy Customer.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 last registrationNo write; 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.