cisco-scc-sdk 1.0.5
Python SDK for Cisco Security Cloud Control Platform Management APIs
Requires Python: >=3.7

# Cisco Security Cloud Control Python SDK

A Python SDK for interacting with the Cisco Security Cloud Control Platform Management APIs.

## Installation

```bash
pip3 install cisco-scc-sdk
```

Or install from source:

```bash
pip3 install .
```

Using a virtual environment (recommended):

```bash
python3 -m venv venv
source venv/bin/activate && pip install -e .
```

## Quick Start

```python
from scc_sdk import Client

# Initialize the client with your access token
client = Client(access_token="your_bearer_token_here")

# Work with organizations
organizations = client.organizations.list()
org = client.organizations.get(org_id="550e8400-e29b-41d4-a716-446655440000")
new_org = client.organizations.create(name="New Org", region_code="NAM")

# Work with subscriptions
subscriptions = client.subscriptions.list(org_id="org-uuid")
subscription = client.subscriptions.get(org_id="org-uuid", subscription_id="sub-uuid")

# Work with groups
groups = client.groups.list(org_id="org-uuid")
group = client.groups.get(org_id="org-uuid", group_id="group-uuid")
```

## Features

- **Simple Authentication**: Just pass your bearer token
- **Resource-based API**: Organized into logical resource classes
- **Type Hints**: Full type hint support for better IDE integration
- **Context Manager Support**: Use with `with` statement for automatic cleanup
- **Comprehensive Error Handling**: Custom exceptions for different error types

## Configuration

The SDK uses a configurable base URL. By default, it points to `https://api.security.cisco.com/v1`.

To use a different URL:

```python
client = Client(
    access_token="your_token",
    base_url="https://api.security.cisco.com",
    base_path="v1"
)
```

## Workshop Guide

### Interactive Learning Template

We've created a **workshop template** designed for hands-on learning of the Cisco SCC SDK. This template is perfect for:
- Self-paced learning (30-minute completion time)
- Workshop sessions and training
- Learning by doing without AI assistance

**Workshop File**: [`examples/workshop_template.py`](examples/workshop_template.py)

The template includes:
- ✅ Complete prerequisite setup instructions
- ✅ TODO comments marking where you need to implement code
- ✅ HINT comments with method signatures and guidance
- ✅ Step-by-step exercises covering all major SDK features
- ✅ Error handling structure already in place

**Workshop Exercises:**
1. **Getting Organization Details** - Implement `client.organizations.get()`
2. **Reading and Claiming Subscriptions** - Implement claim code validation and subscription creation
3. **Inviting Users** - Implement user invitation logic
4. **Creating Admin Groups** - Implement group creation and management
5. **Adding Users to Groups** - Implement group membership updates
6. **Assigning Roles** - Implement role discovery and assignment

**To complete the workshop:**
1. Open [`examples/workshop_template.py`](examples/workshop_template.py)
2. Update the configuration section with your credentials
3. Search for `TODO:` comments and implement the missing code
4. Reference the API Reference section below for method details
5. Run the script to test your implementation

### Complete Working Example

For a fully implemented reference, see [`examples/setup_org_flow.py`](examples/setup_org_flow.py).

**To run the complete example:**

1. **Get your API Access Token**: Generate an API key access token from the Security Cloud Control console
2. **Create or identify your Organization**: Note the Organization ID you want to configure
3. **Update the configuration** in `setup_org_flow.py`:
   - Set `ACCESS_TOKEN` to your API key access token
   - Set `ORG_ID` to your organization ID
   - Update `USERS_TO_INVITE` with the users you want to invite
   - Set `CLAIM_CODE` with your subscription claim code

4. **Run the script**:
   ```bash
   python3 examples/setup_org_flow.py
   ```

This example demonstrates a complete workflow including:
- Retrieving organization details
- Claiming subscriptions
- Inviting users to the organization
- Creating an admin group
- Assigning product roles to the group

## API Reference

### Organizations API

The Organizations resource provides methods to manage Security Cloud Control organizations.

#### `list()`

List organizations with optional filtering and pagination.

**Method Signature:**
```python
client.organizations.list(
    name: Optional[str] = None,
    type: Optional[str] = None,
    region_code: Optional[str] = None,
    country_code: Optional[str] = None,
    manager_org_id: Optional[str] = None,
    max: int = 100,
    cursor: Optional[str] = None,
    sort_by: Optional[str] = None,
    order: Optional[str] = None
) -> Dict[str, Any]
```

**Parameters:**
- `name` (str, optional): Filter by organization name (partial match)
- `type` (str, optional): Filter by organization type (STANDALONE, MANAGER, MANAGED)
- `region_code` (str, optional): Filter by region code (NAM, EMEA, APJC, GLOBAL)
- `country_code` (str, optional): Filter by country code
- `manager_org_id` (str, optional): Filter by manager organization ID
- `max` (int, optional): Maximum number of items to return (default: 100, max: 500)
- `cursor` (str, optional): Cursor for pagination
- `sort_by` (str, optional): Field to sort by (e.g., "name")
- `order` (str, optional): Sort order (ASC or DESC)

