Project

General

Profile

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

  1. Base URL: https://api-test.docura.net/ap1/partner/api/
  2. Authentication: the header X-Authorization: <api_key> (a user token, the api_key field in the users table).
  3. The X-App-Key header 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.
  4. Format — JSON (except where explicitly stated otherwise).
  5. 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
email 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:

  1. 409 Conflict (organization.create.conflict.tax_id) — an organisation with this taxId already exists.
  2. 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:

  1. 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 as CLIENT_ADMIN.
  2. If the caller's organisation is a provider (role='P', but not softera) → the operator = the caller's own organisation.
  3. Otherwise (neither softera nor a provider) → the operator = the organisation from profile.defaultOperatorOrganizationIln (fallback to the "default operator").
  4. 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

  1. Caller is softera → always Payment (hard-coded, regardless of anything).
  2. Otherwise → connectionType = the connectionType of the operator organisation itself (the one calculated above — the provider or the fallback organisation). The client cannot specify connectionType/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:

  1. exact match on id (as text);
  2. exact match on iln (GLN);
  3. exact match on regNr (case-insensitive);
  4. exact match on vatNr (case-insensitive);
  5. substring (ILIKE) in the organisation's name;
  6. substring (ILIKE) in the name of 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):

  1. SYSTEM_ADMIN / SYSTEM_SUPPORT — no restrictions.
  2. PROVIDER_ADMIN — only if the caller's organisation = the operatorId of the target organisation (a provider manages the locations of the organisations it operates itself).
  3. Other roles — 403 (not.have.access).
  4. Organisation not found by GLN → 400 (organization.not.exists).

3.1 POST /organization/{gln}/locations — create/update a location

Upsert logic:

  1. If id is passed in the body — an explicit update of an existing location by its internal id (it must belong to the specified organisation, otherwise 404).
  2. If id is not passed but iln is — an existing location of the organisation with that iln is looked up; if found it is updated, if not a new one is created.
  3. If iln is not passed either (or is "empty" according to IlnHelper.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
email 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):

  1. If the organisation already has locations (maxIln found) → the next GLN = the body of the last generated GLN + 1.
  2. If there are no locations and the organisation is a test one (isTest) → the GLN is built from the organisation's glnPrefix padded with zeros.
  3. 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.
  4. 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):

  1. There is no transactional lock between "get the next GLN" and "create a location with it" — with parallel requests without iln a race is possible and 400 duplicate.line.iln.
  2. 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.
  3. The legal flag is NOT set automatically on the first location created without iln — if the organisation has no legal location, a repeated POST without iln will try to generate the same organisation GLN again and fail with 400 duplicate.line.iln. The client must either pass iln explicitly on the second and subsequent requests, or manage legal itself.

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:

  1. SYSTEM_ADMIN / SYSTEM_SUPPORT — no restrictions.
  2. PROVIDER_ADMIN — only if the caller's organisation = the operatorId of the target organisation.
  3. Others — 403 not.have.access.
  4. 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):

  1. Validation of the GLN format + check digit → 400 on error.
  2. Duplicate check — the GLN must not belong to another organisation → 400 Duplicate ILN.
  3. The old value is saved in previousIln.
  4. If the organisation has a "legal" LOCCAT location — its iln is automatically synchronised with the organisation's new GLN (loccatReconciliation). If there is no legal location — the step is skipped silently.
  5. 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>'