{
  "openapi": "3.1.0",
  "info": {
    "title": "Envoke Group API",
    "version": "2.0.0",
    "description": "CRUD API for managing groups. Groups are used to organise tags, subscriptions, messages, and templates into named categories. Each item type has its own group namespace. Groups support one level of nesting: a top-level group may have subgroups, but subgroups cannot themselves have children. A group cannot be deleted while it has subgroups or assigned items."
  },
  "servers": [
    {
      "url": "https://e1.envoke.com/v2",
      "description": "Production"
    }
  ],
  "security": [
    {
      "basicAuth": []
    }
  ],
  "tags": [
    {
      "name": "Groups",
      "description": "Operations on groups."
    }
  ],
  "components": {
    "securitySchemes": {
      "basicAuth": {
        "type": "http",
        "scheme": "basic",
        "description": "HTTP Basic Auth. Use your API ID as the username and API Key as the password."
      }
    },
    "schemas": {
      "Group": {
        "type": "object",
        "description": "A group definition.",
        "required": [
          "id",
          "name",
          "parent_group"
        ],
        "properties": {
          "id": {
            "type": "string",
            "readOnly": true,
            "description": "Unique identifier assigned on creation. Returned as a string.",
            "examples": [
              "7"
            ]
          },
          "name": {
            "type": "string",
            "description": "The group name.",
            "examples": [
              "Academic Programs"
            ]
          },
          "parent_group": {
            "type": "string",
            "description": "Name of the parent group. Empty string (`\"\"`) when the group is top-level (has no parent).",
            "examples": [
              "Academic Programs",
              ""
            ]
          }
        }
      },
      "GroupCreate": {
        "type": "object",
        "description": "Request body for creating a new group. `name` is required.",
        "required": [
          "name"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "The group name.",
            "examples": [
              "Academic Programs"
            ]
          },
          "parent_group": {
            "type": [
              "string",
              "integer",
              "null"
            ],
            "description": "Parent group name (string) or parent group ID (integer) to nest this group under. Omit, pass `null`, or pass `\"\"` to create a top-level group. The parent must itself be a top-level group — nesting deeper than one level is not supported.",
            "examples": [
              "Academic Programs",
              7,
              null
            ]
          }
        }
      },
      "GroupUpdate": {
        "type": "object",
        "description": "Request body for updating a group. At least one field must be provided. If `id` is included, it must match the `{id}` in the URL.",
        "minProperties": 1,
        "properties": {
          "id": {
            "type": [
              "integer",
              "string"
            ],
            "description": "Optional. If provided, must match the `{id}` in the URL. A mismatch returns a validation envelope.",
            "examples": [
              7
            ]
          },
          "name": {
            "type": "string",
            "description": "New name for the group.",
            "examples": [
              "Updated Group Name"
            ]
          },
          "parent_group": {
            "type": [
              "string",
              "integer",
              "null"
            ],
            "description": "Parent group name or ID to assign. Pass `null` or `\"\"` to promote the group to top-level (remove its parent). The parent must itself be top-level.",
            "examples": [
              "Academic Programs",
              7,
              null,
              ""
            ]
          }
        }
      },
      "CreateSuccessResponse": {
        "type": "object",
        "description": "Returned on successful group creation.",
        "required": [
          "success",
          "result_text",
          "result_data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "result_text": {
            "type": "string",
            "const": "Created"
          },
          "result_data": {
            "$ref": "#/components/schemas/Group"
          }
        }
      },
      "UpdateSuccessResponse": {
        "type": "object",
        "description": "Returned on successful group update.",
        "required": [
          "success",
          "result_text",
          "result_data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "result_text": {
            "type": "string",
            "const": "Updated"
          },
          "result_data": {
            "$ref": "#/components/schemas/Group"
          }
        }
      },
      "DeleteSuccessResponse": {
        "type": "object",
        "description": "Returned on successful group deletion.",
        "required": [
          "success",
          "result_text",
          "result_data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "result_text": {
            "type": "string",
            "const": "Deleted"
          },
          "result_data": {
            "type": "array",
            "maxItems": 0
          }
        }
      },
      "ValidationErrorResponse": {
        "type": "object",
        "description": "Returned when request body or path parameter validation fails.",
        "required": [
          "success",
          "result_text",
          "result_data"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": false
          },
          "result_text": {
            "type": "string",
            "const": "Validation failed, see 'result_data' -> 'errors'"
          },
          "result_data": {
            "type": "object",
            "required": [
              "errors"
            ],
            "properties": {
              "errors": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "One or more human-readable validation error messages."
              }
            }
          }
        }
      }
    },
    "parameters": {
      "GroupType": {
        "name": "type",
        "in": "path",
        "required": true,
        "description": "The item type whose groups are being managed.",
        "schema": {
          "type": "string",
          "enum": [
            "tags",
            "subscriptions",
            "messages",
            "templates"
          ]
        }
      },
      "GroupId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "The unique ID of the group.",
        "schema": {
          "type": "integer",
          "minimum": 1
        }
      },
      "Limit": {
        "name": "limit",
        "in": "query",
        "description": "Maximum number of records to return. Default is 10, hard cap is 100. When used alongside `?fields=` or `?result_type=kvp`, the cap is lifted and an explicit limit above 100 is honoured as-is.",
        "schema": {
          "type": "integer",
          "minimum": 1,
          "default": 10
        }
      },
      "Skip": {
        "name": "skip",
        "in": "query",
        "description": "Number of records to skip. Used for pagination.",
        "schema": {
          "type": "integer",
          "minimum": 0,
          "default": 0
        }
      },
      "Fields": {
        "name": "fields",
        "in": "query",
        "description": "Comma-separated list of fields to include in each response object. When set, the default limit increases to 10,000 and the 100-record hard cap is lifted.",
        "schema": {
          "type": "string"
        },
        "example": "id,name"
      },
      "ResultType": {
        "name": "result_type",
        "in": "query",
        "description": "Set to `kvp` to receive results as a flat key-value object (`{ \"<id>\": \"<name>\" }`) instead of an array of objects. When set, the default limit increases to 10,000 and the hard cap is lifted.",
        "schema": {
          "type": "string",
          "enum": [
            "kvp"
          ]
        }
      }
    }
  },
  "paths": {
    "/groups/{type}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/GroupType"
        }
      ],
      "get": {
        "summary": "List groups",
        "description": "Returns a paginated array of groups for the authenticated account and the specified item type, sorted alphabetically by name. Supports filtering by `id`, `name`, and `parent_group`. Use `?result_type=kvp` for a flat id-to-name map.",
        "operationId": "listGroups",
        "tags": [
          "Groups"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Limit"
          },
          {
            "$ref": "#/components/parameters/Skip"
          },
          {
            "$ref": "#/components/parameters/Fields"
          },
          {
            "$ref": "#/components/parameters/ResultType"
          },
          {
            "name": "filter[id]",
            "in": "query",
            "description": "Filter by group ID. Supports `(ne)` operator.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "filter[name]",
            "in": "query",
            "description": "Filter by group name. Supports `(ne)` operator.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "filter[parent_group]",
            "in": "query",
            "description": "Filter by parent group name or numeric ID. Supports `(ne)` operator. Pass `\"\"` or `0` to filter for top-level groups only.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sort[name]",
            "in": "query",
            "description": "Sort by name. Value: `asc` or `desc`.",
            "schema": {
              "type": "string",
              "enum": [
                "asc",
                "desc"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A list of groups.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Group"
                      }
                    },
                    {
                      "type": "object",
                      "description": "Key-value map of group ID to name.",
                      "additionalProperties": {
                        "type": "string"
                      }
                    }
                  ]
                },
                "examples": {
                  "array": {
                    "summary": "Default array response",
                    "value": [
                      {
                        "id": "7",
                        "name": "Graduate Studies",
                        "parent_group": "Academic Programs"
                      },
                      {
                        "id": "3",
                        "name": "Student Life",
                        "parent_group": ""
                      }
                    ]
                  },
                  "kvp": {
                    "summary": "Key-value pair response (result_type=kvp)",
                    "value": {
                      "3": "Student Life",
                      "7": "Graduate Studies"
                    }
                  },
                  "empty": {
                    "summary": "No groups",
                    "value": []
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid query parameters.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrorResponse"
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Create a group",
        "description": "Creates a new group of the specified type. `name` is required. Optionally nest the group under a parent by providing `parent_group` as a group name or ID. The parent must itself be a top-level group — only one level of nesting is supported.",
        "operationId": "createGroup",
        "tags": [
          "Groups"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GroupCreate"
              },
              "examples": {
                "top_level": {
                  "summary": "Create a top-level group",
                  "value": {
                    "name": "Academic Programs"
                  }
                },
                "subgroup_by_name": {
                  "summary": "Create a subgroup by parent name",
                  "value": {
                    "name": "Graduate Studies",
                    "parent_group": "Academic Programs"
                  }
                },
                "subgroup_by_id": {
                  "summary": "Create a subgroup by parent ID",
                  "value": {
                    "name": "Graduate Studies",
                    "parent_group": 7
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Group created successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreateSuccessResponse"
                },
                "example": {
                  "success": true,
                  "result_text": "Created",
                  "result_data": {
                    "id": "8",
                    "name": "Graduate Studies",
                    "parent_group": "Academic Programs"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrorResponse"
                },
                "examples": {
                  "name_required": {
                    "summary": "Missing name",
                    "value": {
                      "success": false,
                      "result_text": "Validation failed, see 'result_data' -> 'errors'",
                      "result_data": {
                        "errors": [
                          "The field 'name' is required"
                        ]
                      }
                    }
                  },
                  "parent_not_found": {
                    "summary": "Parent group not found",
                    "value": {
                      "success": false,
                      "result_text": "Validation failed, see 'result_data' -> 'errors'",
                      "result_data": {
                        "errors": [
                          "The 'parent_group' 'Nonexistent Group' not found"
                        ]
                      }
                    }
                  },
                  "parent_nested": {
                    "summary": "Parent group is itself a subgroup",
                    "value": {
                      "success": false,
                      "result_text": "Validation failed, see 'result_data' -> 'errors'",
                      "result_data": {
                        "errors": [
                          "The field 'parent_group' cannot be a nested group (maximum depth is 1)"
                        ]
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/groups/{type}/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/GroupType"
        },
        {
          "$ref": "#/components/parameters/GroupId"
        }
      ],
      "get": {
        "summary": "Get a single group",
        "description": "Returns a single group by its ID. Returns an empty array (`[]`) if the ID is not found.",
        "operationId": "getGroup",
        "tags": [
          "Groups"
        ],
        "parameters": [
          {
            "$ref": "#/components/parameters/Fields"
          }
        ],
        "responses": {
          "200": {
            "description": "The requested group, or an empty array if not found.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/Group"
                    },
                    {
                      "type": "array",
                      "maxItems": 0
                    }
                  ]
                },
                "examples": {
                  "found": {
                    "summary": "Group found",
                    "value": {
                      "id": "7",
                      "name": "Graduate Studies",
                      "parent_group": "Academic Programs"
                    }
                  },
                  "not_found": {
                    "summary": "Group not found",
                    "value": []
                  }
                }
              }
            }
          }
        }
      },
      "patch": {
        "summary": "Update a group",
        "description": "Updates one or more fields on an existing group. `name` and `parent_group` are updatable. If `id` is included in the body, it must match the `{id}` in the URL — if they differ, a validation envelope is returned. Pass `null` or `\"\"` for `parent_group` to promote the group to top-level.",
        "operationId": "updateGroup",
        "tags": [
          "Groups"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/GroupUpdate"
              },
              "examples": {
                "rename": {
                  "summary": "Rename the group",
                  "value": {
                    "name": "Updated Group Name"
                  }
                },
                "assign_parent": {
                  "summary": "Assign a parent group",
                  "value": {
                    "parent_group": "Academic Programs"
                  }
                },
                "remove_parent_null": {
                  "summary": "Remove parent (promote to top-level) using null",
                  "value": {
                    "parent_group": null
                  }
                },
                "remove_parent_empty": {
                  "summary": "Remove parent (promote to top-level) using empty string",
                  "value": {
                    "parent_group": ""
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Group updated successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/UpdateSuccessResponse"
                },
                "example": {
                  "success": true,
                  "result_text": "Updated",
                  "result_data": {
                    "id": "7",
                    "name": "Graduate Studies",
                    "parent_group": "Academic Programs"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation error, group not found, or id mismatch.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationErrorResponse"
                },
                "examples": {
                  "not_found": {
                    "summary": "Group not found",
                    "value": {
                      "success": false,
                      "result_text": "Validation failed, see 'result_data' -> 'errors'",
                      "result_data": {
                        "errors": [
                          "Item with id '999999' not found"
                        ]
                      }
                    }
                  },
                  "name_empty": {
                    "summary": "name cannot be empty",
                    "value": {
                      "success": false,
                      "result_text": "Validation failed, see 'result_data' -> 'errors'",
                      "result_data": {
                        "errors": [
                          "The field 'name' cannot be empty"
                        ]
                      }
                    }
                  },
                  "parent_not_found": {
                    "summary": "Parent group not found",
                    "value": {
                      "success": false,
                      "result_text": "Validation failed, see 'result_data' -> 'errors'",
                      "result_data": {
                        "errors": [
                          "The 'parent_group' 'Nonexistent Group' not found"
                        ]
                      }
                    }
                  },
                  "id_mismatch": {
                    "summary": "id in body does not match URL id",
                    "value": {
                      "success": false,
                      "result_text": "Provided 'id': 7 does not match 'id' in request body (999)",
                      "result_data": []
                    }
                  }
                }
              }
            }
          }
        }
      },
      "delete": {
        "summary": "Delete a group",
        "description": "Permanently deletes a group. **The group must be empty** — it cannot have subgroups or assigned items (tags, subscriptions, messages, or templates depending on type). The same validation error is returned in both cases. Remove all subgroups and reassign or delete all assigned items before deleting the group.",
        "operationId": "deleteGroup",
        "tags": [
          "Groups"
        ],
        "responses": {
          "200": {
            "description": "Group deleted, or a validation error.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/DeleteSuccessResponse"
                    },
                    {
                      "$ref": "#/components/schemas/ValidationErrorResponse"
                    }
                  ]
                },
                "examples": {
                  "success": {
                    "summary": "Deleted successfully",
                    "value": {
                      "success": true,
                      "result_text": "Deleted",
                      "result_data": []
                    }
                  },
                  "not_empty": {
                    "summary": "Group has subgroups or assigned items",
                    "value": {
                      "success": false,
                      "result_text": "Validation failed, see 'result_data' -> 'errors'",
                      "result_data": {
                        "errors": [
                          "Only empty groups can be deleted"
                        ]
                      }
                    }
                  },
                  "not_found": {
                    "summary": "Group not found",
                    "value": {
                      "success": false,
                      "result_text": "Validation failed, see 'result_data' -> 'errors'",
                      "result_data": {
                        "errors": [
                          "Item with id '999999' not found"
                        ]
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}
