Skip to content

Exceptions

All library exceptions inherit from MixpanelHeadlessError, enabling callers to catch all library errors with a single except clause.

Explore on DeepWiki

🤖 Error Handling Guide →

Ask questions about specific exceptions, error recovery patterns, or debugging strategies.

Exception Hierarchy

MixpanelHeadlessError
├── ConfigError
│   ├── AccountNotFoundError
│   ├── AccountExistsError
│   ├── AccountInUseError
│   ├── InvalidArgumentError
│   └── ProjectNotFoundError
├── APIError
│   ├── AuthenticationError
│   ├── RateLimitError
│   ├── QueryError
│   ├── ServerError
│   └── JQLSyntaxError
├── OAuthError
│   └── RegionProbeError
│       └── RegionProbeNetworkError
├── WorkspaceScopeError
└── BusinessContextValidationError

Catching Errors

import mixpanel_headless as mp

try:
    ws = mp.Workspace()
    result = ws.segmentation(event="Purchase", from_date="2025-01-01", to_date="2025-01-31")
except mp.AuthenticationError as e:
    print(f"Auth failed: {e.message}")
except mp.RateLimitError as e:
    print(f"Rate limited, retry after {e.retry_after}s")
except mp.OAuthError as e:
    print(f"OAuth error [{e.code}]: {e.message}")
except mp.WorkspaceScopeError as e:
    print(f"Workspace error [{e.code}]: {e.message}")
except mp.AccountInUseError as e:
    print(f"Account '{e.account_name}' referenced by targets: {e.referenced_by}")
except mp.MixpanelHeadlessError as e:
    print(f"Error [{e.code}]: {e.message}")

Base Exception

mixpanel_headless.MixpanelHeadlessError

MixpanelHeadlessError(
    message: str,
    code: str = "UNKNOWN_ERROR",
    details: dict[str, Any] | None = None,
)

Bases: Exception

Base exception for all mixpanel_headless errors.

All library exceptions inherit from this class, allowing callers to: - Catch all library errors: except MixpanelHeadlessError - Handle specific errors: except AccountNotFoundError - Serialize errors: error.to_dict()

Initialize exception.

PARAMETER DESCRIPTION
message

Human-readable error message.

TYPE: str

code

Machine-readable error code for programmatic handling.

TYPE: str DEFAULT: 'UNKNOWN_ERROR'

details

Additional structured data about the error.

TYPE: dict[str, Any] | None DEFAULT: None

Source code in src/mixpanel_headless/exceptions.py
def __init__(
    self,
    message: str,
    code: str = "UNKNOWN_ERROR",
    details: dict[str, Any] | None = None,
) -> None:
    """Initialize exception.

    Args:
        message: Human-readable error message.
        code: Machine-readable error code for programmatic handling.
        details: Additional structured data about the error.
    """
    super().__init__(message)
    self._message = message
    self._code = code
    self._details = details or {}

code property

code: str

Machine-readable error code.

message property

message: str

Human-readable error message.

details property

details: dict[str, Any]

Additional structured error data.

to_dict

to_dict() -> dict[str, Any]

Serialize exception for logging/JSON output.

RETURNS DESCRIPTION
dict[str, Any]

Dictionary with keys: code, message, details.

dict[str, Any]

All values are JSON-serializable.

Source code in src/mixpanel_headless/exceptions.py
def to_dict(self) -> dict[str, Any]:
    """Serialize exception for logging/JSON output.

    Returns:
        Dictionary with keys: code, message, details.
        All values are JSON-serializable.
    """
    return {
        "code": self._code,
        "message": self._message,
        "details": self._details,
    }

__str__

__str__() -> str

Return human-readable error message.

Source code in src/mixpanel_headless/exceptions.py
def __str__(self) -> str:
    """Return human-readable error message."""
    return self._message

__repr__

__repr__() -> str

Return detailed string representation.

Source code in src/mixpanel_headless/exceptions.py
def __repr__(self) -> str:
    """Return detailed string representation."""
    return (
        f"{self.__class__.__name__}(message={self._message!r}, code={self._code!r})"
    )

API Exceptions

mixpanel_headless.APIError

APIError(
    message: str,
    *,
    status_code: int,
    response_body: str | dict[str, Any] | None = None,
    request_method: str | None = None,
    request_url: str | None = None,
    request_params: dict[str, Any] | None = None,
    request_body: dict[str, Any] | None = None,
    code: str = "API_ERROR",
)

Bases: MixpanelHeadlessError

Base class for Mixpanel API HTTP errors.

Provides structured access to HTTP request/response context for debugging and automated recovery by AI agents. All API-related exceptions inherit from this class, enabling agents to:

  • Understand what went wrong (status code, error message)
  • See exactly what was sent (request method, URL, params, body)
  • See exactly what came back (response body, headers)
  • Modify their approach and retry autonomously
Example
try:
    result = client.segmentation(event="signup", ...)
except APIError as e:
    print(f"Status: {e.status_code}")
    print(f"Response: {e.response_body}")
    print(f"Request URL: {e.request_url}")
    print(f"Request params: {e.request_params}")

Initialize APIError.

PARAMETER DESCRIPTION
message

Human-readable error message.

TYPE: str

status_code

HTTP status code from response.

TYPE: int

response_body

Raw response body (string or parsed dict).

TYPE: str | dict[str, Any] | None DEFAULT: None

request_method

HTTP method used (GET, POST).

TYPE: str | None DEFAULT: None

request_url

Full request URL.

