# Vault

> Store secrets, grant them to hosts and fetch with a secret injected, never reading it.

Source: https://useprimitives.com/docs/api/vault

These routes apply to a primitive of type `vault`. See [Vault](/docs/primitives/vault) for the concepts. Calling one of them on a primitive of another type answers `400 wrong_type`.

No endpoint returns a secret's value. You write a value once, and from then on it can only be used: `fetch` injects it into one outgoing HTTPS request and redacts it from the response.

| Method | Path | Description |
| --- | --- | --- |
| `PUT` | `/v1/primitives/{id}/secrets/{NAME}` | Set a secret |
| `DELETE` | `/v1/primitives/{id}/secrets/{NAME}` | Delete a secret and its grant |
| `GET` | `/v1/primitives/{id}/secrets` | List secrets, with their grants |
| `POST` | `/v1/primitives/{id}/grants` | Grant a secret |
| `GET` | `/v1/primitives/{id}/grants` | List grants |
| `DELETE` | `/v1/primitives/{id}/grants/{NAME}` | Revoke a grant |
| `POST` | `/v1/primitives/{id}/fetch` | Make a request with a secret injected |

Secret names match `^[A-Z][A-Z0-9_]{0,63}$`: 1–64 uppercase letters, digits or `_`, starting with a letter. Any other name is `400 invalid_request`.

Every route answers `423 paused` while the primitive is paused. Timestamps are milliseconds since the Unix epoch. See [Errors](/docs/api/errors) for the full list of codes.

Sections marked **Account.** reject scoped API keys with `403 forbidden`. See [Authentication](/docs/api/authentication).

## Set a secret

`PUT /v1/primitives/{id}/secrets/{NAME}`

**Account.** Creates the secret, or replaces its value. Every write bumps `version` by one; the grant, if any, stays. Returns SecretInfo, which never holds the value. Scoped API keys get `403 forbidden`.

The value can't be empty (`400 invalid_request`) and is at most 64 KiB (`413 payload_too_large`). On a Starter workspace at its storage cap, it answers `429 plan_limit`; see [Usage and limits](/docs/api/limits#usage-and-limits).

**Body**

| Parameter | Type | Description |
| --- | --- | --- |
| `value` | string | Required. The secret's value. |

```bash
curl -X PUT https://api.useprimitives.com/v1/primitives/01jbz3k8m5q2w7x9v4n6t0r1ce/secrets/GITHUB_TOKEN \
  -H "Authorization: Bearer $PRIMITIVES_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"value\": \"$GITHUB_TOKEN\"}"
```

```json
{
  "name": "GITHUB_TOKEN",
  "version": 1,
  "created_at": 1760486400000,
  "updated_at": 1760486400000
}
```

## Delete a secret

`DELETE /v1/primitives/{id}/secrets/{NAME}`

**Account.** Deletes the secret and its grant. A secret that doesn't exist answers `404 not_found`. Scoped API keys get `403 forbidden`.

```bash
curl -X DELETE https://api.useprimitives.com/v1/primitives/01jbz3k8m5q2w7x9v4n6t0r1ce/secrets/GITHUB_TOKEN \
  -H "Authorization: Bearer $PRIMITIVES_API_KEY"
```

Returns `204` with no body.

## List secrets

`GET /v1/primitives/{id}/secrets`

