# Projects (/docs/projects)

Projects group resources and credentials inside an [organization](/docs/organizations). Organizations contain projects, and projects contain everything else: every sandbox, image, snapshot, volume, secret, registry credential, and API key is scoped to exactly one project.

Projects are the unit of isolation. Nothing is shared between projects implicitly: a secret or volume in one project is invisible to sandboxes in another, and a project's API key cannot see other projects' resources. Organizations are the unit of membership and billing.

| **Concept**      | **Description**                                                                                                                      |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| **Organization** | The unit of membership and billing. Members, invitations, and credits belong to the organization. Contains any number of projects.   |
| **Project**      | The unit of isolation. Holds sandboxes, images, snapshots, volumes, secrets, registry credentials, and API keys.                     |
| **API key**      | Minted per project. Confined to that project and to its permission scopes; revoking a key takes effect immediately for new requests. |

Every organization has a project named default that cannot be renamed or deleted. The name is reserved, so no other project can use it.

### Project access [#project-access]

Projects also decide which organization members can reach their resources:

- Organization admins can access every project.
- Every member can access the `default` project.
- Any other project is visible only to members who have been added to it. Members who are not on a project get a `404` for it, as if it did not exist.

Whoever creates a project is added to it automatically.

Projects are managed on the dashboard's **Projects** page and switched in the header's organization and project switcher. These routes use your dashboard session token (an OIDC bearer JWT), not a project API key, and need matching organization project permissions.

## Create projects [#create-projects]

Create a project. Names are trimmed, unique per organization, and up to 100 characters.

