# Cloud Connect

Reference for the Cloud Connect operations of the Spice Cloud Management API: issue a Cloud Connect enrollment token, enroll a Cloud Connect cluster or standalone runtime, attach an existing project to an enrolled instance, and list projects available for Cloud Connect attachment.

OpenAPI: [api.spice.ai/openapi.json](https://api.spice.ai/openapi.json)

- [`POST /v1/cloud-connect/token`: Issue a Cloud Connect enrollment token](https://spice.ai/docs/cloud/api/management/cloud-connect/post-v1-cloud-connect-token)
- [`POST /v1/cloud-connect/enroll`: Enroll a Cloud Connect cluster or standalone runtime](https://spice.ai/docs/cloud/api/management/cloud-connect/post-v1-cloud-connect-enroll)
- [`POST /v1/cloud-connect/attach`: Attach an existing project to an enrolled instance](https://spice.ai/docs/cloud/api/management/cloud-connect/post-v1-cloud-connect-attach)
- [`GET /v1/cloud-connect/attachable-projects`: List projects available for Cloud Connect attachment](https://spice.ai/docs/cloud/api/management/cloud-connect/get-v1-cloud-connect-attachable-projects)

## Issue a Cloud Connect enrollment token

`POST /v1/cloud-connect/token`

Issues a one-time standalone or cluster enrollment token. Requires a user-derived bearer, X-Org-Name, the kind-appropriate write scope, and org admin/owner role. Client-credential tokens are rejected because enrollment admission must retain user audit attribution.

### Header parameters

- `Authorization` (string, required): `Bearer <token>`. [Authentication](https://spice.ai/docs/cloud/api/management#authentication) lists the tokens it accepts.
- `X-Org-Name` (string, required)

### Request body

Required.

Content type: `application/json`

One of:

- **kind: standalone** (object)
  - `kind` (string, required): Value: `standalone`.
  - `ttl_seconds` (integer): Default: `3600`. Minimum: `1`. Maximum: `86400`.
- **kind: cluster, with name** (object)
  - `kind` (string, required): Value: `cluster`.
  - `name` (string, required)
  - `region` (string)
  - `cloud_account_id` (string)
  - `cloud_provider` (string): Value: `aws`.
  - `operations_mode` (string): Allowed values: `managed`, `approval_required`.
  - `ttl_seconds` (integer): Default: `3600`. Minimum: `1`. Maximum: `86400`.
- **kind: cluster, with connection\_id** (object)
  - `kind` (string, required): Value: `cluster`.
  - `connection_id` (string, required)
  - `ttl_seconds` (integer): Default: `3600`. Minimum: `1`. Maximum: `86400`.

### Example request

```bash
curl -X POST "https://api.spice.ai/v1/cloud-connect/token" \
  -H "Authorization: Bearer <token>" \
  -H "X-Org-Name: <X-Org-Name>" \
  -H "Content-Type: application/json" \
  -d '{
    "kind": "standalone"
  }'
```

```http
POST /v1/cloud-connect/token HTTP/1.1
Host: api.spice.ai
Authorization: Bearer <token>
X-Org-Name: <X-Org-Name>
Content-Type: application/json
Content-Length: 26

{
  "kind": "standalone"
}
```

### Responses

**201 Created**

Enrollment token issued

Content type: `application/json`

- `kind` (string, required): Allowed values: `standalone`, `cluster`.
- `token` (string, required): Pattern: `^spice-enroll-[A-Za-z0-9_-]{32}$`.
- `expires_at` (string (date-time), required)
- `organization` (object, required)
  - `id` (integer, required)
  - `name` (string, required)
- `target` (object | null, required): The created cluster or rotated connection identifier.

**400 Bad Request**

Invalid request or missing organization assertion

**401 Unauthorized**

Unauthorized

**403 Forbidden**

Insufficient scope, role, or plan

**409 Conflict**

Cluster is decommissioned

**413 Content Too Large**

Request body too large

**429 Too Many Requests**

Token issuance rate limit exceeded

**500 Internal Server Error**

Internal token issuance failure

## Enroll a Cloud Connect cluster or standalone runtime

`POST /v1/cloud-connect/enroll`

Token enrollment accepts a canonical kind-bound enrollment token. Standalone enrollment may instead use user Authorization plus X-Org-Name. Idempotency-Key is required for standalone enrollment.

### Header parameters

- `Authorization` (string, optional): `Bearer <token>`. [Authentication](https://spice.ai/docs/cloud/api/management#authentication) lists the tokens it accepts.
- `Idempotency-Key` (string (uuid), optional)
- `X-Org-Name` (string, optional): Required with Bearer authorization for authenticated standalone enrollment; omitted for token enrollment.

### Request body

Required.

Content type: `application/json`

One of:

- **kind: standalone, with token** (object)
  - `kind` (string, required): Value: `standalone`.
  - `token` (string, required): Pattern: `^spice-enroll-[A-Za-z0-9_-]{32}$`.
  - `expected_org` (string)
  - `csr_pem` (string, required)
  - `enc_pubkey_pem` (string, required)
  - `instance` (object, required)
  - `region` (string)
- **kind: cluster** (object)
  - `kind` (string, required): Value: `cluster`.
  - `token` (string, required): Pattern: `^spice-enroll-[A-Za-z0-9_-]{32}$`.
  - `expected_org` (string)
  - `csr_pem` (string, required)
  - `enc_pubkey_pem` (string, required)
- **kind: standalone, without token** (object)
  - `kind` (string, required): Value: `standalone`.
  - `csr_pem` (string, required)
  - `enc_pubkey_pem` (string, required)
  - `instance` (object, required)
  - `region` (string)

### Example request

```bash
curl -X POST "https://api.spice.ai/v1/cloud-connect/enroll" \
  -H "Content-Type: application/json" \
  -d '{
    "kind": "standalone",
    "token": "string",
    "csr_pem": "string",
    "enc_pubkey_pem": "string",
    "instance": {}
  }'
```

```http
POST /v1/cloud-connect/enroll HTTP/1.1
Host: api.spice.ai
Content-Type: application/json
Content-Length: 118

{
  "kind": "standalone",
  "token": "string",
  "csr_pem": "string",
  "enc_pubkey_pem": "string",
  "instance": {}
}
```

### Responses

**200 OK**

Enrollment completed or exactly replayed

Content type: `application/json`

One of:

- **kind: standalone** (object)
  - `kind` (string, required): Value: `standalone`.
  - `instance_id` (string, required)
  - `identity_cert_pem` (string, required)
  - `ca_bundle_pem` (string, required)
  - `gateway_addr` (string, required)
  - `not_after` (string (date-time), required)
  - `organization` (object, required)
    - `id` (integer, required)
    - `name` (string, required)
  - `region` (string, required)
  - `portal` (object, required)
    - `new_project_url` (string (uri), required)
  - `attachment` (null, required)
  - `recovered` (boolean, required)
- **kind: cluster** (object)
  - `kind` (string, required): Value: `cluster`.
  - `connection_id` (string, required)
  - `identity_cert_pem` (string, required)
  - `ca_bundle_pem` (string, required)
  - `gateway_addr` (string, required)
  - `not_after` (string (date-time), required)
  - `organization` (object, required)
    - `id` (integer, required)
    - `name` (string, required)
  - `region` (string, required)
  - `attachment` (null, required)

**400 Bad Request**

Invalid request, kind mismatch, or unsupported project field

**401 Unauthorized**

Invalid credential or authorization

**403 Forbidden**

Authenticated caller lacks the required scope or organization role

**409 Conflict**

Consumed token, connection already enrolled, unavailable connection, organization mismatch, or idempotency mismatch

**410 Gone**

Enrollment token or authenticated recovery window expired

**413 Content Too Large**

Request body too large

**500 Internal Server Error**

Internal or identity-provider failure; use the retryable response field

## Attach an existing project to an enrolled instance

`POST /v1/cloud-connect/attach`

Attaches an existing standalone project to an enrolled standalone instance and assigns the project's region and cname from the stamp whose gateway terminates that instance's control stream. The optional location field is reserved for multi-region use and is ignored today.
This operation is dual-authorized. A user-derived bearer plus the X-Org-Name assertion authorizes the project mutation; OAuth client credentials are refused. The caller must have the instances:write and apps:write scopes and the organization admin or owner role. The cert\_pem and pop\_sig body fields prove only that the request concerns the named instance. Instance proof by itself never authorizes project attachment.
pop\_sig is a base64 DER ECDSA (SHA-256) signature over the UTF-8 bytes `spice-cloud-connect/attach/v1\n{X-Org-Name}\n{instance_id}\n{project_id}`. The project must already have kind standalone and no cluster target. The database is the authority for attachment conflicts; clients must not pre-check or infer an attachment limit from this contract.
Errors use `{code, error, retryable}`. A 409 always has `retryable: false`; a 5xx has `retryable: true` so the client exact replays any request whose attachment may already have committed.

### Header parameters

- `Authorization` (string, required): `Bearer <token>`. [Authentication](https://spice.ai/docs/cloud/api/management#authentication) lists the tokens it accepts.
- `X-Org-Name` (string, required): Organization that owns both the project and instance; also bound into pop\_sig. Pattern: `^[A-Za-z0-9._-]{1,255}$`.

### Request body

Required.

Content type: `application/json`

- `instance_id` (string, required): Enrolled instance caller-facing id (`inst_…`). Minimum length: `1`. Maximum length: `200`.
- `project_id` (integer, required): Existing standalone project id. Minimum: `1`. Maximum: `9007199254740991`.
- `location` (string): Reserved for multi-region use; accepted and ignored today. Minimum length: `1`. Maximum length: `200`.
- `cert_pem` (string, required): Cloud Connect identity certificate for instance\_id. Minimum length: `1`. Maximum length: `8192`.
- `pop_sig` (string, required): Base64 DER ECDSA signature over the documented attach challenge. Minimum length: `1`. Maximum length: `512`.

### Example request

```bash
curl -X POST "https://api.spice.ai/v1/cloud-connect/attach" \
  -H "Authorization: Bearer <token>" \
  -H "X-Org-Name: <X-Org-Name>" \
  -H "Content-Type: application/json" \
  -d '{
    "instance_id": "string",
    "project_id": 1,
    "cert_pem": "string",
    "pop_sig": "string"
  }'
```

```http
POST /v1/cloud-connect/attach HTTP/1.1
Host: api.spice.ai
Authorization: Bearer <token>
X-Org-Name: <X-Org-Name>
Content-Type: application/json
Content-Length: 95

{
  "instance_id": "string",
  "project_id": 1,
  "cert_pem": "string",
  "pop_sig": "string"
}
```

### Responses

**200 OK**

Project attached, or exact same instance/project replay repaired

Content type: `application/json`

- `instance_id` (string, required)
- `organization` (object, required)
  - `id` (integer, required)
  - `name` (string, required): Canonical organization name
- `project` (object, required)
  - `id` (integer, required)
  - `name` (string, required): Canonical project name
- `monitor_url` (string (uri), required): Credential-free portal monitoring link for the project. Pattern: `^https://`.

**400 Bad Request**

Invalid body or X-Org-Name (`invalid_request` or `invalid_org_assertion`)

Content type: `application/json`

- `code` (string, required): Allowed values: `invalid_request`, `invalid_org_assertion`, `unauthenticated`, `forbidden`, `org_forbidden`, `org_assertion_mismatch`, `org_deleted`, `invalid_instance_proof`, `instance_not_found`, `instance_not_enrolled`, `instance_already_attached`, `project_not_found`, `project_not_standalone`, `project_already_attached`, `internal`.
- `error` (string, required)
- `retryable` (boolean, required)

**401 Unauthorized**

Missing/invalid user bearer or invalid instance proof (`unauthenticated` or `invalid_instance_proof`)

Content type: `application/json`

- `code` (string, required): Allowed values: `invalid_request`, `invalid_org_assertion`, `unauthenticated`, `forbidden`, `org_forbidden`, `org_assertion_mismatch`, `org_deleted`, `invalid_instance_proof`, `instance_not_found`, `instance_not_enrolled`, `instance_already_attached`, `project_not_found`, `project_not_standalone`, `project_already_attached`, `internal`.
- `error` (string, required)
- `retryable` (boolean, required)

**403 Forbidden**

Non-user bearer, insufficient scope, org access, or role refusal (`forbidden`, `org_forbidden`, `org_assertion_mismatch`, or `org_deleted`)

Content type: `application/json`

- `code` (string, required): Allowed values: `invalid_request`, `invalid_org_assertion`, `unauthenticated`, `forbidden`, `org_forbidden`, `org_assertion_mismatch`, `org_deleted`, `invalid_instance_proof`, `instance_not_found`, `instance_not_enrolled`, `instance_already_attached`, `project_not_found`, `project_not_standalone`, `project_already_attached`, `internal`.
- `error` (string, required)
- `retryable` (boolean, required)

**404 Not Found**

Instance or project not found in the authorized organization (`instance_not_found` or `project_not_found`)

Content type: `application/json`

- `code` (string, required): Allowed values: `invalid_request`, `invalid_org_assertion`, `unauthenticated`, `forbidden`, `org_forbidden`, `org_assertion_mismatch`, `org_deleted`, `invalid_instance_proof`, `instance_not_found`, `instance_not_enrolled`, `instance_already_attached`, `project_not_found`, `project_not_standalone`, `project_already_attached`, `internal`.
- `error` (string, required)
- `retryable` (boolean, required)

**409 Conflict**

Instance not enrolled or already attached, project not standalone, or the database reports the project is already attached (`instance_not_enrolled`, `instance_already_attached`, `project_not_standalone`, or `project_already_attached`). These refusals carry `retryable: false` and prove this request did not commit an attachment.

Content type: `application/json`

- `code` (string, required): Allowed values: `invalid_request`, `invalid_org_assertion`, `unauthenticated`, `forbidden`, `org_forbidden`, `org_assertion_mismatch`, `org_deleted`, `invalid_instance_proof`, `instance_not_found`, `instance_not_enrolled`, `instance_already_attached`, `project_not_found`, `project_not_standalone`, `project_already_attached`, `internal`.
- `error` (string, required)
- `retryable` (boolean, required)

**413 Content Too Large**

Request body exceeds 1 MiB (`invalid_request`)

Content type: `application/json`

- `code` (string, required): Allowed values: `invalid_request`, `invalid_org_assertion`, `unauthenticated`, `forbidden`, `org_forbidden`, `org_assertion_mismatch`, `org_deleted`, `invalid_instance_proof`, `instance_not_found`, `instance_not_enrolled`, `instance_already_attached`, `project_not_found`, `project_not_standalone`, `project_already_attached`, `internal`.
- `error` (string, required)
- `retryable` (boolean, required)

**500 Internal Server Error**

Internal failure, including post-commit routing repair (`internal`, retryable)

Content type: `application/json`

- `code` (string, required): Allowed values: `invalid_request`, `invalid_org_assertion`, `unauthenticated`, `forbidden`, `org_forbidden`, `org_assertion_mismatch`, `org_deleted`, `invalid_instance_proof`, `instance_not_found`, `instance_not_enrolled`, `instance_already_attached`, `project_not_found`, `project_not_standalone`, `project_already_attached`, `internal`.
- `error` (string, required)
- `retryable` (boolean, required)

## List projects available for Cloud Connect attachment

`GET /v1/cloud-connect/attachable-projects`

Returns every project the authenticated caller may attach an enrolled Cloud Connect instance to. Policy is evaluated entirely by the server: today, results include only live, unattached standalone projects in the resolved organization, and user credentials require that organization's owner or admin role. Exactly one organization is searched per request and the response names it in `org`, so an empty `projects` states which organization has no eligible project rather than leaving a client to wonder whether the one it meant was searched at all; name another with `X-Org-Name`. Clients must render every returned project and instance without applying their own eligibility filters. The `instances` array is part of the stable contract and may be populated when server policy permits projects with existing attachments.

### Query parameters

- `limit` (integer, optional): Maximum number of projects to return. Default: `50`. Minimum: `1`. Maximum: `100`.
- `cursor` (string, optional): Opaque cursor returned as `next_cursor` by the previous page.

### Header parameters

- `Authorization` (string, required): `Bearer <token>`. [Authentication](https://spice.ai/docs/cloud/api/management#authentication) lists the tokens it accepts.
- `X-Org-Name` (string, optional): Organization to list. A user token may name any organization its owner belongs to; naming one they do not is `org_forbidden`. Never ignored: a token bound to one organization that names another is `org_assertion_mismatch`. A portal session token is bound to none and must send it (`org_assertion_required`). Pattern: `^[A-Za-z0-9._-]{1,255}$`.

### Example request

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

```http
GET /v1/cloud-connect/attachable-projects HTTP/1.1
Host: api.spice.ai
Authorization: Bearer <token>
```

### Responses

**200 OK**

A page of projects currently eligible for attachment.

Content type: `application/json`

- `org` (string, required): Handle of the organization the page was scoped to, being the one `X-Org-Name` named or the credential's own when it named none. Always present, so an empty `projects` says which organization has no eligible project rather than leaving a client to guess whether its active organization was the one searched.
- `projects` (array of object, required)
  - `id` (integer (int64), required)
  - `name` (string, required)
  - `org` (string, required): Organization handle.
  - `region` (string | null, required): Project control-plane region, if assigned.
  - `instances` (array of object, required)
    - `id` (string, required): Instance external identifier.
    - `location` (string | null, required): Customer-declared instance location.
    - `enrolled_at` (string (date-time) | null, required)
- `next_cursor` (string | null, required): Opaque cursor for the next page, or null at the end.

**400 Bad Request**

Invalid organization assertion (`invalid_org_assertion`), none from a portal session (`org_assertion_required`), or invalid limit/cursor (`invalid_pagination`).

**401 Unauthorized**

Missing or invalid bearer token.

**403 Forbidden**

Organization access (`org_forbidden`, `org_assertion_mismatch`, or `org_deleted`), apps:read scope (`insufficient_scope`), or owner/admin role (`forbidden`) was refused.

**500 Internal Server Error**

Organization role, project, or organization-handle lookup failed.