TYPE: str | None DEFAULT: None

request_params

Query parameters sent.

TYPE: dict[str, Any] | None DEFAULT: None

request_body

Request body sent (for POST requests).

TYPE: dict[str, Any] | None DEFAULT: None

code

Machine-readable error code.

TYPE: str DEFAULT: 'API_ERROR'

Source code in src/mixpanel_headless/exceptions.py
def __init__(
    self,
    message: str,
    *,
    status_code: int,
    response_body: str | dict[str, Any] | None = None,
    request_method: str | None = None,
    request_url: str | None = None,
    request_params: dict[str, Any] | None = None,
    request_body: dict[str, Any] | None = None,
    code: str = "API_ERROR",
) -> None:
    """Initialize APIError.

    Args:
        message: Human-readable error message.
        status_code: HTTP status code from response.
        response_body: Raw response body (string or parsed dict).
        request_method: HTTP method used (GET, POST).
        request_url: Full request URL.
        request_params: Query parameters sent.
        request_body: Request body sent (for POST requests).
        code: Machine-readable error code.
    """
    self._status_code = status_code
    self._response_body = response_body
    self._request_method = request_method
    self._request_url = request_url
    self._request_params = request_params
    self._request_body = request_body

    details: dict[str, Any] = {
        "status_code": status_code,
    }
    if response_body is not None:
        details["response_body"] = response_body
    if request_method is not None:
        details["request_method"] = request_method
    if request_url is not None:
        details["request_url"] = request_url
    if request_params is not None:
        details["request_params"] = request_params
    if request_body is not None:
        details["request_body"] = request_body

    super().__init__(message, code=code, details=details)

status_code property

status_code: int

HTTP status code from response.

response_body property

response_body: str | dict[str, Any] | None

Raw response body (string or parsed dict).

request_method property

request_method: str | None

HTTP method used (GET, POST).

request_url property

request_url: str | None

Full request URL.

request_params property

request_params: dict[str, Any] | None

Query parameters sent.

request_body property

request_body: dict[str, Any] | None

Request body sent (for POST requests).

mixpanel_headless.AuthenticationError

AuthenticationError(
    message: str = "Authentication failed",
    *,
    status_code: int = 401,
    response_body: str | dict[str, Any] | None = None,
    request_method: str | None = None,
    request_url: str | None = None,
    request_params: dict[str, Any] | None = None,
)

Bases: APIError

Authentication with Mixpanel API failed (HTTP 401).

Raised when credentials are invalid, expired, or lack required permissions. Inherits from APIError to provide full request/response context.

Example
try:
    client.segmentation(...)
except AuthenticationError as e:
    print(f"Auth failed: {e.message}")
    print(f"Request URL: {e.request_url}")
    # Check if project_id is correct, credentials are valid, etc.

Initialize AuthenticationError.

PARAMETER DESCRIPTION
message

Human-readable error message.

TYPE: str DEFAULT: 'Authentication failed'

status_code

HTTP status code (default 401).

TYPE: int DEFAULT: 401

response_body

Raw response body.

TYPE: str | dict[str, Any] | None DEFAULT: None

request_method

HTTP method used.

TYPE: str | None DEFAULT: None

request_url

Full request URL.

TYPE: str | None DEFAULT: None

request_params

Query parameters sent.

TYPE: dict[str, Any] | None DEFAULT: None

Source code in src/mixpanel_headless/exceptions.py
def __init__(
    self,
    message: str = "Authentication failed",
    *,
    status_code: int = 401,
    response_body: str | dict[str, Any] | None = None,
    request_method: str | None = None,
    request_url: str | None = None,
    request_params: dict[str, Any] | None = None,
) -> None:
    """Initialize AuthenticationError.

    Args:
        message: Human-readable error message.
        status_code: HTTP status code (default 401).
        response_body: Raw response body.
        request_method: HTTP method used.
        request_url: Full request URL.
        request_params: Query parameters sent.
    """
    super().__init__(
        message,
        status_code=status_code,
        response_body=response_body,
        request_method=request_method,
        request_url=request_url,
        request_params=request_params,
        code="AUTH_FAILED",
    )

mixpanel_headless.RateLimitError

RateLimitError(
    message: str = "Rate limit exceeded",
    *,
    retry_after: int | None = None,
    status_code: int = 429,
    response_body: str | dict[str, Any] | None = None,
    request_method: str | None = None,
    request_url: str | None = None,
    request_params: dict[str, Any] | None = None,
)

Bases: APIError

Mixpanel API rate limit exceeded (HTTP 429).

Raised when the API returns a 429 status. The retry_after property indicates when the request can be retried. Inherits from APIError to provide full request context for debugging.

Example
try:
    for _ in range(1000):
        client.segmentation(...)
except RateLimitError as e:
    print(f"Rate limited! Retry after {e.retry_after}s")
    print(f"Request: {e.request_method} {e.request_url}")
    time.sleep(e.retry_after or 60)

Initialize RateLimitError.

PARAMETER DESCRIPTION
message

Human-readable error message.

TYPE: str DEFAULT: 'Rate limit exceeded'

retry_after

Seconds until retry is allowed (from Retry-After header).

TYPE: int | None DEFAULT: None

status_code

HTTP status code (default 429).

TYPE: int DEFAULT: 429

response_body

Raw response body.

TYPE: str | dict[str, Any] | None DEFAULT: None

request_method

HTTP method used.

TYPE: str | None DEFAULT: None

request_url

Full request URL.

TYPE: str | None DEFAULT: None

