> ## Documentation Index
> Fetch the complete documentation index at: https://developer.box.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Automate new hire onboarding with Box APIs

> Build a middleware service that listens for HR hiring events and provisions a standardized, governed Box workspace for every new hire using the Users, Groups, Folders, and Collaboration APIs.

export const RelatedLinks = ({title, items = []}) => {
  const getBadgeClass = badge => {
    if (!badge) return "badge-default";
    const badgeType = badge.toLowerCase().replace(/\s+/g, "-");
    return `badge-${badge === "ガイド" ? "guide" : badgeType}`;
  };
  if (!items || items.length === 0) {
    return null;
  }
  return <div className="my-8">
      {}
      <h3 className="text-sm font-bold uppercase tracking-wider mb-4">{title}</h3>

      {}
      <div className="flex flex-col gap-3">
        {items.map((item, index) => <a key={index} href={item.href} className="py-2 px-3 rounded related_link hover:bg-[#f2f2f2] dark:hover:bg-[#111827] flex items-center gap-3 group no-underline hover:no-underline border-b-0">
            {}
            <span className={`px-2 py-1 rounded-full text-xs font-semibold uppercase tracking-wide flex-shrink-0 ${getBadgeClass(item.badge)}`}>
              {item.badge}
            </span>

            {}
            <span className="text-base">{item.label}</span>
          </a>)}
      </div>
    </div>;
};

export const Link = ({href, children, className, ...props}) => {
  const localizedHref = localizeLink(href);
  return <a href={localizedHref} className={className} {...props}>
      {children}
    </a>;
};

When a new employee joins, someone usually has to click through the Box web app to create their folders, share the right resources, and add them to the correct teams. That manual work is slow, inconsistent, and easy to get wrong. Two hires in the same role can end up with different folder structures and different access - a governance and audit problem waiting to happen.

This tutorial builds a middleware service that removes that manual work. When your HR system fires a `hired` event, the service authenticates to Box, provisions a standardized personal folder tree for the new hire, grants the right groups access at the right levels, and optionally invites the new hire to their workspace. Every new hire automatically gets an identical, governed structure.

## What you are building

By the end of this tutorial, you have a working Python service that:

* Receives an HR `hired` event through a webhook endpoint.
* Resolves (or creates) the governance groups that control access, such as IT and HR.
* Builds a standardized personal folder tree for the new hire using the Folders API.
* Grants each governance group access at the correct folder level using the Collaboration API.
* Optionally invites the new hire to their workspace so they have access on day one.
* Runs idempotently, so replaying the same event never creates duplicate folders or collaborations.

## Prerequisites

Before you start, make sure you have the following:

* A Box account with administrator access. Any <Link href="https://www.box.com/pricing">Business plan or above</Link> works, and a free <Link href="https://account.box.com/signup/n/developer">Box developer account</Link> also works (it authorizes Platform Apps automatically, so you can skip the Admin Console authorization step).
* A Box application configured with **Client Credentials Grant** authentication, authorized in the Admin Console.
* Python 3.11 or higher.
* The following scopes enabled on your app:
  * Read and write all files and folders stored in Box
  * Manage groups
  * Manage users