**Returns:** Dictionary containing list of organizations and pagination info

**Example:**
```python
# List all organizations
orgs = client.organizations.list()

# List with filtering and pagination
orgs = client.organizations.list(
    name="Acme",
    type="STANDALONE",
    region_code="NAM",
    max=50,
    sort_by="name",
    order="ASC"
)
```

**Common Use Cases:**
- Browse all organizations in your Security Cloud Control account
- Find organizations by name or type
- Paginate through large sets of organizations

---

#### `get(org_id)`

Get a specific organization by ID.

**Method Signature:**
```python
client.organizations.get(org_id: str) -> Dict[str, Any]
```

**Parameters:**
- `org_id` (str, required): The organization UUID

**Returns:** Organization dictionary with details including name, type, region, and creation date

**Example:**
```python
org = client.organizations.get(org_id="550e8400-e29b-41d4-a716-446655440000")
print(f"Organization: {org['name']}")
print(f"Type: {org['type']}")
print(f"Region: {org['regionCode']}")
```

**Common Use Cases:**
- Retrieve detailed information about a specific organization
- Verify organization exists before performing operations
- Display organization details to users

---

#### `create(name, region_code, type)`

Create a new organization.

**Method Signature:**
```python
client.organizations.create(
    name: str,
    region_code: str,
    type: str = "STANDALONE"
) -> Dict[str, Any]
```

**Parameters:**
- `name` (str, required): Organization name
- `region_code` (str, required): Region code (NAM, EMEA, APJC)
- `type` (str, optional): Organization type (STANDALONE, MANAGER, MANAGED). Default: "STANDALONE"

**Returns:** Created organization dictionary

**Example:**
```python
new_org = client.organizations.create(
    name="New Organization",
    region_code="NAM",
    type="STANDALONE"
)
print(f"Created organization with ID: {new_org['id']}")
```

**Common Use Cases:**
- Provision new organizations for customers
- Set up multi-tenant environments
- Create managed organizations under a manager organization

---

#### `update(org_id, name)`

Update an organization's name.

**Method Signature:**
```python
client.organizations.update(org_id: str, name: str) -> Dict[str, Any]
```

**Parameters:**
- `org_id` (str, required): The organization UUID
- `name` (str, required): New organization name

**Returns:** Updated organization dictionary

**Example:**
```python
updated_org = client.organizations.update(
    org_id="550e8400-e29b-41d4-a716-446655440000",
    name="Updated Name"
)
```

**Common Use Cases:**
- Rename organizations after company rebrand
- Correct organization naming errors
- Update organization display names

---

### Subscriptions API

The Subscriptions resource provides methods to manage product subscriptions within organizations.

#### `list(org_id, name)`

List all subscriptions for an organization.

**Method Signature:**
```python
client.subscriptions.list(
    org_id: str,
    name: Optional[str] = None
) -> List[Dict[str, Any]]
```

**Parameters:**
- `org_id` (str, required): The organization UUID
- `name` (str, optional): Optional subscription name filter

**Returns:** List of subscription dictionaries

**Example:**
```python
subscriptions = client.subscriptions.list(org_id="org-uuid")
for sub in subscriptions:
    print(f"Subscription: {sub['name']}")
```

**Common Use Cases:**
- View all active subscriptions for an organization
- Find specific subscriptions by name
- Audit subscription inventory

---

#### `get(org_id, subscription_id)`

Get a specific subscription by ID.

**Method Signature:**
```python
client.subscriptions.get(org_id: str, subscription_id: str) -> Dict[str, Any]
```

**Parameters:**
- `org_id` (str, required): The organization UUID
- `subscription_id` (str, required): The subscription UUID

**Returns:** Subscription dictionary with full details

**Example:**
```python
subscription = client.subscriptions.get(
    org_id="org-uuid",
    subscription_id="3742d652-6740-489c-b628-143f00690fea"
)
```

**Common Use Cases:**
- Retrieve detailed subscription information
- Verify subscription status and entitlements
- Check product activation status

---

#### `create(org_id, claim_code, products)`

Create a new subscription using a claim code.

**Method Signature:**
```python
client.subscriptions.create(
    org_id: str,
    claim_code: str,
    products: Optional[List[Dict[str, Any]]] = None
) -> Dict[str, Any]
```

**Parameters:**
- `org_id` (str, required): The organization UUID
- `claim_code` (str, required): Claim code for subscription creation
- `products` (List[Dict], optional): List of products to provision with configuration

**Returns:** Dictionary with success status