request_params

Query parameters sent.

TYPE: dict[str, Any] | None DEFAULT: None

Source code in src/mixpanel_headless/exceptions.py
def __init__(
    self,
    message: str = "Rate limit exceeded",
    *,
    retry_after: int | None = None,
    status_code: int = 429,
    response_body: str | dict[str, Any] | None = None,
    request_method: str | None = None,
    request_url: str | None = None,
    request_params: dict[str, Any] | None = None,
) -> None:
    """Initialize RateLimitError.

    Args:
        message: Human-readable error message.
        retry_after: Seconds until retry is allowed (from Retry-After header).
        status_code: HTTP status code (default 429).
        response_body: Raw response body.
        request_method: HTTP method used.
        request_url: Full request URL.
        request_params: Query parameters sent.
    """
    self._retry_after = retry_after
    if retry_after is not None:
        message = f"{message}. Retry after {retry_after} seconds."

    super().__init__(
        message,
        status_code=status_code,
        response_body=response_body,
        request_method=request_method,
        request_url=request_url,
        request_params=request_params,
        code="RATE_LIMITED",
    )
    # Add retry_after to details
    if retry_after is not None:
        self._details["retry_after"] = retry_after

retry_after property

retry_after: int | None

Seconds until retry is allowed, or None if unknown.

mixpanel_headless.QueryError

QueryError(
    message: str = "Query execution failed",
    *,
    status_code: int = 400,
    response_body: str | dict[str, Any] | None = None,
    request_method: str | None = None,
    request_url: str | None = None,
    request_params: dict[str, Any] | None = None,
    request_body: dict[str, Any] | None = None,
)

Bases: APIError

Query execution failed (HTTP 400 or query-specific error).

Raised when an API query fails due to invalid parameters, syntax errors, or other query-specific issues. Inherits from APIError to provide full request/response context for debugging.

Example
try:
    client.segmentation(event="nonexistent", ...)
except QueryError as e:
    print(f"Query failed: {e.message}")
    print(f"Response: {e.response_body}")
    print(f"Request params: {e.request_params}")

Initialize QueryError.

PARAMETER DESCRIPTION
message

Human-readable error message.

TYPE: str DEFAULT: 'Query execution failed'

status_code

HTTP status code (default 400).

TYPE: int DEFAULT: 400

response_body

Raw response body with error details.

TYPE: str | dict[str, Any] | None DEFAULT: None

request_method

HTTP method used.

TYPE: str | None DEFAULT: None

request_url

Full request URL.

TYPE: str | None DEFAULT: None

request_params

Query parameters sent.

TYPE: dict[str, Any] | None DEFAULT: None

request_body

Request body sent (for POST).

TYPE: dict[str, Any] | None DEFAULT: None

Source code in src/mixpanel_headless/exceptions.py
def __init__(
    self,
    message: str = "Query execution failed",
    *,
    status_code: int = 400,
    response_body: str | dict[str, Any] | None = None,
    request_method: str | None = None,
    request_url: str | None = None,
    request_params: dict[str, Any] | None = None,
    request_body: dict[str, Any] | None = None,
) -> None:
    """Initialize QueryError.

    Args:
        message: Human-readable error message.
        status_code: HTTP status code (default 400).
        response_body: Raw response body with error details.
        request_method: HTTP method used.
        request_url: Full request URL.
        request_params: Query parameters sent.
        request_body: Request body sent (for POST).
    """
    super().__init__(
        message,
        status_code=status_code,
        response_body=response_body,
        request_method=request_method,
        request_url=request_url,
        request_params=request_params,
        request_body=request_body,
        code="QUERY_FAILED",
    )

mixpanel_headless.ServerError

ServerError(
    message: str = "Server error",
    *,
    status_code: int = 500,
    response_body: str | dict[str, Any] | None = None,
    request_method: str | None = None,
    request_url: str | None = None,
    request_params: dict[str, Any] | None = None,
    request_body: dict[str, Any] | None = None,
)

Bases: APIError

Mixpanel server error (HTTP 5xx).

Raised when the Mixpanel API returns a server error. These are typically transient issues that may succeed on retry. The response_body property contains the full error details from Mixpanel, which often include actionable information (e.g., "unit and interval both specified").

Example
try:
    client.retention(born_event="signup", ...)
except ServerError as e:
    print(f"Server error {e.status_code}: {e.message}")
    print(f"Response: {e.response_body}")
    print(f"Request params: {e.request_params}")
    # AI agent can analyze response_body to fix the request

Initialize ServerError.

PARAMETER DESCRIPTION
message

Human-readable error message.

TYPE: str DEFAULT: 'Server error'

status_code

HTTP status code (5xx).

TYPE: int DEFAULT: 500

response_body

Raw response body with error details.

TYPE: str | dict[str, Any] | None DEFAULT: None

request_method

HTTP method used.

TYPE: str | None DEFAULT: None

request_url

Full request URL.

TYPE: str | None DEFAULT: None

request_params

Query parameters sent.

TYPE: dict[str, Any] | None DEFAULT: None

request_body

Request body sent (for POST).

TYPE: dict[str, Any] | None DEFAULT: None

