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

# Provision a Customer Project

> Create a project for a new customer that mirrors a model project — servers, navigators, tool permissions, and runtime keys — in a single API call.

If you run Caylex on behalf of your own customers (a multi-tenant SaaS pattern), you'll often want **one project per customer**, each pre-loaded with the same servers, navigators, and tool-permission policy. Rather than building each one by hand in the Caylex Platform UI — or scripting the half-dozen calls it takes — you can stand up a **model project** once and clone it for every customer with a single request.

## Clone a project in one call

`POST /projects/from-seed` takes a reference to a **seed (model) project** and a **new project name**, then does everything needed to stand up an equivalent project:

1. Creates the new project (`409` if the name is already taken — names are unique per tenant).
2. Connects the **same servers** as the seed project.
3. Connects the **same navigators** as the seed project.
4. Copies each navigator's **tool-permission policy** onto the new navigator instances.
5. Mints a **runtime API key** for each new navigator instance and returns it (shown once).

### Request

```
POST https://api.caylex.ai/api/v1/projects/from-seed
Authorization: Bearer <platform_access_token>
```

| Field               | Required | Description                                                                                       |
| ------------------- | -------- | ------------------------------------------------------------------------------------------------- |
| `name`              | Yes      | Name for the new project. Unique within your tenant — `409` if it already exists.                 |
| `seed_project_id`   | One of   | UUID of the project to clone.                                                                     |
| `seed_project_name` | One of   | Name of the project to clone. Provide **exactly one** of `seed_project_id` / `seed_project_name`. |
| `description`       | No       | Description for the new project.                                                                  |
| `icon`              | No       | Icon for the new project.                                                                         |

### Response

```json theme={null}
{
  "id": "456e7890-e89b-12d3-a456-426614174015",
  "name": "Acme Corp",
  "seed_project_id": "123e4567-e89b-12d3-a456-426614174003",
  "seeded_server_count": 9,
  "seeded_navigator_count": 2,
  "servers": [
    { "server_id": "…", "server_name": "Shopify", "server_instance_id": "…" }
  ],
  "navigators": [
    {
      "navigator_id": "…",
      "navigator_name": "Customer Support",
      "navigator_instance_id": "…",
      "api_key_id": "…",
      "api_key": "ck_…full-secret-shown-once"
    }
  ],
  "warnings": []
}
```

The `navigator_instance_id` and `server_instance_id` values are the handles for any follow-up calls (tool permissions, key rotation, pausing a server, etc.). Each navigator's **`api_key` is returned only once** — store it immediately.

<Warning>
  The `api_key` values are shown **only in this response**. Capture them and store them in your secret manager keyed by customer + navigator. The runtime API key is what your agent submits to Caylex on each tool call to identify which navigator to use — see [Connecting your agent](/integration/connecting).
</Warning>

### Example