1. Go to [Daytona Dashboard](https://app.daytona.com/dashboard/projects)
2. Click <Button>Create Project</Button>
3. Enter a <Button>Name</Button> for the project
4. Click <Button>Create Project</Button>

You can also click the project switcher in the header and choose **Create Project**.

<Tabs syncKey="language">
<TabItem label="CLI" icon="seti:shell">

```bash
daytona project create my-project --org ORG_ID
```

</TabItem>
<TabItem label="API" icon="seti:json">

```bash
curl 'https://api.daytona.com/orgs/ORG_ID/projects' \
  --request POST \
  --header 'Authorization: Bearer YOUR_OIDC_TOKEN' \
  --header 'Content-Type: application/json' \
  --data '{ "name": "my-project" }'
```

```json
{
  "id": "PROJECT_ID",
  "org_id": "ORG_ID",
  "name": "my-project",
  "default": false,
  "created_by": "USER_ID",
  "created_at": "2026-09-10T12:00:00Z",
  "updated_at": "2026-09-10T12:00:00Z"
}
```

</TabItem>
</Tabs>

The creator field is null unless the caller holds the members read permission.

## Operations [#operations]

### List projects [#list-projects]

List the projects you can access in an organization. The default project is always listed first.

<Tabs syncKey="language">
<TabItem label="CLI" icon="seti:shell">

```bash
daytona project list --org ORG_ID
```

</TabItem>
<TabItem label="API" icon="seti:json">

```bash
curl 'https://api.daytona.com/orgs/ORG_ID/projects' \
  --header 'Authorization: Bearer YOUR_OIDC_TOKEN'
```

```json
{
  "projects": [
    {
      "id": "DEFAULT_PROJECT_ID",
      "org_id": "ORG_ID",
      "name": "default",
      "default": true,
      "created_by": null,
      "created_at": "2026-09-01T09:00:00Z",
      "updated_at": "2026-09-01T09:00:00Z"
    },
    {
      "id": "PROJECT_ID",
      "org_id": "ORG_ID",
      "name": "my-project",
      "default": false,
      "created_by": "USER_ID",
      "created_at": "2026-09-10T12:00:00Z",
      "updated_at": "2026-09-10T12:00:00Z"
    }
  ]
}
```

</TabItem>
</Tabs>

### Select project [#select-project]

Select the project that later CLI commands run against. The selection is per organization, so switching organizations restores your last project there. Pass an ID or an unambiguous name; with no argument, the CLI shows a picker at a terminal.

<Tabs syncKey="language">
<TabItem label="CLI" icon="seti:shell">

```bash
daytona project use PROJECT_ID
```

</TabItem>
</Tabs>

### Get by ID [#get-by-id]

Get a single project.

<Tabs syncKey="language">
<TabItem label="CLI" icon="seti:shell">

```bash
daytona project get PROJECT_ID
```

</TabItem>
<TabItem label="API" icon="seti:json">

```bash
curl 'https://api.daytona.com/projects/PROJECT_ID' \
  --header 'Authorization: Bearer YOUR_OIDC_TOKEN'
```

</TabItem>
</Tabs>

### Rename projects [#rename-projects]

Rename a project. The default project cannot be renamed.

1. Go to [Daytona Dashboard](https://app.daytona.com/dashboard/projects)
2. Click the <Button>⋮</Button> menu of the project
3. Click <Button>Rename</Button>
4. Enter the new name and confirm

<Tabs syncKey="language">
<TabItem label="CLI" icon="seti:shell">

```bash
daytona project update PROJECT_ID --name my-renamed-project --org ORG_ID
```

</TabItem>
<TabItem label="API" icon="seti:json">

```bash
curl 'https://api.daytona.com/projects/PROJECT_ID' \
  --request PATCH \
  --header 'Authorization: Bearer YOUR_OIDC_TOKEN' \
  --header 'Content-Type: application/json' \
  --data '{ "name": "my-renamed-project" }'
```

</TabItem>
</Tabs>

### Delete projects [#delete-projects]

Delete a project, revoking its API keys and removing its secrets and registries.

The default project cannot be deleted.

<Callout type="caution" title="Warning">
A project that still owns sandboxes, volumes, or snapshots cannot be deleted. Delete the remaining resources first.
</Callout>

1. Go to [Daytona Dashboard](https://app.daytona.com/dashboard/projects)
2. Click the <Button>⋮</Button> menu of the project
3. Click <Button>Delete</Button>
4. Confirm the deletion

<Tabs syncKey="language">
<TabItem label="CLI" icon="seti:shell">

```bash
daytona project delete PROJECT_ID --org ORG_ID
```

</TabItem>
<TabItem label="API" icon="seti:json">

```bash
curl 'https://api.daytona.com/projects/PROJECT_ID' \
  --request DELETE \
  --header 'Authorization: Bearer YOUR_OIDC_TOKEN'
```

```json
{ "deleted": "PROJECT_ID" }
```

</TabItem>
</Tabs>

## Project budgets [#project-budgets]

A project budget caps the project's spend in each calendar month, in UTC, at list price and before credits. When the month-to-date spend reaches the budget, creating, forking, starting, or resuming a sandbox in the project, or restoring one from a snapshot, is refused. After the organization's grace period, running persistent sandboxes are stopped and running ephemeral sandboxes are deleted. macOS sandboxes, which run on a prepaid Mac reservation, keep running until the reservation's paid period ends; while the project is held, a renewal answers `402 PROJECT_BUDGET_EXCEEDED`, so the reservation ends at its `period_end`. The hold lifts at the start of the next month; when the grace period would end after that, no sandbox is ended that month.

Spend is evaluated periodically and can lag usage by several minutes. Reading a budget requires the budgets read permission; setting or removing it requires budgets write.

### Get a project budget [#get-a-project-budget]

Get a project's monthly budget and this month's spend. The SDKs, and the API with an API key, read the budget of the API key's project.

<Tabs syncKey="language">
<TabItem label="Python" icon="seti:python">

```python
from daytona import Daytona

daytona = Daytona()

budget = daytona.v2.get_project_budget()
if budget is None:
    print("The project has no budget")
else:
    print(f"Spent {budget.spent_cents} of {budget.monthly_limit_cents} cents")
```

<Callout type="note" title="Notes">
- The API key needs the `read:spending` permission.
</Callout>

</TabItem>
<TabItem label="TypeScript" icon="seti:typescript">

```typescript
import { Daytona } from '@daytona/sdk'

const daytona = new Daytona()

const budget = await daytona.v2.getProjectBudget()
if (budget === null) {
  console.log('The project has no budget')
} else {
  console.log(`Spent ${budget.spent_cents} of ${budget.monthly_limit_cents} cents`)
}
```

<Callout type="note" title="Notes">
- The API key needs the `read:spending` permission.
</Callout>

</TabItem>
<TabItem label="CLI" icon="seti:shell">

```bash
# The argument is optional; omit it to use the selected project
daytona project budget get PROJECT_ID
```

</TabItem>
<TabItem label="API" icon="seti:json">

```bash
# Any project, with a signed-in user token
curl 'https://api.daytona.com/projects/PROJECT_ID/budget' \
  --header 'Authorization: Bearer YOUR_OIDC_TOKEN'

# The API key's project
curl 'https://api.daytona.com/usage/budget' \
  --header 'Authorization: Bearer YOUR_API_KEY'
```

```json
{
  "project_id": "PROJECT_ID",
  "monthly_limit_cents": 50000,
  "period_start": "2026-10-01",
  "spent_cents": 12345,
  "evaluated_at": "2026-10-09T01:20:00Z",
  "exceeded_at": null,
  "stop_at": null,
  "stop_grace_hours": 1
}
```

The response has the following fields:

| **Field**             | **Description**                                                                                                                                                                     |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `monthly_limit_cents` | The monthly budget, in US cents.                                                                                                                                                    |
| `spent_cents`         | The spend this period, in US cents. The value is `null` until the first evaluation in the period.                                                                                   |
| `evaluated_at`        | The value is `null` until the first evaluation in the period.                                                                                                                       |
| `exceeded_at`         | The time the budget was reached this period. The value is `null` while the spend is under the budget.                                                                               |
| `stop_at`             | The time running sandboxes are ended, which is `exceeded_at` plus `stop_grace_hours`. The value is `null` when `exceeded_at` is `null`. The value is also `null` when `exceeded_at` plus `stop_grace_hours` falls in the next month. |

<Callout type="note" title="Notes">
- `/projects/{id}/budget` accepts only a signed-in user token.
- `/projects/{id}/budget` rejects `dtno_` organization API keys.
- `/usage/budget` accepts an API key with the `read:spending` permission.
- `/usage/budget` returns the same fields as `/projects/{id}/budget`.
</Callout>

</TabItem>
</Tabs>

### Set a project budget [#set-a-project-budget]

Set or replace a project's monthly budget. Project budgets require the Team plan or above.

1. Go to [Daytona Dashboard](https://app.daytona.com/dashboard/projects)
2. Click the project
3. In the **Budget** section, click <Button>Set budget</Button>, or <Button>Edit budget</Button> to replace an existing one
4. Enter the **Monthly budget** in USD
5. Click <Button>Save</Button>

<Tabs syncKey="language">
<TabItem label="CLI" icon="seti:shell">

```bash
# The argument is optional; omit it to use the selected project
daytona project budget set PROJECT_ID --monthly-usd 500
```

| **Flag**        | **Type** | **Description**                                                                          |
| --------------- | -------- | ---------------------------------------------------------------------------------------- |
| `--monthly-usd` | `string` | Required. Monthly budget in US dollars, at least `1.00` and at most two decimals, such as `500` or `499.99`. |

</TabItem>
<TabItem label="API" icon="seti:json">

```bash
curl 'https://api.daytona.com/projects/PROJECT_ID/budget' \
  --request PUT \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Bearer YOUR_OIDC_TOKEN' \
  --data '{ "monthly_limit_cents": 50000 }'
```

| **Field**             | **Type**  | **Description**                                                                    |
| --------------------- | --------- | ---------------------------------------------------------------------------------- |
| `monthly_limit_cents` | `integer` | Required. Monthly budget in US cents, from `100` ($1.00) to `100000000000` ($1 billion). |

<Callout type="note" title="Notes">
- The request returns the budget after the write, in the same shape as [get a project budget](#get-a-project-budget).
</Callout>

</TabItem>
</Tabs>

### Remove a project budget [#remove-a-project-budget]

Remove a project's monthly budget. Removing it releases a budget hold on the project; sandboxes that were already stopped stay stopped.

1. Go to [Daytona Dashboard](https://app.daytona.com/dashboard/projects)
2. Click the project
3. In the **Budget** section, click <Button>Remove budget</Button>
4. Confirm with <Button>Remove</Button>

<Tabs syncKey="language">
<TabItem label="CLI" icon="seti:shell">

```bash
# The argument is optional; omit it to use the selected project
daytona project budget remove PROJECT_ID
```

<Callout type="note" title="Notes">
- `daytona project budget remove` asks for confirmation.
</Callout>

</TabItem>
<TabItem label="API" icon="seti:json">

```bash
curl 'https://api.daytona.com/projects/PROJECT_ID/budget' \
  --request DELETE \
  --header 'Authorization: Bearer YOUR_OIDC_TOKEN'
```

</TabItem>
</Tabs>

## Lifecycle defaults [#lifecycle-defaults]

Lifecycle defaults set the [timeouts](/docs/linux-sandboxes#timeouts) that new Linux sandboxes in a project receive when the create request leaves them out. A project stores them in the `ephemeral` block, and these timeouts always delete the sandbox. A macOS create does not take the project's lifecycle defaults: it gets the platform defaults, which the `effective.macos` block shows.

Daytona reads the defaults only when it creates a sandbox. Each field the create request sets overrides the matching default, and saving new defaults never changes existing sandboxes.

### Get lifecycle defaults [#get-lifecycle-defaults]

Get a project's stored lifecycle defaults and the policy a create without lifecycle settings receives for each sandbox type.

1. Go to [Daytona Dashboard](https://app.daytona.com/dashboard/projects)
2. Click the project
3. Open the **Settings** tab and find the **Sandbox Lifecycle Policy** section

<Tabs syncKey="language">
<TabItem label="API" icon="seti:json">

```bash
curl 'https://api.daytona.com/projects/PROJECT_ID/lifecycle-policy' \
  --header 'Authorization: Bearer YOUR_OIDC_TOKEN'
```

The following response is abridged to the `effective.linux.ephemeral` block. The full `effective` object also has `linux.persistent`, `windows`, `gpu`, and `macos` blocks, and each policy also has `wake_on_request`.

```json
{
  "policy": {
    "ephemeral": { "timeout_seconds": 1800, "idle_timeout_seconds": 600 }
  },
  "effective": {
    "linux": {
      "ephemeral": { "timeout_seconds": 1800, "on_timeout": "delete", "timeout_source": "project", "timeout_capped": false, "idle_timeout_seconds": 600, "on_idle_timeout": "delete" }
    }
  }
}
```

The response has the following fields:

| **Field**          | **Description**                                                                                                                  |
| ------------------ | -------------------------------------------------------------------------------------------------------------------------------- |
| `policy`           | Holds the stored values. An unset field is `null`.                                                                               |
| `effective`        | Holds what a create with no lifecycle settings receives.                                                                         |
| `timeout_source`   | The value in each `effective` block is `project`, `platform`, or `none`.                                                         |
| `timeout_capped`   | The value in each `effective` block is `true` when a stored timeout above the organization's [timeout cap](/docs/organizations#set-the-timeout-cap) is capped to it. |

<Callout type="note" title="Notes">
- The request requires the projects read permission.
- The request accepts only a signed-in user token.
- The request rejects `dtno_` organization API keys.
</Callout>

</TabItem>
</Tabs>

### Update lifecycle defaults [#update-lifecycle-defaults]

Update a project's lifecycle defaults. Each block is updated as a whole: a block left out of the request is kept, an empty value clears it, and a sent block replaces the stored one, so a field left out of a sent block is unset.

1. Go to [Daytona Dashboard](https://app.daytona.com/dashboard/projects)
2. Click the project
3. Open the **Settings** tab
4. In the **Sandbox Lifecycle Policy** section, click the edit button on the **Ephemeral sandboxes** card
5. Set the timeouts, then click <Button>Save</Button>

<Tabs syncKey="language">
<TabItem label="API" icon="seti:json">

```bash
curl 'https://api.daytona.com/projects/PROJECT_ID/lifecycle-policy' \
  --request PATCH \
  --header 'Content-Type: application/json' \
  --header 'Authorization: Bearer YOUR_OIDC_TOKEN' \
  --data '{
    "ephemeral": { "timeout_seconds": 1800, "idle_timeout_seconds": 600 }
  }'
```

| **Field**                      | **Type**  | **Description**                                                                                                           |
| ------------------------------ | --------- | ------------------------------------------------------------------------------------------------------------------------- |
| `ephemeral` | `object` | Sets the timeouts for sandboxes. `null` clears the block. If omitted, the stored block is kept. |
| `ephemeral.timeout_seconds` | `integer` | Sets the seconds until a sandbox is deleted, from `1` to the organization's maximum timeout. By default, it is unset. |
| `ephemeral.idle_timeout_seconds` | `integer` | Sets the idle seconds until a sandbox is deleted. Values below `60` are raised to `60`. By default, it is unset. |

<Callout type="note" title="Notes">
- The request returns the defaults after the update, in the same shape as [get lifecycle defaults](#get-lifecycle-defaults).
- Daytona validates only the blocks in the request.
- Daytona accepts a timeout equal to the stored value even when the organization's maximum has since dropped below the stored value.
- The request requires the projects write permission.
- Changing the `ephemeral` block also requires the delete sandboxes permission in the project.
- The request accepts only a signed-in user token.
- The request rejects `dtno_` organization API keys.
</Callout>

</TabItem>
</Tabs>

## Project members [#project-members]

Control which organization members can access a non-default project. Listing members requires the members read permission; adding and removing them requires the members write permission. Organization admins are members of every project implicitly and do not need to be added.

### List project members [#list-project-members]

<Tabs syncKey="language">
<TabItem label="CLI" icon="seti:shell">

```bash
daytona project member list --project PROJECT_ID
```

</TabItem>
<TabItem label="API" icon="seti:json">

```bash
curl 'https://api.daytona.com/projects/PROJECT_ID/members' \
  --header 'Authorization: Bearer YOUR_OIDC_TOKEN'
```

```json
{
  "members": [
    {
      "user_id": "USER_ID",
      "email": "jane@example.com",
      "name": "Jane Doe",
      "implicit": false,
      "added_at": "2026-09-10T12:00:00Z"
    }
  ]
}
```

</TabItem>
</Tabs>

The implicit flag is true for organization admins, who have access without a membership entry.

### Add project members [#add-project-members]

Add one or more members in one request.

<Tabs syncKey="language">
<TabItem label="CLI" icon="seti:shell">

```bash
daytona project member add USER_ID --project PROJECT_ID --org ORG_ID

daytona project member add --bulk USER_ID_1,USER_ID_2 --project PROJECT_ID --org ORG_ID
```

| **Flag** | **Type** | **Description**                                                                                      |
| -------- | -------- | ---------------------------------------------------------------------------------------------------- |
| `--bulk` | `string` | A comma-separated list of user IDs, emails, or names. Cannot be combined with a positional user.     |

</TabItem>
<TabItem label="API" icon="seti:json">

```bash
curl 'https://api.daytona.com/projects/PROJECT_ID/members' \
  --request POST \
  --header 'Authorization: Bearer YOUR_OIDC_TOKEN' \
  --header 'Content-Type: application/json' \
  --data '{ "user_ids": ["USER_ID_1", "USER_ID_2"] }'
```

```json
{ "added_user_ids": ["USER_ID_1", "USER_ID_2"] }
```

| **Field**  | **Type**   | **Description**                                                                         |
| ---------- | ---------- | --------------------------------------------------------------------------------------- |
| `user_ids` | `string[]` | Required. At most 200 user IDs per request; a larger list returns `400 Bad Request`.    |

</TabItem>
</Tabs>

### Remove project members [#remove-project-members]

Remove a member from a project. Unless they are an organization admin, removal also revokes the API keys they minted in the project; admins keep access through their role.

<Tabs syncKey="language">
<TabItem label="CLI" icon="seti:shell">

```bash
daytona project member remove USER_ID --project PROJECT_ID --org ORG_ID
```

</TabItem>
<TabItem label="API" icon="seti:json">

```bash
curl 'https://api.daytona.com/projects/PROJECT_ID/members/USER_ID' \
  --request DELETE \
  --header 'Authorization: Bearer YOUR_OIDC_TOKEN'
```

The response has the following fields:

| **Field**      | **Description**                      |
| -------------- | ------------------------------------ |
| `removed`      | The ID of the removed user.          |
| `revoked_keys` | The number of revoked keys.          |

</TabItem>
</Tabs>

## Project service accounts [#project-service-accounts]

### List project service accounts [#list-project-service-accounts]

List the service accounts that can hold API keys in a project. A service account mints keys only in projects it has been added to. The default project is open to every service account.

<Tabs syncKey="language">
<TabItem label="CLI" icon="seti:shell">

```bash
daytona project service-account list --project PROJECT_ID
```

</TabItem>
<TabItem label="API" icon="seti:json">

```bash
curl 'https://api.daytona.com/projects/PROJECT_ID/service-accounts' \
  --header 'Authorization: Bearer YOUR_OIDC_TOKEN'
```

</TabItem>
</Tabs>

### Add project service accounts [#add-project-service-accounts]

Add a service account to a project. Archived service accounts cannot be added, and you must hold every permission the account holds.

<Tabs syncKey="language">
<TabItem label="CLI" icon="seti:shell">

```bash
daytona project service-account add SERVICE_ACCOUNT_ID --project PROJECT_ID --org ORG_ID
```

</TabItem>
<TabItem label="API" icon="seti:json">

```bash
curl 'https://api.daytona.com/projects/PROJECT_ID/service-accounts' \
  --request POST \
  --header 'Authorization: Bearer YOUR_OIDC_TOKEN' \
  --header 'Content-Type: application/json' \
  --data '{ "service_account_id": "SERVICE_ACCOUNT_ID" }'
```

</TabItem>
</Tabs>

### Remove project service accounts [#remove-project-service-accounts]

Remove a service account from a project, revoking every API key it holds there.

<Tabs syncKey="language">
<TabItem label="CLI" icon="seti:shell">

```bash
daytona project service-account remove SERVICE_ACCOUNT_ID --project PROJECT_ID --org ORG_ID
```

</TabItem>
<TabItem label="API" icon="seti:json">

```bash
curl 'https://api.daytona.com/projects/PROJECT_ID/service-accounts/SERVICE_ACCOUNT_ID' \
  --request DELETE \
  --header 'Authorization: Bearer YOUR_OIDC_TOKEN'
```

The response has the following fields:

| **Field**      | **Description**                           |
| -------------- | ----------------------------------------- |
| `removed`      | The ID of the removed service account.    |
| `revoked_keys` | The number of revoked keys.               |

</TabItem>
</Tabs>

## Project secrets and registries [#project-secrets-and-registries]

[Secrets](/docs/secrets) and [registry credentials](/docs/registries) belong to a project. Manage them with a credential that acts in the project: a project API key, or the access token from [exchanging a token](/docs/api-keys#exchange-a-token) for that project. The CLI uses the selected project's credential after you sign in.