Source code in src/mixpanel_headless/exceptions.py
def __init__(
    self,
    message: str = "Server error",
    *,
    status_code: int = 500,
    response_body: str | dict[str, Any] | None = None,
    request_method: str | None = None,
    request_url: str | None = None,
    request_params: dict[str, Any] | None = None,
    request_body: dict[str, Any] | None = None,
) -> None:
    """Initialize ServerError.

    Args:
        message: Human-readable error message.
        status_code: HTTP status code (5xx).
        response_body: Raw response body with error details.
        request_method: HTTP method used.
        request_url: Full request URL.
        request_params: Query parameters sent.
        request_body: Request body sent (for POST).
    """
    super().__init__(
        message,
        status_code=status_code,
        response_body=response_body,
        request_method=request_method,
        request_url=request_url,
        request_params=request_params,
        request_body=request_body,
        code="SERVER_ERROR",
    )

mixpanel_headless.JQLSyntaxError

JQLSyntaxError(
    raw_error: str, script: str | None = None, request_path: str | None = None
)

Bases: QueryError

JQL script execution failed with syntax or runtime error (HTTP 412).

Raised when a JQL script fails to execute due to syntax errors, type errors, or other JavaScript runtime issues. Provides structured access to error details from Mixpanel's response.

Inherits from QueryError (and thus APIError) to provide full HTTP context.

Example
try:
    result = live_query.jql(script)
except JQLSyntaxError as e:
    print(f"Error: {e.error_type}: {e.error_message}")
    print(f"Script: {e.script}")
    print(f"Line info: {e.line_info}")
    # AI agent can use this to fix the script and retry

Initialize JQLSyntaxError.

PARAMETER DESCRIPTION
raw_error

Raw error string from Mixpanel API response.

TYPE: str

script

The JQL script that caused the error.

TYPE: str | None DEFAULT: None

request_path

API request path from error response.

TYPE: str | None DEFAULT: None

Source code in src/mixpanel_headless/exceptions.py
def __init__(
    self,
    raw_error: str,
    script: str | None = None,
    request_path: str | None = None,
) -> None:
    """Initialize JQLSyntaxError.

    Args:
        raw_error: Raw error string from Mixpanel API response.
        script: The JQL script that caused the error.
        request_path: API request path from error response.
    """
    # Parse structured error info from raw error string
    self._error_type = self._extract_error_type(raw_error)
    self._error_message = self._extract_message(raw_error)
    self._line_info = self._extract_line_info(raw_error)
    self._stack_trace = self._extract_stack_trace(raw_error)
    self._script = script
    self._raw_error = raw_error
    self._request_path = request_path

    # Build human-readable message
    message = f"JQL {self._error_type}: {self._error_message}"
    if self._line_info:
        message += f"\n{self._line_info}"

    # Build response body dict for APIError
    response_body: dict[str, Any] = {
        "error": raw_error,
    }
    if request_path:
        response_body["request"] = request_path

    super().__init__(
        message,
        status_code=412,
        response_body=response_body,
        request_body={"script": script} if script else None,
    )
    self._code = "JQL_SYNTAX_ERROR"

    # Add JQL-specific details
    self._details["error_type"] = self._error_type
    self._details["error_message"] = self._error_message
    self._details["line_info"] = self._line_info
    self._details["stack_trace"] = self._stack_trace
    self._details["script"] = script
    self._details["request_path"] = request_path
    self._details["raw_error"] = raw_error

error_type property

error_type: str

JavaScript error type (TypeError, SyntaxError, ReferenceError, etc.).

error_message property

error_message: str

Error message describing what went wrong.

line_info property

line_info: str | None

Code snippet with caret showing error location, if available.

stack_trace property

stack_trace: str | None

JavaScript stack trace, if available.

script property

script: str | None

The JQL script that caused the error.

raw_error property

raw_error: str

Complete raw error string from Mixpanel.

Configuration Exceptions

mixpanel_headless.ConfigError

ConfigError(message: str, details: dict[str, Any] | None = None)

Bases: MixpanelHeadlessError

Base for configuration-related errors.

Raised when there's a problem with configuration files, environment variables, or credential resolution.

Initialize ConfigError.

PARAMETER DESCRIPTION
message

Human-readable error message.

TYPE: str

details

Additional structured data.

TYPE: dict[str, Any] | None DEFAULT: None

Source code in src/mixpanel_headless/exceptions.py
def __init__(
    self,
    message: str,
    details: dict[str, Any] | None = None,
) -> None:
    """Initialize ConfigError.

    Args:
        message: Human-readable error message.
        details: Additional structured data.
    """
    super().__init__(message, code="CONFIG_ERROR", details=details)

mixpanel_headless.AccountNotFoundError

AccountNotFoundError(
    account_name: str, available_accounts: list[str] | None = None
)

Bases: ConfigError

Named account does not exist in configuration.

Raised when attempting to access an account that hasn't been configured. The available_accounts property lists valid account names to help users.

Initialize AccountNotFoundError.

PARAMETER DESCRIPTION
account_name

The requested account name that wasn't found.

TYPE: str

available_accounts

List of valid account names for suggestions.

TYPE: list[str] | None DEFAULT: None

Source code in src/mixpanel_headless/exceptions.py
def __init__(
    self,
    account_name: str,
    available_accounts: list[str] | None = None,
) -> None:
    """Initialize AccountNotFoundError.

    Args:
        account_name: The requested account name that wasn't found.
        available_accounts: List of valid account names for suggestions.
    """
    available = available_accounts or []
    if available:
        available_str = ", ".join(f"'{a}'" for a in available)
        message = (
            f"Account '{account_name}' not found. "
            f"Available accounts: {available_str}"
        )
    else:
        message = f"Account '{account_name}' not found. No accounts configured."

    details = {
        "account_name": account_name,
        "available_accounts": available,
    }
    super().__init__(message, details=details)
    self._code = "ACCOUNT_NOT_FOUND"