**Example:**
```python
# First, read claim code details
claim_info = client.subscriptions.read_claim_code(
    org_id="org-uuid",
    claim_code="PHJZ-DWMF-ZBYH-FYMB"
)

# Build products list from claim info
products = client.subscriptions.build_products_from_claim_info(claim_info)

# Create subscription
new_subscription = client.subscriptions.create(
    org_id="org-uuid",
    claim_code="PHJZ-DWMF-ZBYH-FYMB",
    products=products
)
```

**Common Use Cases:**
- Activate new product subscriptions
- Claim trial or promotional subscriptions
- Provision products during customer onboarding

---

#### `update(org_id, subscription_id, product_id, status, ...)`

Update a subscription (activate/deactivate product).

**Method Signature:**
```python
client.subscriptions.update(
    org_id: str,
    subscription_id: str,
    product_id: str,
    status: bool,
    source_instance_id: Optional[str] = None,
    region_code: Optional[str] = None,
    initial_admin: Optional[str] = None
) -> Dict[str, Any]
```

**Parameters:**
- `org_id` (str, required): The organization UUID
- `subscription_id` (str, required): The subscription UUID
- `product_id` (str, required): The product UUID
- `status` (bool, required): True to activate, False to deactivate
- `source_instance_id` (str, optional): Source instance ID for using existing tenant
- `region_code` (str, optional): Product region code (NAM, EMEA, APJC, US, GLOBAL)
- `initial_admin` (str, optional): Initial admin email for new tenant provisioning

**Returns:** Updated subscription dictionary

**Example:**
```python
# Activate a product
updated_sub = client.subscriptions.update(
    org_id="org-uuid",
    subscription_id="sub-uuid",
    product_id="product-uuid",
    status=True,
    region_code="NAM",
    initial_admin="admin@example.com"
)

# Deactivate a product
deactivated = client.subscriptions.update(
    org_id="org-uuid",
    subscription_id="sub-uuid",
    product_id="product-uuid",
    status=False
)
```

**Common Use Cases:**
- Enable or disable specific products within a subscription
- Provision new product tenants
- Migrate products between regions

---

#### `patch(org_id, subscription_id, entitlements, use_existing_tenants)`

Patch a subscription (update entitlement quantities for managed organizations).

**Method Signature:**
```python
client.subscriptions.patch(
    org_id: str,
    subscription_id: str,
    entitlements: List[Dict[str, Any]],
    use_existing_tenants: Optional[List[str]] = None
) -> Dict[str, Any]
```

**Parameters:**
- `org_id` (str, required): The managed organization UUID
- `subscription_id` (str, required): The subscription UUID
- `entitlements` (List[Dict], required): List of entitlements with id and quantity
- `use_existing_tenants` (List[str], optional): List of existing tenant UUIDs

**Returns:** Updated subscription dictionary

**Example:**
```python
patched_sub = client.subscriptions.patch(
    org_id="org-uuid",
    subscription_id="sub-uuid",
    entitlements=[
        {"id": "E3S-AIDEF-ADV", "quantity": 10},
        {"id": "CPT-SEC-ADV", "quantity": 50}
    ],
    use_existing_tenants=["8a9f43fa-f8f5-4aac-8d0b-380cf6656255"]
)
```

**Common Use Cases:**
- Update SKU quantities for managed organizations
- Adjust entitlements after license changes
- Scale product usage up or down

---

#### `read_claim_code(org_id, claim_code)`

Read and validate claim code details before creating a subscription.

**Method Signature:**
```python
client.subscriptions.read_claim_code(org_id: str, claim_code: str) -> Dict[str, Any]
```

**Parameters:**
- `org_id` (str, required): The organization UUID
- `claim_code` (str, required): Claim code to validate

**Returns:** Claim code information dictionary with products, entitlements, and allowed regions

**Example:**
```python
claim_info = client.subscriptions.read_claim_code(
    org_id="org-uuid",
    claim_code="PHJZ-DWMF-ZBYH-FYMB"
)
print(f"Claim code name: {claim_info['name']}")
for product in claim_info['products']:
    print(f"Product: {product['name']}")
```

**Common Use Cases:**
- Validate claim codes before subscription creation
- Preview products and entitlements in a claim code
- Display available regions for product deployment

---

#### `build_products_from_claim_info(claim_info, preferred_region)`

Build products list from claim info with region selection.

**Method Signature:**
```python
client.subscriptions.build_products_from_claim_info(
    claim_info: Dict[str, Any],
    preferred_region: str = "NAM"
) -> List[Dict[str, Any]]
```

**Parameters:**
- `claim_info` (Dict, required): Claim code information dictionary (from read_claim_code())
- `preferred_region` (str, optional): Preferred region code (default: "NAM")

**Returns:** List of products ready for subscription creation

**Example:**
```python
claim_info = client.subscriptions.read_claim_code(org_id="org-uuid", claim_code="XXXX")
products = client.subscriptions.build_products_from_claim_info(
    claim_info,
    preferred_region="EMEA"
)
client.subscriptions.create(org_id="org-uuid", claim_code="XXXX", products=products)
```

