Skip to content
Updated Sep 26, 2026 by Barča Dvořáková · Owner: analysisactiveapi-a Edit on GitHub

Client Groups — API Analysis ​

Seven endpoints on the platform administration application, following the pattern the other cross-tenant catalogues use (see VAT Rate Administration): a flat collection under v1/admin, no paging (the catalogue and any one group's membership are both small), writes that return the identifier only.

The project has no shared API-conventions document yet, so the two envelope shapes these endpoints use are stated once here and not repeated per endpoint. Success:

json
{
  "data": {},
  "status": 200,
  "message": "OK",
  "requestId": "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b21"
}

Error:

json
{
  "errorCode": "ERR_CLIENT_GROUP_FULL",
  "errorMessage": "This group already holds its maximum number of clients.",
  "status": 409,
  "requestId": "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b21"
}

Authentication and permission failures are answered by the platform application's shared handling and are not restated per endpoint.

Permission codes: platform.client-groups.read (list groups, read one group's members), platform.client-groups.write (create, rename, resize, delete a group; add or remove a member).

The member picker on the manage-groups screen searches the existing platform client catalogue — see Client Management — API Analysis, GET /v1/clients — rather than a client list documented here; this file only adds the group and membership operations.


📥 GET /v1/admin/client-groups ​

Returns every client group, for the manage-groups screen's list view.

Authorization ​

Requires permission code platform.client-groups.read, platform-scoped.

Request Headers ​

None beyond the platform application's standard bearer authentication.

Request Body ​

None.

Request Parameters ​

None.

Request Logic ​

  • Reads platform.client_group and the active-row count per group from platform.client_group_membership; writes nothing.
sql
SELECT g.id, g.name, g.max_client_count,
       COUNT(m.id) FILTER (WHERE m.deleted_at IS NULL) AS member_count
FROM platform.client_group g
LEFT JOIN platform.client_group_membership m ON m.client_group_id = g.id
WHERE g.deleted_at IS NULL
GROUP BY g.id
ORDER BY g.name ASC;

Transactional Operations ​

N/A — read-only.

✅ Success Response (200) ​

json
{
  "data": [
    {
      "id": "018fa51f-fda1-79f4-8461-2cb8f1cabc20",
      "name": "Plzeňský kraj",
      "maxClientCount": 25,
      "memberCount": 8
    }
  ],
  "status": 200,
  "message": "OK",
  "requestId": "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b31"
}

Response Data Mapping ​

Response FieldSource / Value
idclientGroup.id
nameclientGroup.name
maxClientCountclientGroup.maxClientCount
memberCountDerived: count of active clientGroupMembership rows for this group

❌ Error Responses ​

None beyond the shared authentication/permission handling.


📤 POST /v1/admin/client-groups ​

Creates a new client group.

Authorization ​

Requires permission code platform.client-groups.write, platform-scoped.

Request Headers ​

None beyond the shared bearer authentication.

Request Body ​

json
{
  "name": "Plzeňský kraj",
  "maxClientCount": 25
}

Request Parameters ​

None — body only.

Request Logic ​

  • Inserts one row into platform.client_group.
sql
INSERT INTO platform.client_group (name, max_client_count, created_by, updated_by)
VALUES (@name, @maxClientCount, @actor, @actor)
RETURNING id;

Transactional Operations ​

Single-row insert; no related write. The insert plus its audit-log entry commit together.

✅ Success Response (201) ​

json
{
  "data": { "id": "018fa51f-fda1-79f4-8461-2cb8f1cabc20" },
  "status": 201,
  "message": "Created",
  "requestId": "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b31"
}

Response Data Mapping ​

Response FieldSource / Value
idclientGroup.id, generated in code

❌ Error Responses ​

400 ERR_CLIENT_GROUP_NAME_REQUIRED ​

Thrown when name is missing or empty.

json
{
  "errorCode": "ERR_CLIENT_GROUP_NAME_REQUIRED",
  "errorMessage": "A client group needs a name.",
  "status": 400,
  "requestId": "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b31"
}

400 ERR_CLIENT_GROUP_CAPACITY_REQUIRED ​

Thrown when maxClientCount is missing, not a positive integer, or zero — every group states a real cap, never "unlimited".

json
{
  "errorCode": "ERR_CLIENT_GROUP_CAPACITY_REQUIRED",
  "errorMessage": "maxClientCount must be a positive integer.",
  "status": 400,
  "requestId": "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b31"
}

📝 PATCH /v1/admin/client-groups/:id ​

Renames a group and/or changes its capacity.

Authorization ​

Requires permission code platform.client-groups.write, platform-scoped.

Request Headers ​

None beyond the shared bearer authentication.

Request Body ​

json
{
  "name": "Plzeňský kraj",
  "maxClientCount": 30
}

Both fields optional; only the fields present are changed.

Request Parameters ​

NameInTypeRequiredDefaultDescriptionValidation / NotesDB Mapping
idPathUUIDYes-The group to update.Must reference an active, existing group.clientGroup.id

Request Logic ​

  • Reads the current active member count, then updates platform.client_group.
sql
UPDATE platform.client_group
SET name = COALESCE(@name, name),
    max_client_count = COALESCE(@maxClientCount, max_client_count),
    updated_at = now(),
    updated_by = @actor
WHERE id = @id AND deleted_at IS NULL
RETURNING id;

Transactional Operations ​

The member-count read and the update happen inside the same transaction, so a concurrent membership add cannot slip past the capacity check.

✅ Success Response (200) ​

json
{
  "data": { "id": "018fa51f-fda1-79f4-8461-2cb8f1cabc20" },
  "status": 200,
  "message": "OK",
  "requestId": "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b31"
}

Response Data Mapping ​

Response FieldSource / Value
idclientGroup.id

❌ Error Responses ​

404 ERR_CLIENT_GROUP_NOT_FOUND ​

Thrown when id does not reference an active group.

json
{
  "errorCode": "ERR_CLIENT_GROUP_NOT_FOUND",
  "errorMessage": "Client group not found.",
  "status": 404,
  "requestId": "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b31"
}

409 ERR_CLIENT_GROUP_CAPACITY_BELOW_MEMBERS ​

Thrown when the requested maxClientCount is lower than the group's current active member count.

json
{
  "errorCode": "ERR_CLIENT_GROUP_CAPACITY_BELOW_MEMBERS",
  "errorMessage": "maxClientCount cannot be lower than the group's current member count.",
  "status": 409,
  "requestId": "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b31"
}

🗑️ DELETE /v1/admin/client-groups/:id ​

Deletes a client group. Blocked while it holds any active member.

Authorization ​

Requires permission code platform.client-groups.write, platform-scoped.

Request Headers ​

None beyond the shared bearer authentication.

Request Body ​

None.

Request Parameters ​

NameInTypeRequiredDefaultDescriptionValidation / NotesDB Mapping
idPathUUIDYes-The group to delete.Must reference an active, existing group.clientGroup.id

Request Logic ​

  • Checks for any active platform.client_group_membership row before soft-deleting the group.
sql
SELECT COUNT(*) FROM platform.client_group_membership
WHERE client_group_id = @id AND deleted_at IS NULL;

UPDATE platform.client_group
SET deleted_at = now(), updated_at = now(), updated_by = @actor
WHERE id = @id AND deleted_at IS NULL;

Transactional Operations ​

The member-count check and the soft-delete happen inside the same transaction, so a concurrent membership add cannot race a delete into an inconsistent state.

✅ Success Response (200) ​

json
{
  "data": { "id": "018fa51f-fda1-79f4-8461-2cb8f1cabc20" },
  "status": 200,
  "message": "OK",
  "requestId": "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b31"
}

Response Data Mapping ​

Response FieldSource / Value
idclientGroup.id

❌ Error Responses ​

404 ERR_CLIENT_GROUP_NOT_FOUND ​

Thrown when id does not reference an active group.

json
{
  "errorCode": "ERR_CLIENT_GROUP_NOT_FOUND",
  "errorMessage": "Client group not found.",
  "status": 404,
  "requestId": "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b31"
}

409 ERR_CLIENT_GROUP_HAS_MEMBERS ​

Thrown when the group holds at least one active member. The response lists the blocking members so the operator can remove them first.

json
{
  "errorCode": "ERR_CLIENT_GROUP_HAS_MEMBERS",
  "errorMessage": "This group still has members; remove them before deleting the group.",
  "status": 409,
  "requestId": "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b31",
  "memberCount": 8
}

📥 GET /v1/admin/client-groups/:id/members ​

Returns the clients currently in a group, for the group's detail view.

Authorization ​

Requires permission code platform.client-groups.read, platform-scoped.

Request Headers ​

None beyond the shared bearer authentication.

Request Body ​

None.

Request Parameters ​

NameInTypeRequiredDefaultDescriptionValidation / NotesDB Mapping
idPathUUIDYes-The group whose members to list.Must reference an active, existing group.clientGroup.id

Request Logic ​

  • Reads active platform.client_group_membership rows joined to platform.clients.
sql
SELECT c.id, c.name, c.ico, m.id AS membership_id, m.created_at AS joined_at
FROM platform.client_group_membership m
JOIN platform.clients c ON c.id = m.client_id
WHERE m.client_group_id = @id AND m.deleted_at IS NULL
ORDER BY c.name ASC;

Transactional Operations ​

N/A — read-only.

✅ Success Response (200) ​

json
{
  "data": [
    {
      "clientId": "018ed0b3-c298-7c7a-96d5-8b36f5a7f8d2",
      "clientName": "Město Plzeň",
      "clientIco": "00075370",
      "joinedAt": "2026-09-16T00:00:00Z"
    }
  ],
  "status": 200,
  "message": "OK",
  "requestId": "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b31"
}

Response Data Mapping ​

Response FieldSource / Value
clientIdclient.id
clientNameclient.name
clientIcoclient.ico
joinedAtclientGroupMembership.createdAt

❌ Error Responses ​

404 ERR_CLIENT_GROUP_NOT_FOUND ​

Thrown when id does not reference an active group.

json
{
  "errorCode": "ERR_CLIENT_GROUP_NOT_FOUND",
  "errorMessage": "Client group not found.",
  "status": 404,
  "requestId": "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b31"
}

📤 POST /v1/admin/client-groups/:id/members ​

Adds a client to a group.

Authorization ​

Requires permission code platform.client-groups.write, platform-scoped.

Request Headers ​

None beyond the shared bearer authentication.

Request Body ​

json
{
  "clientId": "018ed0b3-c298-7c7a-96d5-8b36f5a7f8d2"
}

Request Parameters ​

NameInTypeRequiredDefaultDescriptionValidation / NotesDB Mapping
idPathUUIDYes-The group to add the client to.Must reference an active, existing group.clientGroup.id

Request Logic ​

  • Reads the group's maxClientCount and current active member count, checks the client isn't already an active member, then inserts.
sql
SELECT max_client_count FROM platform.client_group WHERE id = @id AND deleted_at IS NULL;
SELECT COUNT(*) FROM platform.client_group_membership WHERE client_group_id = @id AND deleted_at IS NULL;
SELECT 1 FROM platform.client_group_membership
WHERE client_group_id = @id AND client_id = @clientId AND deleted_at IS NULL;

INSERT INTO platform.client_group_membership (client_id, client_group_id, created_by, updated_by)
VALUES (@clientId, @id, @actor, @actor)
RETURNING id;

Transactional Operations ​

The capacity check, the duplicate check and the insert happen inside one transaction, so two concurrent adds cannot both slip past a group's last free slot.

✅ Success Response (201) ​

json
{
  "data": { "id": "018fa51f-fda1-79f4-8461-2cb8f1cabc21" },
  "status": 201,
  "message": "Created",
  "requestId": "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b31"
}

Response Data Mapping ​

Response FieldSource / Value
idclientGroupMembership.id, generated in code

❌ Error Responses ​

404 ERR_CLIENT_GROUP_NOT_FOUND ​

Thrown when id does not reference an active group.

json
{
  "errorCode": "ERR_CLIENT_GROUP_NOT_FOUND",
  "errorMessage": "Client group not found.",
  "status": 404,
  "requestId": "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b31"
}

404 ERR_CLIENT_NOT_FOUND ​

Thrown when clientId does not reference an active client.

json
{
  "errorCode": "ERR_CLIENT_NOT_FOUND",
  "errorMessage": "Client not found.",
  "status": 404,
  "requestId": "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b31"
}

409 ERR_CLIENT_ALREADY_IN_GROUP ​

Thrown when the client is already an active member of this group.

json
{
  "errorCode": "ERR_CLIENT_ALREADY_IN_GROUP",
  "errorMessage": "This client is already in the group.",
  "status": 409,
  "requestId": "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b31"
}

409 ERR_CLIENT_GROUP_FULL ​

Thrown when the group's active member count already equals its maxClientCount.

json
{
  "errorCode": "ERR_CLIENT_GROUP_FULL",
  "errorMessage": "This group already holds its maximum number of clients.",
  "status": 409,
  "requestId": "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b31"
}

🗑️ DELETE /v1/admin/client-groups/:id/members/:clientId ​

Removes a client from a group.

Authorization ​

Requires permission code platform.client-groups.write, platform-scoped.

Request Headers ​

None beyond the shared bearer authentication.

Request Body ​

None.

Request Parameters ​

NameInTypeRequiredDefaultDescriptionValidation / NotesDB Mapping
idPathUUIDYes-The group to remove the client from.Must reference an active, existing group.clientGroup.id
clientIdPathUUIDYes-The client to remove.Must reference an active membership in this group.client.id

Request Logic ​

  • Soft-deletes the active membership row for this (group, client) pair.
sql
UPDATE platform.client_group_membership
SET deleted_at = now(), updated_at = now(), updated_by = @actor
WHERE client_group_id = @id AND client_id = @clientId AND deleted_at IS NULL
RETURNING id;

Transactional Operations ​

Single-row update; no related write.

✅ Success Response (200) ​

json
{
  "data": { "id": "018fa51f-fda1-79f4-8461-2cb8f1cabc21" },
  "status": 200,
  "message": "OK",
  "requestId": "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b31"
}

Response Data Mapping ​

Response FieldSource / Value
idclientGroupMembership.id

❌ Error Responses ​

404 ERR_CLIENT_GROUP_MEMBERSHIP_NOT_FOUND ​

Thrown when no active membership matches the (group, client) pair.

json
{
  "errorCode": "ERR_CLIENT_GROUP_MEMBERSHIP_NOT_FOUND",
  "errorMessage": "This client is not a member of this group.",
  "status": 404,
  "requestId": "018f9c4d-2c11-7a3e-9b77-4f0d3a5e6b31"
}