<Tabs>
  <Tab title="Python">
    ```python provision_from_seed.py theme={null}
    import os

    import requests

    BASE_URL = "https://api.caylex.ai/api/v1"
    HEADERS = {"Authorization": f"Bearer {os.environ['CAYLEX_PLATFORM_TOKEN']}"}

    SEED_PROJECT = "Model Project"
    CUSTOMERS = ["Acme Corp", "Globex", "Initech"]


    def provision(customer: str) -> dict | None:
        resp = requests.post(
            f"{BASE_URL}/projects/from-seed",
            headers=HEADERS,
            json={"name": customer, "seed_project_name": SEED_PROJECT},
        )
        if resp.status_code == 409:
            print(f"skip (already exists): {customer}")
            return None
        resp.raise_for_status()
        return resp.json()


    for customer in CUSTOMERS:
        result = provision(customer)
        if not result:
            continue
        print(
            f"created {customer} ({result['id']}): "
            f"{result['seeded_server_count']} servers, "
            f"{result['seeded_navigator_count']} navigators"
        )
        # The runtime key for each navigator is returned once — store it now.
        for nav in result["navigators"]:
            if nav["api_key"]:
                # store_secret(f"caylex/{customer}/{nav['navigator_name']}", nav["api_key"])
                print(f"  store key for {nav['navigator_name']}: {nav['api_key'][:8]}…")
        for warning in result["warnings"]:
            print(f"  warning: {warning}")
    ```
  </Tab>

  <Tab title="TypeScript">
    ```typescript provisionFromSeed.ts theme={null}
    const BASE_URL = "https://api.caylex.ai/api/v1";
    const headers = {
      Authorization: `Bearer ${process.env.CAYLEX_PLATFORM_TOKEN}`,
      "Content-Type": "application/json",
    };

    const SEED_PROJECT = "Model Project";
    const CUSTOMERS = ["Acme Corp", "Globex", "Initech"];

    async function provision(customer: string) {
      const res = await fetch(`${BASE_URL}/projects/from-seed`, {
        method: "POST",
        headers,
        body: JSON.stringify({ name: customer, seed_project_name: SEED_PROJECT }),
      });
      if (res.status === 409) {
        console.log(`skip (already exists): ${customer}`);
        return null;
      }
      if (!res.ok) throw new Error(`Provision ${customer} failed: HTTP ${res.status}`);
      return res.json();
    }

    for (const customer of CUSTOMERS) {
      const result = await provision(customer);
      if (!result) continue;
      console.log(
        `created ${customer} (${result.id}): ${result.seeded_server_count} servers, ` +
          `${result.seeded_navigator_count} navigators`,
      );
      // The runtime key for each navigator is returned once — store it now.
      for (const nav of result.navigators) {
        if (nav.api_key) {
          // await storeSecret(`caylex/${customer}/${nav.navigator_name}`, nav.api_key);
          console.log(`  store key for ${nav.navigator_name}: ${nav.api_key.slice(0, 8)}…`);
        }
      }
      for (const warning of result.warnings) console.log(`  warning: ${warning}`);
    }
    ```
  </Tab>

  <Tab title="cURL">
    ```bash theme={null}
    curl -X POST "https://api.caylex.ai/api/v1/projects/from-seed" \
      -H "Authorization: Bearer $CAYLEX_PLATFORM_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"name": "Acme Corp", "seed_project_name": "Model Project"}'
    ```
  </Tab>
</Tabs>

<Note>
  The call returns `409` if a project with that name already exists, so the loop above is safe to re-run — already-provisioned customers are skipped.
</Note>

<Note>
  Servers with **user-level** authentication only need to be connected — each end user still authenticates individually at runtime via [auth links](/auth/auth-links) or [in-chat authentication](/navigator/in-chat-authentication). Connecting the server does not grant access to anyone's data.
</Note>

<Warning>
  The endpoint runs its steps incrementally rather than as a single transaction. If a later step fails after the project is created, the project will exist (so a retry hits the `409` path) — finish the setup with the granular endpoints below, or delete the project and retry.
</Warning>

## Under the hood: the granular endpoints

`from-seed` wraps the procedures below. You don't need them for the common case, but expand any step to see exactly what it does — useful if you want finer control, or to apply one piece (like copying permissions) to projects that already exist.