**Common Use Cases:**
- Prepare product list for subscription creation
- Select specific regions for product deployment
- Automatically configure products from claim code

---

### Users API

The Users resource provides methods to manage user accounts within organizations.

#### `list(org_id, group_id, status)`

List all users for an organization.

**Method Signature:**
```python
client.users.list(
    org_id: str,
    group_id: Optional[str] = None,
    status: Optional[str] = None
) -> Dict[str, Any]
```

**Parameters:**
- `org_id` (str, required): The organization UUID
- `group_id` (str, optional): Filter users by group membership
- `status` (str, optional): Filter by status (ACTIVE, DISABLE, PENDING, SHARED)

**Returns:** Dictionary containing list of users

**Example:**
```python
# List all users
users = client.users.list(org_id="org-uuid")

# Filter by group
group_users = client.users.list(org_id="org-uuid", group_id="group-uuid")

# Filter by status
active_users = client.users.list(org_id="org-uuid", status="ACTIVE")
```

**Common Use Cases:**
- View all users in an organization
- Find users belonging to specific groups
- Filter users by account status

---

#### `get(org_id, user_id)`

Get a specific user by ID.

**Method Signature:**
```python
client.users.get(org_id: str, user_id: str) -> Dict[str, Any]
```

**Parameters:**
- `org_id` (str, required): The organization UUID
- `user_id` (str, required): The user UUID

**Returns:** User dictionary with full details

**Example:**
```python
user = client.users.get(org_id="org-uuid", user_id="user-uuid")
print(f"User: {user['firstName']} {user['lastName']}")
print(f"Email: {user['email']}")
```

**Common Use Cases:**
- Retrieve detailed user information
- Verify user account details
- Check user status and group memberships

---

#### `invite(org_id, email, first_name, last_name)`

Invite a new user to the organization.

**Method Signature:**
```python
client.users.invite(
    org_id: str,
    email: str,
    first_name: str,
    last_name: str
) -> Dict[str, Any]
```

**Parameters:**
- `org_id` (str, required): The organization UUID
- `email` (str, required): User's email address
- `first_name` (str, required): User's first name
- `last_name` (str, required): User's last name

**Returns:** User operation response dictionary

**Example:**
```python
result = client.users.invite(
    org_id="org-uuid",
    email="user@example.com",
    first_name="John",
    last_name="Doe"
)
```

**Common Use Cases:**
- Add new users to an organization
- Onboard employees or customers
- Send invitation emails to new users

---

#### `disable(org_id, email)`

Disable a user account.

**Method Signature:**
```python
client.users.disable(org_id: str, email: str) -> Dict[str, Any]
```

**Parameters:**
- `org_id` (str, required): The organization UUID
- `email` (str, required): User's email address

**Returns:** User operation response dictionary

**Example:**
```python
client.users.disable(org_id="org-uuid", email="user@example.com")
```

**Common Use Cases:**
- Temporarily deactivate user accounts
- Suspend access for terminated employees
- Disable compromised accounts

---

#### `enable(org_id, email)`

Enable a user account.

**Method Signature:**
```python
client.users.enable(org_id: str, email: str) -> Dict[str, Any]
```

**Parameters:**
- `org_id` (str, required): The organization UUID
- `email` (str, required): User's email address

**Returns:** User operation response dictionary

**Example:**
```python
client.users.enable(org_id="org-uuid", email="user@example.com")
```

**Common Use Cases:**
- Re-enable previously disabled accounts
- Restore access after suspension
- Activate accounts after review

---

#### `remove(org_id, email)`

Remove a user from the organization.

**Method Signature:**
```python
client.users.remove(org_id: str, email: str) -> Dict[str, Any]
```

**Parameters:**
- `org_id` (str, required): The organization UUID
- `email` (str, required): User's email address

**Returns:** User operation response dictionary

**Example:**
```python
client.users.remove(org_id="org-uuid", email="user@example.com")
```

**Common Use Cases:**
- Permanently remove users from organization
- Clean up inactive accounts
- Remove users after offboarding

---

#### `resend_email_invite(org_id, email)`

Resend email invitation to a user.

**Method Signature:**
```python
client.users.resend_email_invite(org_id: str, email: str) -> Dict[str, Any]
```

**Parameters:**
- `org_id` (str, required): The organization UUID
- `email` (str, required): User's email address

**Returns:** User operation response dictionary

**Example:**
```python
client.users.resend_email_invite(org_id="org-uuid", email="user@example.com")
```

**Common Use Cases:**
- Resend invitations that were not received
- Send reminder emails to pending users
- Address email delivery issues

---

#### `update(org_id, user_id, first_name, last_name)`

Update a user's first and last name.

**Method Signature:**
```python
client.users.update(
    org_id: str,
    user_id: str,
    first_name: Optional[str] = None,
    last_name: Optional[str] = None
) -> Dict[str, Any]
```

