# Management APIs

Management and Deployment API documentation for api.spice.ai

The Spice.ai Management API (also known as the control-plane API) provides programmatic access to manage Spice.ai Cloud resources: projects, deployments, secrets, API keys, and org members.

> **Note**
>
> **Projects were previously called apps.** Every `/v1/projects` endpoint is also served at the legacy `/v1/apps` path, which remains supported. Existing integrations continue to work without changes.
>
> The legacy paths are marked deprecated in the OpenAPI specification, and new integrations should use `/v1/projects`. See [Projects and apps](https://spice.ai/docs/cloud/api/management#projects-and-apps).

## Base URL

```
https://api.spice.ai
```

## API Version

All API endpoints are versioned under `/v1`:

```
https://api.spice.ai/v1
```

## Projects and apps

What the API and portal now call a **project** was previously called an **app**. The resource is unchanged. Only the name is different.

Both path prefixes reach the same handlers:

| Path | Status | List response envelope |
| - | - | - |
| `/v1/projects` | Canonical | `{ "projects": [...] }` |
| `/v1/apps` | Legacy, still served | `{ "apps": [...] }` |

Two details matter when migrating:

- **The list envelope differs.** `GET /v1/projects` returns results under a `projects` key, while `GET /v1/apps` keeps its original `apps` key. A client switching to the canonical path must read the new key. All other response shapes and field names are identical, including the `id` and `name` fields on each resource.
- **OAuth scope names are unchanged.** The scopes are still `apps:read`, `apps:write`, and `apps:delete`, because they are embedded in already-issued tokens. They grant access to projects under either path.

The Spice CLI, Terraform provider, and SDKs continue to call the legacy paths and are unaffected.

## Authentication

The Management API supports three authentication methods:

### 1. Personal Access Tokens (PATs)

PATs are user-scoped tokens with an expiration that you select, from 24 hours to 180 days. Recommended for:

- CLI tools and automation scripts
- Personal integrations

**Creating a PAT:**

1. Sign in to [Spice.ai Cloud Portal](https://spice.ai/)
2. Click the profile picture, select **Settings**, then select **Personal Access Tokens**
3. Enter a **Token Name** and select an **Expiration**
4. Click **Create**
5. Copy the token (it won't be shown again)

**Using a PAT:**

```bash
curl -H "Authorization: Bearer <your-pat-token>" \
  https://api.spice.ai/v1/projects
```

Learn more: [Personal Access Tokens](https://spice.ai/docs/cloud/portal/profile/personal-access-tokens)

### 2. OAuth 2.0 Client Credentials

OAuth client credentials are org-scoped tokens. Ideal for:

- CI/CD pipelines
- Service-to-service authentication
- Multi-tenant applications
- Third-party integrations

**Step 1: Exchange client credentials for an access token**

```bash
curl -X POST https://spice.ai/api/oauth/token \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "your-client-id",
    "client_secret": "your-client-secret",
    "grant_type": "client_credentials"
  }'
```

The response contains an `access_token`:

```json
{
  "access_token": "eyJhbGciOiJSUzI...",
  "token_type": "Bearer",
  "expires_in": 3600
}
```

**Step 2: Use the access token in subsequent requests**

```bash
curl -H "Authorization: Bearer <access-token>" \
  https://api.spice.ai/v1/projects
```

### 3. User Session Tokens (CLI)

The Spice CLI stores a credential for the Management API with `spice login`. On a terminal it asks which one to use:

```bash
spice login
? How would you like to authenticate to Spice Cloud? ›
❯ Login with a web browser
  Paste an access token
```

**Login with a web browser** signs in through the browser and stores the resulting user session token, which carries the user's identity. **Paste an access token** stores a personal access token instead.

The prompt appears only when the CLI is attached to a terminal. Scripts and CI pipelines name the method as a subcommand, which also lets them supply the credential without being prompted for it:

| Command | Credential stored | Supplying it without a prompt |
| - | - | - |
| `spice login subscription` | User session token | `--device` prints the URL and a one-time code to enter on another device instead of opening a browser, for SSH and headless shells |
| `spice login token` | Personal access token | `--token`, or the `SPICE_CLOUD_PAT` environment variable |
| `spice login api` | OAuth client credentials | `--client-id` and `--client-secret`, or `SPICE_CLOUD_CLIENT_ID` and `SPICE_CLOUD_CLIENT_SECRET` |

Run with no terminal and no subcommand, `spice login` reports that the login type must be chosen explicitly rather than waiting on a prompt that cannot be answered.

## Org context

Every Management API request acts on exactly one org. By default that is the org the credential was minted against. Send the `X-Org-Name` header with an org handle to act on a different one.

A portal session token is bound to no org. A request with one must send `X-Org-Name`, except on `GET /v1/orgs` and on `/v1/orgs/{orgId}/...` routes. Without the header, the request is refused with `org_assertion_required`.

A `/v1/orgs/{orgId}/...` route takes the org from the `orgId` in its path. Use an `id` from `GET /v1/orgs`. On these routes, `X-Org-Name` is optional and may only name the same org.

| Credential | Orgs it can act on |
| - | - |
| Personal access token or CLI session | Any org the token owner belongs to |
| OAuth client credentials | Only the org the client was issued to |

A user credential carries a user identity, so the API authorizes the request against that user's membership. A machine credential carries no user identity and stays pinned to one org: a header naming a different org is refused rather than ignored.

> **Note**
>
> **Reach never widens on its own.** Without `X-Org-Name`, a credential acts on the org it was minted against. Reaching another org requires the client to send the header, and the caller to hold the membership, role, and scope the action needs.

Handles are matched case-insensitively and may contain letters, numbers, dots, hyphens, and underscores.

```bash
curl -H "Authorization: Bearer <token>" \
  -H "X-Org-Name: acme-corp" \
  https://api.spice.ai/v1/projects
```

### Discovering orgs

`GET /v1/orgs` lists the orgs the caller belongs to, with the caller's role in each. Use it to choose a value for `X-Org-Name`.

```bash
curl -H "Authorization: Bearer <token>" \
  https://api.spice.ai/v1/orgs
```

```json
{
  "orgs": [
    { "id": 42, "name": "acme-corp", "role": "owner" },
    { "id": 57, "name": "example-org", "role": "member" }
  ]
}
```

- `name` is the org handle: the value `X-Org-Name` accepts, and the one used in `<org>/<project>` addressing.
- `role` is the caller's highest-privilege role in that org: `owner`, `admin`, `member`, or `viewer`.
- A caller who belongs to no org receives `200` with an empty `orgs` array, never a `404`.
- An OAuth client credential lists only the org it was issued to, with the role `owner`.
- Requires the `apps:read` scope.

### Refused requests

A refusal returns a machine-readable `code` alongside `error`, because each one has a different remedy:

| Code | Status | Meaning | Remedy |
| - | - | - | - |
| `org_forbidden` | `403` | The named org does not exist or is not visible to the caller | Verify the handle; if it is correct, ask an org owner for an invitation |
| `forbidden` | `403` | The caller is a member but lacks the role the action needs | Ask an owner or admin for a higher role |
| `insufficient_scope` | `403` | The credential was not granted the required scope | Reissue the credential with the scope |
| `org_assertion_mismatch` | `403` | A machine credential named an org other than its own | Use a credential issued for that org |
| `invalid_org_assertion` | `400` | The header value is not a valid org handle | Correct the handle |
| `org_assertion_required` | `400` | A credential bound to no org, such as a portal session token, sent no `X-Org-Name` | Send `X-Org-Name` with the org handle |
| `org_deleted` | `403` | The org the credential is bound to was deleted | Use a credential for another org |
| `invalid_org_id` | `400` | The `orgId` in the path is not a positive integer | Use an `id` from `GET /v1/orgs` |
| `org_path_mismatch` | `400` | `X-Org-Name` names a different org than the `orgId` in the path | Send the handle for that org, or omit the header |

Two details are easy to miss:

- **A blank `X-Org-Name` is an error, not an omission.** It returns `invalid_org_assertion`, because a client that sent the header believes it named an org.
- **An org that does not exist and one the caller cannot see both return `org_forbidden`.** The header cannot be used to test whether an org exists.

## OAuth Scopes

Access to API resources is controlled through scopes. PATs and OAuth clients must be granted appropriate scopes:

| Scope | Description |
| - | - |
| `*` | Full access to all resources (not recommended for production) |
| `apps:read` | Read project information |
| `apps:write` | Create and update projects |
| `apps:delete` | Delete projects |
| `deployments:read` | View deployment status and history |
| `deployments:write` | Create new deployments |
| `secrets:read` | List and view secrets (values are masked) |
| `secrets:write` | Create, update, and delete secrets |
| `config:read` | Read project configuration |
| `config:write` | Update project configuration |
| `members:read` | View org members |
| `members:write` | Add, update, and remove org members |
| `monitors:read` | Read project and cluster monitors |
| `monitors:write` | Create, update, and delete project and cluster monitors |
| `reactions:read` | Read data reactions |
| `reactions:write` | Create, update, and delete data reactions |

**Scope hierarchy:**

- A write scope automatically includes its corresponding read scope (e.g. `apps:write` implies `apps:read`).
- `apps:delete` includes `apps:write` and `apps:read`.
- The wildcard scope (`*`) grants all permissions.
- The `apps:*` scope names are unchanged by the projects rename, and apply to projects.

## Rate Limiting

Requests are rate-limited per project. These limits are a high-level failsafe; actual throughput depends on the size of your deployed Spice instance or cluster.

### Per-Project Request Rate Limits

| Plan | Requests / second |
| - | - |
| Community | 100 |
| Developer | 1,000 |
| Pro Teams | 10,000 |
| Enterprise | 100,000 |

### Concurrent Query Limits

These limits apply to SQL queries executed against your Spice runtime, not to management API calls.

| Plan | Concurrent Queries | Query Timeout |
| - | - | - |
| Developer | 16 | 90 seconds |
| Pro Teams | 64 | 5 minutes |
| Enterprise | 1,024 | 30 minutes |

## Error Responses

The API uses standard HTTP status codes:

| Status Code | Description |
| - | - |
| `200 OK` | Request succeeded |
| `201 Created` | Resource created successfully |
| `202 Accepted` | Request accepted (async operation) |
| `204 No Content` | Request succeeded with no response body |
| `400 Bad Request` | Invalid request body or parameters |
| `401 Unauthorized` | Missing or invalid authentication |
| `403 Forbidden` | Insufficient scope or permissions |
| `404 Not Found` | Resource not found |
| `409 Conflict` | Resource already exists or conflict |
| `410 Gone` | The token or resource has expired |
| `413 Payload Too Large` | Request body exceeds the 1 MiB limit |
| `422 Unprocessable Entity` | Valid request that cannot be completed as sent |
| `429 Too Many Requests` | Rate limit exceeded |
| `500 Internal Server Error` | Server error |
| `502 Bad Gateway` | An upstream service failed |
| `503 Service Unavailable` | A required service is temporarily unavailable |

**Error Response Format:**

```json
{
  "error": "Human-readable error message",
  "details": {
    "fieldErrors": {
      "field_name": ["Validation error message"]
    }
  }
}
```

Authorization failures also carry a machine-readable `code`, so a client can tell the refusals apart instead of guessing:

```json
{
  "error": "You are not a member of the requested organization",
  "code": "org_forbidden"
}
```

See [Refused requests](https://spice.ai/docs/cloud/api/management#refused-requests) for the codes and their remedies.

## Pagination

The project and deployment list endpoints take a `limit` query parameter. Its default and maximum depend on the endpoint:

| Endpoint | Parameters | `limit` default | `limit` maximum |
| - | - | - | - |
| `GET /v1/projects` | `limit`, `offset`, `after_id` | 1000 | 1000 |
| `GET /v1/projects/{projectId}/deployments` | `limit` | 20 | 100 |

`GET /v1/projects` returns projects in descending `id` order. To read the next page, pass the smallest `id` from the current page as `after_id`. `offset` skips a number of projects and applies only when `after_id` is not set. Other list endpoints use their own pagination parameters and limits. See each endpoint's reference page for its parameters.

## OpenAPI Specification

The API publishes its own OpenAPI specification, which describes every endpoint and the headers it accepts:

```
https://api.spice.ai/openapi.json
```

The same document is also served at `https://api.spice.ai/v1/docs` and `https://api.spice.ai/v1/docs/openapi.json`.

## SDK Support

Official SDKs are available for popular languages:

- [Python SDK](https://spice.ai/docs/sdks/python-sdk)
- [Node.js SDK](https://spice.ai/docs/sdks/node.js-sdk)
- [Go SDK](https://spice.ai/docs/sdks/go)
- [Rust SDK](https://spice.ai/docs/sdks/rust-sdk)

## Endpoints

- [Health](https://spice.ai/docs/cloud/api/management/health) - API health check
- [Regions](https://spice.ai/docs/cloud/api/management/regions) - List available deployment regions
- [Projects](https://spice.ai/docs/cloud/api/management/projects) - Manage Spice projects
- [Deployments](https://spice.ai/docs/cloud/api/management/deployments) - Deploy and manage project deployments
- [Secrets](https://spice.ai/docs/cloud/api/management/secrets) - Manage project secrets
- [API Keys](https://spice.ai/docs/cloud/api/management/api-keys) - Manage project API keys
- [Members](https://spice.ai/docs/cloud/api/management/members) - Manage org members
- [Orgs](https://spice.ai/docs/cloud/api/management#discovering-orgs) - List the orgs the caller belongs to
- [Metrics](https://spice.ai/docs/cloud/api/metrics) - Scrape per-project runtime metrics
- [Monitors](https://spice.ai/docs/cloud/api/management/monitors) - Manage project monitors
- [Reactions](https://spice.ai/docs/cloud/api/management/reactions) - Manage data reactions (private preview)
- [Limits](https://spice.ai/docs/cloud/api/management/limits) - Read org limits

## Terraform Provider

Manage Spice.ai resources as infrastructure-as-code with the [Spice.ai Terraform Provider](https://spice.ai/docs/cloud/api/management-api/terraform). See the [Terraform Provider](https://spice.ai/docs/cloud/api/management-api/terraform) page for resources, data sources, import instructions, and complete examples.

## Examples

### List all projects

```bash
curl -H "Authorization: Bearer <token>" \
  https://api.spice.ai/v1/projects
```

Results are returned under a `projects` key. The legacy `GET /v1/apps` path returns the same records under an `apps` key.

### Create a new project

```bash
curl -X POST https://api.spice.ai/v1/projects \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "my-project",
    "region": "us-west-2",
    "description": "My Spice project",
    "visibility": "private"
  }'
```

Orgs with a [dedicated cluster](https://spice.ai/docs/cloud/api/management/dedicated-clusters) can pass `cluster_name` in place of `region` to create the project on their dedicated infrastructure.

> **Note**
>
> **Project name requirements.** `name` must be 4 to 38 characters and contain only letters, numbers, and hyphens. A name outside those bounds is rejected with `400`.
>
> Names are unique within an org and are compared case-insensitively, so a name that differs from an existing project only by case is rejected with `409`.

### Update a project

```bash
curl -X PUT https://api.spice.ai/v1/projects/123 \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "replicas": 2
  }'
```

> **Warning**
>
> **An update that races a pause, resume, or restore is not saved.** If the project's lifecycle state changes while the update is in flight, the request returns `409` with `code: project_state_changed`:
>
> ```json
> {
>   "error": "The project changed state while this update was in flight, so the configuration was not saved. Re-read the project before retrying.",
>   "code": "project_state_changed"
> }
> ```
>
> Re-read the project with `GET /v1/projects/{projectId}` before retrying. A configuration update clears `paused_at`, so repeating the request against a project that has since been paused also resumes it.

### Create a deployment

```bash
curl -X POST https://api.spice.ai/v1/projects/123/deployments \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{}'
```

An empty body deploys the project's current configuration. For a project connected to a GitHub repository, it deploys the head of the production branch.

To deploy a specific revision of a connected repository, set `branch`, `commit_sha`, or both. `branch` names a branch or a tag, and it must exist. `commit_sha` takes 7 to 40 hexadecimal characters. The ref in `branch` must contain the commit. When `branch` is omitted, the production branch must contain it. A project with no connected repository refuses both fields with `400` `repository_not_connected`.

```bash
curl -X POST https://api.spice.ai/v1/projects/123/deployments \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "branch": "main",
    "commit_sha": "9fceb02d0ae598e95dc970b74767f19372d61af8"
  }'
```

### Deployment status and errors

#### Deployment statuses

Deployments use these statuses:

| Status | Meaning |
| - | - |
| `queued` | The deployment is awaiting platform processing. |
| `in_progress` | The deployment is active. When Cloud is retrying a scheduling block, `error_code` and `error_message` are set together. Ordinary waiting has both fields `null`. |
| `succeeded` | The deployment is terminal. For Spice-managed compute, the runtime is ready. For a standalone Cloud Connect target, Cloud accepted the deployment for dispatch. This status does not confirm that the remote runtime is ready. |
| `failed` | The deployment is terminal. Current writes include the catalog `error_code` and `error_message`. |
| `created` | The deployment is a superseded historical record. |

#### Deployment errors

The error catalog classifies codes as retriable or terminal:

| `error_code` | Classification |
| - | - |
| `insufficient_cpu` | Retriable |
| `insufficient_memory` | Retriable |
| `insufficient_storage` | Retriable |
| `insufficient_instances` | Retriable |
| `pod_exceeds_node_capacity` | Terminal |
| `unable_to_start` | Terminal |
| `internal_error` | Terminal |

`error_code` is optional. When it is present, it is a catalog code. `error_message` contains human-readable catalog text. On current writes, the two fields are set together and cleared together. Older failed deployments can carry an `error_message` without an `error_code`. Branch on `error_code` when it is present, not on the message text.

A project instance that cannot start because the cluster lacks CPU, memory, storage, or instance capacity is a retriable scheduling block. `GET /v1/projects/{projectId}/deployments` and `GET /v1/projects/{projectId}/deployments/{deploymentId}` keep `status` as `in_progress` and return both `error_code` (`insufficient_cpu`, `insufficient_memory`, `insufficient_storage`, or `insufficient_instances`) and `error_message` while Cloud retries placement. That is not a terminal `failed` deployment. Terminal codes (`pod_exceeds_node_capacity`, `unable_to_start`, `internal_error`) record `failed`.

The [Deployments](https://spice.ai/docs/cloud/portal/app-spicepod/deployments) page in the portal reports the same codes.

### Add a secret

```bash
curl -X POST https://api.spice.ai/v1/projects/123/secrets \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "DATABASE_URL",
    "value": "postgresql://user:pass@host:5432/db"
  }'
```

## Support

Have questions or running into issues?

- [Community Slack](https://spice.ai/slack)
- [Help Center](https://spice.ai/docs/help)
