Partner API (#19245)¶
Documentation of the softera API endpoints implemented/extended within ticket #19245: creating a partner organisation, searching organisations, CRUD of LOCCAT catalog locations, changing an organisation's GLN.
General¶
- Base URL:
https://api-test.docura.net/ap1/partner/api/ - Authentication: the header
X-Authorization: <api_key>(a user token, theapi_keyfield in theuserstable). - The
X-App-Keyheader is not used by clients — on the backend this check is currently not connected to any filter chain (dead code since 2023), so it may be omitted. - Format — JSON (except where explicitly stated otherwise).
- Roles (authority):
SYSTEM_ADMIN,SYSTEM_SUPPORT,CLIENT_ADMIN,CLIENT_EDITOR,PROVIDER_ADMIN— for the specific allowed roles see the description of each endpoint.
1. Creating a partner organisation¶
POST /user/organization¶
Creates a new organisation and adds the calling user to it as CLIENT_ADMIN (through a separate, earlier step in the creation pipeline).
Rights: no role restrictions — available to any authenticated user. The rights are expressed not through the caller's role but through which organisation the caller itself belongs to (see the operatorId logic below).
Request body:
| Field | Type | Required | Constraints |
|---|---|---|---|
| taxId | string | yes | 2–20 chars |
| registrationNumber | string | yes | 2–50 chars |
| name | string | yes | 2–50 chars |
| countryCode | string | yes | 2 chars |
| locale | string | yes | 2 chars, must exist in the locale reference list |
| errorEmail | string | no | up to 250 chars, email format |
| notes | string | no | up to 255 chars |
| phones | string | no | up to 50 chars |
| string | no | up to 150 chars, email format |
The integrationProfileType field, which appeared in earlier iterations of this feature within the same ticket, was removed from the final version — the client cannot choose the profile/connectionType explicitly (see below).
Response (200): the same set of fields + id (Long), iln (the generated GLN of the organisation).
Errors:
409 Conflict(organization.create.conflict.tax_id) — an organisation with thistaxIdalready exists.400— invalid request body fields (bean validation), or the locale was not found (not.exists.specified.organization.locale).
Logic for determining the operator (operatorId) of the new organisation¶
It is calculated from the organisation of the CALLING user itself (callerIln — the ILN of the current organisation of the session), with no explicit parameters in the request:
- If the caller's organisation is softera itself (
profile.softeraOrganizationIln) → the operator of the new organisation = softera itself. Additionally, in this case the default softera user (profile.softeraDefaultUserId, if set) is added to the new organisation asCLIENT_ADMIN. - If the caller's organisation is a provider (
role='P', but not softera) → the operator = the caller's own organisation. - Otherwise (neither softera nor a provider) → the operator = the organisation from
profile.defaultOperatorOrganizationIln(fallback to the "default operator"). - If neither the caller's organisation nor the fallback organisation is found by ILN —
500(IllegalStateException, should not normally happen).
Logic of connectionType for the new organisation¶
- Caller is softera → always
Payment(hard-coded, regardless of anything). - Otherwise →
connectionType= theconnectionTypeof the operator organisation itself (the one calculated above — the provider or the fallback organisation). The client cannot specifyconnectionType/the integration profile explicitly.
2. Searching organisations¶
GET /organizations/search?query=<string>¶
Rights: SYSTEM_ADMIN, SYSTEM_SUPPORT, PROVIDER_ADMIN. There is no restriction by operatorId — a provider sees results across the whole organisation database, not only its own clients (a deliberate decision taken within this ticket).
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| query | string | yes | Search string. An empty/missing string → an empty list without an error. |
Search logic in the DB — a single OR over the organisation:
- exact match on
id(as text); - exact match on
iln(GLN); - exact match on
regNr(case-insensitive); - exact match on
vatNr(case-insensitive); - substring (
ILIKE) in the organisation'sname; - substring (
ILIKE) in thenameof any of its LOCCAT locations.
Response (200): an array, each element wrapped in an organization object:
[
{
"organization": {
"id": 100099815,
"name": "New brand sof",
"iln": "2000005810008",
"taxId": "...",
"registrationNumber": "...",
"countryCode": "EE",
"locations": [ { "id": ..., "iln": "...", "name": "...", "formattedAddress": "...", ... } ]
}
}
]
Important nuance: locations[] contains only those locations whose own name matched the search string — not all locations of the found organisation. If the organisation was found by id/iln/regNr/vatNr/name, and not by a location name, locations will be an empty array. This is deliberate behaviour, not a bug.
3. LOCCAT catalog locations of an organisation¶
Base path: /organization/{gln}/locations, where {gln} is the organisation's GLN (not the numeric id).
Rights (common to all three methods below):
SYSTEM_ADMIN/SYSTEM_SUPPORT— no restrictions.PROVIDER_ADMIN— only if the caller's organisation = theoperatorIdof the target organisation (a provider manages the locations of the organisations it operates itself).- Other roles —
403(not.have.access). - Organisation not found by GLN →
400(organization.not.exists).
3.1 POST /organization/{gln}/locations — create/update a location¶
Upsert logic:
- If
idis passed in the body — an explicit update of an existing location by its internal id (it must belong to the specified organisation, otherwise404). - If
idis not passed butilnis — an existing location of the organisation with thatilnis looked up; if found it is updated, if not a new one is created. - If
ilnis not passed either (or is "empty" according toIlnHelper.isEmpty) — the location's GLN is generated automatically (see below).
Request body (LoccatLocationWithIdData — LoccatLocationData + optional id):
| Field | Type | Required | Constraints |
|---|---|---|---|
| id | Long | no | for an explicit update |
| iln | string | no | up to 13 chars; when absent — auto-generated |
| codeBySender | string | yes | up to 70 chars |
| codeByReceiver | string | no | up to 70 chars |
| name | string | yes | up to 175 chars |
| legal | boolean | no | the "legal address" flag |
| formattedAddress | string | no | up to 300 chars |
| streetAndNumber | string | no | up to 140 chars |
| streetAndNumber2 | string | no | up to 140 chars |
| city | string | no | up to 175 chars |
| state | string | no | up to 175 chars |
| postalCode | string | no | up to 9 chars |
| countryCode | string | no | up to 2 chars |
| latitude / longitude | double | no | |
| remarks | string | no | up to 350 chars |
| phoneNumber | string | no | up to 175 chars |
| string | no | up to 350 chars, email format | |
| web | string | no | up to 175 chars |
Response (200): the same body + the id of the created/updated location.
Auto-generation of a location GLN (when iln is not passed)¶
It reuses the legacy algorithm UserWebController.getNextLocalizationCatalogGln (core):
- If the organisation already has locations (
maxIlnfound) → the next GLN = the body of the last generated GLN + 1. - If there are no locations and the organisation is a test one (
isTest) → the GLN is built from the organisation'sglnPrefixpadded with zeros. - If there are no locations, the organisation is not a test one and there is no "legal" location → the organisation's own GLN is returned.
- If there is a legal location but no others (edge case) → the GLN is built from the
glnPrefix.
Additionally (a change from this ticket): if the GLN generated in this way coincides with the organisation's own GLN (branches 2 and 3 above) — it is incremented by 1, so that it never duplicates the organisation's GLN.
Known limitations (accepted as they are, without additional protection):
- There is no transactional lock between "get the next GLN" and "create a location with it" — with parallel requests without
ilna race is possible and400 duplicate.line.iln. - An increment on overflow (the GLN body =
999999999999) could theoretically produce a GLN one character longer than normal — an extremely unlikely edge case, no fix was requested. - The
legalflag is NOT set automatically on the first location created withoutiln— if the organisation has no legal location, a repeatedPOSTwithoutilnwill try to generate the same organisation GLN again and fail with400 duplicate.line.iln. The client must either passilnexplicitly on the second and subsequent requests, or managelegalitself.
3.2 GET /organization/{gln}/locations — get locations¶
Specifics: the filter is passed in the body of the GET request (JSON); these are not query parameters. If the body is empty (Content-Length <= 0) — all locations of the organisation are returned.
Request body (optional):
| Field | Type | Description |
|---|---|---|
| id | Long | exact search by the location id (plus an optional check against iln, if passed) |
| iln | string | filter by GLN |
| name | string | filter by name (substring) |
| address | string | filter by address (substring) |
Priority: if id is passed — (at most) one location by id is returned; otherwise if at least one of iln/name/address is set — a filtered search; otherwise — all locations of the organisation.
Response (200): an array of locations (the same format as in the POST response).
3.3 DELETE /organization/{gln}/locations¶
Request body (mandatory):
| Field | Type | Required |
|---|---|---|
| id | Long | yes |
Deletion only by the internal location id; deletion by iln is not implemented (it was considered and rolled back within this ticket).
Response: 200 on success, 404 if a location with that id is not found in the organisation, 400 location.id.should.be.specified if id is not passed.
4. Changing an organisation's GLN¶
PATCH /organization/{organizationId}/gln?gln=<new GLN>¶
Note: here {organizationId} is the numeric internal id of the organisation (unlike the location endpoints above, which take a GLN in the path).
Rights:
SYSTEM_ADMIN/SYSTEM_SUPPORT— no restrictions.PROVIDER_ADMIN— only if the caller's organisation = theoperatorIdof the target organisation.- Others —
403 not.have.access. - Organisation not found by id →
404(empty body).
Parameters:
| Parameter | Type | Required |
|---|---|---|
| gln | string | yes, query parameter |
What happens inside (the standard organisation update pipeline is reused, OrganizationUpdateEvent → OrganizationCommonUpdateHandler):
- Validation of the GLN format + check digit →
400on error. - Duplicate check — the GLN must not belong to another organisation →
400 Duplicate ILN. - The old value is saved in
previousIln. - If the organisation has a "legal" LOCCAT location — its
ilnis automatically synchronised with the organisation's new GLN (loccatReconciliation). If there is no legal location — the step is skipped silently. - The standard reconciliation steps of the organisation update (partner/condition/notifications etc., not specific to this feature).
Response (200): the organisation body (OrganizationWithIdData) with the new GLN already applied.
5. Summary table of endpoints¶
| Method | Path | Path identifier | Roles |
|---|---|---|---|
| POST | /user/organization | — | any authenticated |
| GET | /organizations/search | — | SYSTEM_ADMIN, SYSTEM_SUPPORT, PROVIDER_ADMIN |
| POST | /organization/{gln}/locations | GLN | SYSTEM_ADMIN, SYSTEM_SUPPORT, PROVIDER_ADMIN (operatorId-match) |
| GET | /organization/{gln}/locations | GLN | SYSTEM_ADMIN, SYSTEM_SUPPORT, PROVIDER_ADMIN (operatorId-match) |
| DELETE | /organization/{gln}/locations | GLN | SYSTEM_ADMIN, SYSTEM_SUPPORT, PROVIDER_ADMIN (operatorId-match) |
| PATCH | /organization/{organizationId}/gln | numeric id | SYSTEM_ADMIN, SYSTEM_SUPPORT, PROVIDER_ADMIN (operatorId-match) |
6. Examples¶
# Create an organisation
curl --location --request POST 'https://api-test.docura.net/ap1/partner/api/user/organization' \
--header 'X-Authorization: <token>' \
--header 'Content-Type: application/json' \
--data '{
"taxId": "EE100000001",
"registrationNumber": "12345678",
"name": "Example OU",
"countryCode": "EE",
"locale": "en"
}'
# Search organisations
curl --location 'https://api-test.docura.net/ap1/partner/api/organizations/search?query=Example' \
--header 'X-Authorization: <token>'
# Create a location without an explicit GLN (auto-generation)
curl --location --request POST 'https://api-test.docura.net/ap1/partner/api/organization/<org_gln>/locations' \
--header 'X-Authorization: <token>' \
--header 'Content-Type: application/json' \
--data '{
"name": "Warehouse 1",
"codeBySender": "WH1",
"countryCode": "EE"
}'
# Get all locations of an organisation
curl --location 'https://api-test.docura.net/ap1/partner/api/organization/<org_gln>/locations' \
--header 'X-Authorization: <token>'
# Delete a location by id
curl --location --request DELETE 'https://api-test.docura.net/ap1/partner/api/organization/<org_gln>/locations' \
--header 'X-Authorization: <token>' \
--header 'Content-Type: application/json' \
--data '{"id": 682021}'
# Change an organisation's GLN
curl --location --request PATCH 'https://api-test.docura.net/ap1/partner/api/organization/<numeric_id>/gln?gln=2000005819995' \
--header 'X-Authorization: <token>'