**Parameters:**
- `org_id` (str, required): The organization UUID
- `user_id` (str, required): The user UUID
- `first_name` (str, optional): User's first name
- `last_name` (str, optional): User's last name

**Returns:** Updated user dictionary

**Example:**
```python
updated_user = client.users.update(
    org_id="org-uuid",
    user_id="user-uuid",
    first_name="Jane",
    last_name="Smith"
)
```

**Common Use Cases:**
- Update user profile information
- Correct name spelling errors
- Reflect name changes after marriage or legal change

---

#### `patch(org_id, users)`

Perform bulk operations on users (invite, disable, enable, remove, resend_email_invite).

**Method Signature:**
```python
client.users.patch(
    org_id: str,
    users: List[Dict[str, Any]]
) -> Dict[str, Any]
```

**Parameters:**
- `org_id` (str, required): The organization UUID
- `users` (List[Dict], required): List of user operations with email, operation, and optional firstName/lastName

**Returns:** Operation result dictionary with results array

**Example:**
```python
# Bulk invite multiple users
result = client.users.patch(
    org_id="org-uuid",
    users=[
        {
            "email": "user1@example.com",
            "operation": "invite",
            "firstName": "John",
            "lastName": "Doe"
        },
        {
            "email": "user2@example.com",
            "operation": "invite",
            "firstName": "Jane",
            "lastName": "Smith"
        }
    ]
)

# Bulk disable users
client.users.patch(
    org_id="org-uuid",
    users=[
        {"email": "user1@example.com", "operation": "disable"},
        {"email": "user2@example.com", "operation": "disable"}
    ]
)
```

**Common Use Cases:**
- Bulk invite multiple users at once
- Perform mass account operations
- Automate user lifecycle management

---

#### `get_groups(org_id, user_id)`

List all groups to which a user is assigned.

**Method Signature:**
```python
client.users.get_groups(org_id: str, user_id: str) -> Dict[str, Any]
```

**Parameters:**
- `org_id` (str, required): The organization UUID
- `user_id` (str, required): The user UUID

**Returns:** Dictionary containing list of groups

**Example:**
```python
groups = client.users.get_groups(org_id="org-uuid", user_id="user-uuid")
for group in groups.get("groups", []):
    print(f"Group: {group['name']}")
```

**Common Use Cases:**
- View user's group memberships
- Audit user access and permissions
- Verify role assignments through groups

---

### Groups API

The Groups resource provides methods to manage admin groups for role-based access control.

#### `list(org_id)`

List all admin groups for an organization.

**Method Signature:**
```python
client.groups.list(org_id: str) -> Dict[str, Any]
```

**Parameters:**
- `org_id` (str, required): The organization UUID

**Returns:** Dictionary containing list of groups

**Example:**
```python
groups = client.groups.list(org_id="org-uuid")
for group in groups.get("groups", []):
    print(f"Group: {group['name']}")
```

**Common Use Cases:**
- View all admin groups in an organization
- List groups for user assignment
- Audit group structure

---

#### `get(org_id, group_id)`

Get a specific admin group by ID.

**Method Signature:**
```python
client.groups.get(org_id: str, group_id: str) -> Dict[str, Any]
```

**Parameters:**
- `org_id` (str, required): The organization UUID
- `group_id` (str, required): The group UUID

**Returns:** Group dictionary with full details

**Example:**
```python
group = client.groups.get(org_id="org-uuid", group_id="group-uuid")
print(f"Group: {group['name']}")
print(f"Description: {group['description']}")
```

**Common Use Cases:**
- Retrieve detailed group information
- Verify group configuration
- Display group details to administrators

---

#### `create(org_id, name, description, applies_to)`

Create a new admin group.

**Method Signature:**
```python
client.groups.create(
    org_id: str,
    name: str,
    description: Optional[str] = None,
    applies_to: Optional[str] = None
) -> Dict[str, Any]
```

**Parameters:**
- `org_id` (str, required): The organization UUID
- `name` (str, required): Group name
- `description` (str, optional): Group description
- `applies_to` (str, optional): Scope (all, manager, selected_managed)

**Returns:** Created group dictionary

**Example:**
```python
new_group = client.groups.create(
    org_id="org-uuid",
    name="Engineering Team",
    description="Engineering department group",
    applies_to="all"
)
print(f"Created group with ID: {new_group['id']}")
```

**Common Use Cases:**
- Create groups for departments or teams
- Set up role-based access control
- Organize users by function or location

---

#### `update(org_id, group_id, name, description)`

Update an existing admin group.

**Method Signature:**
```python
client.groups.update(
    org_id: str,
    group_id: str,
    name: Optional[str] = None,
    description: Optional[str] = None
) -> Dict[str, Any]
```

