Admin - Groups

Groups collect users within one tenant so you can share apps, workflows and agents with all of them at once. Use these endpoints to list, create, rename, re-staff and delete groups in the tenants your MSP manages.

Who Can Call These Endpoints

The API key must belong to a user on your MSP (admin.hatz.ai) account whose role includes the Manage Groups permission. Among the default MSP roles, Primary Admin, Admin, Helpdesk, Tenant Manager and Compliance Manager include it; Billing Manager and Sales User do not. A custom role includes it only if you added it.

Creating a group (POST) also needs the Create Groups permission. The default roles above that have Manage Groups also have Create Groups; a custom role with Manage Groups but not Create Groups gets a 400 on create.

Caller Result
MSP user with Manage Groups Can manage groups in your MSP's own company and in every tenant your MSP manages
MSP user without Manage Groups 403 with "Insufficient permissions to manage groups"
Tenant (company) user, including a tenant admin 403 with "Insufficient permissions"
Missing or invalid API key 401

Groups in tenants that belong to another MSP are never returned or changed.

IDs

  • Groups use HashIDs such as group_X7VAK72dp5nX3Cd6JXwdPg6OxD.
  • Members are user HashIDs (user_...). Get them from Get Tenant Users (GET /v1/admin/tenants/{tenant_id}/users) on the Admin - Tenant page.
  • tenant_id is the tenant ID returned by List Tenants (GET /v1/admin/tenants), for example tenant_abc123.
  • Workshop items are identified by their UUID and an item_type of app, workflow or agent.

List Groups

List groups, newest first. With tenant_id, only that tenant's groups are returned. Without it, the list covers every tenant you manage, plus your MSP's own company.

Endpoint

GET /v1/admin/groups

Query Parameters

  • tenant_id (optional): Tenant ID to filter by (e.g., "tenant_abc123")
  • limit (optional): Number of groups to return (1-100, default: 50)
  • offset (optional): Number of groups to skip (default: 0)
  • cursor (optional): The next_cursor from the previous page
  • search_term (optional): Case-insensitive substring match on the group name
  • name (optional): Exact, case-sensitive group name

Pagination

Pass the next_cursor from one response as cursor on the next request, keeping the same filters and limit. Stop when next_cursor is null.

The cursor is the last group's created_at. If several groups share the same created_at and a page ends among them, the next page skips the rest of them. When you need every group, page with limit and offset instead; results are ordered by created_at and then id, so offsets are stable while the groups don't change.

next_cursor is returned whenever a page is full, so the final request can return an empty groups array.

Use cursor or offset, not both. If you send both, offset skips groups after the cursor is applied.

Example Query

curl 'https://ai.hatz.ai/v1/admin/groups?tenant_id=tenant_abc123&limit=2' \
  -H "X-API-Key: $HATZ_API_KEY"

Response

{
  "groups": [
    {
      "id": "group_9OYJl94zb6glNS0wPZxQG8XYQl",
      "name": "Finance",
      "description": null,
      "tenant_id": "tenant_abc123",
      "owner": {
        "id": "user_gE4JlrAxlgKXEfvA3jZexy6Pxq",
        "email": "admin@yourmsp.com",
        "first_name": "Alex",
        "last_name": "Admin"
      },
      "members": [
        {
          "id": "user_766Epo4MVqxZeTJA8gL3Zmy31e",
          "email": "jordan@example.com",
          "first_name": "Jordan",
          "last_name": "Lee"
        }
      ],
      "workshop_items": [],
      "created_at": "2026-11-05T20:02:43.159868+00:00",
      "updated_at": "2026-11-05T20:02:43.159868+00:00"
    },
    {
      "id": "group_Xx9Xlk30byXXnSmrlZjBOmdY93",
      "name": "Support",
      "description": null,
      "tenant_id": "tenant_abc123",
      "owner": {
        "id": "user_gE4JlrAxlgKXEfvA3jZexy6Pxq",
        "email": "admin@yourmsp.com",
        "first_name": "Alex",
        "last_name": "Admin"
      },
      "members": [],
      "workshop_items": [],
      "created_at": "2026-11-05T20:02:42.936913+00:00",
      "updated_at": "2026-11-05T20:02:42.936913+00:00"
    }
  ],
  "next_cursor": "2026-11-05T20:02:42.936913+00:00"
}