<AccordionGroup>
  <Accordion title="1. Connect servers and navigators" icon="plug">
    Servers and navigators are defined once at the **tenant** level. Adding one to a project creates an *instance* (a **server instance** or **navigator instance**). To replicate a seed project, connect the same tenant-level servers and navigators to the new project.

    | Method & path                                 | Purpose                                            |
    | --------------------------------------------- | -------------------------------------------------- |
    | `GET /server-instances?project_id={id}`       | List the servers connected to the seed project.    |
    | `GET /navigator-instances?project_id={id}`    | List the navigators connected to the seed project. |
    | `GET /servers/{server_id}/connections`        | Read a server's current project connections.       |
    | `POST /servers/{server_id}/connections`       | Set a server's project connections (full set).     |
    | `POST /navigators/{navigator_id}/connections` | Set a navigator's project connections (full set).  |

    <Warning>
      The connection endpoints **replace the full set** of projects a server or navigator is connected to. To *add* a project without detaching the resource from others, GET the current connections, append the new project, and POST the union.
    </Warning>

    ```python connect_servers_navigators.py theme={null}
    import os

    import requests

    BASE_URL = "https://api.caylex.ai/api/v1"
    HEADERS = {"Authorization": f"Bearer {os.environ['CAYLEX_PLATFORM_TOKEN']}"}


    def get_items(path: str, params: dict | None = None) -> list[dict]:
        params = {**(params or {}), "size": 100}
        resp = requests.get(f"{BASE_URL}{path}", headers=HEADERS, params=params)
        resp.raise_for_status()
        return resp.json()["items"]


    def project_id_by_name(name: str) -> str | None:
        return next((p["id"] for p in get_items("/projects") if p["name"] == name), None)


    def connect_resource(resource: str, resource_id: str, project_id: str) -> None:
        """Add `project_id` to a server/navigator's connections without dropping the rest.

        `resource` is "servers" or "navigators".
        """
        current = requests.get(
            f"{BASE_URL}/{resource}/{resource_id}/connections", headers=HEADERS
        )
        current.raise_for_status()
        project_ids = {c["project_id"] for c in current.json()["connections"]}
        project_ids.add(project_id)
        resp = requests.post(
            f"{BASE_URL}/{resource}/{resource_id}/connections",
            headers=HEADERS,
            json={"project_ids": sorted(project_ids)},
        )
        resp.raise_for_status()


    def seed_project(seed_name: str, new_project_id: str) -> None:
        seed_id = project_id_by_name(seed_name)
        servers = {
            si["server_id"] for si in get_items("/server-instances", {"project_id": seed_id})
        }
        navigators = {
            ni["navigator_id"]
            for ni in get_items("/navigator-instances", {"project_id": seed_id})
        }
        for server_id in servers:
            connect_resource("servers", server_id, new_project_id)
        for navigator_id in navigators:
            connect_resource("navigators", navigator_id, new_project_id)
    ```
  </Accordion>

  <Accordion title="2. Generate navigator API keys" icon="key-skeleton">
    Connecting a navigator creates a navigator instance, but it has **no runtime key yet** — and your agent needs a navigator API key (`ck_…`) to connect. `from-seed` mints one per navigator automatically; to do it yourself (or to rotate keys later):

    | Method & path                                        | Purpose                                       |
    | ---------------------------------------------------- | --------------------------------------------- |
    | `GET /navigator-instances/{id}/api-keys`             | List existing keys (only previews).           |
    | `POST /navigator-instances/{id}/api-keys`            | Mint a key — returns the full value **once**. |
    | `DELETE /navigator-instances/{id}/api-keys/{key_id}` | Revoke a key (rotation).                      |

    ```bash theme={null}
    curl -X POST "https://api.caylex.ai/api/v1/navigator-instances/$INSTANCE_ID/api-keys" \
      -H "Authorization: Bearer $CAYLEX_PLATFORM_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{"name": "runtime", "description": "Runtime key for Acme Corp"}'
    ```

    See the full recipe → [Generate Navigator API Keys](/cookbooks/generate-navigator-api-keys).
  </Accordion>

  <Accordion title="3. Copy tool permissions" icon="shield-halved">
    Tool permissions are configured per navigator instance (`always_execute`, `require_approval`, or `disabled`). `from-seed` copies the seed navigator's policy onto each new navigator instance. To replicate a policy across projects that already exist:

    | Method & path                                    | Purpose                                  |
    | ------------------------------------------------ | ---------------------------------------- |
    | `GET /navigator-instances/{id}/tool-permissions` | Read a navigator instance's policy.      |
    | `PUT /navigator-instances/{id}/tool-permissions` | Apply a policy (`{tool_id, mode}` list). |

    Because tools belong to tenant-level servers, a tool's `tool_id` is stable across projects that share that server — so copying is a `tool_id → mode` mapping.

    See the full recipe → [Copy Tool Permissions Across Projects](/cookbooks/copy-tool-permissions).
  </Accordion>
</AccordionGroup>

## Next steps

<CardGroup cols={2}>
  <Card title="Generate navigator API keys" icon="key-skeleton" href="/cookbooks/generate-navigator-api-keys">
    Rotate or mint additional runtime keys for a navigator instance.
  </Card>

  <Card title="Copy tool permissions" icon="shield-halved" href="/cookbooks/copy-tool-permissions">
    Replicate a navigator's tool-permission policy onto existing projects.
  </Card>

  <Card title="Managing users" icon="users" href="/auth/managing-users">
    Add end users to each customer project and generate their auth links.
  </Card>

  <Card title="REST API Reference" icon="book" href="https://developers.caylex.ai/">
    Full request/response schemas for every endpoint used here.
  </Card>
</CardGroup>