**Parameters:**
- `org_id` (str, required): The organization UUID
- `group_id` (str, required): The group UUID
- `name` (str, optional): New group name
- `description` (str, optional): New group description

**Returns:** Updated group dictionary

**Example:**
```python
updated_group = client.groups.update(
    org_id="org-uuid",
    group_id="group-uuid",
    name="Updated Team Name",
    description="Updated description"
)
```

**Common Use Cases:**
- Rename groups after reorganization
- Update group descriptions
- Maintain accurate group information

---

#### `delete(org_id, group_id)`

Delete an admin group.

**Method Signature:**
```python
client.groups.delete(org_id: str, group_id: str) -> bool
```

**Parameters:**
- `org_id` (str, required): The organization UUID
- `group_id` (str, required): The group UUID

**Returns:** True if deletion was successful

**Example:**
```python
success = client.groups.delete(org_id="org-uuid", group_id="group-uuid")
if success:
    print("Group deleted successfully")
```

**Common Use Cases:**
- Remove obsolete groups
- Clean up after reorganization
- Delete duplicate or test groups

---

#### `patch(org_id, group_id, users, managed_orgs)`

Patch admin group membership (add/remove users and managed organizations).

**Method Signature:**
```python
client.groups.patch(
    org_id: str,
    group_id: str,
    users: Optional[List[Dict[str, str]]] = None,
    managed_orgs: Optional[List[Dict[str, str]]] = None
) -> Dict[str, Any]
```

**Parameters:**
- `org_id` (str, required): The organization UUID
- `group_id` (str, required): The group UUID
- `users` (List[Dict], optional): User operations with 'operation' (add/remove) and 'id' (email)
- `managed_orgs` (List[Dict], optional): Managed org operations with 'operation' (add/remove) and 'id'

**Returns:** Patch operation results

**Example:**
```python
# Add users to group
result = client.groups.patch(
    org_id="org-uuid",
    group_id="group-uuid",
    users=[
        {"operation": "add", "id": "user@example.com"},
        {"operation": "add", "id": "admin@example.com"}
    ]
)

# Remove user and managed org
result = client.groups.patch(
    org_id="org-uuid",
    group_id="group-uuid",
    users=[{"operation": "remove", "id": "user@example.com"}],
    managed_orgs=[{"operation": "remove", "id": "managed-org-id"}]
)
```

**Common Use Cases:**
- Add users to groups for access control
- Remove users from groups
- Manage shared group access to managed organizations

---

#### `get_managed_organizations(org_id, group_id)`

Get managed organizations for a shared admin group.

**Method Signature:**
```python
client.groups.get_managed_organizations(org_id: str, group_id: str) -> Dict[str, Any]
```

**Parameters:**
- `org_id` (str, required): The organization UUID
- `group_id` (str, required): The group UUID

**Returns:** Dictionary containing list of managed organizations

**Example:**
```python
managed_orgs = client.groups.get_managed_organizations(
    org_id="org-uuid",
    group_id="group-uuid"
)
```

**Common Use Cases:**
- View organizations accessible to shared group
- Audit multi-tenant access
- Verify managed organization assignments

---

#### `get_assigned_roles(org_id, group_id)`

Get assigned roles for a specific admin group.

**Method Signature:**
```python
client.groups.get_assigned_roles(org_id: str, group_id: str) -> Dict[str, Any]
```

**Parameters:**
- `org_id` (str, required): The organization UUID
- `group_id` (str, required): The group UUID

**Returns:** Dictionary containing list of roles

**Example:**
```python
roles = client.groups.get_assigned_roles(org_id="org-uuid", group_id="group-uuid")
for role in roles.get("roles", []):
    print(f"Role: {role['displayName']}")
```

**Common Use Cases:**
- View permissions assigned to a group
- Audit group access levels
- Verify role assignments

---

#### `get_users(org_id, group_id)`

Get all users that belong to a specific admin group.

**Method Signature:**
```python
client.groups.get_users(org_id: str, group_id: str) -> Dict[str, Any]
```

**Parameters:**
- `org_id` (str, required): The organization UUID
- `group_id` (str, required): The group UUID

**Returns:** Dictionary containing list of users

**Example:**
```python
users = client.groups.get_users(org_id="org-uuid", group_id="group-uuid")
for user in users.get("users", []):
    print(f"User: {user['email']}")
```

**Common Use Cases:**
- List group members
- Audit group membership
- Verify user assignments

---

### Roles API

The Roles resource provides methods to manage role assignments for users and groups.

#### `list(org_id, type)`

List all roles for an organization.

**Method Signature:**
```python
client.roles.list(
    org_id: str,
    type: Optional[str] = None
) -> Dict[str, Any]
```

**Parameters:**
- `org_id` (str, required): The organization UUID
- `type` (str, optional): Filter by role type (CUSTOM, BUNDLED, STATIC)

**Returns:** Dictionary containing list of roles