account_name property

account_name: str

The requested account name that wasn't found.

available_accounts property

available_accounts: list[str]

List of valid account names.

mixpanel_headless.AccountExistsError

AccountExistsError(account_name: str)

Bases: ConfigError

Account name already exists in configuration.

Raised when attempting to add an account with a name that's already in use.

Initialize AccountExistsError.

PARAMETER DESCRIPTION
account_name

The conflicting account name.

TYPE: str

Source code in src/mixpanel_headless/exceptions.py
def __init__(self, account_name: str) -> None:
    """Initialize AccountExistsError.

    Args:
        account_name: The conflicting account name.
    """
    message = f"Account '{account_name}' already exists."
    details = {"account_name": account_name}
    super().__init__(message, details=details)
    self._code = "ACCOUNT_EXISTS"

account_name property

account_name: str

The conflicting account name.

mixpanel_headless.AccountInUseError

AccountInUseError(account_name: str, referenced_by: list[str] | None = None)

Bases: ConfigError

Account is referenced by one or more targets and cannot be removed.

Raised by mp.accounts.remove(name) when the account is referenced by one or more [targets.NAME] blocks and the caller did not pass force=True. The list of dependent target names is available in referenced_by so callers can show a helpful error message or pass force=True to delete the account and orphan the targets.

Initialize AccountInUseError.

PARAMETER DESCRIPTION
account_name

The account that callers tried to remove.

TYPE: str

referenced_by

Names of targets that reference the account.

TYPE: list[str] | None DEFAULT: None

Source code in src/mixpanel_headless/exceptions.py
def __init__(
    self, account_name: str, referenced_by: list[str] | None = None
) -> None:
    """Initialize AccountInUseError.

    Args:
        account_name: The account that callers tried to remove.
        referenced_by: Names of targets that reference the account.
    """
    targets = referenced_by or []
    if targets:
        target_str = ", ".join(f"'{t}'" for t in targets)
        message = (
            f"Account '{account_name}' is referenced by target(s): {target_str}. "
            f"Pass `force=True` to remove anyway."
        )
    else:
        message = (
            f"Account '{account_name}' is in use. Pass `force=True` to remove."
        )

    details: dict[str, Any] = {
        "account_name": account_name,
        "referenced_by": list(targets),
    }
    super().__init__(message, details=details)
    self._code = "ACCOUNT_IN_USE"

account_name property

account_name: str

The account name that callers tried to remove.

referenced_by property

referenced_by: list[str]

Target names that reference the account.

mixpanel_headless.ProjectNotFoundError

ProjectNotFoundError(
    project_id: str, available_projects: list[str] | None = None
)

Bases: ConfigError

Raised when a specified project is not accessible.

Includes the requested project ID and optionally a list of accessible project IDs to help the user correct their selection.

Example
try:
    projects = ws.projects()
    match = [p for p in projects if p.id == target_id]
    if not match:
        raise ProjectNotFoundError(
            target_id,
            available_projects=[p.id for p in projects],
        )
except ProjectNotFoundError as e:
    print(f"Project '{e.project_id}' not found.")
    if e.available_projects:
        print(f"Available: {', '.join(e.available_projects)}")

Initialize ProjectNotFoundError.

PARAMETER DESCRIPTION
project_id

The requested project ID that wasn't found.

TYPE: str

available_projects

List of accessible project IDs for suggestions.

TYPE: list[str] | None DEFAULT: None

Source code in src/mixpanel_headless/exceptions.py
def __init__(
    self,
    project_id: str,
    available_projects: list[str] | None = None,
) -> None:
    """Initialize ProjectNotFoundError.

    Args:
        project_id: The requested project ID that wasn't found.
        available_projects: List of accessible project IDs for suggestions.
    """
    available = available_projects or []
    if available:
        available_str = ", ".join(f"'{p}'" for p in available)
        message = (
            f"Project '{project_id}' not found. Available projects: {available_str}"
        )
    else:
        message = (
            f"Project '{project_id}' not found. No accessible projects discovered."
        )

    details: dict[str, Any] = {
        "project_id": project_id,
        "available_projects": available,
    }
    super().__init__(message, details=details)
    self._code = "PROJECT_NOT_FOUND"

project_id property

project_id: str

The requested project ID that wasn't found.

available_projects property

available_projects: list[str]

List of accessible project IDs.

InvalidArgumentError

