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

# Secrets

> Store and manage encrypted workspace secrets whose values never leave the platform.

export const ErrorTable = ({errors}) => <div className="gv-error-table" role="table" aria-label="Errors and fixes">
    <div className="gv-error-table__head" role="row">
      <span className="gv-error-table__col-head" role="columnheader">Error</span>
      <span className="gv-error-table__col-head" role="columnheader">Cause</span>
      <span className="gv-error-table__col-head" role="columnheader">Fix</span>
    </div>
    {errors.map((err, i) => <div className="gv-error-table__row" role="row" key={i}>
        <code className="gv-error-table__code" role="cell">{err.code}</code>
        <div className="gv-error-table__cause" role="cell">{err.cause}</div>
        <div className="gv-error-table__fix" role="cell">{err.fix}</div>
      </div>)}
  </div>;

export const ExpectedOutput = ({caption = "Expected output", children}) => <div className="gv-output">
    <div className="gv-output__label">
      <svg className="gv-output__icon" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
        <polyline points="15 10 20 15 15 20" />
        <path d="M4 4v7a4 4 0 0 0 4 4h12" />
      </svg>
      <span>{caption}</span>
    </div>
    <div className="gv-output__body">{children}</div>
  </div>;

export const APIBadge = ({method = "GET", path, scope}) => {
  const m = method.toUpperCase();
  return <span className="gv-api-badge">
      <span className={`gv-api-badge__method gv-api-badge__method--${m.toLowerCase()}`}>
        {m}
      </span>
      <code className="gv-api-badge__path">{path}</code>
      {scope && <span className="gv-api-badge__scope">
          <span className="gv-api-badge__scope-label">scope</span>
          <code>{scope}</code>
        </span>}
    </span>;
};

export const Eyebrow = ({label, tone = "default"}) => <div className={`gv-eyebrow gv-eyebrow--${tone}`}>
    <span className="gv-eyebrow__bar" aria-hidden="true" />
    <span className="gv-eyebrow__label">{label}</span>
  </div>;

<Eyebrow label="RESOURCE" />

Secrets is a workspace-scoped encrypted store for sensitive values — third-party API keys, signing secrets, webhook tokens, anything you do not want to hardcode. The store is deliberately asymmetric: you can write and rotate values, but you can never read them back. List and fetch responses return names and metadata only.

<Warning>
  Secret values are **write-only**. No endpoint returns a stored value. If you lose access to a secret, rotate it.
</Warning>

## A secret in shape

<ExpectedOutput>
  ```json theme={null}
  {
    "name":       "STRIPE_SECRET_KEY",
    "created_at": "2025-10-01T12:00:00Z",
    "updated_at": "2025-10-15T09:30:00Z"
  }
  ```
</ExpectedOutput>

The value is absent on every read. It is supplied on `PUT` and `rotate`, then disappears from the API surface.

## Endpoints

<APIBadge method="GET" path="/v1/workspaces/{slug}/secrets" scope="secrets:read" />

List secret names and metadata.

<APIBadge method="PUT" path="/v1/workspaces/{slug}/secrets" scope="secrets:set" />

Set (create or overwrite) a secret value.

<APIBadge method="DELETE" path="/v1/workspaces/{slug}/secrets/{name}" scope="secrets:destructive" />

Permanently delete a secret.

<APIBadge method="POST" path="/v1/workspaces/{slug}/secrets/{name}/rotate" scope="secrets:write" />

Generate a new value, with a brief overlap window.

<APIBadge method="POST" path="/v1/workspaces/{slug}/secrets/proxy-url" scope="secrets:read" />

Mint a short-lived proxy URL for runtime access.

<Note>
  `secrets:set` is a distinct verb from `secrets:write`. Setting a value is a one-way write with no corresponding read; requiring the explicit scope makes least-privilege grants clearer.
</Note>

## Rotation vs DELETE + PUT

Use rotation rather than a delete-and-recreate. Rotation runs an overlap window during which both the old and new values are accepted, so in-flight requests holding a reference to the old value do not fail. A DELETE + PUT pair has no overlap and is racy.

## Proxy URLs

A proxy URL is a short-lived endpoint your tenant runtime can call instead of holding a secret value. The platform resolves the value server-side at the moment the proxied request executes — the secret never appears in client-side code, browser network logs, or agent task definitions.

<ExpectedOutput>
  ```json theme={null}
  {
    "url":        "https://proxy.gavai.app/s/eph_01H...",
    "expires_at": "2026-05-11T08:05:00Z"
  }
  ```
</ExpectedOutput>

Mint a proxy URL close to the moment you use it. Do not store or cache them — they expire quickly.

## How values are stored

Secret values are encrypted at rest under a workspace-specific data key. The data key itself is wrapped by a master key the platform rotates on a schedule, and every wrap/unwrap is logged with the requesting principal — that is what makes the audit trail truthful even when a value is read by a capability rather than a human.

The store records a small amount of non-secret metadata alongside the encrypted blob:

<DefList>
  <Def term="purpose" type="string">
    Free-text tag that names what the secret is *for* (`stripe_secret_key`, `webhook_signing`, `outbound_smtp`). Set on `PUT` and visible on reads. The platform uses `purpose` to decide which capability can resolve which secret — a `purpose: "stripe_secret_key"` value is not handed to the email capability even if both run in the same workspace.
  </Def>

  <Def term="residency" type="enum">
    Which provider region the value is allowed to live in. Defaults to the workspace's home region; can be locked to a specific region for customers with data-residency commitments. Once set, the secret cannot be served by a runtime in a different region — the call fails with `residency_violation` rather than transparently falling back.
  </Def>

  <Def term="zdr" type="boolean">
    Marks the secret for zero-data-retention handling. ZDR secrets are never written to caches, audit blobs, or log lines outside the encrypted store; they are resolved live at the moment of use and dropped immediately after. Use this flag for credentials with the strictest contractual handling.
  </Def>
</DefList>

These metadata fields are returned on list and fetch alongside `name`, `created_at`, and `updated_at`. The encrypted blob is never returned by any endpoint.

## Rotation triggers

Rotation happens for three reasons. The endpoint shape is identical for all of them; only the trigger differs.

<DefList>
  <Def term="Scheduled">
    Each secret has a configurable rotation cadence. When the cadence elapses, the platform calls the upstream provider's rotation API (where one exists), accepts the new value, and runs the same overlap window as a manual rotation.
  </Def>

  <Def term="Manual">
    A workspace admin calls `POST /secrets/{name}/rotate` from the console or via the API. Use this on suspicion of compromise or after an offboarding event.
  </Def>

  <Def term="Forced by upstream">
    A downstream provider can signal a rotation requirement out-of-band (an expired key, a revoked credential). The platform marks the secret `pending_rotation` on next use and surfaces `upstream_unavailable` with `details.hint = "rotate_secret"` until rotation completes.
  </Def>
</DefList>

## Common errors

<ErrorTable
  errors={[
{ code: "404 not_found", cause: "No secret with that name exists in this workspace.", fix: "Verify the name; list `/secrets` to see what exists." },
{ code: "403 insufficient_scope", cause: "Token lacks the required scope for the operation.", fix: "Mint a token with `secrets:read`, `secrets:set`, `secrets:write`, or `secrets:destructive` as appropriate." },
]}
/>

## Related

<Card title="Workspaces" icon="building" href="/api-reference/resources/workspaces">
  Manage the workspaces secrets belong to.
</Card>