Next page:

curl 'https://ai.hatz.ai/v1/admin/groups?tenant_id=tenant_abc123&limit=2&cursor=2026-11-05T20%3A02%3A42.936913%2B00%3A00' \
  -H "X-API-Key: $HATZ_API_KEY"

URL-encode the cursor: it contains : and +.

A tenant_id that is not one of your tenants returns an empty list.

Get Group

Get one group with its owner, members and workshop items.

Endpoint

GET /v1/admin/groups/{group_id}

Path Parameters

  • group_id (required): Group ID (e.g., "group_abc123")

Example Query

curl 'https://ai.hatz.ai/v1/admin/groups/group_X7VAK72dp5nX3Cd6JXwdPg6OxD' \
  -H "X-API-Key: $HATZ_API_KEY"

Response

{
  "id": "group_X7VAK72dp5nX3Cd6JXwdPg6OxD",
  "name": "Sales",
  "description": "Sales team",
  "tenant_id": "tenant_abc123",
  "owner": {
    "id": "user_gE4JlrAxlgKXEfvA3jZexy6Pxq",
    "email": "admin@yourmsp.com",
    "first_name": "Alex",
    "last_name": "Admin"
  },
  "members": [
    {
      "id": "user_n55abP89ZowO6iKAx7vqWjrqnZ",
      "email": "sam@example.com",
      "first_name": "Sam",
      "last_name": "Rivera"
    },
    {
      "id": "user_ExxypMkWV0PLDUAyLpdJlOEJ69",
      "email": "taylor@example.com",
      "first_name": "Taylor",
      "last_name": "Kim"
    }
  ],
  "workshop_items": [
    {
      "id": "17a1718a-4be8-42db-bbc2-36f345e53ef2",
      "item_type": "app"
    },
    {
      "id": "5143da6d-c5de-4c2f-a0b6-3df4d0e542f0",
      "item_type": "agent"
    }
  ],
  "created_at": "2026-11-05T20:02:42.715415+00:00",
  "updated_at": "2026-11-05T20:02:42.715415+00:00"
}

Workflows are returned with item_type app. You can send them back as app or workflow.

Create Group

Create a group in a tenant, optionally with members and workshop items. The user who owns the API key becomes the group's owner.

Endpoint

POST /v1/admin/groups

Request Body

{
  "name": "string",                    // Required: 3-72 characters; letters, numbers and spaces only
  "description": "string",             // Optional
  "tenant_id": "string",               // Optional: Tenant ID (e.g., "tenant_abc123"). Defaults to your MSP's own company
  "members": ["string"],               // Optional: User IDs (e.g., "user_abc123"), all from the same tenant
  "workshop_items": [                  // Optional: Apps, workflows or agents to share with the group
    {
      "id": "string",                  // Workshop item UUID
      "item_type": "string"            // "app", "workflow" or "agent"
    }
  ]
}

Always pass tenant_id when you create a group for a customer. Without it, the group is created in your MSP's own company, not in a tenant.

Group names don't have to be unique: creating a second group with an existing name succeeds and returns a new id.

Example Query

curl 'https://ai.hatz.ai/v1/admin/groups' \
  -H "X-API-Key: $HATZ_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Sales",
    "description": "Sales team",
    "tenant_id": "tenant_abc123",
    "members": ["user_n55abP89ZowO6iKAx7vqWjrqnZ", "user_ExxypMkWV0PLDUAyLpdJlOEJ69"]
  }'

Response

201 Created, with the new group in the same shape as Get Group.

Status codes

Status Meaning
201 The group was created.
400 The group could not be created: for example, the caller lacks Create Groups, a member is not in the tenant, a workshop item was not found or can't be shared with the tenant, or tenant_id is not a tenant you manage. error.message describes the problem.
422 The body is invalid: a name outside 3-72 characters or with other characters, a tenant_id that isn't tenant_ followed by letters and digits, or a member ID not starting with user_.