Raised by accounts.login_unified (and the CLI's mp login) when a public-API call combines mutually incompatible arguments. Subclass of ConfigError. The CLI maps this to exit code 3 (INVALID_ARGS) instead of the generic 1.

violation Raised When
mutually_exclusive --service-account + --token-env (or equivalent kwargs)
no_browser_misuse --no-browser against a non-browser auth type
secret_stdin_misuse --secret-stdin against a non-SA auth type

The details dict carries violation and (when detection ran) detected_auth_type. Pattern-match by class so non-CLI callers (Cowork's auth_manager.py, JSON consumers) can dispatch without parsing the human message.

mixpanel_headless.InvalidArgumentError

InvalidArgumentError(
    message: str,
    *,
    violation: Literal[
        "mutually_exclusive", "no_browser_misuse", "secret_stdin_misuse"
    ],
    detected_auth_type: str | None = None,
)

Bases: ConfigError

Raised when a public API call combines mutually incompatible arguments.

Carries a violation discriminator and the resolved detected_auth_type so non-CLI callers (Cowork's auth_manager.py, JSON consumers) can dispatch programmatically without parsing the human message. The CLI handle_errors decorator maps this subclass to ExitCode.INVALID_ARGS (3) instead of the generic GENERAL_ERROR (1) that ConfigError would otherwise produce.

Used by accounts.login_unified for the three documented flag-combination rejections (043 contract, cli-commands.md §5): --service-account + --token-env, --no-browser against a non-browser auth type, and --secret-stdin against a non-SA auth type.

Example
try:
    accounts.login_unified(service_account=True, token_env="X")
except InvalidArgumentError as exc:
    assert exc.violation == "mutually_exclusive"
    assert exc.detected_auth_type == "service_account"

Initialize InvalidArgumentError.

PARAMETER DESCRIPTION
message

Human-readable error message.

TYPE: str

violation

Discriminator for the kind of misuse. One of "mutually_exclusive", "no_browser_misuse", "secret_stdin_misuse".

TYPE: Literal['mutually_exclusive', 'no_browser_misuse', 'secret_stdin_misuse']

detected_auth_type

The auth type the orchestrator resolved from the supplied flags / env. None only when the violation was caught BEFORE detection ran (currently no such case, but kept optional for future-proofing).

TYPE: str | None DEFAULT: None

Source code in src/mixpanel_headless/exceptions.py
def __init__(
    self,
    message: str,
    *,
    violation: Literal[
        "mutually_exclusive", "no_browser_misuse", "secret_stdin_misuse"
    ],
    detected_auth_type: str | None = None,
) -> None:
    """Initialize InvalidArgumentError.

    Args:
        message: Human-readable error message.
        violation: Discriminator for the kind of misuse. One of
            ``"mutually_exclusive"``, ``"no_browser_misuse"``,
            ``"secret_stdin_misuse"``.
        detected_auth_type: The auth type the orchestrator resolved
            from the supplied flags / env. ``None`` only when the
            violation was caught BEFORE detection ran (currently
            no such case, but kept optional for future-proofing).
    """
    if violation not in self._VALID_VIOLATIONS:
        raise ValueError(
            f"Invalid violation {violation!r}; must be one of "
            f"{self._VALID_VIOLATIONS}."
        )
    details: dict[str, Any] = {"violation": violation}
    if detected_auth_type is not None:
        details["detected_auth_type"] = detected_auth_type
    super().__init__(message, details=details)
    self._code = "INVALID_ARGUMENT"

violation property

violation: str

The kind of misuse — see _VALID_VIOLATIONS.

detected_auth_type property

detected_auth_type: str | None

The auth type the orchestrator resolved (or None if pre-detection).

OAuth Exceptions

Raised during OAuth 2.0 PKCE authentication flows and the mp login region probe.

Error Code Raised When
OAUTH_TOKEN_ERROR Token exchange fails
OAUTH_REFRESH_ERROR Token refresh fails (transient)
OAUTH_REFRESH_REVOKED Refresh token rejected by IdP as invalid_grant (re-run mp login --name NAME)
OAUTH_REGISTRATION_ERROR Dynamic client registration fails
OAUTH_TIMEOUT Callback server times out waiting for authorization
OAUTH_PORT_ERROR Cannot bind to a local port for the callback server
OAUTH_BROWSER_ERROR Cannot open the authorization URL in the browser
OAUTH_REGION_PROBE_FAILED mp login probed every region and none accepted the credential — see RegionProbeError below
OAUTH_NETWORK_UNREACHABLE Every region probe failed at the network layer (DNS / TLS / connect refused) — see RegionProbeNetworkError below

mixpanel_headless.OAuthError

OAuthError(
    message: str,
    code: str = "OAUTH_TOKEN_ERROR",
    details: dict[str, Any] | None = None,
)

Bases: MixpanelHeadlessError

OAuth authentication flow error.

Raised for failures during the OAuth 2.0 PKCE flow, including token exchange, token refresh, client registration, callback timeout, port unavailability, and browser launch failures.

Error codes: - OAUTH_TOKEN_ERROR: Token exchange or validation failed - OAUTH_REFRESH_ERROR: Token refresh failed - OAUTH_REGISTRATION_ERROR: Dynamic Client Registration failed - OAUTH_TIMEOUT: Callback server timed out waiting for authorization - OAUTH_PORT_ERROR: All callback ports are occupied - OAUTH_BROWSER_ERROR: Could not open browser for authorization

Example
try:
    flow = OAuthFlow(region="us")
    tokens = flow.login()
except OAuthError as e:
    print(f"OAuth failed: {e.message} (code: {e.code})")

Initialize OAuthError.

PARAMETER DESCRIPTION
message

Human-readable error message.

TYPE: str

code

Machine-readable error code. One of: OAUTH_TOKEN_ERROR, OAUTH_REFRESH_ERROR, OAUTH_REGISTRATION_ERROR, OAUTH_TIMEOUT, OAUTH_PORT_ERROR, OAUTH_BROWSER_ERROR.

TYPE: str DEFAULT: 'OAUTH_TOKEN_ERROR'

details

Additional structured data about the error.

TYPE: dict[str, Any] | None DEFAULT: None

Source code in src/mixpanel_headless/exceptions.py
def __init__(
    self,
    message: str,
    code: str = "OAUTH_TOKEN_ERROR",
    details: dict[str, Any] | None = None,
) -> None:
    """Initialize OAuthError.

    Args:
        message: Human-readable error message.
        code: Machine-readable error code. One of: OAUTH_TOKEN_ERROR,
            OAUTH_REFRESH_ERROR, OAUTH_REGISTRATION_ERROR, OAUTH_TIMEOUT,
            OAUTH_PORT_ERROR, OAUTH_BROWSER_ERROR.
        details: Additional structured data about the error.
    """
    super().__init__(message, code=code, details=details)

RegionProbeError

Raised by mp login (and accounts.login_unified) when the us → eu → in region probe fails for every region. Subclass of OAuthError. The attempts attribute carries the full (region, status_code, error_body) list; status 0 indicates a network-layer failure (DNS / TLS / connect refused) — those cases raise RegionProbeNetworkError (subclass) so the CLI can render a different remediation hint.

import mixpanel_headless as mp

try:
    mp.accounts.login_unified()
except mp.RegionProbeNetworkError as exc:
    print("Could not reach any Mixpanel region. Check connectivity.")
    for region, status, body in exc.attempts:
        print(f"  {region}: {body}")
except mp.RegionProbeError as exc:
    print("Credential not valid in any region.")
    for region, status, body in exc.attempts:
        print(f"  {region}: {status} {body}")

mixpanel_headless.RegionProbeError

RegionProbeError(
    message: str,
    *,
    attempts: list[tuple[Region, int, str]],
    code: str = "OAUTH_REGION_PROBE_FAILED",
)

Bases: OAuthError

Raised when no region accepts the credential during region probing.

The region probe walks a configured order (default useuin) against /api/app/me, returning the first 200. When every probe attempt fails, this exception is raised carrying the full attempt list for diagnostic and telemetry use.

A status code of 0 indicates the request never reached the server (network error); the third tuple element carries the failure detail (HTTP response text or the network error reason).

See :class:RegionProbeNetworkError for the all-network-error subclass — the probe distinguishes "credential rejected" from "could not reach any region" so the CLI can render different remediation hints.

Example
try:
    result = probe_region(client_factory, headers)
except RegionProbeError as exc:
    for region, status, body in exc.attempts:
        print(f"{region}: {status} {body}")

Initialize RegionProbeError.

PARAMETER DESCRIPTION
message

Human-readable error message.

TYPE: str

attempts

Ordered list of (region, status_code, error_body) tuples for every probed region. status_code is 0 for network errors; error_body carries the failure detail.

TYPE: list[tuple[Region, int, str]]

code

Machine-readable error code. Defaults to OAUTH_REGION_PROBE_FAILED for the generic case; :class:RegionProbeNetworkError overrides to OAUTH_NETWORK_UNREACHABLE.

TYPE: str DEFAULT: 'OAUTH_REGION_PROBE_FAILED'

Source code in src/mixpanel_headless/exceptions.py
def __init__(
    self,
    message: str,
    *,
    attempts: list[tuple[Region, int, str]],
    code: str = "OAUTH_REGION_PROBE_FAILED",
) -> None:
    """Initialize RegionProbeError.

    Args:
        message: Human-readable error message.
        attempts: Ordered list of ``(region, status_code, error_body)``
            tuples for every probed region. ``status_code`` is ``0``
            for network errors; ``error_body`` carries the failure
            detail.
        code: Machine-readable error code. Defaults to
            ``OAUTH_REGION_PROBE_FAILED`` for the generic case;
            :class:`RegionProbeNetworkError` overrides to
            ``OAUTH_NETWORK_UNREACHABLE``.
    """
    self._attempts: list[tuple[Region, int, str]] = list(attempts)
    super().__init__(
        message,
        code=code,
        details={"attempts": [list(a) for a in self._attempts]},
    )

attempts property

attempts: list[tuple[Region, int, str]]

Ordered list of (region, status_code, error_body) tuples.

to_dict

to_dict() -> dict[str, Any]

Serialize the exception to a JSON-friendly dict.

Includes attempts at the top level so consumers can inspect the per-region probe outcomes without unpacking details.

RETURNS DESCRIPTION
dict[str, Any]

Dictionary with keys code, message, details, and

dict[str, Any]

attempts. Each attempts entry is a 3-element list

dict[str, Any]

[region, status_code, error_body].

Source code in src/mixpanel_headless/exceptions.py
def to_dict(self) -> dict[str, Any]:
    """Serialize the exception to a JSON-friendly dict.

    Includes ``attempts`` at the top level so consumers can inspect
    the per-region probe outcomes without unpacking ``details``.

    Returns:
        Dictionary with keys ``code``, ``message``, ``details``, and
        ``attempts``. Each ``attempts`` entry is a 3-element list
        ``[region, status_code, error_body]``.
    """
    base = super().to_dict()
    base["attempts"] = [list(a) for a in self._attempts]
    return base

mixpanel_headless.RegionProbeNetworkError

RegionProbeNetworkError(
    message: str, *, attempts: list[tuple[Region, int, str]]
)

Bases: RegionProbeError

Raised when every region probe attempt failed at the network layer.

Subclass of :class:RegionProbeError used when ALL recorded attempts have status_code == 0 — i.e. the credential was never actually evaluated because no region was reachable (DNS failure, TLS rejection, captive portal, no internet). The CLI catches this before the generic RegionProbeError so it can render "could not reach any Mixpanel region" instead of "credential not valid", which would mislead a user who is actually offline.

Carries the same attempts shape as the parent so existing consumers can render the per-region detail without changes.

Initialize RegionProbeNetworkError.

PARAMETER DESCRIPTION
message

Human-readable error message.

TYPE: str

attempts

Ordered list of (region, 0, error_body) tuples — every entry must have status 0 by construction (the probe loop only raises this subclass when that invariant holds).

TYPE: list[tuple[Region, int, str]]

Source code in src/mixpanel_headless/exceptions.py
def __init__(
    self,
    message: str,
    *,
    attempts: list[tuple[Region, int, str]],
) -> None:
    """Initialize RegionProbeNetworkError.

    Args:
        message: Human-readable error message.
        attempts: Ordered list of ``(region, 0, error_body)``
            tuples — every entry must have status 0 by construction
            (the probe loop only raises this subclass when that
            invariant holds).
    """
    super().__init__(
        message,
        attempts=attempts,
        code="OAUTH_NETWORK_UNREACHABLE",
    )

Workspace / Organization Scope Exceptions

Raised when an auth-axis identifier (workspace or organization) cannot be resolved during App API requests.

Error Code Raised When
NO_WORKSPACES No workspaces found for the project
AMBIGUOUS_WORKSPACE Multiple workspaces found and none is marked as default
WORKSPACE_NOT_FOUND Specified workspace ID does not exist
ORGANIZATION_AMBIGUOUS An org-scoped business-context call could not auto-resolve the organization (active project absent from /me AND >1 accessible organization). details carries project_id and available_organizations. Pass organization_id=N explicitly to bypass auto-resolution.

mixpanel_headless.WorkspaceScopeError

WorkspaceScopeError(
    message: str,
    code: str = "NO_WORKSPACES",
    details: dict[str, Any] | None = None,
)

Bases: MixpanelHeadlessError

Scope resolution error (workspace or organization).

Raised when an auth-axis identifier cannot be resolved during App API requests. Originally introduced for workspace resolution; also raised when the organization ID for an org-scoped business-context call cannot be auto-derived from the cached /me response.

Error codes: - NO_WORKSPACES: Project has no accessible workspaces - AMBIGUOUS_WORKSPACE: Multiple workspaces, none default; must specify --workspace-id - WORKSPACE_NOT_FOUND: Explicit workspace ID doesn't match any workspace - ORGANIZATION_AMBIGUOUS: Cannot auto-resolve the organization for an org-scoped call (active project absent from /me AND >1 accessible organization). The details dict carries project_id and available_organizations. Pass organization_id=N explicitly to bypass auto-resolution.

Example
try:
    workspace_id = ws.resolve_workspace_id()
except WorkspaceScopeError as e:
    print(f"Scope issue: {e.message} (code: {e.code})")

Initialize WorkspaceScopeError.

PARAMETER DESCRIPTION
message

Human-readable error message.

TYPE: str

code

Machine-readable error code. One of: NO_WORKSPACES, AMBIGUOUS_WORKSPACE, WORKSPACE_NOT_FOUND, ORGANIZATION_AMBIGUOUS.

TYPE: str DEFAULT: 'NO_WORKSPACES'

details

Additional structured data about the error.

TYPE: dict[str, Any] | None DEFAULT: None

Source code in src/mixpanel_headless/exceptions.py
def __init__(
    self,
    message: str,
    code: str = "NO_WORKSPACES",
    details: dict[str, Any] | None = None,
) -> None:
    """Initialize WorkspaceScopeError.

    Args:
        message: Human-readable error message.
        code: Machine-readable error code. One of: NO_WORKSPACES,
            AMBIGUOUS_WORKSPACE, WORKSPACE_NOT_FOUND,
            ORGANIZATION_AMBIGUOUS.
        details: Additional structured data about the error.
    """
    super().__init__(message, code=code, details=details)

Business Context Exceptions

Raised by Workspace.set_business_context() when content exceeds the 50,000-character cap. The check runs before the HTTP call, so callers fail fast and don't waste a round-trip; the server enforces the same limit and would otherwise return QueryError (HTTP 400). See the Business Context guide for usage.

Error Code Raised When
BUSINESS_CONTEXT_TOO_LONG len(content) > BUSINESS_CONTEXT_MAX_CHARS (50,000)

The details dict carries length (the actual content length) and max (the configured limit) for programmatic recovery.

mixpanel_headless.BusinessContextValidationError

BusinessContextValidationError(
    message: str, details: dict[str, Any] | None = None
)

Bases: MixpanelHeadlessError

Business context content failed client-side validation.

Raised by Workspace.set_business_context() when the supplied content exceeds BUSINESS_CONTEXT_MAX_CHARS (50,000 characters). The check runs before the HTTP call so callers can fail fast and avoid a wasted round-trip — the server enforces the same limit server-side and would otherwise return QueryError (HTTP 400).

The details dict carries length (the actual content length) and max (the configured limit) for programmatic recovery.

Example
try:
    ws.set_business_context("x" * 60_000, level="project")
except BusinessContextValidationError as e:
    print(f"Too long: {e.details['length']} > {e.details['max']}")

Initialize BusinessContextValidationError.

PARAMETER DESCRIPTION
message

Human-readable error message.

TYPE: str

details

Additional structured data — typically length and max.

TYPE: dict[str, Any] | None DEFAULT: None

Source code in src/mixpanel_headless/exceptions.py
def __init__(
    self,
    message: str,
    details: dict[str, Any] | None = None,
) -> None:
    """Initialize BusinessContextValidationError.

    Args:
        message: Human-readable error message.
        details: Additional structured data — typically ``length``
            and ``max``.
    """
    super().__init__(
        message,
        code="BUSINESS_CONTEXT_TOO_LONG",
        details=details,
    )