* The app's **App Access Level** set to **App + Enterprise Access** (on the **Configuration** tab). The default **App Access Only** limits the service account to its own content and cannot create or manage enterprise users and groups.
* Your Box enterprise ID (found in the [Developer Console](https://app.box.com/developers/console) by clicking your profile icon, or in the Admin Console under **Account & Billing**).

<Note>
  Creating and managing groups and users requires admin privileges. When you use Client Credentials Grant with an `enterprise_id`, the app acts as the enterprise service account. This service account can only manage enterprise users and groups when the app's **App Access Level** is set to **App + Enterprise Access**. After you enable the **Manage groups** and **Manage users** scopes and change the access level, an administrator must reauthorize the app in the Admin Console under **Integrations > Platform Apps** for those changes to take effect.
</Note>

## Step-by-step process

This solution combines four Box Platform capabilities:

| Component          | Purpose                                                                                                | API                                   |
| ------------------ | ------------------------------------------------------------------------------------------------------ | ------------------------------------- |
| **Groups**         | Resolve or create the governance groups that control access                                            | `POST /2.0/groups`, `GET /2.0/groups` |
| **Folders**        | Build the standardized personal folder tree                                                            | `POST /2.0/folders`                   |
| **Collaborations** | Grant groups and the new hire access at the right levels                                               | `POST /2.0/collaborations`            |
| **Users**          | Optionally add the new hire to enterprise groups (see [Scaling to production](#scaling-to-production)) | `POST /2.0/group_memberships`         |

<Info>
  Box collaborations cascade. A collaboration added to a parent folder automatically grants the same access to everything inside it. This tutorial uses that behavior deliberately: broad access is granted high in the tree, and narrower access is granted only on the specific subfolders that need it.
</Info>

<Steps>
  <Step title="Set up the development environment">
    1. Open your terminal and create a new project directory:

    ```bash theme={null}
    mkdir onboarding-workspace && cd onboarding-workspace
    ```

    2. Create and activate a Python virtual environment:

    ```bash theme={null}
    python3 -m venv .venv
    source .venv/bin/activate
    ```

    After activation, your terminal prompt shows `(.venv)` at the beginning. This confirms you are working inside the virtual environment.

    <Note>
      Every time you open a new terminal window or tab, you must re-activate the virtual environment by running `source .venv/bin/activate` from the project directory. If you see `ModuleNotFoundError` when running commands, it usually means the venv is not activated.
    </Note>

    3. Install the required packages:

    ```bash theme={null}
    pip install box-sdk-gen flask python-dotenv
    ```

    4. Create a `.env` file to store your credentials, then add the following content. Replace the placeholder values with your actual credentials from the Box Developer Console:

    ```bash theme={null}
    BOX_CLIENT_ID=your_client_id
    BOX_CLIENT_SECRET=your_client_secret
    BOX_ENTERPRISE_ID=your_enterprise_id
    ONBOARDING_ROOT_FOLDER_ID=0
    ```

    `ONBOARDING_ROOT_FOLDER_ID` is the folder where each new hire's workspace is created. Leave it as `0` to create workspaces in the service account's root, or set it to the ID of a dedicated `New Hires` folder.

    <Warning>
      Never commit `.env` files to version control. Add `.env` to your `.gitignore`.
    </Warning>

    <Note>
      **Understanding environment variables:** The `.env` file stores sensitive values (your actual credentials). Your Python code reads these values by referencing their **names** using `os.getenv("VARIABLE_NAME")`. For example, `os.getenv("BOX_CLIENT_ID")` looks up the value stored next to `BOX_CLIENT_ID=` in your `.env` file. When you copy the code in the following steps, keep the quoted variable names exactly as shown. Do not replace them with your actual credentials.
    </Note>
  </Step>

  <Step title="Authenticate the Box client">
    Create a file called `box_client.py` in your project directory and paste the following code:

    ```python theme={null}
    import os
    from dotenv import load_dotenv
    from box_sdk_gen import (
        BoxClient,
        BoxCCGAuth,
        CCGConfig,
    )

    load_dotenv()

    def get_box_client() -> BoxClient:
        config = CCGConfig(
            client_id=os.getenv("BOX_CLIENT_ID"),
            client_secret=os.getenv("BOX_CLIENT_SECRET"),
            enterprise_id=os.getenv("BOX_ENTERPRISE_ID"),
        )
        auth = BoxCCGAuth(config=config)
        return BoxClient(auth=auth)
    ```

    <Tip>
      Client Credentials Grant is recommended for server-to-server automations where no end user is present. If your organization requires JWT, swap `CCGConfig`/`BoxCCGAuth` for `JWTConfig`/`BoxJWTAuth`; the rest of the code in this tutorial is unchanged. For a comparison of both, see <Link href="/guides/authentication/select">Select an authentication method</Link>.
    </Tip>
  </Step>

  <Step title="Define the workspace template">
    The heart of a governed onboarding process is a single, declarative definition of what every new hire's workspace looks like and who can access each part of it. Create a file called `config.py` and paste the following code:

    ```python theme={null}
    # Maps a logical role key to the actual Box group name.
    # Edit the names to match the groups in your enterprise.
    GOVERNANCE_GROUPS = {
        "IT": "IT Onboarding",
        "HR": "HR Onboarding",
        "All Employees": "All Employees",
    }

    # The standardized folder tree created for every new hire.
    # `collaborators` grants a governance group access at that exact level.
    # Because collaborations cascade, access granted on a folder also
    # applies to everything nested inside it.
    WORKSPACE_TEMPLATE = {
        "name": "Onboarding",
        "collaborators": [],
        "children": [
            {"name": "Personal", "collaborators": [], "children": []},
            {
                "name": "IT Setup",
                "collaborators": [{"group": "IT", "role": "editor"}],
                "children": [],
            },
            {
                "name": "HR Documents",
                "collaborators": [{"group": "HR", "role": "editor"}],
                "children": [],
            },
            {
                "name": "Policies & Templates",
                "collaborators": [{"group": "All Employees", "role": "viewer"}],
                "children": [],
            },
            {"name": "Team Resources", "collaborators": [], "children": []},
        ],
    }
    ```

    <Tip>
      Keeping the structure and access rules in one template is what makes onboarding *consistent* and *governed*. To change how every future hire is set up, you edit this file - not your provisioning logic.
    </Tip>
  </Step>

  <Step title="Resolve the governance groups">
    Before you can share folders with a group, you need its group ID. Create a file called `groups.py` and paste the following code. It creates each governance group if it does not exist, and looks up the existing group if it does:

    ```python theme={null}
    from box_sdk_gen import BoxClient, BoxAPIError

    from config import GOVERNANCE_GROUPS

    def get_or_create_group(client: BoxClient, name: str) -> str:
        try:
            group = client.groups.create_group(
                name,
                description="Governance group managed by onboarding automation.",
            )
            print(f"Created group '{group.name}' ({group.id})")
            return group.id
        except BoxAPIError as error:
            if error.response_info.status_code != 409:
                raise
            for group in client.groups.get_groups(filter_term=name).entries:
                if group.name == name:
                    print(f"Using existing group '{group.name}' ({group.id})")
                    return group.id
            raise

    def resolve_governance_groups(client: BoxClient) -> dict:
        """Return a map of logical group key -> Box group ID."""
        return {
            key: get_or_create_group(client, name)
            for key, name in GOVERNANCE_GROUPS.items()
        }
    ```

    A group name must be unique in the enterprise, so Box returns `409 Conflict` if the group already exists. Catching that status and falling back to a lookup makes the function safe to run repeatedly.
  </Step>

  <Step title="Build the workspace and apply collaborations">
    Create a file called `workspace.py` and paste the following code. It recursively creates each folder in the template and, at every level, shares the folder with the groups defined for that node:

    ```python theme={null}
    from box_sdk_gen import (
        BoxClient,
        BoxAPIError,
        CreateFolderParent,
        CreateCollaborationItem,
        CreateCollaborationItemTypeField,
        CreateCollaborationAccessibleBy,
        CreateCollaborationAccessibleByTypeField,
        CreateCollaborationRole,
    )

    ROLE_MAP = {
        "editor": CreateCollaborationRole.EDITOR,
        "viewer": CreateCollaborationRole.VIEWER,
        "uploader": CreateCollaborationRole.UPLOADER,
        "co-owner": CreateCollaborationRole.CO_OWNER,
    }

    def is_already_collaborator(error: BoxAPIError) -> bool:
        # Box reports an existing collaborator differently for groups and users:
        # a group returns 409 with code "conflict", while a user can return
        # "user_already_collaborator". Match on both so re-runs are safe.
        body = error.response_info.body
        message = body.get("message", "").lower()
        return (
            body.get("code") == "user_already_collaborator"
            or "already a collaborator" in message
        )

    def create_folder(client: BoxClient, name: str, parent_id: str) -> str:
        try:
            folder = client.folders.create_folder(
                name, CreateFolderParent(id=parent_id)
            )
            return folder.id
        except BoxAPIError as error:
            # 409 means a folder with this name already exists under the parent.
            # Reuse it so re-running the service never creates duplicates.
            if error.response_info.status_code != 409:
                raise
            conflicts = error.response_info.body.get("context_info", {}).get(
                "conflicts", []
            )
            return conflicts[0]["id"]

    def add_group_collaboration(
        client: BoxClient, folder_id: str, group_id: str, role: str
    ):
        try:
            client.user_collaborations.create_collaboration(
                item=CreateCollaborationItem(
                    type=CreateCollaborationItemTypeField.FOLDER, id=folder_id
                ),
                accessible_by=CreateCollaborationAccessibleBy(
                    type=CreateCollaborationAccessibleByTypeField.GROUP, id=group_id
                ),
                role=ROLE_MAP[role],
            )
        except BoxAPIError as error:
            # Ignore if the group is already a collaborator on this folder.
            if not is_already_collaborator(error):
                raise

    def build_workspace(
        client: BoxClient, node: dict, parent_id: str, group_ids: dict
    ) -> str:
        folder_id = create_folder(client, node["name"], parent_id)

        for collaborator in node.get("collaborators", []):
            group_id = group_ids[collaborator["group"]]
            add_group_collaboration(
                client, folder_id, group_id, collaborator["role"]
            )
            print(
                f"  Shared '{node['name']}' with "
                f"{collaborator['group']} ({collaborator['role']})"
            )

        for child in node.get("children", []):
            build_workspace(client, child, folder_id, group_ids)

        return folder_id
    ```

    Because `build_workspace` walks the template recursively, the same code handles a flat structure or a deeply nested one. To change the tree, you only edit the template in `config.py`.
  </Step>

  <Step title="Orchestrate onboarding">
    Create a file called `onboarding.py` and paste the following code. This ties the pieces together: it resolves the groups, builds the workspace for a specific hire, and optionally invites the new hire to their workspace root:

    ```python theme={null}
    import os
    from dotenv import load_dotenv
    from box_sdk_gen import (
        BoxClient,
        BoxAPIError,
        CreateCollaborationItem,
        CreateCollaborationItemTypeField,
        CreateCollaborationAccessibleBy,
        CreateCollaborationAccessibleByTypeField,
        CreateCollaborationRole,
    )

    from box_client import get_box_client
    from config import WORKSPACE_TEMPLATE
    from groups import resolve_governance_groups
    from workspace import build_workspace, is_already_collaborator

    load_dotenv()

    def invite_new_hire(client: BoxClient, folder_id: str, login: str):
        """Give the new hire access to their workspace root.

        Co-owner is used when the service account owns the folder. Otherwise we
        fall back to editor, because only owners and co-owners can grant co-owner.
        """
        me = client.users.get_user_me()
        folder = client.folders.get_folder_by_id(folder_id, fields=["owned_by"])
        if folder.owned_by and folder.owned_by.id == me.id:
            role = CreateCollaborationRole.CO_OWNER
        else:
            role = CreateCollaborationRole.EDITOR

        try:
            client.user_collaborations.create_collaboration(
                item=CreateCollaborationItem(
                    type=CreateCollaborationItemTypeField.FOLDER, id=folder_id
                ),
                accessible_by=CreateCollaborationAccessibleBy(
                    type=CreateCollaborationAccessibleByTypeField.USER, login=login
                ),
                role=role,
            )
            print(f"Invited {login} to workspace {folder_id} as {role.value}")
        except BoxAPIError as error:
            if not is_already_collaborator(error):
                raise

    def onboard_new_hire(hire: dict) -> str:
        client = get_box_client()

        group_ids = resolve_governance_groups(client)

        template = dict(WORKSPACE_TEMPLATE)
        template["name"] = f"{hire['name']} - Onboarding"

        parent_id = os.getenv("ONBOARDING_ROOT_FOLDER_ID", "0")
        workspace_id = build_workspace(client, template, parent_id, group_ids)

        if hire.get("email"):
            invite_new_hire(client, workspace_id, hire["email"])

        print(f"Workspace ready for {hire['name']} (ID: {workspace_id})")
        return workspace_id

    if __name__ == "__main__":
        # Simulate a hire event for a quick local test.
        onboard_new_hire({"name": "Jordan Rivera", "email": None})
    ```

    Inviting the new hire at the workspace root means they can see every folder inside it, while the group collaborations on subfolders continue to govern who else has access. `invite_new_hire` grants **co-owner** when the service account owns the workspace folder, and falls back to **editor** otherwise - because only an owner or co-owner can grant the co-owner role. If you point `ONBOARDING_ROOT_FOLDER_ID` at a folder owned by another user, the workspace folders are owned by that user, so the fallback prevents a `403`. Leave `email` as `None` to provision the structure without inviting anyone yet - useful when the hire's Box account does not exist on their start date.
  </Step>

  <Step title="Create the hire event listener">
    Create a file called `app.py` and paste the following code. This Flask application receives `hired` events from your HR system and provisions a workspace for each one:

    ```python theme={null}
    from dotenv import load_dotenv
    from flask import Flask, request, jsonify

    from onboarding import onboard_new_hire

    load_dotenv()
    app = Flask(__name__)

    @app.route("/hire", methods=["POST"])
    def handle_hire_event():
        event = request.get_json()

        if event.get("event") != "employee.hired":
            return jsonify({"status": "ignored"}), 200

        employee = event["employee"]
        hire = {
            "name": employee["full_name"],
            "email": employee.get("work_email"),
        }

        workspace_id = onboard_new_hire(hire)

        return jsonify({"status": "provisioned", "workspace_id": workspace_id}), 200

    if __name__ == "__main__":
        app.run(port=5000)
    ```

    <Note>
      In production, you should verify that incoming requests genuinely originate from your HR system, typically by validating a signature or shared secret on each request. Never provision resources from an unauthenticated public endpoint.
    </Note>

    At this point, your project directory should contain the following files:

    ```
    onboarding-workspace/
    ├── .env
    ├── .venv/
    ├── app.py
    ├── box_client.py
    ├── config.py
    ├── groups.py
    ├── onboarding.py
    └── workspace.py
    ```
  </Step>

  <Step title="Test the integration">
    You can test the full provisioning flow locally without connecting a real HR system. This step simulates the event your HR system would send when a new hire is added.

    <Note>
      This step requires **two terminal windows** open at the same time. Terminal 1 runs the Flask server (which must stay running). Terminal 2 sends a test request to it.
    </Note>

    **Terminal 1 - start the server:**

    Make sure you are in the `onboarding-workspace` directory and the virtual environment is activated:

    ```bash theme={null}
    cd ~/onboarding-workspace
    source .venv/bin/activate
    python3 app.py
    ```

    You should see:

    ```
    * Running on http://127.0.0.1:5000
    ```

    Leave this terminal running.

    **Terminal 2 - send a test hire event:**

    Open a new terminal tab or window. Send a simulated HR event using curl. Replace the email with a valid address to test the invite, or remove `work_email` to skip it:

    ```bash theme={null}
    curl -X POST http://127.0.0.1:5000/hire \
      -H "Content-Type: application/json" \
      -d '{
        "event": "employee.hired",
        "employee": {
          "full_name": "Jordan Rivera",
          "work_email": "jordan.rivera@example.com"
        }
      }'
    ```

    **Check the result:**

    Switch back to Terminal 1. You should see output confirming the groups, folders, and collaborations were created:

    ```
    Created group 'IT Onboarding' (12345678)
    Created group 'HR Onboarding' (12345679)
    Created group 'All Employees' (12345680)
      Shared 'IT Setup' with IT (editor)
      Shared 'HR Documents' with HR (editor)
      Shared 'Policies & Templates' with All Employees (viewer)
    Invited jordan.rivera@example.com to workspace 98765432
    Workspace ready for Jordan Rivera (ID: 98765432)
    ```

    **Verify in Box:**

    The workspace is owned by the app's <Link href="/platform/user-types/#service-account">Service Account</Link>, not by your personal or admin login. The folder is created in the Service Account's own root so it does **not** appear in your **All Files** in the Box web app unless you were added as a collaborator. This is expected. Verify the result one of these ways:

    * **Query the API as the Service Account (recommended).** This uses the same identity that created the workspace. List the workspace's contents and confirm each subfolder and its collaborators:

    ```python theme={null}
    from box_client import get_box_client

    client = get_box_client()

    # Replace with the workspace_id returned by the service.
    workspace_id = "98765432"

    for folder in client.folders.get_folder_items(workspace_id).entries:
        print(folder.type, folder.id, folder.name)
        collaborations = client.list_collaborations.get_folder_collaborations(folder.id)
        for collaboration in collaborations.entries:
            print(f"  {collaboration.accessible_by.name} - {collaboration.role.value}")
    ```

    * **Open it in the Box web app.** You only see the folder here if you are a collaborator on it. If you passed your own email as the new hire, open the [Box web app](https://app.box.com), find `Jordan Rivera - Onboarding` (it appears as a shared folder), open a subfolder, click **Sharing**, and check the collaborator list. If no one was invited, the folder is visible only to the Service Account.

    * **Preview it in Content Manager.** Navigate to the **Admin Console** > **Content** > **Content Manager**, then select the relevant user. You are able to view the shared folder.

    <Tip>
      To make workspaces appear in a real, admin-visible location instead of the Service Account's root, create a dedicated **New Hires** folder owned by an admin, add the Service Account as a co-owner collaborator on it, and set `ONBOARDING_ROOT_FOLDER_ID` to that folder's ID. New workspaces are then owned by that admin and show up in their **All Files**.
    </Tip>

    **Run it again:**

    Send the same curl request a second time. The service completes without errors and without creating duplicate folders, groups, or collaborations. This idempotency is what makes it safe to retry failed or replayed events in production.
  </Step>
</Steps>

## Troubleshooting

<AccordionGroup>
  <Accordion title="ModuleNotFoundError: No module named '...'">
    Your virtual environment is not activated. Run `source .venv/bin/activate` from the project directory before running any `python3` commands. Each new terminal tab needs its own activation.
  </Accordion>

  <Accordion title="invalid_client: The client credentials are invalid">
    Check your `.env` file:

    * Verify `BOX_CLIENT_ID` and `BOX_CLIENT_SECRET` match the values in Developer Console > Configuration.
    * Confirm `BOX_ENTERPRISE_ID` is your enterprise ID (found in Developer Console > General Settings, or Admin Console > Account & Billing).
    * Ensure your app is authorized in the Developer Console and uses Client Credentials Grant.
  </Accordion>

  <Accordion title="403 Forbidden or 'access_denied_insufficient_permissions'">
    The app is missing the scopes or access level required to manage groups, users, or content:

    * Set the **App Access Level** to **App + Enterprise Access** in Developer Console > Configuration. The default **App Access Only** cannot create or manage enterprise users and groups, which returns a `403`.
    * Enable **Read and write all files and folders**, **Manage groups**, and **Manage users** in Developer Console > Configuration > Application Scopes.
    * After changing the access level or scopes, an administrator must reauthorize the app in the Admin Console under **Integrations > Platform Apps**. These changes do not take effect until the app is reauthorized.
  </Accordion>

  <Accordion title="Users or groups are not created and creation returns 403">
    Creating enterprise users and groups requires administrative privileges *and* enterprise access. Confirm the app's **App Access Level** is set to **App + Enterprise Access** (the default **App Access Only** returns a `403` for these operations), that the **Manage groups** and **Manage users** scopes are enabled, and that an enterprise admin reauthorized the app after those changes.
  </Accordion>

  <Accordion title="A folder or collaboration already exists (409 Conflict)">
    This is expected on re-runs and is handled for you. `create_folder` reuses the existing folder returned in the conflict response, and `is_already_collaborator` detects an existing collaborator so the collaboration helpers skip it. Note that Box reports this differently for groups (`409` with code `conflict` and the message "Group is already a collaborator") and users (`user_already_collaborator`), which is why the helper checks both. If you see an unhandled 409, confirm you copied the `try`/`except` blocks and the `is_already_collaborator` helper exactly as shown.
  </Accordion>

  <Accordion title="The new hire invite fails or stays pending">
    If the email does not belong to an existing Box user, the collaboration is created in a `pending` state until they accept the invitation. To provision managed accounts for new hires as part of this flow, see <Link href="/guides/users/create-managed-user">Create a managed user</Link>.
  </Accordion>
</AccordionGroup>

## Scaling to production

<AccordionGroup>
  <Accordion title="Add the new hire to enterprise groups">
    Sharing folders with groups is only half of a governance model. You can also add the new hire *into* groups so they automatically inherit every collaboration those groups already hold across Box. Use the Memberships API:

    ```python theme={null}
    from box_sdk_gen import (
        BoxClient,
        CreateGroupMembershipUser,
        CreateGroupMembershipGroup,
    )

    def add_user_to_group(client: BoxClient, user_id: str, group_id: str):
        client.memberships.create_group_membership(
            CreateGroupMembershipUser(id=user_id),
            CreateGroupMembershipGroup(id=group_id),
        )
    ```

    A common pattern is to add every new hire to an `All Employees` group so org-wide shares reach them on day one. See <Link href="/guides/collaborations/groups">Sharing with groups</Link>.

    <Note>
      Adding a user who is already a member returns a `409` `BoxAPIError`. To keep this step safe to re-run like the rest of the service, wrap the call in the same `try`/`except` pattern used in `workspace.py` and ignore the conflict.
    </Note>
  </Accordion>

  <Accordion title="Give the new hire access to an onboarding Hub">
    Beyond folders, you can give each new hire access to a <Link href="/guides/hubs-api">Box Hub</Link> - a curated content portal for onboarding materials, policies, and FAQs that is integrated with Box AI, so the hire can ask natural-language questions across its content. A common pattern is to maintain one shared "New Hire" Hub and add each hire (or the `All Employees` group) as a collaborator during provisioning.

    Add the following helper to `workspace.py`. Unlike a folder, a hub returns a generic `409 Conflict` (not the `already a collaborator` body that `is_already_collaborator` matches) when the collaborator already exists, so the helper treats any `409` as an idempotent skip - keeping it safe to re-run:

    ```python theme={null}
    from box_sdk_gen import (
        CreateHubCollaborationV2025R0Hub,
        CreateHubCollaborationV2025R0AccessibleBy,
    )

    def add_hub_collaborator(
        client: BoxClient, hub_id: str, login: str, role: str = "viewer"
    ):
        try:
            client.hub_collaborations.create_hub_collaboration_v2025_r0(
                CreateHubCollaborationV2025R0Hub(id=hub_id),
                CreateHubCollaborationV2025R0AccessibleBy(type="user", login=login),
                role,
            )
        except BoxAPIError as error:
            # A hub returns a generic 409 "conflict" (not the folder-style
            # "already a collaborator" body) when the collaborator already
            # exists, so treat any 409 here as the idempotent skip case.
            if error.response_info.status_code != 409:
                raise
    ```

    Store the Hub's ID in your `.env` as `ONBOARDING_HUB_ID`, then call the helper from `onboard_new_hire` when the hire has an email:

    ```python theme={null}
    hub_id = os.getenv("ONBOARDING_HUB_ID")
    if hub_id and hire.get("email"):
        add_hub_collaborator(client, hub_id, hire["email"])
    ```

    To grant access at the group level instead, pass `CreateHubCollaborationV2025R0AccessibleBy(type="group", id=group_id)`. Valid roles are `viewer`, `editor`, and `co-owner`.

    <Note>
      Box Hubs must be <Link href="https://support.box.com/hc/en-us/articles/25822332454547-Configuring-Box-Hubs">enabled for your enterprise</Link> before these calls succeed. The `*_v2025_r0` SDK methods target the `2025.0` API version, which the Hubs endpoints require.

      The identity running the service - the Service Account when you use Client Credentials Grant - must **own or co-own the Hub** to manage its collaborations. If it does not, Box returns `404 Not Found` with the message `Authorization Failed`, because it cannot see the Hub. The simplest approach is to have the Service Account create the Hub (via <Link href="/guides/hubs-api/hubs/create-hub">Create a hub</Link>) so it owns it, or add the Service Account as a co-owner of an existing Hub. To manage roles and removals, see <Link href="/guides/hubs-api/hubs-collaborations/hub-collaborations">Manage hub collaborations</Link>.
    </Note>
  </Accordion>

  <Accordion title="Apply governance policies to the workspace">
    Layer additional controls on the provisioned tree. Attach a <Link href="/guides/metadata/index">metadata</Link> instance to tag each workspace with the hire's department and start date, and apply <Link href="/guides/retention-policies">retention policies</Link> so onboarding documents are archived or deleted on a schedule. Metadata also makes workspaces searchable and reportable across the enterprise.
  </Accordion>

  <Accordion title="Secure the event endpoint">
    Treat the `/hire` endpoint as privileged. Require a shared secret or signature on every request from your HR system, restrict inbound traffic to known sources, and run the service behind HTTPS. Reject any request you cannot authenticate before it reaches your provisioning code.
  </Accordion>

  <Accordion title="Make provisioning resilient">
    HR integrations can deliver events more than once or out of order. The idempotent helpers in this tutorial already prevent duplicates, but for high volume you should also queue events, retry transient failures with backoff, and log each provisioning run so you have an audit trail of who was onboarded and when.
  </Accordion>

  <Accordion title="Pair onboarding with deprovisioning">
    A complete lifecycle needs an offboarding counterpart. When your HR system fires a termination event, transfer the departing employee's content and remove their access. See <Link href="/guides/users/deprovision/index">Deprovision users</Link> and <Link href="/guides/users/deprovision/transfer-folders">Transfer files and folders</Link>.
  </Accordion>
</AccordionGroup>

## Next steps

<CardGroup cols={2}>
  <Card title="Provision users and content" href={localizeLink("/guides/users/provision/index")} icon="users" arrow="true">
    Explore Box's end-to-end patterns for provisioning users, groups, and shared folder structures.
  </Card>

  <Card title="Collaborations API reference" href={localizeLink("/reference/post-collaborations")} icon="code" arrow="true">
    See the full API specification for creating collaborations.
  </Card>
</CardGroup>

<RelatedLinks
  title="RELATED GUIDES"
  items={[
{ label: translate("Sharing with groups"), href: "/guides/collaborations/groups", badge: "GUIDE" },
{ label: translate("Setup shared folders"), href: "/guides/users/provision/shared-folders", badge: "GUIDE" },
{ label: translate("Create a managed user"), href: "/guides/users/create-managed-user", badge: "GUIDE" },
{ label: translate("Create a folder"), href: "/guides/folders/single/create", badge: "GUIDE" },
{ label: translate("Select an authentication method"), href: "/guides/authentication/select", badge: "GUIDE" }
]}
/>
