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

# Vaults

> Write-only credential storage, injected at the network boundary — values are never returned.

<Info>See the [Credential vault](/docs/identity/vault) guide for the injection model.</Info>

A vault holds credentials an agent needs but must never see. Secret values are **write-only**: they are accepted on create, injected at runtime, and **never returned** by any read.

## The vault object

<ResponseField name="id" type="string">Unique id (e.g. `vlt_01H...`).</ResponseField>
<ResponseField name="object" type="string">Always `vault`.</ResponseField>
<ResponseField name="identity_id" type="string | null">The [identity](/docs/api/identities) the vault belongs to, or `null` when it was created without one.</ResponseField>
<ResponseField name="display_name" type="string">Human-readable name.</ResponseField>
<ResponseField name="credential_count" type="integer">How many credentials it holds. The metadata itself comes from `GET /v1/vaults/{id}/credentials` — a vault read never embeds it.</ResponseField>
<ResponseField name="created_at" type="string">Creation timestamp.</ResponseField>

## Create a vault

`POST /v1/vaults` — scope `admin`

<ParamField body="display_name" type="string" required>Human-readable name.</ParamField>
<ParamField body="identity_id" type="string">The identity that owns the vault. Defaults to `null`.</ParamField>

## Add a credential

`POST /v1/vaults/{id}/credentials`

<ParamField body="kind" type="string" required>
  One of `env_var`, `static_bearer`, or `mcp_oauth`. **`env_var` is refused with `feature_not_configured`** —
  substituting it needs an egress boundary on the sandbox's network path, and storing a secret under a
  guarantee that cannot be kept is worse than refusing it. Use an MCP kind, which is injected before the
  sandbox sees the value.
</ParamField>

<ParamField body="key" type="string" required>
  The reference name — for `env_var`, the environment variable name (e.g. `PAYMENTS_API_KEY`).
</ParamField>

<ParamField body="host" type="string">
  Required for `env_var` — the one exact destination host the value may be substituted for. Never a wildcard.
</ParamField>

<ParamField body="mcp_server_url" type="string">
  Required for `static_bearer` and `mcp_oauth` — the server URL the token is matched to.
</ParamField>

<ParamField body="value" type="string" required>
  The secret. **Write-only** — accepted here and never returned by any endpoint.
</ParamField>

<ParamField body="expires_at" type="string">Optional expiry, as an RFC 3339 timestamp with an offset.</ParamField>

<CodeGroup>
  ```bash cURL theme={"system"}
  curl -fsSL https://api.vetta.sh/v1/vaults/vlt_01H.../credentials \
    -H "authorization: Bearer sk_live_..." \
    -H "content-type: application/json" \
    -d '{
      "kind": "mcp_oauth",
      "key": "Payments MCP",
      "mcp_server_url": "https://mcp.example.com/sse",
      "value": "••••••••"
    }'
  ```
</CodeGroup>

<ResponseExample>
  ```json Response theme={"system"}
  {
    "id": "vcr_01H9CD...",
    "object": "vault_credential",
    "vault_id": "vlt_01H...",
    "kind": "mcp_oauth",
    "key": "Payments MCP",
    "host": null,
    "mcp_server_url": "https://mcp.example.com/sse",
    "placeholder_ref": "vetta_ref_9f2c...",
    "expires_at": null,
    "last_injected_at": null,
    "created_at": "2026-08-20T17:00:00Z"
  }
  ```
</ResponseExample>

<Warning>
  Reads never include secret values. `GET` on a credential returns only metadata — `kind`, `key`, `host`, `mcp_server_url`, `placeholder_ref`, `expires_at`, `last_injected_at`, `created_at`. There is no reveal route and no update route: rotation is create-new then delete-old.
</Warning>

## How injection works

* **`env_var`** — *not available on this deployment.* The design is an opaque placeholder in the sandbox with the real value substituted at the [egress boundary](/docs/computer/networking#egress-pinning-coming-soon); that boundary does not exist yet, so the route refuses the kind rather than storing a secret it cannot protect.
* **`static_bearer` / `mcp_oauth`** — injected server-side when the agent connects to the matching [MCP server](/docs/capabilities/tools#mcp-connector) URL. Matching is by URL: a credential is injected when its `mcp_server_url` equals one of the agent version's `mcp_servers[].url`. There is no per-session credential selection.

At rest, each value is protected with envelope encryption: a per-record data key (AES-256-GCM) wrapped by a managed key service and bound to the organization and identity.

## Scopes

| Route                                      | Scope         |
| ------------------------------------------ | ------------- |
| `POST /v1/vaults`                          | `admin`       |
| `GET /v1/vaults` · `GET /v1/vaults/{id}`   | `agents:read` |
| `DELETE /v1/vaults/{id}`                   | `admin`       |
| `POST /v1/vaults/{id}/credentials`         | `admin`       |
| `GET /v1/vaults/{id}/credentials`          | `agents:read` |
| `DELETE /v1/vaults/{id}/credentials/{cid}` | `admin`       |

Writing a secret is an `admin` act; listing what exists is not. That split lets an agent-facing key confirm a credential is present without ever being able to add or remove one.

## Retrieve a vault

`GET /v1/vaults/{id}` — scope `agents:read`. Returns the vault object with `credential_count`. Metadata only, like every other read here — no secret value is reachable through this or any route.

```json Response theme={"system"}
{
  "id": "vlt_01H...",
  "object": "vault",
  "identity_id": "idn_01H...",
  "display_name": "payments",
  "credential_count": 3,
  "created_at": "2026-08-20T17:00:00Z"
}
```

## List vaults & credentials

```bash theme={"system"}
GET /v1/vaults                      # list vaults (cursor-paginated)
GET /v1/vaults/{id}/credentials     # list credential metadata (cursor-paginated)
```

Both accept `limit` and `after`. See [Pagination](/docs/api/pagination).

## Delete

```bash theme={"system"}
DELETE /v1/vaults/{id}                       # delete the vault
DELETE /v1/vaults/{id}/credentials/{cid}     # remove one credential
```

<Warning>
  Deleting a vault **soft-deletes the record and hard-deletes its secrets**. The credential values are destroyed, not archived — this cannot be undone, and any agent that was relying on injection from this vault starts failing at the egress boundary on its next call.
</Warning>

```json Response theme={"system"}
{ "id": "vlt_01H...", "object": "vault", "deleted": true }
```

### Rotating a credential

There is no update route for a credential, and that is the contract rather than an omission. Rotate by **adding the new secret and then deleting the old one** — two records, two ids. That keeps `last_injected_at` attributable to exactly one secret, so an audit can still answer which value was in use at the moment of a given call.

<Card title="Back to core resources" icon="arrow-left" href="/docs/api/agents">
  The Phase 1 resource set.
</Card>