Update Group

Change a group's name, description, members or workshop items. Send only the fields you want to change; fields you leave out keep their current values.

members replaces the whole list. The group ends up with exactly the users you send, and anyone not in the list is removed. Send "members": [] to remove everyone. To add or remove one person, read the group with Get Group, take the id of each object in its members, change that list of IDs, and send all of it back as members. Updates are not locked or merged, so don't update the same group from two places at once. workshop_items works the same way.

If the tenant syncs groups from an identity provider, see Groups Managed by SCIM before changing members.

Endpoint

PATCH /v1/admin/groups/{group_id}

Path Parameters

  • group_id (required): Group ID (e.g., "group_abc123")

Request Body

{
  "name": "string",                    // Optional: 3-72 characters; letters, numbers and spaces only
  "description": "string",             // Optional
  "members": ["string"],               // Optional: Replaces all members. User IDs from the group's tenant
  "workshop_items": [                  // Optional: Replaces all workshop items
    {
      "id": "string",
      "item_type": "string"            // "app", "workflow" or "agent"
    }
  ]
}

Example Query

Rename a group without touching its members:

curl -X PATCH 'https://ai.hatz.ai/v1/admin/groups/group_X7VAK72dp5nX3Cd6JXwdPg6OxD' \
  -H "X-API-Key: $HATZ_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"name": "Sales EMEA"}'

Set the group's members to exactly one user:

curl -X PATCH 'https://ai.hatz.ai/v1/admin/groups/group_X7VAK72dp5nX3Cd6JXwdPg6OxD' \
  -H "X-API-Key: $HATZ_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"members": ["user_766Epo4MVqxZeTJA8gL3Zmy31e"]}'

Response

200 OK, with the updated group in the same shape as Get Group.

Status codes

Status Meaning
200 The group was updated. An empty body ({}) changes nothing and returns the group.
400 The group could not be updated: for example, a member is not in the group's tenant, or a workshop item was not found. error.message describes the problem.
422 The body is invalid, for example a name with characters other than letters, numbers and spaces, or an unknown item_type.

Delete Group

Delete a group. Its memberships and workshop item shares are removed with it; the users, apps, workflows and agents themselves are not affected.

Endpoint

DELETE /v1/admin/groups/{group_id}

Path Parameters

  • group_id (required): Group ID (e.g., "group_abc123")

Example Query

curl -X DELETE 'https://ai.hatz.ai/v1/admin/groups/group_X7VAK72dp5nX3Cd6JXwdPg6OxD' \
  -H "X-API-Key: $HATZ_API_KEY"

Response

{
  "message": "Group deleted successfully",
  "group_id": "group_X7VAK72dp5nX3Cd6JXwdPg6OxD"
}

Errors

Errors use this shape:

{
  "error": {
    "type": "HTTPException",
    "message": "Insufficient permissions to manage groups",
    "details": {},
    "path": "/v1/admin/groups"
  }
}
Status Meaning
400 A malformed group_id or tenant_id, or a create or update the platform refused. error.message describes the problem; its wording may change, so don't parse it.
401 Missing or invalid API key.
403 The API key's user is not an MSP user, or lacks the Manage Groups permission.
422 Invalid query parameters or body. error.details.errors lists each field.

A group that doesn't exist, has been deleted, or is in a tenant your MSP doesn't manage is never returned or changed. Don't rely on a specific status code for that case on GET, PATCH or DELETE of a single group.

Groups Managed by SCIM

If a tenant syncs users and groups from an identity provider (for example Microsoft Entra ID or Okta) through SCIM, the groups it creates appear here like any other group, with nothing marking them as synced. If you rename one or change its members through this API, the next sync from the identity provider can overwrite your change. Manage SCIM-synced groups in the identity provider instead.

Groups created by SCIM have no owner: owner.id is an empty string and the other owner fields are null.