zscaler-sdk-python 1.9.16
Official Python SDK for the Zscaler Products
License: MIT
Requires Python: <4.0,>=3.9.2
Keywords: zscaler, sdk, zpa, zia, zdx, zcc, ztw, zwa, zidentity

# Official Python SDK for the Zscaler Products

[![PyPI - Downloads](https://img.shields.io/pypi/dw/zscaler-sdk-python)](https://pypistats.org/packages/zscaler-sdk-python)
[![License](https://img.shields.io/github/license/zscaler/zscaler-sdk-python.svg)](https://github.com/zscaler/zscaler-sdk-python)
[![Documentation Status](https://readthedocs.org/projects/zscaler-sdk-python/badge/?version=latest)](https://zscaler-sdk-python.readthedocs.io/en/latest/?badge=latest)
[![Latest version released on PyPi](https://img.shields.io/pypi/v/zscaler-sdk-python.svg)](https://pypi.org/project/zscaler-sdk-python)
[![PyPI pyversions](https://img.shields.io/pypi/pyversions/zscaler-sdk-python.svg)](https://pypi.python.org/pypi/zscaler-sdk-python/)
[![codecov](https://codecov.io/gh/zscaler/zscaler-sdk-python/graph/badge.svg?token=56B53PITU8)](https://codecov.io/gh/zscaler/zscaler-sdk-python)
[![Zscaler Community](https://img.shields.io/badge/zscaler-community-blue)](https://community.zscaler.com/)

## Support Disclaimer

-> **Disclaimer:** Please refer to our [General Support Statement](docsrc/zs/guides/support.rst) before proceeding with the use of this provider. You can also refer to our [troubleshooting guide](docsrc/zs/guides/troubleshooting.rst) for guidance on typical problems.

# Official Zscaler Python SDK Overview

* [Release status](#release-status)
* [Breaking Changes & Migration Guide to Multi-Client SDK](#breaking-changes--migration-guide-to-multi-client-sdk)
* [Need help?](#need-help)
* [Getting Started](#getting-started)
- [Building the SDK](#building-the-sdk)
* [Usage guide](#usage-guide)
* [Authentication](#authentication)
* [Zscaler OneAPI New Framework](#zscaler-oneapi-new-framework)
* [Zscaler Legacy API Framework](#zscaler-legacy-api-framework)
* [Configuration reference](#configuration-reference)
* [Pagination](#pagination)
* [Contributing](#contributing)

The Zscaler SDK for Python includes functionality to accelerate development via [Python](https://www.python.org/). This SDK can be
used in your server-side code to interact with the Zscaler API platform across multiple products such as:

* [ZPA API](https://help.zscaler.com/zpa/zpa-api/api-developer-reference-guide)
* [ZIA API](https://help.zscaler.com/zia/getting-started-zia-api)
* [ZDX API](https://help.zscaler.com/zdx/understanding-zdx-api)
* [ZCC API](https://help.zscaler.com/client-connector/getting-started-client-connector-api)
* [ZIdentity](https://help.zscaler.com/zidentity/getting-started-zidentity-api)
* [ZTW API](https://help.zscaler.com/cloud-branch-connector/getting-started-cloud-branch-connector-api)
* [ZWA API](https://help.zscaler.com/workflow-automation/getting-started-workflow-automation-api)
* [EASM API](https://hhttps://help.zscaler.com/easm/easm-api/api-developer-reference-guide/reference-guide)

This SDK is designed to support the new Zscaler API framework [OneAPI](https://help.zscaler.com/oneapi/understanding-oneapi)
via a single OAuth2 HTTP client. The SDK is also backwards compatible with the previous
Zscaler API framework, and each package is supported by an individual and robust HTTP client
designed to handle failures on different levels by performing intelligent retries.

## Release Status

This library uses semantic versioning and updates are posted in ([release notes](/docs/guides/release-notes.md)) |

| Version | Status                           |
| ------- | -------------------------------- |
| 0.x     | :warning: Beta Release (Retired) |
| 1.x     | :heavy_check_mark: Release       |

The latest release can always be found on the ([releases page](github-releases))

> Requires Python version 3.8.0 or higher.
Zscaler SDK for Python is compatible with Python 3.8 _(until [June 2023](https://devguide.python.org/versions/))_, 3.9, 3.10, 3.11, and 3.12

## Need help?

If you run into problems, please refer to our [General Support Statement](docs/guides/support.md) before proceeding with the use of this SDK. You can also refer to our [troubleshooting guide](docs/guides/troubleshooting.md) for guidance on typical problems. You can also raise an issue via ([github issues page](https://github.com/zscaler/zscaler-sdk-go/issues))

- Ask questions on the [Zenith Community][zenith]
- Post [issues on GitHub][github-issues] (for code errors)
- Support [customer support portal][zscaler-support]

## Breaking Changes & Migration Guide to Multi-Client SDK

This SDK is a complete redesign from the older `zscaler-sdk-python` or `pyzscaler packages`. If you've used either of those, please review the following before upgrading:

### What's Changed

| Feature                         | Legacy SDK (Restfly + Box)                      | New SDK (OneAPI + Pythonic Dict)                |
|---------------------------------|--------------------------------------------------|--------------------------------------------------|
| **Data Structure**              | Used `Python-Box` objects (dot notation)        | Uses native Python `dict` with `snake_case`     |
| **HTTP Engine**                 | [Restfly](https://github.com/tdunham/restfly)   | Custom HTTP executor with retries, caching, etc.|
| **Auth Model**                  | One set of credentials per service              | Unified OAuth2 (Zidentity) with scoped access    |
| **Multi-Service Support**       | Separate SDKs or config per service             | Unified client with `.zia`, `.zpa`, `.zcc`      |
| **Pagination**                  | Inconsistent or manual                          | Built-in with `resp.has_next()` and `resp.next()` |
| **Error Handling**              | Raw HTTP exceptions                             | Returns `(result, response, error)` tuples      |
| **Models**                      | Custom models + `.attribute` access             | Plain Python dict access: `object["field"]`     |
| **Return Types**                | Box-style nested objects                        | Pure JSON-serializable `dict` responses          |

### Legacy SDK Examples

```py
# Old SDK (Pyzscaler / Restfly)
client = ZIAClientHelper(api_key="...", cloud="...")

users = client.users.list()
print(users[0].name)  # Box-style access
```

### New SDK Example
```py
from zscaler import ZscalerClient

config = {
    "clientId": "...",
    "clientSecret": "...",
    "vanityDomain": "...",
    "cloud": "beta", # (Optional)
}

with ZscalerClient(config) as client:
    users, _, err = client.zia.user_management.list_users()
    if err:
        print("Error:", err)
    else:
        print(users[0]["name"])  # Pythonic dict access
```

### Migration Summary

If you're upgrading from a previous version:

* Refactor any `.attribute` access to dictionary access: `user["name"]` instead of `user.name`
* Update authentication to use OAuth2 via OneAPI:
Choose either:
```py
client = ZscalerClient({
    "client_id": "...",
    "client_secret": "...",
    "vanity_domain": "..."
})
```

or (for JWT private key auth):
```py
client = ZscalerClient({
    "client_id": "...",
    "private_key": "...",
    "vanity_domain": "..."
})
```

* If your tenant is still `NOT` migrated to Zidentity:
You can still use this SDK by instantiating the respective legacy API client directly. See section: [Zscaler Legacy API Framework](#zscaler-legacy-api-framework)
```py
from zscaler.oneapi_client import LegacyZIAClient

def main():
    with LegacyZIAClient(config) as client:
        users, _, _ = client.user_management.list_users()
        ...
```

* All data returned from the SDK is pure `dict` — no Box, no attribute-style access — just native, Pythonic, serializable output.

## Getting started

To install the Zscaler Python SDK in your project:

```sh
pip install zscaler-sdk-python
```

## Building the SDK

In most cases, you won't need to build the SDK from source. If you want to build it yourself, you'll need these prerequisites:

- Clone the repo
- Install `poetry`
- Run `poetry build` from the root of the project
- Run `pip install dist/zscalerdist/zscaler_sdk_python-x.x.x.tar.gz`

### You'll also need

* An administrator account in the Zscaler products you want to interact with.
* [OneAPI](https://help.zscaler.com/oneapi/understanding-oneapi): If you are using the OneAPI entrypoint you must have a API Client created in the [Zidentity platform](https://help.zscaler.com/zidentity/about-api-clients)
* Legacy Framework: If using the legacy API framework you must have API Keys credentials in the the respective Zscaler cloud products.
* For more information on getting started with Zscaler APIs visit one of the following links:

* [OneAPI](https://help.zscaler.com/oneapi/understanding-oneapi)
* [ZPA API](https://help.zscaler.com/zpa/zpa-api/api-developer-reference-guide)
* [ZIA API](https://help.zscaler.com/zia/getting-started-zia-api)
* [ZIdentity](https://help.zscaler.com/zidentity/getting-started-zidentity-api)
* [ZDX API](https://help.zscaler.com/zdx/understanding-zdx-api)
* [ZCC API](https://help.zscaler.com/client-connector/getting-started-client-connector-api)
* [ZTW API](https://help.zscaler.com/cloud-branch-connector/getting-started-cloud-branch-connector-api)
* [ZWA API](https://help.zscaler.com/workflow-automation/getting-started-workflow-automation-api)
* [EASM API](https://hhttps://help.zscaler.com/easm/easm-api/api-developer-reference-guide/reference-guide)

## Usage guide

These examples will help you understand how to use this library.

Once you initialize a specific service client, you can call methods to make requests to the Zscaler API. Each Zscaler Service has its own package and is grouped by the API endpoint they belong to. For example, ZPA methods that call the [Application Segment API][application-segment-api-docs] are organized under [the zscaler/zpa resource (zscaler.zpa.application_segment.py)][application_segment]. The same logic applies to all other services.

**NOTE:** Zscaler APIs DO NOT support Asynchronous I/O calls, which made its debut in Python 3.5 and is powered by the `asyncio` library which provides avenues to produce concurrent code.

## Authentication

The latest versions => 0.20.0 of this SDK provides dual API client capability and can be used to interact both with new Zscaler [OneAPI](https://help.zscaler.com/oneapi/understanding-oneapi) framework and the legacy API platform.

If your Zscaler tenant has not been migrated to the new Zscaler [Zidentity platform](https://help.zscaler.com/zidentity/what-zidentity), you must use the respective Legacy API client described in the following section: [Zscaler Legacy API Framework](#zscaler-legacy-api-framework)

   :warning: **Caution**: Zscaler does not recommend hard-coding credentials into arguments, as they can be exposed in plain text in version control systems. Use environment variables instead.

## Zscaler OneAPI New Framework

As of the publication of SDK version => 1.7.x, OneAPI is available for programmatic interaction with the following products:

* [ZPA API](https://help.zscaler.com/zpa/zpa-api/api-developer-reference-guide)
* [ZIA API](https://help.zscaler.com/zia/getting-started-zia-api)
* [ZIdentity](https://help.zscaler.com/zidentity/getting-started-zidentity-api)
* [ZDX API](https://help.zscaler.com/zdx/understanding-zdx-api)
* [ZCC API](https://help.zscaler.com/client-connector/getting-started-client-connector-api)
* [ZTW API](https://help.zscaler.com/cloud-branch-connector/getting-started-cloud-branch-connector-api)
* [EASM API](https://hhttps://help.zscaler.com/easm/easm-api/api-developer-reference-guide/reference-guide)

**NOTE** All other products such as Zscaler Cloud Connector (ZTW) and Zscaler Digital Experience (ZDX) are supported only via the legacy authentication method described in this README.

### OneAPI (API Client Scope)

OneAPI Resources are automatically created within the ZIdentity Admin UI based on the RBAC Roles
applicable to APIs within the various products. For example, in ZIA, navigate to `Administration -> Role Management` and select `Add API Role`.

Once this role has been saved, return to the ZIdentity Admin UI and from the Integration menu
select API Resources. Click the `View` icon to the right of Zscaler APIs and under the ZIA
dropdown you will see the newly created Role. In the event a newly created role is not seen in the
ZIdentity Admin UI a `Sync Now` button is provided in the API Resources menu which will initiate an
on-demand sync of newly created roles.

**WARNING**: Attention Government customers. OneAPI and Zidentity is not currently supported for the following ZIA clouds: `zscalergov` and `zscalerten` or ZPA `GOV`, and `GOVUS`.

### Default Environment Variables

You can provide credentials via the `ZSCALER_CLIENT_ID`, `ZSCALER_CLIENT_SECRET`, `ZSCALER_VANITY_DOMAIN`, `ZSCALER_CLOUD`, `ZSCALER_PARTNER_ID` environment variables, representing your Zidentity OneAPI credentials `clientId`, `clientSecret`, `vanityDomain`, `cloud` and `partnerId` respectively.

| Argument     | Description | Environment variable |
|--------------|-------------|-------------------|
| `clientId`       | _(String)_ Zscaler API Client ID, used with `clientSecret` or `PrivateKey` OAuth auth mode.| `ZSCALER_CLIENT_ID` |
| `clientSecret`       | _(String)_ A string that contains the password for the API admin.| `ZSCALER_CLIENT_SECRET` |
| `privateKey`       | _(String)_ A string Private key value.| `ZSCALER_PRIVATE_KEY` |
| `vanityDomain`       | _(String)_ Refers to the domain name used by your organization `https://<vanity_domain>.zslogin.net/oauth2/v1/token` | `ZSCALER_VANITY_DOMAIN` |
| `cloud`       | _(String)_ The host and basePath for the cloud services API is `$api.<cloud_name>.zsapi.net`.| `ZSCALER_CLOUD` |
| `partnerId`       | _(String)_ Optional partner ID. When provided, the SDK automatically includes the `x-partner-id` header in all API requests.| `ZSCALER_PARTNER_ID` |
| `sandboxToken`       | _(String)_ The Zscaler Internet Access Sandbox Token | `ZSCALER_SANDBOX_TOKEN` |
| `sandboxCloud`       | _(String)_ The Zscaler Internet Access Sandbox cloud name | `ZSCALER_SANDBOX_CLOUD` |

### Alternative OneAPI Cloud Environments

OneAPI supports authentication and can interact with alternative Zscaler enviornments i.e `beta`, `alpha` etc. To authenticate to these environments you must provide the following values:

| Argument     | Description | Environment variable |
|--------------|-------------|-------------------|
| `vanityDomain`       | _(String)_ Refers to the domain name used by your organization `https://<vanity_domain>.zslogin.net/oauth2/v1/token` | `ZSCALER_VANITY_DOMAIN` |
| `cloud`       | _(String)_ The host and basePath for the cloud services API is `$api.<cloud_name>.zsapi.net`.| `ZSCALER_CLOUD` |

For example: Authenticating to Zscaler Beta environment:

```sh
export ZSCALER_VANITY_DOMAIN="acme"
export ZSCALER_CLOUD="beta"
```

**Note 1**: The attribute `cloud` or environment variable `ZSCALER_CLOUD` is optional and only required when authenticating to an alternative Zidentity cloud environment.

**Note 2**: By default this SDK will send the authentication request and subsequent API calls to the default base URL.

**Note 3**: Authentication to Zscaler Sandbox requires the attribute/parameter `sandboxCloud`.The following cloud environments are supported:

* `zscaler`
* `zscalerone`
* `zscalertwo`
* `zscalerthree`
* `zscloud`
* `zscalerbeta`
* `zscalergov`
* `zscalerten`
* `zspreview`

### Authenticating to Zscaler Private Access (ZPA)

The authentication to Zscaler Private Access (ZPA) via the OneAPI framework, requires the extra attribute called `customerId` and optionally the attributes `microtenantId` and `partnerId`.

| Argument     | Description | Environment variable |
|--------------|-------------|-------------------|
| `clientId`       | _(String)_ Zscaler API Client ID, used with `clientSecret` or `PrivateKey` OAuth auth mode.| `ZSCALER_CLIENT_ID` |
| `clientSecret`       | _(String)_ A string that contains the password for the API admin.| `ZSCALER_CLIENT_SECRET` |
| `privateKey`       | _(String)_ A string Private key value.| `ZSCALER_PRIVATE_KEY` |
| `customerId`       | _(String)_ The ZPA tenant ID found under Configuration & Control > Public API > API Keys menu in the ZPA console.| `ZPA_CUSTOMER_ID` |
| `microtenantId`       | _(String)_ The ZPA microtenant ID found in the respective microtenant instance under Configuration & Control > Public API > API Keys menu in the ZPA console.| `ZPA_MICROTENANT_ID` |
| `partnerId`       | _(String)_ Optional partner ID. When provided, the SDK automatically includes the `x-partner-id` header in all API requests.| `ZSCALER_PARTNER_ID` |
| `vanityDomain`       | _(String)_ Refers to the domain name used by your organization `https://<vanity_domain>.zslogin.net/oauth2/v1/token` | `ZSCALER_VANITY_DOMAIN` |
| `cloud`

### Initialize OneAPI OAuth 2.0 Client

#### OneAPI Client ID and Client Secret Authentication

Construct a client instance by passing your Zidentity `clientId`, `clientSecret` and `vanityDomain`:

```py
from zscaler import ZscalerClient

config = {
    "clientId": '{yourClientId}',
    "clientSecret": '{yourClientSecret}',
    "vanityDomain": '{yourvanityDomain}',
    "cloud": "beta", # Optional when authenticating to an alternative cloud environment
    "customerId": "", # Optional parameter. Required only when using ZPA
    "microtenantId": "", # Optional parameter. Required only when using ZPA with Microtenant
    "partnerId": "", # Optional parameter. When provided, automatically includes x-partner-id header in all requests
    "logging": {"enabled": False, "verbose": False},
}

def main():
    with ZscalerClient(config) as client:
        idp_id = "72058304855015574"
        query_params = {'page': '1', 'page_size': '100'}
        groups, resp, err = client.zpa.scim_groups.list_scim_groups(idp_id=idp_id, query_params=query_params)
        if err:
            print(f"Error listing SCIM groups: {err}")
            return
        if groups:
            print(f"Processing {len(groups)} groups:")
            for group in groups:
                print(group)

        try:
            resp.next()
        except StopIteration:
            print("No more groups to retrieve.")

if __name__ == "__main__":
    main()
```

#### OneAPI Client ID and Private Key Authentication

```py
from zscaler import ZscalerClient

config = {
    "clientId": '{yourClientId}',
    "privateKey": '{yourPrivateKey}',
    "vanityDomain": '{yourvanityDomain}',
    "cloud": "beta", # Optional when authenticating to an alternative cloud environment
    "customerId": "", # Optional parameter. Required only when using ZPA
    "microtenantId": "", # Optional parameter. Required only when using ZPA with Microtenant
    "partnerId": "", # Optional parameter. When provided, automatically includes x-partner-id header in all requests
    "logging": {"enabled": False, "verbose": False},
}

def main():
    with ZscalerClient(config) as client:
        idp_id = "72058304855015574"
        query_params = {'page': '1', 'page_size': '100'}
        groups, resp, err = client.zpa.scim_groups.list_scim_groups(idp_id=idp_id, query_params=query_params)
        if err:
            print(f"Error listing SCIM groups: {err}")
            return
        if groups:
            print(f"Processing {len(groups)} groups:")
            for group in groups:
                print(group)

        try:
            resp.next()
        except StopIteration:
            print("No more groups to retrieve.")

if __name__ == "__main__":
    main()
```

Note, that `privateKey` can be passed in JWK format or in PEM format, i.e. (examples generated with https://mkjwk.org):

> Using a Python dictionary to hard-code the Zscaler API credentials is encouraged for development ONLY; In production, you should use a more secure way of storing these values. This library supports a few different configuration sources, covered in the [configuration reference](#configuration-reference) section.

> **NOTE**: THIS IS NOT A PRODUCTION KEY AND IS DISPLAYED FOR EXAMPLE PURPOSES ONLY

![JWK Example](https://raw.githubusercontent.com/zscaler/zscaler-sdk-python/refs/heads/master/docsrc/jwk.svg)

or

> **NOTE**: THIS IS NOT A PRODUCTION KEY AND IS DISPLAYED FOR EXAMPLE PURPOSES ONLY

```
-----BEGIN PRIVATE KEY-----
# Example private key (not a real key)
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCv3krdYg3z7h0H
60QoePJMghllQxsfPxp3mgFfYEaIbF88Z8dvPZEfhAtP19/Mv62ASjwgqzQzKHRV
-----END PRIVATE KEY-----
```

### Get and set custom headers

It is possible to set custom headers, which will be sent with each request. This feature is only supported when instantiating the OneAPI Client `ZscalerClient`.

```py
from zscaler import ZscalerClient

def main():
    with ZscalerClient(config) as client:
        client.set_custom_headers({'Custom-Header': 'custom value'})
        groups, resp, err = client.zpa.segment_groups.list_groups()
        for group in groups:
            print(group.name, group.description)

    # clear all custom headers
    client.clear_custom_headers()

    # output should be: {}
    print(client.get_custom_headers())
```

Note, that custom headers will be overwritten with default headers with the same name.
This doesn't allow breaking the client. Get default headers:

### Automatic x-partner-id Header Injection

The SDK automatically includes the `x-partner-id` header in all API requests when `partnerId` is provided in the configuration. This feature works seamlessly across all services (ZIA, ZPA, ZTW, ZCC, ZDX, ZWA) and both OneAPI and Legacy clients.

**How it works:**
- When `partnerId` is provided via config dictionary or `ZSCALER_PARTNER_ID` environment variable, the SDK automatically adds `x-partner-id: <partnerId>` to all request headers
- If `partnerId` is not provided, the header is not included
- No additional code is required - the header injection is handled automatically by the SDK

**Example:**

```py
from zscaler import ZscalerClient

config = {
    "clientId": '{yourClientId}',
    "clientSecret": '{yourClientSecret}',
    "vanityDomain": '{yourvanityDomain}',
    "partnerId": "542585sdsdw", # Automatically adds x-partner-id header to all requests
    "logging": {"enabled": False, "verbose": False},
}

def main():
    with ZscalerClient(config) as client:
        # All API requests will automatically include: x-partner-id: 542585sdsdw
        groups, resp, err = client.zpa.segment_groups.list_groups()
        # ... rest of your code
```

**Note:** This feature is also supported in Legacy clients. When using `LegacyZPAClient`, `LegacyZIAClient`, etc., you can provide `partnerId` in the config dictionary and the header will be automatically included in all requests.

### ZIA and ZTW Context Manager

The Zscaler SDK provides a context manager pattern that automatically handles authentication and session cleanup for both ZIA and ZTW services. This pattern ensures that all configuration changes are properly activated when the context manager exits.

#### How Context Manager Works

When you use the `with` statement with a Zscaler client, the following happens automatically:

1. **Authentication**: The client authenticates when entering the context
2. **Session Management**: A session is established and maintained throughout the context
3. **Automatic Deauthentication**: When exiting the context, the client automatically deauthenticates, which activates all staged configuration changes

#### Implicit Activation Process

The context manager implements an "implicit activation" approach where:

- **All changes are final**: Configuration changes are automatically activated when the context exits
- **No manual activation required**: You don't need to remember to call activation endpoints
- **Deterministic behavior**: You always know that exiting the context will activate changes
- **Automation-friendly**: Perfect for scripts and automation scenarios

#### Example Usage

```py
from zscaler import ZscalerClient

config = {
    "clientId": '{yourClientId}',
    "clientSecret": '{yourClientSecret}',
    "vanityDomain": '{yourvanityDomain}',
    "cloud": "beta", # Optional when authenticating to an alternative cloud environment
    "customerId": "", # Optional parameter. Required only when using ZPA
    "microtenantId": "", # Optional parameter. Required only when using ZPA with Microtenant
    "partnerId": "", # Optional parameter. When provided, automatically includes x-partner-id header in all requests
    "logging": {"enabled": False, "verbose": False},
}

def main():
    with ZscalerClient(config) as client:
        # Make ZIA configuration changes
        added_role, response, error = client.zia.admin_roles.add_role(
            name="New API Role",
            description="Role created via API",
            feature_permissions={"ZIA_ADMIN_ROLE": "READ"}
        )
        if error:
            print(f"Error adding role: {error}")
            return
        
        # Make ZTW configuration changes
        added_group, response, error = client.ztw.ip_destination_groups.add_group(
            name="New IP Group",
            description="IP group created via API"
        )
        if error:
            print(f"Error adding IP group: {error}")
            return
        
        print("All changes made successfully")
    
    # Context manager automatically deauthenticates here
    # All staged changes are activated automatically for both ZIA and ZTW
    print("Context exited - all changes have been activated")

if __name__ == "__main__":
    main()
```

#### Benefits

- **Automatic cleanup**: No need to manually deauthenticate
- **Error handling**: Even if an exception occurs, the context manager ensures proper cleanup
- **Staged configuration activation**: All changes are activated when the context exits
- **Simplified code**: No need to remember activation steps
- **Multi-service support**: Works seamlessly with both ZIA and ZTW services

## Zscaler OneAPI Rate Limiting

Zscaler OneAPI provides unique rate limiting numbers for each individual product. Regardless of the product, a 429 response will be returned if too many requests are made within a given time.

### Built-In Retry

This SDK uses a built-in retry strategy to automatically retry on 429 errors based on the response headers returned by each respective API service.

The header `x-ratelimit-reset` is returned in the API response for each API call, which indicates the time in seconds until the rate limit resets. The SDK uses the returned value in this header to calculate the retry time for the following services:

* [ZCC Rate Limiting][rate-limiting-zcc] for rate limiting requirements.
* [ZIA Rate Limiting][rate-limiting-zia] for rate limiting requirements.
* [ZPA Rate Limiting][rate-limiting-zpa] for rate limiting requirements.

## Pagination

The pagination system in this SDK is unified across `ZCC`, `ZTW`, `ZDX`, `ZIA`, `ZPA`, `ZWA`
and is applied transparently whether you're using the Legacy API Client or the new OneAPI OAuth2 Client.

✅ This means no code changes are needed when transitioning from the legacy API framework to OneAPI framework.

When calling a method that supports pagination (e.g., `list_users`, `list_groups`, `list_app_segments`), only the first page of results is returned initially. The SDK returns a response tuple:

```py
items, response, error = client.zia.user_management.list_groups()
```

You can then use the `response.has_next()` and `response.next()` methods to retrieve subsequent pages.

### Basic Pagination Example

```py
query_parameters = {'page_size': 100}
groups, resp, err = client.zia.user_management.list_groups(query_parameters)

while resp.has_next():
    more_groups, resp, err = resp.next()  # Unpack all 3 return values
    if err:
        break
    if more_groups:
        groups.extend(more_groups)
```

### ZPA Searching and Filtering

The ZPA API uses a filtering/query parameter format for search operations. Search strings must follow the format: `fieldName operator fieldValue`. The SDK provides automatic conversion for simple name searches while allowing full control for advanced filtering.

#### Simple Name Search (Automatic Conversion)

For convenience, you can provide a simple search string when searching by name. The SDK automatically converts it to the `name+EQ+<search_string>` format for exact name matching:

```py
# Simple string search - automatically converted to name+EQ+CDE Segment Group
query_parameters = {'search': 'CDE Segment Group'}
groups, resp, err = client.zpa.segment_groups.list_groups(query_parameters)
```

#### Advanced Filtering (Explicit Format)

To search by other fields or use different operators, you must provide the complete filter format: `fieldName+operator+fieldValue`. Common operators include:

- `EQ` - Equals
- `NE` - Not equals
- `GT` - Greater than
- `LT` - Less than
- `GE` - Greater than or equal
- `LE` - Less than or equal
- `CONTAINS` - Contains substring
- `STARTSWITH` - Starts with
- `ENDSWITH` - Ends with

**Examples:**

```py
# Search by enabled status
query_parameters = {'search': 'enabled+EQ+true'}
groups, resp, err = client.zpa.segment_groups.list_groups(query_parameters)

# Search by description with CONTAINS operator
query_parameters = {'search': 'description+CONTAINS+test'}
groups, resp, err = client.zpa.segment_groups.list_groups(query_parameters)

# Search by name with STARTSWITH operator
query_parameters = {'search': 'name+STARTSWITH+CDE'}
groups, resp, err = client.zpa.segment_groups.list_groups(query_parameters)
```

**Note:** If your search string already contains a filter operator pattern (like `+EQ+` or `+CONTAINS+`), the SDK will use it as-is without modification. This allows full control over filtering criteria while maintaining convenience for simple name searches.

#### Combining Search with Pagination

You can combine search filters with pagination parameters:

```py
query_parameters = {
    'search': 'name+EQ+CDE Segment Group',
    'page': 1,
    'pagesize': 20
}
groups, resp, err = client.zpa.segment_groups.list_groups(query_parameters)

# Pagination works seamlessly with filtered searches
while resp.has_next():
    more_groups, resp, err = resp.next()
    if err:
        break
    if more_groups:
        groups.extend(more_groups)
```

### Full Example with Error Handling
```py
def main():
    with ZscalerClient(config) as client:
        query_parameters = {}
        groups, resp, err = client.zia.user_management.list_groups(query_parameters)

        if err:
            print(f"Error: {err}")
            return

        print(f"Processing {len(groups)} groups:")
        for group in groups:
            print(group)

        while resp.has_next():
            next_page, resp, err = resp.next()  # Unpack all 3 return values
            if err:
                print(f"Error fetching next page: {err}")
                break
            if next_page:
                for group in next_page:
                    print(group)

        try:
            resp.next()  # Will raise StopIteration if no more data
        except StopIteration:
            print("✅ No more groups to retrieve.")

if __name__ == "__main__":
    main()
```

### Pagination Limits and Controls

Each Zscaler service has its own pagination requirements and limits. The SDK automatically respects these API defaults when no `page_size` is provided:

| Service | API Default Page Size | Max Page Size | Pagination Parameters                           |
|---------|-----------------------|---------------|-------------------------------------------------|
| ZCC     | Varies by endpoint    | Varies        | Uses `page`, `pageSize`                         |
| ZDX     | 10                    | Varies        | Uses `limit` + `offset` (cursor-based)          |
| ZIA     | 100                   | 1000          | Uses `page`, `pageSize`                         |
| ZPA     | 20                    | 500           | Uses `page`, `pagesize`                         |
| ZTW     | 100                   | Varies        | Uses `page`, `pageSize`                         |
| ZWA     | Varies by endpoint    | Varies        | Uses `page`, `pageSize`                         |

**Important Notes:**
- ✅ The SDK automatically uses each API's default page size when no `page_size` is specified
- ✅ Always use `snake_case` for parameter names (e.g., `page_size`). The SDK handles conversion internally
- ✅ Pagination stops automatically when fewer results than the page size are returned


You can control how many total items or pages the SDK will fetch even if more data is available.

###  Internal Pagination Handling

The `ZscalerAPIResponse` object returned as resp handles:

* Tracking the current page
* Automatically applying proper pagination parameters per service
* Mapping pagination fields like page, pagesize, limit, offset, next_offset, etc.
* Fallback handling when the API doesn't indicate the total count

You don’t need to worry about API quirks—just use `resp.has_next()` and `resp.next()` safely.

⚠️ Note on StopIteration
The SDK raises a StopIteration if `next()` is called and no more pages are available:

```py
try:
    resp.next()
except StopIteration:
    print("All data fetched.")
```

## Logging

The Zscaler SDK Python, provides robust logging for debug purposes.
Logs are disabled by default and should be enabled explicitly via client configuration or via a [configuration file](#configuration-reference):

```py
from zscaler import ZscalerClient

config = {"logging": {"enabled": True}}
client = ZscalerClient(config)
```
You can also enable debug logging via the following environment variables:
* `ZSCALER_SDK_LOG` - Turn on logging
* `ZSCALER_SDK_VERBOSE` - Turn on logging in verbose mode

```sh
export ZSCALER_SDK_LOG=true
export ZSCALER_SDK_VERBOSE=true
```

This SDK utilizes the standard Python library `logging`. By default, log level INFO is set. You can set another log level by setting the argument `verbose` to `True`.

**NOTE**: DO NOT SET DEBUG LEVEL IN PRODUCTION!

```py
from zscaler import ZscalerClient

config = {
    "clientId": '{yourClientId}',
    "clientSecret": '{yourClientSecret}',
    "vanityDomain": '{yourvanityDomain}',
    "cloud": "beta", # Optional when authenticating to an alternative cloud environment
    "customerId": "", # Optional parameter. Required only when using ZPA
    "microtenantId": "", # Optional parameter. Required only when using ZPA with Microtenant
    "logging": {"enabled": True, "verbose": True},
}

def main():
    with ZscalerClient(config) as client:
        groups, resp, err = client.zpa.segment_groups.list_groups()
        for group in groups:
            print(group.name, group.description)
if __name__ == "__main__":
    main()
```

You should now see logs in your console. Notice that API Credentials i.e `clientId` and `clientSecret` are **NOT** logged to the console; however, Bearer tokens are still visible. We still advise to use caution and never use `verbose` level logging in production.

What it being logged? `requests`, `responses`, `http errors`, `caching responses`.

### Using Your Own Logger (Optional)
If your script defines its own logging configuration (e.g., for file or custom formatting), the SDK will not interfere with it. You can continue using your own logger like this:

```py
import logging

my_logger = logging.getLogger("my_app_logger")
my_logger.setLevel(logging.INFO)
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s"))
my_logger.addHandler(handler)

my_logger.info("This is your app-level log, independent from the SDK.")
```

To control SDK logging separately, use:

```py
logging.getLogger("zscaler-sdk-python").setLevel(logging.WARNING)  # or .ERROR to silence SDK logs
```

The SDK will never globally disable logging or interfere with your existing logging configuration.

## Configuration reference

This library looks for configuration in the following sources:

0. An `zscaler.yaml` file in a `.zscaler` folder in the current user's home directory (`~/.zscaler/zscaler.yaml` or `%userprofile%\.zscaler\zscaler.yaml`). See a sample [YAML Configuration](#yaml-configuration)
1. A `zscaler.yaml` file in the application or project's root directory. See a sample [YAML Configuration](#yaml-configuration)
2. [Environment variables](#environment-variables)
3. Configuration explicitly passed to the constructor (see the example in [Getting started](#getting-started))

> Only ONE source needs to be provided!

Higher numbers win. In other words, configuration passed via the constructor will OVERRIDE configuration found in environment variables, which will override configuration in the designated `zscaler.yaml` files.

**NOTE:** This option is only supported for OneAPI Zidentity credentials at the moment.

### YAML configuration

When you use an API Token instead of OAuth 2.0 the full YAML configuration looks like:

```yaml
zscaler:
  client:
    clientId: { yourClientId }
    clientSecret: { yourClientSecret }
    vanityDomain: { yourVanityDomain }
    customerId: { yourCustomerId}
    proxy:
      port: { proxy_port }
      host: { proxy_host }
      username: { proxy_username }
      password: { proxy_password }
    logging:
      enabled: true
      verbose: true
```

> **NOTE**: THIS IS NOT A PRODUCTION KEY AND IS DISPLAYED FOR EXAMPLE PURPOSES ONLY

When you use OAuth 2.0 the full YAML configuration looks like:

```yaml
zscaler:
  client:
    clientId: "YOUR_CLIENT_ID"
    privateKey: |
      -----BEGIN RSA PRIVATE KEY-----
      MIIEogIBAAKCAQEAl4F5CrP6Wu2kKwH1Z+CNBdo0iteHhVRIXeHdeoqIB1iXvuv4
      THQdM5PIlot6XmeV1KUKuzw2ewDeb5zcasA4QHPcSVh2+KzbttPQ+RUXCUAr5t+r
      0r6gBc5Dy1IPjCFsqsPJXFwqe3RzUb...
      -----END RSA PRIVATE KEY-----
    vanityDomain: { yourVanityDomain }
    customerId: { yourCustomerId}
    proxy:
      port: { proxy_port }
      host: { proxy_host }
      username: { proxy_username }
      password: { proxy_password }
    logging:
      enabled: true
      verbose: true
```

### Environment variables

Each one of the configuration values above can be turned into an environment variable name with the `_` (underscore) character and UPPERCASE characters. The following are accepted:

| Argument     | Description | Environment variable |
|--------------|-------------|-------------------|
| `clientId`       | _(String)_ Zscaler API Client ID, used with `clientSecret` or `PrivateKey` OAuth auth mode.| `ZSCALER_CLIENT_ID` |
| `clientSecret`       | _(String)_ A string that contains the password for the API admin.| `ZSCALER_CLIENT_SECRET` |
| `privateKey`       | _(String)_ A string Private key value.| `ZSCALER_CLIENT_PRIVATEKEY` |
| `vanityDomain`       | _(String)_ Refers to the domain name used by your organization `https://<vanity_domain>.zslogin.net/oauth2/v1/token` | `ZSCALER_VANITY_DOMAIN` |
| `cloud`       | _(String)_ The host and basePath for the cloud services API is `$api.<cloud_name>.zsapi.net`.| `ZSCALER_CLOUD` |
| `userAgent`       | _(String)_ Append additional information to the HTTP User-Agent | `ZSCALER_CLIENT_USERAGENT` |
| `cache.enabled`       | _(String)_ Use request memory cache | `ZSCALER_CLIENT_CACHE_ENABLED` |
| `cache.defaultTti`       | _(String)_ Cache clean up interval in seconds | `ZSCALER_CLIENT_CACHE_DEFAULTTTI` |
| `cache.defaultTtl`       | _(String)_ Cache time to live in seconds | `ZSCALER_CLIENT_CACHE_DEFAULTTTL` |
| `proxyPort`       | _(String)_ HTTP proxy port | `ZSCALER_CLIENT_PROXY_PORT` |
| `proxyHost`       | _(String)_ HTTP proxy host  | `ZSCALER_CLIENT_PROXY_HOST` |
| `proxyUsername`       | _(String)_ HTTP proxy username  | `ZSCALER_CLIENT_PROXY_USERNAME` |
| `proxyPassword`       | _(String)_ HTTP proxy password  | `ZSCALER_CLIENT_PROXY_PASSWORD` |
| `disableHttpsCheck`       | _(String)_ Disable SSL checks  | `ZSCALER_TESTING_TESTINGDISABLEHTTPSCHECK` |
| `sandboxToken`       | _(String)_ The Zscaler Internet Access Sandb