Skip to content

HomeboxClient

The HomeboxClient is the top-level entry point for the py-homebox library. It manages authentication and exposes all resource-specific sub-clients as attributes.

HomeboxClient

Top-level client for the Homebox REST API.

Provides direct access to authentication and top-level endpoints, and exposes namespaced sub-clients for every resource group (items, labels, locations, etc.).

Attributes:

Name Type Description
base_url

Base URL of the Homebox API (e.g. https://demo.homebox.software/api).

token

Bearer token used to authenticate requests, or None when not yet authenticated.

headers

HTTP headers sent with every request.

actions

Sub-client for bulk action endpoints.

assets

Sub-client for asset-ID lookup endpoints.

groups

Sub-client for group and statistics endpoints.

items

Sub-client for item CRUD and attachment endpoints.

labels

Sub-client for label endpoints.

locations

Sub-client for location endpoints.

maintenance

Sub-client for maintenance-log endpoints.

notifiers

Sub-client for notification-channel endpoints.

users

Sub-client for user account endpoints.

reporting

Sub-client for reporting / export endpoints.

labelmaker

Sub-client for printable label endpoints.

products

Sub-client for barcode/QR-code product endpoints.

templates

Sub-client for item template endpoints.

Example

from homebox import HomeboxClient client = HomeboxClient(base_url="https://demo.homebox.software/api") client.login("admin@admin.com", "admin") items = client.items.query_all_items()

Source code in homebox/client.py
class HomeboxClient:
    """Top-level client for the Homebox REST API.

    Provides direct access to authentication and top-level endpoints, and
    exposes namespaced sub-clients for every resource group (items, labels,
    locations, etc.).

    Attributes:
        base_url: Base URL of the Homebox API (e.g. ``https://demo.homebox.software/api``).
        token: Bearer token used to authenticate requests, or ``None`` when not
            yet authenticated.
        headers: HTTP headers sent with every request.
        actions: Sub-client for bulk action endpoints.
        assets: Sub-client for asset-ID lookup endpoints.
        groups: Sub-client for group and statistics endpoints.
        items: Sub-client for item CRUD and attachment endpoints.
        labels: Sub-client for label endpoints.
        locations: Sub-client for location endpoints.
        maintenance: Sub-client for maintenance-log endpoints.
        notifiers: Sub-client for notification-channel endpoints.
        users: Sub-client for user account endpoints.
        reporting: Sub-client for reporting / export endpoints.
        labelmaker: Sub-client for printable label endpoints.
        products: Sub-client for barcode/QR-code product endpoints.
        templates: Sub-client for item template endpoints.

    Example:
        >>> from homebox import HomeboxClient
        >>> client = HomeboxClient(base_url="https://demo.homebox.software/api")
        >>> client.login("admin@admin.com", "admin")
        >>> items = client.items.query_all_items()
    """

    def __init__(self, base_url: Optional[str] = None, token: Optional[str] = None):
        """Initialise the client.

        Args:
            base_url: Base URL of the Homebox API.  Falls back to the
                ``HOMEBOX_URL`` environment variable when not supplied.
            token: Pre-obtained Bearer token.  Falls back to the
                ``HOMEBOX_TOKEN`` environment variable when not supplied.  You
                can omit this and call :meth:`login` instead.

        Raises:
            ValueError: If ``base_url`` is not provided and ``HOMEBOX_URL`` is
                not set.
        """
        base_url = base_url or os.environ.get("HOMEBOX_URL")
        if not base_url:
            raise ValueError("base_url must be provided or the HOMEBOX_URL environment variable must be set")
        token = token or os.environ.get("HOMEBOX_TOKEN")
        self.base_url = base_url
        self.token = token
        self.headers = {"Content-Type": "application/json"}
        if token:
            self.headers["Authorization"] = f"{token}" if token.startswith("Bearer ") else f"Bearer {token}"

        self.actions = ActionsClient(self)
        self.assets = AssetsClient(self)
        self.groups = GroupsClient(self)
        self.items = ItemsClient(self)
        self.tags = TagsClient(self)
        self.labels = LabelsClient(self)
        self.locations = LocationsClient(self)
        self.maintenance = MaintenanceClient(self)
        self.notifiers = NotifiersClient(self)
        self.users = UsersClient(self)
        self.reporting = ReportingClient(self)
        self.labelmaker = LabelMakerClient(self)
        self.products = ProductsClient(self)
        self.templates = TemplatesClient(self)

    def _request(self, method, endpoint, params=None, data=None, files=None, timeout=None) -> dict[Any, Any]:
        """Send an HTTP request and return the parsed JSON response body.

        Args:
            method: HTTP method string (e.g. ``"get"``, ``"post"``).
            endpoint: API path relative to :attr:`base_url` (e.g. ``"/v1/items"``).
            params: Optional query-string parameters.
            data: Optional request body, serialised as JSON.
            files: Optional multipart files mapping.
            timeout: Optional request timeout in seconds.

        Returns:
            Parsed JSON response as a dictionary.  List responses are wrapped
            under the ``"data"`` key.  HTTP 204 responses return an empty dict.

        Raises:
            requests.HTTPError: If the server returns a 4xx or 5xx status code.
        """
        headers = self.headers.copy()
        request_kwargs = {
            "params": params,
            "headers": headers,
            "timeout": timeout,
        }
        if files:
            del headers["Content-Type"]
            request_kwargs["data"] = data
            request_kwargs["files"] = files
        else:
            request_kwargs["json"] = data

        response = requests.request(
            method,
            f"{self.base_url}{endpoint}",
            **request_kwargs,
        )
        response.raise_for_status()

        if response.status_code == 204:
            return {}

        if not response.content:
            return {}

        try:
            result = response.json()
        except ValueError:
            # Some endpoints may return non-JSON success payloads.
            return {"raw": response.text}

        if isinstance(result, list):
            return {"data": result}
        return result

    @overload
    def _get(
        self, endpoint, params=None, data=None, files=None, timeout=None, binary: Literal[True] = True
    ) -> bytes: ...

    @overload
    def _get(
        self, endpoint, params=None, data=None, files=None, timeout=None, binary: Literal[False] = False
    ) -> str: ...

    def _get(self, endpoint, params=None, data=None, files=None, timeout=None, binary=False) -> str | bytes:
        """Send an HTTP GET request and return the raw response body.

        Used for endpoints that return non-JSON content (e.g. CSV exports,
        printable labels, and generated images).

        Args:
            endpoint: API path relative to :attr:`base_url`.
            params: Optional query-string parameters.
            data: Optional request body, serialised as JSON.
            files: Optional multipart files mapping.
            timeout: Optional request timeout in seconds.
            binary: When True return raw bytes, otherwise return decoded text.

        Returns:
            str | bytes: Raw response body (text by default, bytes when
            ``binary=True``). HTTP 204 responses return an empty string.

        Raises:
            requests.HTTPError: If the server returns a 4xx or 5xx status code.
        """
        url = f"{self.base_url}{endpoint}"
        headers = self.headers.copy()
        if files:
            del headers["Content-Type"]

        response = requests.get(url, params=params, json=data, files=files, headers=headers, timeout=timeout)
        response.raise_for_status()

        if response.status_code == 204:
            return ""

        return response.text if not binary else response.content

    def login(
        self, username: str, password: str, stay_logged_in: bool = False, provider: Optional[str] = None
    ) -> TokenResponse:
        """Authenticate with username and password and store the resulting token.

        After a successful login all subsequent requests made through this
        client automatically include the returned Bearer token.

        Args:
            username: Account e-mail address.
            password: Account password.
            stay_logged_in: When ``True`` the server issues a long-lived token.
                Defaults to ``False``.
            provider: Optional OAuth provider identifier.

        Returns:
            TokenResponse: Contains the ``token``, ``attachmentToken``, and
                ``expiresAt`` fields returned by the server.

        Raises:
            requests.HTTPError: If the credentials are rejected (HTTP 401) or
                any other server error occurs.
        """
        login_form = LoginForm(
            username=username,
            password=password,
            stayLoggedIn=stay_logged_in,
        )
        params = {}
        if provider:
            params["provider"] = provider

        response = self._request("post", "/v1/users/login", params=params, data=login_form.model_dump())
        token_response = TokenResponse(**response)
        self.token = token_response.token
        if self.token:
            self.headers["Authorization"] = (
                f"{self.token}" if self.token.startswith("Bearer ") else f"Bearer {self.token}"
            )
        return token_response

    def currency(self) -> Currency:
        """Return the group's configured currency.

        Returns:
            Currency: Currency metadata including code, name, symbol, and decimal
                precision.
        """
        return Currency(**self._request("get", "/v1/currency"))

    def application_info(self) -> APISummary:
        """Return a summary of the running Homebox instance.

        Includes build information, feature flags (registration, demo mode, label
        printing), and the latest available release version.

        Returns:
            APISummary: Application status and build metadata.
        """
        return APISummary(**self._request("get", "/v1/status"))

__init__

__init__(base_url: Optional[str] = None, token: Optional[str] = None)

Initialise the client.

Parameters:

Name Type Description Default
base_url Optional[str]

Base URL of the Homebox API. Falls back to the HOMEBOX_URL environment variable when not supplied.

None
token Optional[str]

Pre-obtained Bearer token. Falls back to the HOMEBOX_TOKEN environment variable when not supplied. You can omit this and call :meth:login instead.

None

Raises:

Type Description
ValueError

If base_url is not provided and HOMEBOX_URL is not set.

Source code in homebox/client.py
def __init__(self, base_url: Optional[str] = None, token: Optional[str] = None):
    """Initialise the client.

    Args:
        base_url: Base URL of the Homebox API.  Falls back to the
            ``HOMEBOX_URL`` environment variable when not supplied.
        token: Pre-obtained Bearer token.  Falls back to the
            ``HOMEBOX_TOKEN`` environment variable when not supplied.  You
            can omit this and call :meth:`login` instead.

    Raises:
        ValueError: If ``base_url`` is not provided and ``HOMEBOX_URL`` is
            not set.
    """
    base_url = base_url or os.environ.get("HOMEBOX_URL")
    if not base_url:
        raise ValueError("base_url must be provided or the HOMEBOX_URL environment variable must be set")
    token = token or os.environ.get("HOMEBOX_TOKEN")
    self.base_url = base_url
    self.token = token
    self.headers = {"Content-Type": "application/json"}
    if token:
        self.headers["Authorization"] = f"{token}" if token.startswith("Bearer ") else f"Bearer {token}"

    self.actions = ActionsClient(self)
    self.assets = AssetsClient(self)
    self.groups = GroupsClient(self)
    self.items = ItemsClient(self)
    self.tags = TagsClient(self)
    self.labels = LabelsClient(self)
    self.locations = LocationsClient(self)
    self.maintenance = MaintenanceClient(self)
    self.notifiers = NotifiersClient(self)
    self.users = UsersClient(self)
    self.reporting = ReportingClient(self)
    self.labelmaker = LabelMakerClient(self)
    self.products = ProductsClient(self)
    self.templates = TemplatesClient(self)

login

login(username: str, password: str, stay_logged_in: bool = False, provider: Optional[str] = None) -> TokenResponse

Authenticate with username and password and store the resulting token.

After a successful login all subsequent requests made through this client automatically include the returned Bearer token.

Parameters:

Name Type Description Default
username str

Account e-mail address.

required
password str

Account password.

required
stay_logged_in bool

When True the server issues a long-lived token. Defaults to False.

False
provider Optional[str]

Optional OAuth provider identifier.

None

Returns:

Name Type Description
TokenResponse TokenResponse

Contains the token, attachmentToken, and expiresAt fields returned by the server.

Raises:

Type Description
HTTPError

If the credentials are rejected (HTTP 401) or any other server error occurs.

Source code in homebox/client.py
def login(
    self, username: str, password: str, stay_logged_in: bool = False, provider: Optional[str] = None
) -> TokenResponse:
    """Authenticate with username and password and store the resulting token.

    After a successful login all subsequent requests made through this
    client automatically include the returned Bearer token.

    Args:
        username: Account e-mail address.
        password: Account password.
        stay_logged_in: When ``True`` the server issues a long-lived token.
            Defaults to ``False``.
        provider: Optional OAuth provider identifier.

    Returns:
        TokenResponse: Contains the ``token``, ``attachmentToken``, and
            ``expiresAt`` fields returned by the server.

    Raises:
        requests.HTTPError: If the credentials are rejected (HTTP 401) or
            any other server error occurs.
    """
    login_form = LoginForm(
        username=username,
        password=password,
        stayLoggedIn=stay_logged_in,
    )
    params = {}
    if provider:
        params["provider"] = provider

    response = self._request("post", "/v1/users/login", params=params, data=login_form.model_dump())
    token_response = TokenResponse(**response)
    self.token = token_response.token
    if self.token:
        self.headers["Authorization"] = (
            f"{self.token}" if self.token.startswith("Bearer ") else f"Bearer {self.token}"
        )
    return token_response

currency

currency() -> Currency

Return the group's configured currency.

Returns:

Name Type Description
Currency Currency

Currency metadata including code, name, symbol, and decimal precision.

Source code in homebox/client.py
def currency(self) -> Currency:
    """Return the group's configured currency.

    Returns:
        Currency: Currency metadata including code, name, symbol, and decimal
            precision.
    """
    return Currency(**self._request("get", "/v1/currency"))

application_info

application_info() -> APISummary

Return a summary of the running Homebox instance.

Includes build information, feature flags (registration, demo mode, label printing), and the latest available release version.

Returns:

Name Type Description
APISummary APISummary

Application status and build metadata.

Source code in homebox/client.py
def application_info(self) -> APISummary:
    """Return a summary of the running Homebox instance.

    Includes build information, feature flags (registration, demo mode, label
    printing), and the latest available release version.

    Returns:
        APISummary: Application status and build metadata.
    """
    return APISummary(**self._request("get", "/v1/status"))