# 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)

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