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

# Storage connections

> Authorize customer-owned S3 or Azure Blob storage for new workspaces.

Customer storage is not generally available. Deployments with the feature disabled and no storage configuration return `501 not_implemented`. An enabled but incomplete deployment returns `503 storage_unavailable`. Until the feature is enabled, creating a connection, or a workspace on customer storage, returns `501 not_implemented`. Existing managed workspaces keep their current storage.

Prepare cloud trust and permissions with the [customer storage setup guide](https://docs.ndi.nace.ai/guides/customer-storage.md).

Use `client.storage_connections` with either `NdiClient` or `AsyncNdiClient`. Create a connection to an existing bucket or container, stage credentials, then wait for validation before selecting it at workspace creation. Creating a connection does not create cloud resources.

```text theme={"dark"}
def create(*, name: str, destination: StorageDestination, idempotency_key: str | None = None) -> StorageConnection
```

```python theme={"dark"}
from ndi_sdk import NdiClient
from ndi_sdk.models import S3StorageDestination, AwsRoleStorageCredential, ExternalWorkspaceStorageInput

client = NdiClient()
connection = client.storage_connections.create(
    name="Customer archive",
    destination=S3StorageDestination(bucket="customer-data", region="us-west-2", prefix="ndi"),
    idempotency_key="customer-archive-setup",
)
# Configure your role to trust connection.aws_trusted_principal_arn and require
# connection.external_id. Grant access only to the selected bucket/prefix.
client.storage_connections.set_credentials(
    connection.id,
    AwsRoleStorageCredential(role_arn="arn:aws:iam::123456789012:role/ndi-workspaces"),
)
validation = client.storage_connections.validate(connection.id)
client.jobs.wait(validation.job_id)
workspace = client.workspaces.create(
    name="Customer workspace",
    storage=ExternalWorkspaceStorageInput(connection_id=connection.id),
)
```

For Azure, use `AzureBlobStorageDestination(account=..., container=..., prefix=...)` and `AzureStorageCredential(tenant_id=..., client_id=..., client_secret=...)`. Supply an Entra application with access to the container. Read its secret from your secret manager or an interactive hidden prompt; never put it in an MCP prompt or a command-line argument. Python masks the credential model's secret in representations and JSON dumps; `set_credentials` sends it only in its authenticated credential request.

| Operation | Result |
| - | - |
| `create(name=..., destination=...)` | Draft connection and AWS trust details, when applicable. |
| `get(connection_id)` | Sanitized metadata and `status` (`draft`, `ready`, `degraded`, `revoked`). |
| `list(cursor=..., limit=...)` / `iter_all(limit=...)` | Connections visible to the caller. |
| `set_credentials(connection_id, credentials)` | Staged credentials; the active identity changes after validation. |
| `validate(connection_id, reactivate=False)` | A job that checks API and worker access; `reactivate=True` explicitly restores revoked access once any required approval is given. |
| `revoke(connection_id)` | Stops access without deleting customer objects. |
| `allow_reactivation(connection_id)` | Console owner or admin approval for revalidation; access remains revoked. |
| `delete(connection_id)` | Removes an unused connection; live bindings prevent deletion. |

To rotate, stage new credentials and validate again. Restoring a revoked connection requires `reactivate=True`. Cancelling validation releases its reservation. A crashed worker can hold it until the bounded lease expires; retry later, or explicitly revoke and reactivate; after a console owner or admin revokes, call `allow_reactivation` first. A failed rotation preserves the prior active identity.

The SDK generates one idempotency key per create call and reuses it across retries. Supply a stable key to recover across process restarts. Reusing a deleted connection's key returns a conflict.

Selecting customer storage changes durable workspace storage. Temporary processing and platform metadata retain their existing placement; this is not a service-wide zero-retention guarantee. External workspaces reject `source_ttl_days`. Workspace deletion removes the NDI-owned namespace; provider versions and soft-delete policies can retain historical bytes.

A connection with `reactivation_requires_approval=true` cannot be revalidated by any key, console owners and admins included (`403 access_denied`), until a console owner or admin approves it; approval from any other key, including integration, service and console member keys, receives `403 access_denied`. A caller with `can_manage` then validates with explicit reactivation; after member removal or user disablement that is a console owner or admin. Revocation by a console owner or admin key, including of a connection that key created, member removal and user disablement place this hold. Repeating revocation as the owner does not remove it. Tenant administrators may inspect and revoke external-account connections and approve reactivation; only the owning external account may set credentials, validate or delete them.

`revoked_reason` (`owner`, `administrator`, `member_removed`, `user_disabled`) and `revoked_at` record the revocation that removed access or placed the approval hold; later revocations of an already-held connection keep that record. Both remain after reactivation.

Use `can_manage` to decide whether to offer credential updates, validation and unused-connection deletion to the current caller. Read and administrative revocation access can remain available when `can_manage` is false.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.