Lists every secret by name, in name order, each with its version, timestamps and grant. `grant` is `null` for a secret that has none. Values are never included. Paged; see [Pagination](/docs/api#pagination).

Key tool: `vault_list`.

**Query parameters**

| Parameter | Type | Description |
| --- | --- | --- |
| `limit` | integer | Page size. Default `50`, max `200`. |
| `cursor` | string | The `next_cursor` of the previous page. |

```bash
curl https://api.useprimitives.com/v1/primitives/01jbz3k8m5q2w7x9v4n6t0r1ce/secrets \
  -H "Authorization: Bearer $PRIMITIVES_API_KEY"
```

```json
{
  "data": [
    {
      "name": "GITHUB_TOKEN",
      "version": 2,
      "created_at": 1760486400000,
      "updated_at": 1760490000000,
      "grant": {
        "name": "GITHUB_TOKEN",
        "allow": { "hosts": ["api.github.com"], "methods": ["GET", "POST"] },
        "expires_at": 1760490900000,
        "created_at": 1760490000000
      }
    },
    {
      "name": "STRIPE_KEY",
      "version": 1,
      "created_at": 1760486400000,
      "updated_at": 1760486400000,
      "grant": null
    }
  ],
  "next_cursor": null
}
```

## Grant a secret

`POST /v1/primitives/{id}/grants`

**Account.** Lets a secret be used against matching hosts and methods. A secret can't be used until it has a grant. Each secret has at most one grant: granting again replaces it. Scoped API keys get `403 forbidden`.

Host patterns are hostnames or IP literals, optionally with `*` wildcards such as `*.github.com`, with no scheme, port or path. They're stored in lowercase, and methods in uppercase. `*.github.com` matches `api.github.com` but not `github.com`. A grant takes at most 64 hosts and 64 methods.

Errors: `404 not_found` if the secret doesn't exist; `400 invalid_request` for an invalid name, no hosts, an invalid host pattern or method, or a `ttl` that isn't positive. On a Starter workspace at its storage cap, it answers `429 plan_limit`; see [Usage and limits](/docs/api/limits#usage-and-limits).

**Body**

| Parameter | Type | Description |
| --- | --- | --- |
| `name` | string | Required. The secret's name. |
| `allow.hosts` | string[] | Required. Host patterns the secret may be sent to. At least one. |
| `allow.methods` | string[] | HTTP methods allowed. Empty or omitted means every method. |
| `ttl` | duration | Lifetime of the grant, such as `"15m"`. Omit it for a grant that doesn't expire. See [Durations](/docs/api#durations). |

```bash
curl -X POST https://api.useprimitives.com/v1/primitives/01jbz3k8m5q2w7x9v4n6t0r1ce/grants \
  -H "Authorization: Bearer $PRIMITIVES_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "GITHUB_TOKEN",
    "allow": { "hosts": ["api.github.com"], "methods": ["GET", "POST"] },
    "ttl": "15m"
  }'
```

Returns `201` with the Grant:

```json
{
  "name": "GITHUB_TOKEN",
  "allow": { "hosts": ["api.github.com"], "methods": ["GET", "POST"] },
  "expires_at": 1760490900000,
  "created_at": 1760490000000
}
```

## List grants

`GET /v1/primitives/{id}/grants`

Lists every grant, in name order. Expired grants are still listed until revoked; check `expires_at`. Paged; see [Pagination](/docs/api#pagination).

Key tool: `vault_list`.

**Query parameters**

| Parameter | Type | Description |
| --- | --- | --- |
| `limit` | integer | Page size. Default `50`, max `200`. |
| `cursor` | string | The `next_cursor` of the previous page. |

```bash
curl https://api.useprimitives.com/v1/primitives/01jbz3k8m5q2w7x9v4n6t0r1ce/grants \
  -H "Authorization: Bearer $PRIMITIVES_API_KEY"
```

```json
{
  "data": [
    {
      "name": "GITHUB_TOKEN",
      "allow": { "hosts": ["api.github.com"], "methods": ["GET", "POST"] },
      "expires_at": 1760490900000,
      "created_at": 1760490000000
    }
  ],
  "next_cursor": null
}
```

## Revoke a grant

`DELETE /v1/primitives/{id}/grants/{NAME}`

**Account.** Removes the secret's grant. The secret stays, but can't be used until it's granted again. A secret with no grant answers `404 not_found`. Scoped API keys get `403 forbidden`.

```bash
curl -X DELETE https://api.useprimitives.com/v1/primitives/01jbz3k8m5q2w7x9v4n6t0r1ce/grants/GITHUB_TOKEN \
  -H "Authorization: Bearer $PRIMITIVES_API_KEY"
```

Returns `204` with no body.

## Fetch with a secret

`POST /v1/primitives/{id}/fetch`

Makes one HTTPS request from the engine with a secret injected, and returns the upstream response with the secret redacted. The request is checked against the secret's grant before the value is decrypted. The call returns `200` whatever the upstream status; the upstream status is in `status`.

Key tool: `vault_use`.

**Body**

| Parameter | Type | Description |
| --- | --- | --- |
| `url` | string | Required. The `https` URL to call. It can't carry credentials and is at most 8,192 characters. |
| `auth` | object | Required. Which secret to inject, and how. See the table below. |
| `method` | string | HTTP method. Default `GET`. |
| `headers` | object | Request headers, as name–value strings. |
| `body` | any | Request body. A string is sent as is; any other JSON is serialized and sent with `Content-Type: application/json` unless you set one. |

`auth` takes exactly one of these forms:

| `auth` | Injected as |
| --- | --- |
| `{ "bearer": "NAME" }` | `Authorization: Bearer <value>` |
| `{ "header": { "name": "X-Api-Key", "secret": "NAME" } }` | `X-Api-Key: <value>` |
| `{ "basic": { "username": "me", "secret": "NAME" } }` | `Authorization: Basic base64(me:<value>)`. `username` can't contain `:` |
| `{ "query": { "param": "api_key", "secret": "NAME" } }` | `?api_key=<value>`. The URL can't already have that parameter |

The response:

- Every occurrence of the value in the response headers and body, including its base64, Basic and URL-encoded forms, is replaced with `[REDACTED]`.
- `headers` has lowercase names; repeated headers are joined with `, `. `Set-Cookie` is dropped. Redirects aren't followed.
- A body over 1 MiB is cut and `truncated` is `true`. A UTF-8 body comes back as text with `body_encoding: "utf8"`; anything else is base64 with `body_encoding: "base64"`.

Errors:

| Status | Code | When |
| --- | --- | --- |
| 400 | `invalid_request` | The URL is invalid, isn't `https`, carries credentials or has no host; the method or a header is invalid; a framing header such as `Content-Length` or `Transfer-Encoding` is set; or the value can't be sent in a header or query string. |
| 403 | `secret_policy_denied` | The secret has no grant or doesn't exist; the grant doesn't allow the host or method; the request sets `Authorization`, `Proxy-Authorization`, `Cookie`, `Host` or the header named in `auth`; or, in the cloud, the destination resolves to a private, loopback, link-local or other reserved address. Nothing is sent. |
| 410 | `secret_grant_expired` | The grant has expired. |
| 502 | `upstream_error` | The upstream request failed, the host couldn't be resolved, or it timed out after 30 seconds. |

`primitives dev` allows plain `http` and private addresses so you can test against local services.

```bash
curl -X POST https://api.useprimitives.com/v1/primitives/01jbz3k8m5q2w7x9v4n6t0r1ce/fetch \
  -H "Authorization: Bearer $PRIMITIVES_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://api.github.com/user",
    "auth": { "bearer": "GITHUB_TOKEN" },
    "headers": { "accept": "application/vnd.github+json" }
  }'
```

```json
{
  "status": 200,
  "headers": {
    "content-type": "application/json; charset=utf-8",
    "x-ratelimit-remaining": "4999"
  },
  "body": "{\"login\":\"octocat\",\"id\":1}",
  "body_encoding": "utf8",
  "truncated": false
}
```

In the cloud, every use is audited as `vault.use` with the host, method, path and upstream status, and every use the grant refuses (no grant, an expired grant, or a host or method it doesn't allow) as `vault.denied` with the reason. Values never are.

## Shapes

| Shape | Fields |
| --- | --- |
| SecretInfo | `{ name, version, created_at, updated_at }`. `version` starts at `1` and goes up by one on every set. Never holds the value |
| SecretEntry | SecretInfo plus `grant`: a Grant, or `null` |
| Grant | `{ name, allow: { hosts, methods }, expires_at, created_at }`. `methods` is empty when every method is allowed. `expires_at` is `null` for a grant that doesn't expire |
| FetchResponse | `{ status, headers, body, body_encoding, truncated }`. `status` is the upstream status. `body_encoding` is `"utf8"` or `"base64"` |

Scoped API keys need the tool named in each section on their allowlist; see [Tool allowlists](/docs/api/authentication#tool-allowlists). For creating, snapshotting, forking and pausing the vault itself, see [Primitives](/docs/api/primitives). See [Limits](/docs/api/limits) for how operations are counted.