**Example:**
```python
# List all roles
roles = client.roles.list(org_id="org-uuid")

# List only static roles
static_roles = client.roles.list(org_id="org-uuid", type="STATIC")
```

**Common Use Cases:**
- View all available roles
- Filter roles by type
- Display role options to administrators

---

#### `get(org_id, role_id)`

Get a specific role by ID.

**Method Signature:**
```python
client.roles.get(org_id: str, role_id: str) -> Dict[str, Any]
```

**Parameters:**
- `org_id` (str, required): The organization UUID
- `role_id` (str, required): The role UUID

**Returns:** Role dictionary with full details

**Example:**
```python
role = client.roles.get(org_id="org-uuid", role_id="role-uuid")
print(f"Role: {role['displayName']}")
for product in role.get('products', []):
    print(f"Product: {product['name']}")
```

**Common Use Cases:**
- Retrieve detailed role information
- View role permissions and products
- Display role details to users

---

#### `patch(org_id, role_id, users, groups)`

Update role assignments by adding or removing users and groups.

**Method Signature:**
```python
client.roles.patch(
    org_id: str,
    role_id: str,
    users: Optional[List[Dict[str, str]]] = None,
    groups: Optional[List[Dict[str, str]]] = None
) -> Dict[str, Any]
```

**Parameters:**
- `org_id` (str, required): The organization UUID
- `role_id` (str, required): The role UUID
- `users` (List[Dict], optional): User operations with 'operation' (add/remove) and 'id' (user UUID)
- `groups` (List[Dict], optional): Group operations with 'operation' (add/remove) and 'id' (group UUID)

**Returns:** Dictionary with 'results' array and 'summary' containing success/failure counts

**Example:**
```python
# Assign role to users and groups
result = client.roles.patch(
    org_id="org-uuid",
    role_id="role-uuid",
    users=[{"operation": "add", "id": "user-uuid"}],
    groups=[{"operation": "add", "id": "group-uuid"}]
)

# Check results
if result["summary"]["all_successful"]:
    print("All role assignments successful")
else:
    print(f"Failed: {result['summary']['failed']} operations")
```

**Common Use Cases:**
- Assign roles to users or groups
- Remove role assignments
- Bulk update role memberships

---

#### `find_role_id(org_id, product_name, role_display_name)`

Find a role ID by product name and role display name.

**Method Signature:**
```python
client.roles.find_role_id(
    org_id: str,
    product_name: str,
    role_display_name: str
) -> Optional[str]
```

**Parameters:**
- `org_id` (str, required): The organization UUID
- `product_name` (str, required): Product name (e.g., "Security Cloud Control")
- `role_display_name` (str, required): Role display name (e.g., "Organization Administrator")

**Returns:** Role ID if found, None otherwise

**Example:**
```python
role_id = client.roles.find_role_id(
    org_id="org-uuid",
    product_name="Security Cloud Control",
    role_display_name="Organization Administrator"
)
if role_id:
    print(f"Found role ID: {role_id}")
```

**Common Use Cases:**
- Find role IDs by human-readable names
- Automate role assignments without hardcoding IDs
- Lookup roles for specific products

---

### Tokens API

The Tokens resource provides methods to manage API token refresh operations.

#### `refresh(org_id, api_key_id, refresh_token)`

Refresh access token using a refresh token.

**Method Signature:**
```python
client.tokens.refresh(
    org_id: str,
    api_key_id: str,
    refresh_token: str
) -> Dict[str, Any]
```

**Parameters:**
- `org_id` (str, required): The organization UUID
- `api_key_id` (str, required): The API key UUID
- `refresh_token` (str, required): The refresh token value

**Returns:** Dictionary containing new access_token, refresh_token, and expires_in

**Example:**
```python
new_tokens = client.tokens.refresh(
    org_id="org-uuid",
    api_key_id="key-uuid",
    refresh_token="your_refresh_token"
)

access_token = new_tokens["access_token"]
refresh_token = new_tokens["refresh_token"]
expires_in = new_tokens["expires_in"]

# Use new access token for subsequent requests
client = Client(access_token=access_token)
```

**Common Use Cases:**
- Refresh expired access tokens
- Implement token rotation for security
- Maintain long-running API sessions

---

## Usage Examples

### Using Context Manager

```python
with Client(access_token="your_token") as client:
    orgs = client.organizations.list()
    # Session is automatically closed when exiting the context
```

### Organizations

```python
# List organizations with filtering and pagination
orgs = client.organizations.list(
    name="Acme",
    type="STANDALONE",
    region_code="NAM",
    max=50,
    sort_by="name",
    order="ASC"
)

# Get a specific organization
org = client.organizations.get(org_id="550e8400-e29b-41d4-a716-446655440000")

# Create a new organization
new_org = client.organizations.create(
    name="New Organization",
    region_code="NAM",
    type="STANDALONE"
)

# Update an organization
updated_org = client.organizations.update(
    org_id="550e8400-e29b-41d4-a716-446655440000",
    name="Updated Name"
)
```

### Subscriptions

```python
org_id = "550e8400-e29b-41d4-a716-446655440000"

# List subscriptions
subscriptions = client.subscriptions.list(org_id=org_id)

# Get a specific subscription
subscription = client.subscriptions.get(
    org_id=org_id,
    subscription_id="3742d652-6740-489c-b628-143f00690fea"
)

# Create a subscription with claim code
new_subscription = client.subscriptions.create(
    org_id=org_id,
    claim_code="PHJZ-DWMF-ZBYH-FYMB",
    type="STANDALONE"
)

# Read claim code details before creating
claim_info = client.subscriptions.read_claim_code(
    org_id=org_id,
    claim_code="PHJZ-DWMF-ZBYH-FYMB"
)

# Update subscription (activate/deactivate product)
updated_sub = client.subscriptions.update(
    org_id=org_id,
    subscription_id="sub-uuid",
    product_id="product-uuid",
    status=True,
    region_code="NAM",
    initial_admin="admin@example.com"
)

# Patch subscription (update SKU quantities for managed orgs)
patched_sub = client.subscriptions.patch(
    org_id=org_id,
    subscription_id="sub-uuid",
    skus=[
        {"name": "E3S-AIDEF-ADV", "quantity": 10},
        {"name": "CPT-SEC-ADV", "quantity": 50}
    ]
)
```

### Groups

```python
org_id = "550e8400-e29b-41d4-a716-446655440000"

# List groups
groups = client.groups.list(org_id=org_id)

# Get a specific group
group = client.groups.get(
    org_id=org_id,
    group_id="e980e881-b61f-11f0-a9b4-06fbd2118f4e"
)

# Create a new group
new_group = client.groups.create(
    org_id=org_id,
    name="Engineering Team",
    description="Engineering department group",
    applies_to="SOME_MANAGED"
)

# Update a group
updated_group = client.groups.update(
    org_id=org_id,
    group_id="group-uuid",
    name="Updated Team Name",
    description="Updated description"
)

# Delete a group
client.groups.delete(org_id=org_id, group_id="group-uuid")

# Remove shared group from managed organization
client.groups.remove_shared_group(org_id=org_id, group_id="group-uuid")
```

### Token Refresh

```python
# Refresh access token using refresh token
new_tokens = client.tokens.refresh(
    org_id="org-uuid",
    api_key_id="key-uuid",
    refresh_token="your_refresh_token"
)

access_token = new_tokens["access_token"]
refresh_token = new_tokens["refresh_token"]
expires_in = new_tokens["expires_in"]
```

## Error Handling

The SDK provides custom exceptions for different error scenarios:

```python
from scc_sdk import (
    Client,
    SCCError,
    AuthenticationError,
    NotFoundError,
    ValidationError,
    ForbiddenError,
    ServerError
)

try:
    client = Client(access_token="your_token")
    org = client.organizations.get(org_id="invalid-uuid")
except AuthenticationError:
    print("Invalid access token")
except NotFoundError:
    print("Organization not found")
except ValidationError:
    print("Invalid request data")
except ForbiddenError:
    print("Access forbidden")
except ServerError:
    print("Server error occurred")
except SCCError as e:
    print(f"API error occurred: {e}")
```

### Detailed Error Information

All `SCCError` exceptions include detailed error information from the API:

```python
try:
    claim_info = client.subscriptions.read_claim_code(
        org_id="org-uuid",
        claim_code="INVALID-CODE"
    )
except SCCError as e:
    # Access error details
    print(f"Error: {e.message}")
    print(f"Status Code: {e.status_code}")
    print(f"Tracking ID: {e.tracking_id}")
    print(f"Timestamp: {e.timestamp}")
    print(f"Path: {e.path}")
    
    # Get full error response
    print(f"Full Response: {e.response}")
    
    # Or convert to dictionary
    error_dict = e.to_dict()
```

**Available error attributes:**
- `message` - Error message
- `status_code` - HTTP status code (e.g., 400, 404)
- `tracking_id` - API tracking ID for support requests
- `timestamp` - Error timestamp
- `path` - API path that generated the error
- `response` - Full response dictionary from the API

**Example error response:**
```json
{
    "trackingId": "bb7d315e-502c-4427-9683-cc6fc824495f",
    "message": "invalid claim code provided",
    "timestamp": "2026-02-05T22:53:38.923039436Z",
    "statusCode": 400,
    "path": "/v1/organizations/94ff7d60-aebb-41d0-82cf-e06c74b63d81/subscriptions/claimInfo"
}
```

## API Resources

### Organizations Resource
- `list()` - List organizations with filtering
- `get(org_id)` - Get organization by ID
- `create(name, region_code, type)` - Create new organization
- `update(org_id, name)` - Update organization name

### Subscriptions Resource
- `list(org_id, name)` - List subscriptio